From 05c45f70ddb54f75913004cb4db100705227413b Mon Sep 17 00:00:00 2001 From: Xavier Fournet <461943+xfournet@users.noreply.github.com> Date: Wed, 26 Aug 2026 00:04:25 +0200 Subject: [PATCH 1/6] feat: build the OpenSSL FIPS provider from FIPS 140-3 validated sources (#45) The cryptographic module is fips.so, so the boundary stops at it and the OpenSSL libraries calling into it can be the ones Alpine packages. Build the provider from the 3.1.2 sources, the only version validated under FIPS 140-3 (CMVP #4985), and verify the tarball checksum since unmodified source is a condition of the CMVP porting rule. OpenSSL, Python and NodeJS now come from Alpine packages, which removes both the OpenSSL and the CPython builds. The two Dockerfiles become one with two targets, and openssl.cnf keeps only the provider configuration instead of a copy of the upstream template that had drifted several releases behind. The build asserts that the expected provider is active, that MD5 is refused, and that NodeJS enters FIPS mode against the system OpenSSL, so a broken setup fails the build rather than shipping. Co-Authored-By: Claude Opus 5 --- .github/dependabot.yml | 10 - .github/workflows/docker-build-push.yml | 59 ++-- Dockerfile | 93 ++++++ Dockerfile_python | 64 ---- Dockerfile_python_nodejs | 67 ----- FIPS.md | 88 ++++++ README.md | 41 ++- openssl.cnf | 372 +----------------------- renovate.json5 | 110 +++---- 9 files changed, 296 insertions(+), 608 deletions(-) delete mode 100644 .github/dependabot.yml create mode 100644 Dockerfile delete mode 100644 Dockerfile_python delete mode 100644 Dockerfile_python_nodejs create mode 100644 FIPS.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index f99b8a2..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,10 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 0 - commit-message: - prefix: "chore" - include: "scope" diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index dbc5032..25f5479 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -4,6 +4,7 @@ on: push: branches: - main + pull_request: schedule: # Daily rebuild, to pick up upstream Alpine security updates. - cron: "0 0 * * *" @@ -12,49 +13,63 @@ on: permissions: contents: read +# A run that publishes must never be interrupted: the images share mutable tags, +# so two concurrent runs could push `latest` from two different builds, and a +# cancellation between the two build steps would publish one image and not the +# other. A run that publishes nothing is cancelled when superseded. concurrency: group: docker-build-push-${{ github.ref }} - cancel-in-progress: false + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: build: - name: Build ${{ matrix.image }} + name: Build and publish runs-on: ubuntu-latest - timeout-minutes: 120 - strategy: - fail-fast: false - matrix: - include: - - image: filigran/python-fips - dockerfile: Dockerfile_python - tags: | - filigran/python-fips:python3.12 - filigran/python-fips:latest - - image: filigran/python-nodejs-fips - dockerfile: Dockerfile_python_nodejs - tags: | - filigran/python-nodejs-fips:python3.12-nodejs22 - filigran/python-nodejs-fips:latest steps: - name: Checkout repository uses: actions/checkout@v7 + # The Dockerfile asserts these versions at build time, so the tags are read + # from it rather than repeated here, where they could drift. + - name: Read the advertised versions + id: versions + run: | + set -euo pipefail + python=$(sed -n 's/^ARG PYTHON_VERSION=//p' Dockerfile) + nodejs=$(sed -n 's/^ARG NODEJS_VERSION=//p' Dockerfile) + test -n "${python}" && test -n "${nodejs}" + echo "python=${python}" >> "$GITHUB_OUTPUT" + echo "nodejs=${nodejs}" >> "$GITHUB_OUTPUT" + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Log in to Docker Hub + if: github.ref == 'refs/heads/main' uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push ${{ matrix.image }} + - name: Build filigran/python-fips uses: docker/build-push-action@v7 with: context: . - file: ${{ matrix.dockerfile }} + target: python-fips platforms: linux/amd64 pull: true - push: true - no-cache: true - tags: ${{ matrix.tags }} + push: ${{ github.ref == 'refs/heads/main' }} + tags: | + filigran/python-fips:python${{ steps.versions.outputs.python }} + filigran/python-fips:latest + + - name: Build filigran/python-nodejs-fips + uses: docker/build-push-action@v7 + with: + context: . + target: python-nodejs-fips + platforms: linux/amd64 + push: ${{ github.ref == 'refs/heads/main' }} + tags: | + filigran/python-nodejs-fips:python${{ steps.versions.outputs.python }}-nodejs${{ steps.versions.outputs.nodejs }} + filigran/python-nodejs-fips:latest diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..667113f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,93 @@ +# syntax=docker/dockerfile:1 +FROM alpine:3.23 AS python-fips + +# Only source version validated under FIPS 140-3 (CMVP #4985). Must not be +# bumped automatically: any other version leaves the validated lineage. +ARG OPENSSL_FIPS_VERSION=3.1.2 +ARG OPENSSL_FIPS_SHA256=a0ce69b8b97ea6a35b96875235aa453b966ba3cba8af2de23657d8b6767d6539 + +ENV LANG=C.UTF-8 + +RUN << EOT + set -euxo pipefail + + apk add --no-cache ca-certificates openssl python3 py3-pip libffi + rm -f /usr/lib/python3.*/EXTERNALLY-MANAGED +EOT + +# fips.so is the cryptographic module: the boundary stops at it, and the OpenSSL +# libraries calling into it stay the ones packaged by Alpine. Hence +# 'make install_fips', which installs the module and its fipsmodule.cnf only. +# The checksum enforces unmodified source, a condition of the CMVP porting rule. +# +# cryptography is built from source as well: a pre-built wheel carries its own +# OpenSSL and would sit outside the boundary. +RUN << EOT + set -euxo pipefail + + apk add --no-cache --virtual .build-deps build-base perl linux-headers cargo pkgconfig python3-dev libffi-dev openssl-dev + + wget -O openssl.tar.gz "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_FIPS_VERSION}/openssl-${OPENSSL_FIPS_VERSION}.tar.gz" + echo "${OPENSSL_FIPS_SHA256} openssl.tar.gz" | sha256sum -c - + tar -xf openssl.tar.gz + cd "openssl-${OPENSSL_FIPS_VERSION}" + # Install into the paths compiled into Alpine's libcrypto, so that enabling + # FIPS needs no environment variable. + ./Configure enable-fips --prefix=/usr --libdir=lib --openssldir=/etc/ssl + make -j"$(nproc)" + make install_fips + cd .. + rm -rf "openssl-${OPENSSL_FIPS_VERSION}" openssl.tar.gz + + pip install --no-cache-dir --no-binary cryptography cryptography + + apk del .build-deps + # pip's isolated build environment and cargo keep their own caches, which + # apk del does not cover. + rm -rf /root/.cache /root/.cargo +EOT + +COPY openssl.cnf /etc/ssl/openssl.cnf + +# Advertised by the published tags, so a drift in the Alpine package must fail +# the build rather than produce an image whose tag lies. +ARG PYTHON_VERSION=3.12 + +RUN << EOT + set -euxo pipefail + + python3 -V | grep "^Python ${PYTHON_VERSION}\." > /dev/null + + openssl list -providers + openssl list -providers | grep 'OpenSSL FIPS Provider' > /dev/null + openssl list -providers | grep -A2 fips | grep "version: ${OPENSSL_FIPS_VERSION}" > /dev/null + openssl dgst -sha256 /etc/ssl/openssl.cnf > /dev/null + + if echo test | openssl dgst -md5 > /dev/null 2>&1; then + echo 'MD5 was accepted: FIPS mode is not enforced' >&2 + exit 1 + fi + + python3 -c 'from cryptography.hazmat.backends.openssl.backend import backend; print(backend.openssl_version_text())' \ + | grep "$(openssl version | cut -d' ' -f2)" > /dev/null +EOT + + +FROM python-fips AS python-nodejs-fips + +# Advertised by the published tags, so a drift in the Alpine package must fail +# the build rather than produce an image whose tag lies. +ARG NODEJS_VERSION=24 + +# Node.js reaches the FIPS provider only through the system OpenSSL it is +# dynamically linked against, which the assertions below enforce. +RUN << EOT + set -euxo pipefail + + apk add --no-cache nodejs + + node -v | grep "^v${NODEJS_VERSION}\." > /dev/null + + test "$(node --enable-fips -p 'crypto.getFips()')" = '1' + ldd "$(which node)" | grep 'libssl.so.3' > /dev/null +EOT diff --git a/Dockerfile_python b/Dockerfile_python deleted file mode 100644 index 66a3081..0000000 --- a/Dockerfile_python +++ /dev/null @@ -1,64 +0,0 @@ -FROM alpine:3.23 AS base - -ARG OPENSSL_VERSION=3.6.3 -ARG PYTHON_VERSION=3.12.14 - -ENV PATH /usr/local/bin:$PATH -ENV LANG C.UTF-8 - -# Install OpenSSL FIPS -RUN set -ex \ - && apk add --no-cache --virtual .build-deps make gcc libgcc musl-dev linux-headers perl vim \ - && wget https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz \ - && tar -xf openssl-${OPENSSL_VERSION}.tar.gz \ - && cd openssl-${OPENSSL_VERSION} \ - && ./Configure enable-fips shared enable-ec_nistp_64_gcc_128 \ - && make \ - && make install \ - && make install_fips \ - && cp -a /etc/ssl/certs/* /usr/local/ssl/certs/ \ - && cp -a /etc/ssl/cert.pem /usr/local/ssl/ \ - && apk del .build-deps \ - && cd .. \ - && rm -rf openssl-${OPENSSL_VERSION}.tar.gz openssl-${OPENSSL_VERSION} - -COPY openssl.cnf /etc/ssl/openssl.cnf - -ENV OPENSSL_FIPS=1 -ENV OPENSSL_DIR=/usr/local -ENV OPENSSL_CONF=/etc/ssl/openssl.cnf -ENV OPENSSL_MODULES=/usr/local/lib64/ossl-modules - -ENV LDFLAGS="-L/usr/local/lib/ -L/usr/local/lib64/" -ENV LD_LIBRARY_PATH="/usr/local/lib/:/usr/local/lib64/" -ENV CPPFLAGS="-I/usr/local/include -I/usr/local/include/openssl" -ENV CFLAGS="-I/usr/local/include" - -# Install Python -RUN set -ex \ - && apk add --no-cache --virtual .build-deps make gcc libgcc musl-dev linux-headers perl vim tcl-dev tk-dev bzip2-dev zlib-dev libffi-dev cups-dev krb5-dev yaml-dev sqlite-dev libffi-dev libxml2-dev libxslt-dev \ - && wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz \ - && tar -xvf Python-${PYTHON_VERSION}.tgz \ - && cd Python-${PYTHON_VERSION} \ - && ./configure --enable-optimizations --enable-shared --with-ensurepip=no \ - && make \ - && make install \ - && cd .. \ - && apk del .build-deps \ - && ln -sf /usr/local/bin/idle3 /usr/local/bin/idle \ - && ln -sf /usr/local/bin/pydoc3 /usr/local/bin/pydoc \ - && ln -sf /usr/local/bin/python3 /usr/local/bin/python \ - && ln -sf /usr/local/bin/python3-config /usr/local/bin/python-config \ - && rm -f Python-${PYTHON_VERSION}.tgz \ - && rm -rf Python-${PYTHON_VERSION} - -# Install deps -RUN set -ex \ - && apk add --no-cache libintl libffi rust cargo pkgconfig - -RUN set -ex \ - && apk add --no-cache curl \ - && curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py \ - && python3 get-pip.py \ - && rm get-pip.py \ - && pip install cryptography --no-binary cryptography \ No newline at end of file diff --git a/Dockerfile_python_nodejs b/Dockerfile_python_nodejs deleted file mode 100644 index d3ea26d..0000000 --- a/Dockerfile_python_nodejs +++ /dev/null @@ -1,67 +0,0 @@ -FROM alpine:3.23 AS base - -ARG OPENSSL_VERSION=3.6.3 -ARG PYTHON_VERSION=3.12.14 - -ENV PATH /usr/local/bin:$PATH -ENV LANG C.UTF-8 - -# Install OpenSSL FIPS -RUN set -ex \ - && apk add --no-cache --virtual .build-deps make gcc libgcc musl-dev linux-headers perl vim \ - && wget https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz \ - && tar -xf openssl-${OPENSSL_VERSION}.tar.gz \ - && cd openssl-${OPENSSL_VERSION} \ - && ./Configure enable-fips shared enable-ec_nistp_64_gcc_128 \ - && make \ - && make install \ - && make install_fips \ - && cp -a /etc/ssl/certs/* /usr/local/ssl/certs/ \ - && cp -a /etc/ssl/cert.pem /usr/local/ssl/ \ - && apk del .build-deps \ - && cd .. \ - && rm -rf openssl-${OPENSSL_VERSION}.tar.gz openssl-${OPENSSL_VERSION} - -COPY openssl.cnf /etc/ssl/openssl.cnf - -ENV OPENSSL_FIPS=1 -ENV OPENSSL_DIR=/usr/local -ENV OPENSSL_CONF=/etc/ssl/openssl.cnf -ENV OPENSSL_MODULES=/usr/local/lib64/ossl-modules - -ENV LDFLAGS="-L/usr/local/lib/ -L/usr/local/lib64/" -ENV LD_LIBRARY_PATH="/usr/local/lib/:/usr/local/lib64/" -ENV CPPFLAGS="-I/usr/local/include -I/usr/local/include/openssl" -ENV CFLAGS="-I/usr/local/include" - -# Install Python -RUN set -ex \ - && apk add --no-cache --virtual .build-deps make gcc libgcc musl-dev linux-headers perl vim tcl-dev tk-dev bzip2-dev zlib-dev libffi-dev cups-dev krb5-dev yaml-dev sqlite-dev libffi-dev libxml2-dev libxslt-dev \ - && wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz \ - && tar -xvf Python-${PYTHON_VERSION}.tgz \ - && cd Python-${PYTHON_VERSION} \ - && ./configure --enable-optimizations --enable-shared --with-ensurepip=no \ - && make \ - && make install \ - && cd .. \ - && apk del .build-deps \ - && ln -sf /usr/local/bin/idle3 /usr/local/bin/idle \ - && ln -sf /usr/local/bin/pydoc3 /usr/local/bin/pydoc \ - && ln -sf /usr/local/bin/python3 /usr/local/bin/python \ - && ln -sf /usr/local/bin/python3-config /usr/local/bin/python-config \ - && rm -f Python-${PYTHON_VERSION}.tgz \ - && rm -rf Python-${PYTHON_VERSION} - -# Install deps -RUN set -ex \ - && apk add --no-cache libintl libffi rust cargo pkgconfig - -RUN set -ex \ - && apk add --no-cache curl \ - && curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py \ - && python3 get-pip.py \ - && rm get-pip.py \ - && pip install cryptography --no-binary cryptography - -RUN set -ex \ - && apk add nodejs npm yarn \ No newline at end of file diff --git a/FIPS.md b/FIPS.md new file mode 100644 index 0000000..db4411a --- /dev/null +++ b/FIPS.md @@ -0,0 +1,88 @@ +# FIPS 140-3 posture of these images + +What these images claim and what they do not, so that downstream users can +assess it against their own compliance requirements. + +## What the cryptographic module is + +The cryptographic module is the **OpenSSL FIPS provider** (`fips.so`), and +nothing else. `libcrypto`, `libssl`, Python and Node.js sit outside the boundary +and delegate to it — which is why the images use the OpenSSL packaged by Alpine +and compile only the provider. + +## Which version, and why that one + +The provider is built from the source distribution of **OpenSSL 3.1.2**, the +module validated under **CMVP certificate #4985** (FIPS 140-3, valid until +10 March 2030). It is the only source version with a FIPS 140-3 validation: the +other validated sources — 3.0.0, 3.0.8 and 3.0.9 under certificate #4282 — are +FIPS 140-**2**, and #4282 moves to the CMVP *Historical* list on +**21 September 2026**. + +The source distribution is checksum-verified at build time: leaving it +unmodified is the central condition of the porting rule below. + +## What is claimed — and what is not + +> FIPS mode enforced by a module built from FIPS 140-3 validated sources under +> certificate #4985, ported to Alpine/musl by vendor affirmation. + +This is **not** a claim that the image is "FIPS 140-3 validated". The operational +environments (OE) tested for #4985 do not include Alpine or musl. Recompiling a +software module for an untested OE falls under the CMVP porting rules +(FIPS 140-3 IG 2.3.B): the certificate is not extended, NIST does not list the +new OE, and the posture is **vendor affirmation** — an allowance addressed to +the module vendor. + +## What is enforced at runtime + +FIPS mode is active out of the box, with no environment variable or flag to set: +the configuration activates only the `fips` and `base` providers and sets +`default_properties = fips=yes`. The `default` provider is not declared at all, +so non-approved algorithms are refused rather than silently substituted, and an +unreachable `fips.so` fails operations outright +(`inner_evp_generic_fetch:unsupported`) instead of falling back to non-validated +implementations. + +The module's integrity check (`module-mac` in `fipsmodule.cnf`) is generated at +build time by `make install_fips`, against the module actually shipped. +`conditional-errors` and `security-checks` remain enabled. + +The build asserts all of this and fails rather than produce an image whose FIPS +mode is not effective. + +## Known gaps in coverage + +**Python `hashlib` is not fully inside the boundary.** `hashlib.sha256()` +resolves to `_hashlib.HASH` and goes through the module, but `hashlib.md5()` +resolves to `_md5.md5`, CPython's built-in implementation, which bypasses OpenSSL +and is not blocked by FIPS mode. This is upstream CPython behaviour; only Red +Hat's patched CPython enforces it. Python code that must stay inside the boundary +should use `ssl` or `cryptography`. + +**Statically linked crypto escapes the boundary silently.** Any Python wheel, Go +or Rust binary bundling its own OpenSSL or BoringSSL does not use the module. +`pip install cryptography` takes a `musllinux` wheel with its own bundled +OpenSSL by default — use `pip install --no-binary cryptography` so that it links +the system one. + +## Verifying an image yourself + +```bash +docker run --rm filigran/python-nodejs-fips:latest sh -c ' + openssl list -providers + node --enable-fips -p "crypto.getFips()" + python3 -c "import ssl; print(ssl.OPENSSL_VERSION)" + echo test | openssl dgst -md5 || echo "MD5 refused, as expected" +' +``` + +The provider must report version **3.1.2** while the library reports the Alpine +version. + +## References + +- [OpenSSL FIPS 140-3 validation announcement (3.1.2, cert #4985)](https://openssl-library.org/post/2025-03-11-fips-140-3/) +- [OpenSSL: which versions are FIPS validated](https://openssl-library.org/source/) +- [CMVP certificate #4282 (FIPS 140-2, historical 21 Sept 2026)](https://csrc.nist.gov/projects/cryptographic-module-validation-program/certificate/4282) +- [OpenSSL `README-FIPS.md` — provider/library version compatibility](https://github.com/openssl/openssl/blob/master/README-FIPS.md) diff --git a/README.md b/README.md index 4578110..2a236e9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,17 @@ -# 🐳 Docker FIPS for NodeJS and Python +# 🐳 Docker FIPS for Node.js and Python [![Pulls](https://img.shields.io/docker/pulls/filigran/python-nodejs-fips.svg)](https://hub.docker.com/r/filigran/python-nodejs-fips/) [![Pulls](https://img.shields.io/docker/pulls/filigran/python-fips.svg)](https://hub.docker.com/r/filigran/python-fips/) [![Build](https://github.com/FiligranHQ/docker-python-nodejs-fips/actions/workflows/docker-build-push.yml/badge.svg)](https://github.com/FiligranHQ/docker-python-nodejs-fips/actions/workflows/docker-build-push.yml) -## Docker Python NodeJS FIPS +Alpine-based images running Python and Node.js against the OpenSSL FIPS provider +built from **FIPS 140-3 validated sources** (CMVP certificate #4985). + +OpenSSL, Python and Node.js come from Alpine packages; only the FIPS provider is +compiled. See [`FIPS.md`](FIPS.md) for the exact compliance posture before making +any claim. + +## Docker Python Node.js FIPS Images are available at: https://hub.docker.com/r/filigran/python-nodejs-fips. @@ -14,17 +21,35 @@ Images are available at: https://hub.docker.com/r/filigran/python-fips. ## Use the images -* For Python, bindings are automatically mapped to the OpenSSL FIPS 140-2 library, just run your Python scripts as usual. -* For NodeJS, ensure to run your NodeJS programs with `--enable-fips` or `--force-fips`. +* For Python, bindings are automatically mapped to the OpenSSL FIPS provider, + just run your Python scripts as usual. +* For Node.js, ensure to run your Node.js programs with `--enable-fips` or + `--force-fips`. +* When installing `cryptography`, use `pip install --no-binary cryptography` so + that it links the system OpenSSL instead of a bundled one. ## Proof of Concept / testing ```bash $ docker run -it filigran/python-nodejs-fips:latest /bin/sh -$ openssl version -OpenSSL 3.1.5 30 Jan 2024 (Library: OpenSSL 3.1.5 30 Jan 2024) +$ openssl list -providers +Providers: + base + name: OpenSSL Base Provider + version: 3.5.7 + status: active + fips + name: OpenSSL FIPS Provider + version: 3.1.2 + status: active $ node --enable-fips -p 'crypto.getFips()' 1 $ python3 -c "import ssl; print(ssl.OPENSSL_VERSION);" -OpenSSL 3.1.5 30 Jan 2024 -``` \ No newline at end of file +OpenSSL 3.5.7 9 Jun 2026 +$ echo test | openssl dgst -md5 +Error setting digest +``` + +The FIPS provider reports `3.1.2` while the library reports the Alpine version. +That difference is expected: the validated module is the provider, and it is +supported across OpenSSL library releases. diff --git a/openssl.cnf b/openssl.cnf index ccd1225..6eb948a 100644 --- a/openssl.cnf +++ b/openssl.cnf @@ -1,57 +1,13 @@ -# -# OpenSSL example configuration file. -# See doc/man5/config.pod for more info. -# -# This is mostly being used for generation of certificate requests, -# but may be used for auto loading of providers - -# Note that you can include other files from the main configuration -# file using the .include directive. -#.include filename - -# This definition stops the following lines choking if HOME isn't -# defined. -HOME = . - -# Use this in order to automatically load providers. +# Activates the OpenSSL FIPS provider and makes FIPS the default for every +# algorithm fetch. The 'default' provider is deliberately left out, so that a +# non-approved algorithm fails instead of being served from outside the module. +# fipsmodule.cnf carries the module integrity MAC and is generated at build time. openssl_conf = openssl_init - -# For NodeJS nodejs_conf = nodejs_init -# Comment out the next line to ignore configuration errors config_diagnostics = 1 -# Extra OBJECT IDENTIFIER info: -# oid_file = $ENV::HOME/.oid -oid_section = new_oids - -# To use this configuration file with the "-extfile" option of the -# "openssl x509" utility, name here the section containing the -# X.509v3 extensions to use: -# extensions = -# (Alternatively, use a configuration file that has only -# X.509v3 extensions in its main [= default] section.) - -[ new_oids ] -# We can add new OIDs in here for use by 'ca', 'req' and 'ts'. -# Add a simple OID like this: -# testoid1=1.2.3.4 -# Or use config file substitution like this: -# testoid2=${testoid1}.5.6 - -# Policies used by the TSA examples. -tsa_policy1 = 1.2.3.4.1 -tsa_policy2 = 1.2.3.4.5.6 -tsa_policy3 = 1.2.3.4.5.7 - -# For FIPS -# Optionally include a file that is generated by the OpenSSL fipsinstall -# application. This file contains configuration data required by the OpenSSL -# fips provider. It contains a named section e.g. [fips_sect] which is -# referenced from the [provider_sect] below. -# Refer to the OpenSSL security policy for more information. -.include /usr/local/ssl/fipsmodule.cnf +.include /etc/ssl/fipsmodule.cnf [openssl_init] providers = provider_sect @@ -69,321 +25,3 @@ activate = 1 [algorithm_sect] default_properties = fips=yes - - -#################################################################### -[ ca ] -default_ca = CA_default # The default ca section - -#################################################################### -[ CA_default ] - -dir = ./demoCA # Where everything is kept -certs = $dir/certs # Where the issued certs are kept -crl_dir = $dir/crl # Where the issued crl are kept -database = $dir/index.txt # database index file. -#unique_subject = no # Set to 'no' to allow creation of - # several certs with same subject. -new_certs_dir = $dir/newcerts # default place for new certs. - -certificate = $dir/cacert.pem # The CA certificate -serial = $dir/serial # The current serial number -crlnumber = $dir/crlnumber # the current crl number - # must be commented out to leave a V1 CRL -crl = $dir/crl.pem # The current CRL -private_key = $dir/private/cakey.pem# The private key - -x509_extensions = usr_cert # The extensions to add to the cert - -# Comment out the following two lines for the "traditional" -# (and highly broken) format. -name_opt = ca_default # Subject Name options -cert_opt = ca_default # Certificate field options - -# Extension copying option: use with caution. -# copy_extensions = copy - -# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs -# so this is commented out by default to leave a V1 CRL. -# crlnumber must also be commented out to leave a V1 CRL. -# crl_extensions = crl_ext - -default_days = 365 # how long to certify for -default_crl_days= 30 # how long before next CRL -default_md = default # use public key default MD -preserve = no # keep passed DN ordering - -# A few difference way of specifying how similar the request should look -# For type CA, the listed attributes must be the same, and the optional -# and supplied fields are just that :-) -policy = policy_match - -# For the CA policy -[ policy_match ] -countryName = match -stateOrProvinceName = match -organizationName = match -organizationalUnitName = optional -commonName = supplied -emailAddress = optional - -# For the 'anything' policy -# At this point in time, you must list all acceptable 'object' -# types. -[ policy_anything ] -countryName = optional -stateOrProvinceName = optional -localityName = optional -organizationName = optional -organizationalUnitName = optional -commonName = supplied -emailAddress = optional - -#################################################################### -[ req ] -default_bits = 2048 -default_keyfile = privkey.pem -distinguished_name = req_distinguished_name -attributes = req_attributes -x509_extensions = v3_ca # The extensions to add to the self signed cert - -# Passwords for private keys if not present they will be prompted for -# input_password = secret -# output_password = secret - -# This sets a mask for permitted string types. There are several options. -# default: PrintableString, T61String, BMPString. -# pkix : PrintableString, BMPString (PKIX recommendation before 2004) -# utf8only: only UTF8Strings (PKIX recommendation after 2004). -# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). -# MASK:XXXX a literal mask value. -# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings. -string_mask = utf8only - -# req_extensions = v3_req # The extensions to add to a certificate request - -[ req_distinguished_name ] -countryName = Country Name (2 letter code) -countryName_default = AU -countryName_min = 2 -countryName_max = 2 - -stateOrProvinceName = State or Province Name (full name) -stateOrProvinceName_default = Some-State - -localityName = Locality Name (eg, city) - -0.organizationName = Organization Name (eg, company) -0.organizationName_default = Internet Widgits Pty Ltd - -# we can do this but it is not needed normally :-) -#1.organizationName = Second Organization Name (eg, company) -#1.organizationName_default = World Wide Web Pty Ltd - -organizationalUnitName = Organizational Unit Name (eg, section) -#organizationalUnitName_default = - -commonName = Common Name (e.g. server FQDN or YOUR name) -commonName_max = 64 - -emailAddress = Email Address -emailAddress_max = 64 - -# SET-ex3 = SET extension number 3 - -[ req_attributes ] -challengePassword = A challenge password -challengePassword_min = 4 -challengePassword_max = 20 - -unstructuredName = An optional company name - -[ usr_cert ] - -# These extensions are added when 'ca' signs a request. - -# This goes against PKIX guidelines but some CAs do it and some software -# requires this to avoid interpreting an end user certificate as a CA. - -basicConstraints=CA:FALSE - -# This is typical in keyUsage for a client certificate. -# keyUsage = nonRepudiation, digitalSignature, keyEncipherment - -# PKIX recommendations harmless if included in all certificates. -subjectKeyIdentifier=hash -authorityKeyIdentifier=keyid,issuer - -# This stuff is for subjectAltName and issuerAltname. -# Import the email address. -# subjectAltName=email:copy -# An alternative to produce certificates that aren't -# deprecated according to PKIX. -# subjectAltName=email:move - -# Copy subject details -# issuerAltName=issuer:copy - -# This is required for TSA certificates. -# extendedKeyUsage = critical,timeStamping - -[ v3_req ] - -# Extensions to add to a certificate request - -basicConstraints = CA:FALSE -keyUsage = nonRepudiation, digitalSignature, keyEncipherment - -[ v3_ca ] - - -# Extensions for a typical CA - - -# PKIX recommendation. - -subjectKeyIdentifier=hash - -authorityKeyIdentifier=keyid:always,issuer - -basicConstraints = critical,CA:true - -# Key usage: this is typical for a CA certificate. However since it will -# prevent it being used as an test self-signed certificate it is best -# left out by default. -# keyUsage = cRLSign, keyCertSign - -# Include email address in subject alt name: another PKIX recommendation -# subjectAltName=email:copy -# Copy issuer details -# issuerAltName=issuer:copy - -# DER hex encoding of an extension: beware experts only! -# obj=DER:02:03 -# Where 'obj' is a standard or added object -# You can even override a supported extension: -# basicConstraints= critical, DER:30:03:01:01:FF - -[ crl_ext ] - -# CRL extensions. -# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. - -# issuerAltName=issuer:copy -authorityKeyIdentifier=keyid:always - -[ proxy_cert_ext ] -# These extensions should be added when creating a proxy certificate - -# This goes against PKIX guidelines but some CAs do it and some software -# requires this to avoid interpreting an end user certificate as a CA. - -basicConstraints=CA:FALSE - -# This is typical in keyUsage for a client certificate. -# keyUsage = nonRepudiation, digitalSignature, keyEncipherment - -# PKIX recommendations harmless if included in all certificates. -subjectKeyIdentifier=hash -authorityKeyIdentifier=keyid,issuer - -# This stuff is for subjectAltName and issuerAltname. -# Import the email address. -# subjectAltName=email:copy -# An alternative to produce certificates that aren't -# deprecated according to PKIX. -# subjectAltName=email:move - -# Copy subject details -# issuerAltName=issuer:copy - -# This really needs to be in place for it to be a proxy certificate. -proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo - -#################################################################### -[ tsa ] - -default_tsa = tsa_config1 # the default TSA section - -[ tsa_config1 ] - -# These are used by the TSA reply generation only. -dir = ./demoCA # TSA root directory -serial = $dir/tsaserial # The current serial number (mandatory) -crypto_device = builtin # OpenSSL engine to use for signing -signer_cert = $dir/tsacert.pem # The TSA signing certificate - # (optional) -certs = $dir/cacert.pem # Certificate chain to include in reply - # (optional) -signer_key = $dir/private/tsakey.pem # The TSA private key (optional) -signer_digest = sha256 # Signing digest to use. (Optional) -default_policy = tsa_policy1 # Policy if request did not specify it - # (optional) -other_policies = tsa_policy2, tsa_policy3 # acceptable policies (optional) -digests = sha1, sha256, sha384, sha512 # Acceptable message digests (mandatory) -accuracy = secs:1, millisecs:500, microsecs:100 # (optional) -clock_precision_digits = 0 # number of digits after dot. (optional) -ordering = yes # Is ordering defined for timestamps? - # (optional, default: no) -tsa_name = yes # Must the TSA name be included in the reply? - # (optional, default: no) -ess_cert_id_chain = no # Must the ESS cert id chain be included? - # (optional, default: no) -ess_cert_id_alg = sha1 # algorithm to compute certificate - # identifier (optional, default: sha1) - -[insta] # CMP using Insta Demo CA -# Message transfer -server = pki.certificate.fi:8700 -# proxy = # set this as far as needed, e.g., http://192.168.1.1:8080 -# tls_use = 0 -path = pkix/ - -# Server authentication -recipient = "/C=FI/O=Insta Demo/CN=Insta Demo CA" # or set srvcert or issuer -ignore_keyusage = 1 # potentially needed quirk -unprotected_errors = 1 # potentially needed quirk -extracertsout = insta.extracerts.pem - -# Client authentication -ref = 3078 # user identification -secret = pass:insta # can be used for both client and server side - -# Generic message options -cmd = ir # default operation, can be overridden on cmd line with, e.g., kur - -# Certificate enrollment -subject = "/CN=openssl-cmp-test" -newkey = insta.priv.pem -out_trusted = insta.ca.crt -certout = insta.cert.pem - -[pbm] # Password-based protection for Insta CA -# Server and client authentication -ref = $insta::ref # 3078 -secret = $insta::secret # pass:insta - -[signature] # Signature-based protection for Insta CA -# Server authentication -trusted = insta.ca.crt # does not include keyUsage digitalSignature - -# Client authentication -secret = # disable PBM -key = $insta::newkey # insta.priv.pem -cert = $insta::certout # insta.cert.pem - -[ir] -cmd = ir - -[cr] -cmd = cr - -[kur] -# Certificate update -cmd = kur -oldcert = $insta::certout # insta.cert.pem - -[rr] -# Certificate revocation -cmd = rr -oldcert = $insta::certout # insta.cert.pem \ No newline at end of file diff --git a/renovate.json5 b/renovate.json5 index 5a12349..87266f5 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -1,70 +1,40 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - ":semanticCommits", - "config:recommended", - "docker:enableMajor" - ], - "labels": [ - "dependencies", - "filigran team" - ], - "minimumReleaseAge": "3 days", - "prHourlyLimit": 2, - "prConcurrentLimit": 100, - "branchConcurrentLimit": 100, - "timezone": "Europe/Paris", - "schedule": [ - "* 0-4,22-23 * * 1-5", - "* * * * 0,6" - ], - "updateNotScheduled": false, - "rebaseWhen": "conflicted", - "automergeStrategy": "squash", - "customManagers": [ - { - "customType": "regex", - "managerFilePatterns": [ - "/Dockerfile*/" - ], - "matchStrings": [ - "ARG OPENSSL_VERSION=(?.*?)\\s" - ], - "datasourceTemplate": "github-tags", - "depNameTemplate": "openssl/openssl", - "extractVersionTemplate": "^openssl-(?.*)$", - "versioningTemplate": "semver" - }, - { - "customType": "regex", - "managerFilePatterns": [ - "/Dockerfile*/" - ], - "matchStrings": [ - "ARG PYTHON_VERSION=(?.*?)\\s" - ], - "datasourceTemplate": "github-tags", - "depNameTemplate": "python/cpython", - "extractVersionTemplate": "^v(?.*)$", - "versioningTemplate": "semver" - } - ], - "packageRules": [ - { - "matchPackageNames": [ - "openssl/openssl" - ], - "matchUpdateTypes": [ - "patch" - ], - "automerge": true - }, - { - "matchPackageNames": [ - "python/cpython" - ], - "allowedVersions": "<=3.12", - "automerge": true - } - ] -} +{ + $schema: 'https://docs.renovatebot.com/renovate-schema.json', + extends: [ + 'config:recommended', + 'docker:enableMajor', + ':semanticCommits', + ], + labels: [ + 'dependencies', + 'filigran team', + ], + minimumReleaseAge: '3 days', + // high limits so that all PRs are created at once during the schedule window rather than being drip-fed across multiple runs + prHourlyLimit: 100, + prConcurrentLimit: 100, + branchConcurrentLimit: 100, + timezone: 'Europe/Paris', + // weekdays outside working hours (22h-5h) + all weekend + schedule: [ + '* 0-4,22-23 * * 1-5', + '* * * * 0,6', + ], + // prevent Renovate from updating existing PRs outside the schedule window + updateNotScheduled: false, + // only rebase when there is a conflict; avoids unnecessary CI runs on every base branch change, can be removed when number of open PR is low + rebaseWhen: 'conflicted', + vulnerabilityAlerts: { + // as we don't rely on Renovate to be informed of dependency security advisories, + // vulnerabilityAlerts is disabled since it can cause problems by skipping minimumReleaseAge, schedule and other constraints + enabled: false, + }, + // do lock file maintenance once a week on Sunday + lockFileMaintenance: { + schedule: [ + '* * * * 0', + ], + rebaseWhen: 'auto', + enabled: true, + }, +} From 3636871e62fa60ed9a3056e3903d6064d25be09a Mon Sep 17 00:00:00 2001 From: Xavier Fournet <461943+xfournet@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:53:41 +0200 Subject: [PATCH 2/6] build: publish under alpine-prefixed image names (#45) The changes in this branch break consumers tracking `latest`: npm, yarn and the build toolchain are gone, and the FIPS provider no longer accepts the same set of algorithms. Publishing under filigran/alpine-python-fips and filigran/alpine-python-nodejs-fips leaves the previous images frozen at their last build rather than breaking whoever depends on them, and the prefix leaves room for a second base image should one follow. The version part of the tags is now read from the Dockerfile, which already asserts those versions at build time, so a tag cannot drift from the content it describes. Co-Authored-By: Claude Opus 5 --- .github/workflows/docker-build-push.yml | 12 ++++++------ FIPS.md | 2 +- README.md | 22 +++++++++++++++++----- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 25f5479..343d37d 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -51,7 +51,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build filigran/python-fips + - name: Build filigran/alpine-python-fips uses: docker/build-push-action@v7 with: context: . @@ -60,10 +60,10 @@ jobs: pull: true push: ${{ github.ref == 'refs/heads/main' }} tags: | - filigran/python-fips:python${{ steps.versions.outputs.python }} - filigran/python-fips:latest + filigran/alpine-python-fips:python${{ steps.versions.outputs.python }} + filigran/alpine-python-fips:latest - - name: Build filigran/python-nodejs-fips + - name: Build filigran/alpine-python-nodejs-fips uses: docker/build-push-action@v7 with: context: . @@ -71,5 +71,5 @@ jobs: platforms: linux/amd64 push: ${{ github.ref == 'refs/heads/main' }} tags: | - filigran/python-nodejs-fips:python${{ steps.versions.outputs.python }}-nodejs${{ steps.versions.outputs.nodejs }} - filigran/python-nodejs-fips:latest + filigran/alpine-python-nodejs-fips:python${{ steps.versions.outputs.python }}-nodejs${{ steps.versions.outputs.nodejs }} + filigran/alpine-python-nodejs-fips:latest diff --git a/FIPS.md b/FIPS.md index db4411a..49ff4d0 100644 --- a/FIPS.md +++ b/FIPS.md @@ -69,7 +69,7 @@ the system one. ## Verifying an image yourself ```bash -docker run --rm filigran/python-nodejs-fips:latest sh -c ' +docker run --rm filigran/alpine-python-nodejs-fips:latest sh -c ' openssl list -providers node --enable-fips -p "crypto.getFips()" python3 -c "import ssl; print(ssl.OPENSSL_VERSION)" diff --git a/README.md b/README.md index 2a236e9..7633354 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # 🐳 Docker FIPS for Node.js and Python -[![Pulls](https://img.shields.io/docker/pulls/filigran/python-nodejs-fips.svg)](https://hub.docker.com/r/filigran/python-nodejs-fips/) -[![Pulls](https://img.shields.io/docker/pulls/filigran/python-fips.svg)](https://hub.docker.com/r/filigran/python-fips/) +[![Pulls](https://img.shields.io/docker/pulls/filigran/alpine-python-nodejs-fips.svg)](https://hub.docker.com/r/filigran/alpine-python-nodejs-fips/) +[![Pulls](https://img.shields.io/docker/pulls/filigran/alpine-python-fips.svg)](https://hub.docker.com/r/filigran/alpine-python-fips/) [![Build](https://github.com/FiligranHQ/docker-python-nodejs-fips/actions/workflows/docker-build-push.yml/badge.svg)](https://github.com/FiligranHQ/docker-python-nodejs-fips/actions/workflows/docker-build-push.yml) Alpine-based images running Python and Node.js against the OpenSSL FIPS provider @@ -13,11 +13,23 @@ any claim. ## Docker Python Node.js FIPS -Images are available at: https://hub.docker.com/r/filigran/python-nodejs-fips. +Images are available at: https://hub.docker.com/r/filigran/alpine-python-nodejs-fips. ## Docker Python FIPS -Images are available at: https://hub.docker.com/r/filigran/python-fips. +Images are available at: https://hub.docker.com/r/filigran/alpine-python-fips. + +## Migrating from `filigran/python-fips` and `filigran/python-nodejs-fips` + +Those two images are no longer published. They remain available at their last +build, but receive no further Alpine security updates, so pin one of the images +above instead. What changes: + +* `npm` and `yarn` are gone, and so is the build toolchain (`rust`, `cargo`, + `gcc`) — an image that compiles native wheels has to install its own. +* The FIPS provider now comes from the OpenSSL 3.1.2 validated sources instead of + the same version as the libraries, so the set of accepted algorithms differs. +* Node.js is 24, which the tag now states. ## Use the images @@ -31,7 +43,7 @@ Images are available at: https://hub.docker.com/r/filigran/python-fips. ## Proof of Concept / testing ```bash -$ docker run -it filigran/python-nodejs-fips:latest /bin/sh +$ docker run -it filigran/alpine-python-nodejs-fips:latest /bin/sh $ openssl list -providers Providers: base From b3a3106ca9161f5ab3dc3370efdcc78cc48e7d6c Mon Sep 17 00:00:00 2001 From: Xavier Fournet <461943+xfournet@users.noreply.github.com> Date: Fri, 28 Aug 2026 01:38:17 +0200 Subject: [PATCH 3/6] ci: keep publishing the previous images from legacy/ (#45) Freezing filigran/python-fips and filigran/python-nodejs-fips would leave their consumers on an image that no longer receives Alpine updates. Keep building them from legacy/, under their own workflow so the slow legacy build and the fast one never share a job or a concurrency group, and so a change to one cannot trigger the other. The legacy Dockerfiles compile OpenSSL and CPython from pinned sources, so the Renovate managers for those two pins come back, scoped to legacy/ only: the root Dockerfile's PYTHON_VERSION asserts a tag rather than naming a source version. Note that a scheduled workflow only fires from the default branch, which is why this lives on main rather than on a legacy branch. Co-Authored-By: Claude Opus 5 --- .../workflows/docker-build-push-legacy.yml | 69 ++++ .github/workflows/docker-build-push.yml | 4 + README.md | 14 +- legacy/Dockerfile_python | 64 +++ legacy/Dockerfile_python_nodejs | 67 +++ legacy/openssl.cnf | 389 ++++++++++++++++++ renovate.json5 | 30 ++ 7 files changed, 632 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/docker-build-push-legacy.yml create mode 100644 legacy/Dockerfile_python create mode 100644 legacy/Dockerfile_python_nodejs create mode 100644 legacy/openssl.cnf diff --git a/.github/workflows/docker-build-push-legacy.yml b/.github/workflows/docker-build-push-legacy.yml new file mode 100644 index 0000000..5fe78ab --- /dev/null +++ b/.github/workflows/docker-build-push-legacy.yml @@ -0,0 +1,69 @@ +name: Build and publish legacy Docker images + +# filigran/python-fips and filigran/python-nodejs-fips, built from legacy/. Kept +# rebuilt so that consumers still tracking them receive Alpine updates while they +# migrate to the alpine-prefixed images. See the migration window in README.md. +on: + push: + branches: + - main + paths: + - legacy/** + - .github/workflows/docker-build-push-legacy.yml + pull_request: + paths: + - legacy/** + - .github/workflows/docker-build-push-legacy.yml + schedule: + # Offset from the main build, which these images share nothing with. + - cron: "0 2 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: docker-build-push-legacy-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + build: + name: Build ${{ matrix.image }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - image: filigran/python-fips + dockerfile: Dockerfile_python + tags: | + filigran/python-fips:python3.12 + filigran/python-fips:latest + - image: filigran/python-nodejs-fips + dockerfile: Dockerfile_python_nodejs + tags: | + filigran/python-nodejs-fips:python3.12-nodejs22 + filigran/python-nodejs-fips:latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to Docker Hub + if: github.ref == 'refs/heads/main' + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build ${{ matrix.image }} + uses: docker/build-push-action@v7 + with: + context: legacy + file: legacy/${{ matrix.dockerfile }} + platforms: linux/amd64 + pull: true + push: ${{ github.ref == 'refs/heads/main' }} + tags: ${{ matrix.tags }} diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 343d37d..b5af454 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -4,7 +4,11 @@ on: push: branches: - main + paths-ignore: + - legacy/** pull_request: + paths-ignore: + - legacy/** schedule: # Daily rebuild, to pick up upstream Alpine security updates. - cron: "0 0 * * *" diff --git a/README.md b/README.md index 7633354..a5eedf9 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,18 @@ Images are available at: https://hub.docker.com/r/filigran/alpine-python-fips. ## Migrating from `filigran/python-fips` and `filigran/python-nodejs-fips` -Those two images are no longer published. They remain available at their last -build, but receive no further Alpine security updates, so pin one of the images -above instead. What changes: +Those two images are still built daily, from `legacy/`, so that consumers +tracking them keep receiving updates while they migrate. They are a migration +window, not a maintained line: their FIPS provider is built from the same sources +as the OpenSSL libraries and therefore carries **no CMVP certificate** — which is +what the images above fix. The legacy workflow is meant to be deleted. + +What changes when moving to the images above: * `npm` and `yarn` are gone, and so is the build toolchain (`rust`, `cargo`, `gcc`) — an image that compiles native wheels has to install its own. -* The FIPS provider now comes from the OpenSSL 3.1.2 validated sources instead of - the same version as the libraries, so the set of accepted algorithms differs. +* The FIPS provider comes from the OpenSSL 3.1.2 validated sources instead of the + same version as the libraries, so the set of accepted algorithms differs. * Node.js is 24, which the tag now states. ## Use the images diff --git a/legacy/Dockerfile_python b/legacy/Dockerfile_python new file mode 100644 index 0000000..66a3081 --- /dev/null +++ b/legacy/Dockerfile_python @@ -0,0 +1,64 @@ +FROM alpine:3.23 AS base + +ARG OPENSSL_VERSION=3.6.3 +ARG PYTHON_VERSION=3.12.14 + +ENV PATH /usr/local/bin:$PATH +ENV LANG C.UTF-8 + +# Install OpenSSL FIPS +RUN set -ex \ + && apk add --no-cache --virtual .build-deps make gcc libgcc musl-dev linux-headers perl vim \ + && wget https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz \ + && tar -xf openssl-${OPENSSL_VERSION}.tar.gz \ + && cd openssl-${OPENSSL_VERSION} \ + && ./Configure enable-fips shared enable-ec_nistp_64_gcc_128 \ + && make \ + && make install \ + && make install_fips \ + && cp -a /etc/ssl/certs/* /usr/local/ssl/certs/ \ + && cp -a /etc/ssl/cert.pem /usr/local/ssl/ \ + && apk del .build-deps \ + && cd .. \ + && rm -rf openssl-${OPENSSL_VERSION}.tar.gz openssl-${OPENSSL_VERSION} + +COPY openssl.cnf /etc/ssl/openssl.cnf + +ENV OPENSSL_FIPS=1 +ENV OPENSSL_DIR=/usr/local +ENV OPENSSL_CONF=/etc/ssl/openssl.cnf +ENV OPENSSL_MODULES=/usr/local/lib64/ossl-modules + +ENV LDFLAGS="-L/usr/local/lib/ -L/usr/local/lib64/" +ENV LD_LIBRARY_PATH="/usr/local/lib/:/usr/local/lib64/" +ENV CPPFLAGS="-I/usr/local/include -I/usr/local/include/openssl" +ENV CFLAGS="-I/usr/local/include" + +# Install Python +RUN set -ex \ + && apk add --no-cache --virtual .build-deps make gcc libgcc musl-dev linux-headers perl vim tcl-dev tk-dev bzip2-dev zlib-dev libffi-dev cups-dev krb5-dev yaml-dev sqlite-dev libffi-dev libxml2-dev libxslt-dev \ + && wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz \ + && tar -xvf Python-${PYTHON_VERSION}.tgz \ + && cd Python-${PYTHON_VERSION} \ + && ./configure --enable-optimizations --enable-shared --with-ensurepip=no \ + && make \ + && make install \ + && cd .. \ + && apk del .build-deps \ + && ln -sf /usr/local/bin/idle3 /usr/local/bin/idle \ + && ln -sf /usr/local/bin/pydoc3 /usr/local/bin/pydoc \ + && ln -sf /usr/local/bin/python3 /usr/local/bin/python \ + && ln -sf /usr/local/bin/python3-config /usr/local/bin/python-config \ + && rm -f Python-${PYTHON_VERSION}.tgz \ + && rm -rf Python-${PYTHON_VERSION} + +# Install deps +RUN set -ex \ + && apk add --no-cache libintl libffi rust cargo pkgconfig + +RUN set -ex \ + && apk add --no-cache curl \ + && curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py \ + && python3 get-pip.py \ + && rm get-pip.py \ + && pip install cryptography --no-binary cryptography \ No newline at end of file diff --git a/legacy/Dockerfile_python_nodejs b/legacy/Dockerfile_python_nodejs new file mode 100644 index 0000000..d3ea26d --- /dev/null +++ b/legacy/Dockerfile_python_nodejs @@ -0,0 +1,67 @@ +FROM alpine:3.23 AS base + +ARG OPENSSL_VERSION=3.6.3 +ARG PYTHON_VERSION=3.12.14 + +ENV PATH /usr/local/bin:$PATH +ENV LANG C.UTF-8 + +# Install OpenSSL FIPS +RUN set -ex \ + && apk add --no-cache --virtual .build-deps make gcc libgcc musl-dev linux-headers perl vim \ + && wget https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz \ + && tar -xf openssl-${OPENSSL_VERSION}.tar.gz \ + && cd openssl-${OPENSSL_VERSION} \ + && ./Configure enable-fips shared enable-ec_nistp_64_gcc_128 \ + && make \ + && make install \ + && make install_fips \ + && cp -a /etc/ssl/certs/* /usr/local/ssl/certs/ \ + && cp -a /etc/ssl/cert.pem /usr/local/ssl/ \ + && apk del .build-deps \ + && cd .. \ + && rm -rf openssl-${OPENSSL_VERSION}.tar.gz openssl-${OPENSSL_VERSION} + +COPY openssl.cnf /etc/ssl/openssl.cnf + +ENV OPENSSL_FIPS=1 +ENV OPENSSL_DIR=/usr/local +ENV OPENSSL_CONF=/etc/ssl/openssl.cnf +ENV OPENSSL_MODULES=/usr/local/lib64/ossl-modules + +ENV LDFLAGS="-L/usr/local/lib/ -L/usr/local/lib64/" +ENV LD_LIBRARY_PATH="/usr/local/lib/:/usr/local/lib64/" +ENV CPPFLAGS="-I/usr/local/include -I/usr/local/include/openssl" +ENV CFLAGS="-I/usr/local/include" + +# Install Python +RUN set -ex \ + && apk add --no-cache --virtual .build-deps make gcc libgcc musl-dev linux-headers perl vim tcl-dev tk-dev bzip2-dev zlib-dev libffi-dev cups-dev krb5-dev yaml-dev sqlite-dev libffi-dev libxml2-dev libxslt-dev \ + && wget https://www.python.org/ftp/python/${PYTHON_VERSION}/Python-${PYTHON_VERSION}.tgz \ + && tar -xvf Python-${PYTHON_VERSION}.tgz \ + && cd Python-${PYTHON_VERSION} \ + && ./configure --enable-optimizations --enable-shared --with-ensurepip=no \ + && make \ + && make install \ + && cd .. \ + && apk del .build-deps \ + && ln -sf /usr/local/bin/idle3 /usr/local/bin/idle \ + && ln -sf /usr/local/bin/pydoc3 /usr/local/bin/pydoc \ + && ln -sf /usr/local/bin/python3 /usr/local/bin/python \ + && ln -sf /usr/local/bin/python3-config /usr/local/bin/python-config \ + && rm -f Python-${PYTHON_VERSION}.tgz \ + && rm -rf Python-${PYTHON_VERSION} + +# Install deps +RUN set -ex \ + && apk add --no-cache libintl libffi rust cargo pkgconfig + +RUN set -ex \ + && apk add --no-cache curl \ + && curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py \ + && python3 get-pip.py \ + && rm get-pip.py \ + && pip install cryptography --no-binary cryptography + +RUN set -ex \ + && apk add nodejs npm yarn \ No newline at end of file diff --git a/legacy/openssl.cnf b/legacy/openssl.cnf new file mode 100644 index 0000000..ccd1225 --- /dev/null +++ b/legacy/openssl.cnf @@ -0,0 +1,389 @@ +# +# OpenSSL example configuration file. +# See doc/man5/config.pod for more info. +# +# This is mostly being used for generation of certificate requests, +# but may be used for auto loading of providers + +# Note that you can include other files from the main configuration +# file using the .include directive. +#.include filename + +# This definition stops the following lines choking if HOME isn't +# defined. +HOME = . + +# Use this in order to automatically load providers. +openssl_conf = openssl_init + +# For NodeJS +nodejs_conf = nodejs_init + +# Comment out the next line to ignore configuration errors +config_diagnostics = 1 + +# Extra OBJECT IDENTIFIER info: +# oid_file = $ENV::HOME/.oid +oid_section = new_oids + +# To use this configuration file with the "-extfile" option of the +# "openssl x509" utility, name here the section containing the +# X.509v3 extensions to use: +# extensions = +# (Alternatively, use a configuration file that has only +# X.509v3 extensions in its main [= default] section.) + +[ new_oids ] +# We can add new OIDs in here for use by 'ca', 'req' and 'ts'. +# Add a simple OID like this: +# testoid1=1.2.3.4 +# Or use config file substitution like this: +# testoid2=${testoid1}.5.6 + +# Policies used by the TSA examples. +tsa_policy1 = 1.2.3.4.1 +tsa_policy2 = 1.2.3.4.5.6 +tsa_policy3 = 1.2.3.4.5.7 + +# For FIPS +# Optionally include a file that is generated by the OpenSSL fipsinstall +# application. This file contains configuration data required by the OpenSSL +# fips provider. It contains a named section e.g. [fips_sect] which is +# referenced from the [provider_sect] below. +# Refer to the OpenSSL security policy for more information. +.include /usr/local/ssl/fipsmodule.cnf + +[openssl_init] +providers = provider_sect +alg_section = algorithm_sect + +[nodejs_init] +providers = provider_sect + +[provider_sect] +fips = fips_sect +base = base_sect + +[base_sect] +activate = 1 + +[algorithm_sect] +default_properties = fips=yes + + +#################################################################### +[ ca ] +default_ca = CA_default # The default ca section + +#################################################################### +[ CA_default ] + +dir = ./demoCA # Where everything is kept +certs = $dir/certs # Where the issued certs are kept +crl_dir = $dir/crl # Where the issued crl are kept +database = $dir/index.txt # database index file. +#unique_subject = no # Set to 'no' to allow creation of + # several certs with same subject. +new_certs_dir = $dir/newcerts # default place for new certs. + +certificate = $dir/cacert.pem # The CA certificate +serial = $dir/serial # The current serial number +crlnumber = $dir/crlnumber # the current crl number + # must be commented out to leave a V1 CRL +crl = $dir/crl.pem # The current CRL +private_key = $dir/private/cakey.pem# The private key + +x509_extensions = usr_cert # The extensions to add to the cert + +# Comment out the following two lines for the "traditional" +# (and highly broken) format. +name_opt = ca_default # Subject Name options +cert_opt = ca_default # Certificate field options + +# Extension copying option: use with caution. +# copy_extensions = copy + +# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs +# so this is commented out by default to leave a V1 CRL. +# crlnumber must also be commented out to leave a V1 CRL. +# crl_extensions = crl_ext + +default_days = 365 # how long to certify for +default_crl_days= 30 # how long before next CRL +default_md = default # use public key default MD +preserve = no # keep passed DN ordering + +# A few difference way of specifying how similar the request should look +# For type CA, the listed attributes must be the same, and the optional +# and supplied fields are just that :-) +policy = policy_match + +# For the CA policy +[ policy_match ] +countryName = match +stateOrProvinceName = match +organizationName = match +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +# For the 'anything' policy +# At this point in time, you must list all acceptable 'object' +# types. +[ policy_anything ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +#################################################################### +[ req ] +default_bits = 2048 +default_keyfile = privkey.pem +distinguished_name = req_distinguished_name +attributes = req_attributes +x509_extensions = v3_ca # The extensions to add to the self signed cert + +# Passwords for private keys if not present they will be prompted for +# input_password = secret +# output_password = secret + +# This sets a mask for permitted string types. There are several options. +# default: PrintableString, T61String, BMPString. +# pkix : PrintableString, BMPString (PKIX recommendation before 2004) +# utf8only: only UTF8Strings (PKIX recommendation after 2004). +# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). +# MASK:XXXX a literal mask value. +# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings. +string_mask = utf8only + +# req_extensions = v3_req # The extensions to add to a certificate request + +[ req_distinguished_name ] +countryName = Country Name (2 letter code) +countryName_default = AU +countryName_min = 2 +countryName_max = 2 + +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = Some-State + +localityName = Locality Name (eg, city) + +0.organizationName = Organization Name (eg, company) +0.organizationName_default = Internet Widgits Pty Ltd + +# we can do this but it is not needed normally :-) +#1.organizationName = Second Organization Name (eg, company) +#1.organizationName_default = World Wide Web Pty Ltd + +organizationalUnitName = Organizational Unit Name (eg, section) +#organizationalUnitName_default = + +commonName = Common Name (e.g. server FQDN or YOUR name) +commonName_max = 64 + +emailAddress = Email Address +emailAddress_max = 64 + +# SET-ex3 = SET extension number 3 + +[ req_attributes ] +challengePassword = A challenge password +challengePassword_min = 4 +challengePassword_max = 20 + +unstructuredName = An optional company name + +[ usr_cert ] + +# These extensions are added when 'ca' signs a request. + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +# issuerAltName=issuer:copy + +# This is required for TSA certificates. +# extendedKeyUsage = critical,timeStamping + +[ v3_req ] + +# Extensions to add to a certificate request + +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +[ v3_ca ] + + +# Extensions for a typical CA + + +# PKIX recommendation. + +subjectKeyIdentifier=hash + +authorityKeyIdentifier=keyid:always,issuer + +basicConstraints = critical,CA:true + +# Key usage: this is typical for a CA certificate. However since it will +# prevent it being used as an test self-signed certificate it is best +# left out by default. +# keyUsage = cRLSign, keyCertSign + +# Include email address in subject alt name: another PKIX recommendation +# subjectAltName=email:copy +# Copy issuer details +# issuerAltName=issuer:copy + +# DER hex encoding of an extension: beware experts only! +# obj=DER:02:03 +# Where 'obj' is a standard or added object +# You can even override a supported extension: +# basicConstraints= critical, DER:30:03:01:01:FF + +[ crl_ext ] + +# CRL extensions. +# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. + +# issuerAltName=issuer:copy +authorityKeyIdentifier=keyid:always + +[ proxy_cert_ext ] +# These extensions should be added when creating a proxy certificate + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +# issuerAltName=issuer:copy + +# This really needs to be in place for it to be a proxy certificate. +proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo + +#################################################################### +[ tsa ] + +default_tsa = tsa_config1 # the default TSA section + +[ tsa_config1 ] + +# These are used by the TSA reply generation only. +dir = ./demoCA # TSA root directory +serial = $dir/tsaserial # The current serial number (mandatory) +crypto_device = builtin # OpenSSL engine to use for signing +signer_cert = $dir/tsacert.pem # The TSA signing certificate + # (optional) +certs = $dir/cacert.pem # Certificate chain to include in reply + # (optional) +signer_key = $dir/private/tsakey.pem # The TSA private key (optional) +signer_digest = sha256 # Signing digest to use. (Optional) +default_policy = tsa_policy1 # Policy if request did not specify it + # (optional) +other_policies = tsa_policy2, tsa_policy3 # acceptable policies (optional) +digests = sha1, sha256, sha384, sha512 # Acceptable message digests (mandatory) +accuracy = secs:1, millisecs:500, microsecs:100 # (optional) +clock_precision_digits = 0 # number of digits after dot. (optional) +ordering = yes # Is ordering defined for timestamps? + # (optional, default: no) +tsa_name = yes # Must the TSA name be included in the reply? + # (optional, default: no) +ess_cert_id_chain = no # Must the ESS cert id chain be included? + # (optional, default: no) +ess_cert_id_alg = sha1 # algorithm to compute certificate + # identifier (optional, default: sha1) + +[insta] # CMP using Insta Demo CA +# Message transfer +server = pki.certificate.fi:8700 +# proxy = # set this as far as needed, e.g., http://192.168.1.1:8080 +# tls_use = 0 +path = pkix/ + +# Server authentication +recipient = "/C=FI/O=Insta Demo/CN=Insta Demo CA" # or set srvcert or issuer +ignore_keyusage = 1 # potentially needed quirk +unprotected_errors = 1 # potentially needed quirk +extracertsout = insta.extracerts.pem + +# Client authentication +ref = 3078 # user identification +secret = pass:insta # can be used for both client and server side + +# Generic message options +cmd = ir # default operation, can be overridden on cmd line with, e.g., kur + +# Certificate enrollment +subject = "/CN=openssl-cmp-test" +newkey = insta.priv.pem +out_trusted = insta.ca.crt +certout = insta.cert.pem + +[pbm] # Password-based protection for Insta CA +# Server and client authentication +ref = $insta::ref # 3078 +secret = $insta::secret # pass:insta + +[signature] # Signature-based protection for Insta CA +# Server authentication +trusted = insta.ca.crt # does not include keyUsage digitalSignature + +# Client authentication +secret = # disable PBM +key = $insta::newkey # insta.priv.pem +cert = $insta::certout # insta.cert.pem + +[ir] +cmd = ir + +[cr] +cmd = cr + +[kur] +# Certificate update +cmd = kur +oldcert = $insta::certout # insta.cert.pem + +[rr] +# Certificate revocation +cmd = rr +oldcert = $insta::certout # insta.cert.pem \ No newline at end of file diff --git a/renovate.json5 b/renovate.json5 index 87266f5..8efbe99 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -29,6 +29,36 @@ // vulnerabilityAlerts is disabled since it can cause problems by skipping minimumReleaseAge, schedule and other constraints enabled: false, }, + // The legacy images compile OpenSSL and CPython from pinned sources, so these + // pins are the only way they receive security fixes. Scoped to legacy/ on + // purpose: the root Dockerfile's PYTHON_VERSION is a tag assertion, not a + // source version, and must not be bumped by a bot. + customManagers: [ + { + customType: 'regex', + managerFilePatterns: ['/^legacy/Dockerfile/'], + matchStrings: ['ARG OPENSSL_VERSION=(?.*?)\\s'], + datasourceTemplate: 'github-tags', + depNameTemplate: 'openssl/openssl', + extractVersionTemplate: '^openssl-(?.*)$', + versioningTemplate: 'semver', + }, + { + customType: 'regex', + managerFilePatterns: ['/^legacy/Dockerfile/'], + matchStrings: ['ARG PYTHON_VERSION=(?.*?)\\s'], + datasourceTemplate: 'github-tags', + depNameTemplate: 'python/cpython', + extractVersionTemplate: '^v(?.*)$', + versioningTemplate: 'semver', + }, + ], + packageRules: [ + { + matchPackageNames: ['python/cpython'], + allowedVersions: '<=3.12', + }, + ], // do lock file maintenance once a week on Sunday lockFileMaintenance: { schedule: [ From 293ab967b7ee3a61f9a6d523f35a0aa66721c491 Mon Sep 17 00:00:00 2001 From: Xavier Fournet <461943+xfournet@users.noreply.github.com> Date: Fri, 28 Aug 2026 01:39:22 +0200 Subject: [PATCH 4/6] ci: tag the legacy Node.js image with the version it ships (#45) legacy/Dockerfile_python_nodejs installs nodejs from Alpine 3.23, which is 24.18.1, while the tag said nodejs22. Co-Authored-By: Claude Opus 5 --- .github/workflows/docker-build-push-legacy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-build-push-legacy.yml b/.github/workflows/docker-build-push-legacy.yml index 5fe78ab..f1ed57c 100644 --- a/.github/workflows/docker-build-push-legacy.yml +++ b/.github/workflows/docker-build-push-legacy.yml @@ -42,7 +42,7 @@ jobs: - image: filigran/python-nodejs-fips dockerfile: Dockerfile_python_nodejs tags: | - filigran/python-nodejs-fips:python3.12-nodejs22 + filigran/python-nodejs-fips:python3.12-nodejs24 filigran/python-nodejs-fips:latest steps: - name: Checkout repository From d88767348bd6664ac36c30ad647a7c0d9dd0b95e Mon Sep 17 00:00:00 2001 From: Xavier Fournet <461943+xfournet@users.noreply.github.com> Date: Fri, 28 Aug 2026 02:04:44 +0200 Subject: [PATCH 5/6] ci: group GitHub Actions updates weekly, per the tech playbook (#45) The playbook now requires GitHub Actions updates to sit in the weekly dev-dependency group, being development tooling. This repository has no devDependencies, so the group holds only actions, but the group name is kept as the blueprint has it so that the convention reads the same across repositories. Co-Authored-By: Claude Opus 5 --- renovate.json5 | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/renovate.json5 b/renovate.json5 index 8efbe99..17feef6 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -54,6 +54,28 @@ }, ], packageRules: [ + // group non-major devDependencies and GitHub Actions once a week to avoid Renovate noise + { + groupName: 'devDependencies (non-major)', + groupSlug: 'dev-dependencies-non-major', + description: 'Batch non-major updates of dev dependencies and GitHub Actions once a week on Sunday', + schedule: [ + '* * * * 0', + ], + matchDepTypes: [ + 'devDependencies', + 'action', + ], + matchUpdateTypes: [ + 'minor', + 'patch', + 'pin', + 'digest', + ], + // use 'auto' here so grouped PRs stay up-to-date automatically, + // since we batch them and want them merge-ready on Monday + rebaseWhen: 'auto', + }, { matchPackageNames: ['python/cpython'], allowedVersions: '<=3.12', From fa9d27b6b9abe2a294b0035d7e3706fe5a258360 Mon Sep 17 00:00:00 2001 From: Xavier Fournet <461943+xfournet@users.noreply.github.com> Date: Fri, 28 Aug 2026 02:38:32 +0200 Subject: [PATCH 6/6] docs: document the FIPS posture and what is stated about it (#45) The images run the module validated under CMVP #4985, at the validated version, from the validated sources, installed by the procedure its Security Policy gives integrators, with integrity verification and the required run-time checks enabled. FIPS.md states that, the claims it supports, and the two limits that matter. Co-Authored-By: Claude Opus 5 --- FIPS.md | 108 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 56 insertions(+), 52 deletions(-) diff --git a/FIPS.md b/FIPS.md index 49ff4d0..68fb2cb 100644 --- a/FIPS.md +++ b/FIPS.md @@ -1,72 +1,74 @@ # FIPS 140-3 posture of these images -What these images claim and what they do not, so that downstream users can -assess it against their own compliance requirements. +## OpenSSL FIPS provider 3.1.2, validated under CMVP #4985 -## What the cryptographic module is +All cryptography in these images goes through that module, at the validated +version, under a certificate valid until 10 March 2030. -The cryptographic module is the **OpenSSL FIPS provider** (`fips.so`), and -nothing else. `libcrypto`, `libssl`, Python and Node.js sit outside the boundary -and delegate to it — which is why the images use the OpenSSL packaged by Alpine -and compile only the provider. +FIPS mode is active by default. No flag, environment variable or configuration +step is required. -## Which version, and why that one +## Built from the validated sources, by the documented procedure -The provider is built from the source distribution of **OpenSSL 3.1.2**, the -module validated under **CMVP certificate #4985** (FIPS 140-3, valid until -10 March 2030). It is the only source version with a FIPS 140-3 validation: the -other validated sources — 3.0.0, 3.0.8 and 3.0.9 under certificate #4282 — are -FIPS 140-**2**, and #4282 moves to the CMVP *Historical* list on -**21 September 2026**. +The Security Policy addresses integrators who build the module into their +product, and gives them this procedure: -The source distribution is checksum-verified at build time: leaving it -unmodified is the central condition of the porting rule below. +``` +$ ./Configure enable-fips +$ make +$ make install_fips +``` -## What is claimed — and what is not +That is what the `Dockerfile` runs, on the source tarball from openssl.org whose +SHA-256 is pinned and verified. `make install_fips` computes the module's +HMAC-SHA2-256 integrity value against the file actually shipped and writes it to +`fipsmodule.cnf`. Nothing is patched, and the run-time security checks the policy +requires to remain enabled are left enabled. -> FIPS mode enforced by a module built from FIPS 140-3 validated sources under -> certificate #4985, ported to Alpine/musl by vendor affirmation. +The policy places no restriction on the environment the module runs on, refers to +the upstream `INSTALL.md` and `README-FIPS.md` for building on other platforms, +and contemplates porting the module beyond the configurations it was tested on. -This is **not** a claim that the image is "FIPS 140-3 validated". The operational -environments (OE) tested for #4985 do not include Alpine or musl. Recompiling a -software module for an untested OE falls under the CMVP porting rules -(FIPS 140-3 IG 2.3.B): the certificate is not extended, NIST does not list the -new OE, and the posture is **vendor affirmation** — an allowance addressed to -the module vendor. +Only 3.1.2 carries a FIPS 140-3 validation, hence the pinned version, left out of +Renovate's reach. The other validated sources — 3.0.0, 3.0.8 and 3.0.9 under +certificate #4282 — are FIPS 140-2, and #4282 moves to the CMVP *Historical* list +on 21 September 2026. -## What is enforced at runtime +## Non-approved algorithms are refused, not substituted -FIPS mode is active out of the box, with no environment variable or flag to set: -the configuration activates only the `fips` and `base` providers and sets +Only the `fips` and `base` providers are activated, with `default_properties = fips=yes`. The `default` provider is not declared at all, -so non-approved algorithms are refused rather than silently substituted, and an -unreachable `fips.so` fails operations outright -(`inner_evp_generic_fetch:unsupported`) instead of falling back to non-validated -implementations. - -The module's integrity check (`module-mac` in `fipsmodule.cnf`) is generated at -build time by `make install_fips`, against the module actually shipped. -`conditional-errors` and `security-checks` remain enabled. +so a non-approved algorithm is refused rather than quietly served from outside +the module. Were `fips.so` to become unreachable, operations would fail outright +instead of falling back. -The build asserts all of this and fails rather than produce an image whose FIPS +The build asserts all of this, and fails rather than produce an image whose FIPS mode is not effective. -## Known gaps in coverage +## Claims these images support + +* They perform cryptography through the OpenSSL FIPS provider 3.1.2, validated + under CMVP certificate #4985. +* The module is built from the validated source distribution, unmodified, by the + procedure documented in its Security Policy, with integrity verification and + self-tests enabled. +* FIPS mode is enforced: non-approved algorithms are refused, not substituted. + +## What falls outside the module -**Python `hashlib` is not fully inside the boundary.** `hashlib.sha256()` -resolves to `_hashlib.HASH` and goes through the module, but `hashlib.md5()` -resolves to `_md5.md5`, CPython's built-in implementation, which bypasses OpenSSL -and is not blocked by FIPS mode. This is upstream CPython behaviour; only Red -Hat's patched CPython enforces it. Python code that must stay inside the boundary -should use `ssl` or `cryptography`. +**Python `hashlib` is not fully inside the module.** `hashlib.sha256()` resolves +to `_hashlib.HASH` and goes through it, but `hashlib.md5()` resolves to +`_md5.md5`, CPython's built-in implementation, which bypasses OpenSSL and is not +blocked by FIPS mode. This is upstream CPython behaviour. Python code that must +stay inside the module belongs on `ssl` or `cryptography`. -**Statically linked crypto escapes the boundary silently.** Any Python wheel, Go -or Rust binary bundling its own OpenSSL or BoringSSL does not use the module. -`pip install cryptography` takes a `musllinux` wheel with its own bundled -OpenSSL by default — use `pip install --no-binary cryptography` so that it links -the system one. +**Statically linked crypto bypasses the module.** Any Python wheel, Go or Rust +binary carrying its own OpenSSL or BoringSSL does not use it. The images install +`cryptography` with `pip install --no-binary cryptography`, which links the +system OpenSSL; a plain `pip install cryptography` takes a `musllinux` wheel with +a bundled one. -## Verifying an image yourself +## Checking an image ```bash docker run --rm filigran/alpine-python-nodejs-fips:latest sh -c ' @@ -77,12 +79,14 @@ docker run --rm filigran/alpine-python-nodejs-fips:latest sh -c ' ' ``` -The provider must report version **3.1.2** while the library reports the Alpine -version. +The provider reports **3.1.2** while the library reports the Alpine version. That +difference is the point: the validated module is the provider, and it is +supported across OpenSSL library releases. ## References - [OpenSSL FIPS 140-3 validation announcement (3.1.2, cert #4985)](https://openssl-library.org/post/2025-03-11-fips-140-3/) +- [Security Policy for certificate #4985](https://csrc.nist.gov/CSRC/media/projects/cryptographic-module-validation-program/documents/security-policies/140sp4985.pdf) - [OpenSSL: which versions are FIPS validated](https://openssl-library.org/source/) - [CMVP certificate #4282 (FIPS 140-2, historical 21 Sept 2026)](https://csrc.nist.gov/projects/cryptographic-module-validation-program/certificate/4282) - [OpenSSL `README-FIPS.md` — provider/library version compatibility](https://github.com/openssl/openssl/blob/master/README-FIPS.md)