Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions .github/dependabot.yml

This file was deleted.

69 changes: 69 additions & 0 deletions .github/workflows/docker-build-push-legacy.yml
Original file line number Diff line number Diff line change
@@ -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-nodejs24
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 }}
63 changes: 41 additions & 22 deletions .github/workflows/docker-build-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +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 * * *"
Expand All @@ -12,49 +17,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/alpine-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/alpine-python-fips:python${{ steps.versions.outputs.python }}
filigran/alpine-python-fips:latest

- name: Build filigran/alpine-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/alpine-python-nodejs-fips:python${{ steps.versions.outputs.python }}-nodejs${{ steps.versions.outputs.nodejs }}
filigran/alpine-python-nodejs-fips:latest
93 changes: 93 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
92 changes: 92 additions & 0 deletions FIPS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# FIPS 140-3 posture of these images

## OpenSSL FIPS provider 3.1.2, validated under CMVP #4985

All cryptography in these images goes through that module, at the validated
version, under a certificate valid until 10 March 2030.

FIPS mode is active by default. No flag, environment variable or configuration
step is required.

## Built from the validated sources, by the documented procedure

The Security Policy addresses integrators who build the module into their
product, and gives them this procedure:

```
$ ./Configure enable-fips
$ make
$ make install_fips
```

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.

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.

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.

## Non-approved algorithms are refused, not substituted

Only the `fips` and `base` providers are activated, with
`default_properties = fips=yes`. The `default` provider is not declared at all,
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
mode is not effective.

## 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 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 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.

## Checking an image

```bash
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)"
echo test | openssl dgst -md5 || echo "MD5 refused, as expected"
'
```

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)
Loading