This is Verkada's fork of astral-sh/python-build-standalone, customized to build FIPS-enabled Python distributions for our specific target platforms.
We build 3 targets with FIPS-enabled OpenSSL:
- aarch64-apple-darwin (macOS ARM64)
- aarch64-unknown-linux-gnu (Linux ARM64)
- x86_64-unknown-linux-gnu (Linux x86_64)
Build variants:
pgo+lto- Optimized build (base for install_only artifacts)freethreaded+pgo+lto- Free-threaded builds (Python 3.13+)
Python versions: 3.10, 3.11, 3.12, 3.13, 3.14
-
Sync from upstream (if needed):
git fetch upstream git checkout main git merge upstream/main git push origin main
-
Ensure all workflows succeed on main:
- Check that vlinux workflow completed successfully
- Check that vmacos workflow completed successfully
- Verify all target/Python version combinations built
- Go to Actions > vRelease
- Click "Run workflow"
- Fill in:
- tag: Release version (e.g.,
20260105) - sha: Full commit SHA from main branch where builds succeeded
- dry-run: Check to test without publishing
- tag: Release version (e.g.,
- Click "Run workflow"
# Get the commit SHA from main
SHA=$(git rev-parse main)
# Dry run first (recommended)
gh workflow run vrelease.yml \
--repo verkada/python-build-standalone \
--ref main \
-f tag=20260105 \
-f sha=$SHA \
-f dry-run=true
# Wait for dry run to complete and verify
gh run watch
# Run actual release
gh workflow run vrelease.yml \
--repo verkada/python-build-standalone \
--ref main \
-f tag=20260105 \
-f sha=$SHA \
-f dry-run=false- Downloads all build artifacts from the commit's workflow runs
- Creates
install_only.tar.gzandinstall_only_stripped.tar.gzarchives - Creates a GitHub release with the tag
- Uploads all artifacts (full
.tar.zstand install_only.tar.gzfiles) - Generates build provenance attestations
Each Python version produces:
cpython-{version}-{target}-pgo+lto-{date}.tar.zst
cpython-{version}-{target}-install_only-{date}.tar.gz
cpython-{version}-{target}-install_only_stripped-{date}.tar.gz
Plus freethreaded variants for Python 3.13+.
We build OpenSSL with FIPS 140-2 module support to meet compliance requirements. The FIPS module is included in all builds except musl-based targets.
Files modified:
cpython-unix/build-openssl-3.5.sh- Enable FIPS during OpenSSL buildcpython-unix/build-cpython.sh- Copy FIPS modules to Python installationsrc/validation.rs- Skip FIPS module files in distribution validation
What gets built:
- OpenSSL configured with
enable-fipsflag - FIPS provider module (
fips.soon Linux,fips.dylibon macOS) - FIPS configuration file (
fipsmodule.cnf)
Where FIPS files are located in the distribution:
python/
share/
ssl/
ossl-modules/
fips.so # FIPS provider module
fipsmodule.cnf # FIPS configuration
To use FIPS mode with these distributions:
-
Set the OpenSSL configuration to load the FIPS provider:
export OPENSSL_CONF=/path/to/openssl-fips.cnf export OPENSSL_MODULES=/path/to/python/share/ssl/ossl-modules
-
Create an
openssl-fips.cnffile:[openssl_init] providers = provider_sect [provider_sect] fips = fips_sect [fips_sect] activate = 1 module = /path/to/python/share/ssl/ossl-modules/fips.so
-
Verify FIPS mode is active:
import ssl import hashlib # Should show FIPS provider loaded print(ssl.OPENSSL_VERSION)
Issue: The FIPS provider module requires the OSSL_provider_init symbol to be exported. To achieve this, we remove -fvisibility=hidden from OpenSSL compilation flags.
Impact:
- Increases OpenSSL's exported symbol surface
- Internal symbols that should be hidden become visible
- Not a security vulnerability, but reduces defense in depth
Code Review Note: This is an acceptable trade-off for FIPS compliance, but be aware that it differs from standard OpenSSL builds.
FIPS module is only available on glibc-based Linux and macOS. Not available for musl-based targets because:
- OpenSSL FIPS requires async support (
no-asyncflag needed for musl) - musl lacks atomic primitives required by FIPS module
- FIPS module fundamentally incompatible with musl
Important: Building with enable-fips creates a FIPS-capable distribution, but does not guarantee FIPS 140-2/140-3 compliance. Full compliance requires:
- Formal validation by NIST/CMVP
- Specific runtime configuration
- Approved cryptographic operations only
- Security policy adherence
Consult your security team for FIPS compliance requirements.
Triggers: Push to main, Pull requests
Runners:
- x86_64:
namespace-profile-ubuntu-22-04-amd64-x86-64-large-caching - aarch64:
namespace-profile-ubuntu-22-04-amd64-arm-large-caching(native ARM64)
Jobs:
- crate-build - Builds the pythonbuild Rust tool on both architectures
- image - Builds Docker images for build environments:
build,build.cross,gcc(x86_64)build.debian9,gcc.debian9(aarch64)
- build - Builds Python distributions for both targets
Key Features:
- Native ARM64 builds (no cross-compilation)
- Docker layer caching via GitHub Container Registry
- Download caching for build dependencies
- Build provenance attestations on main branch
- Distribution validation with runtime tests
Build Matrix:
- 2 targets × 5 Python versions × 2 build options = 20 builds
- Total with freethreaded: 24 builds
Triggers: Push to main, Pull requests
Runner: namespace-profile-mac-small-tahoe (native ARM64)
Jobs:
- crate-build - Builds the pythonbuild Rust tool
- build - Builds Python distributions for macOS
Key Features:
- Native aarch64 builds
- macOS SDK validation
- Build provenance attestations
- Distribution validation with runtime tests
Build Matrix:
- 1 target × 5 Python versions × 2 build options = 10 builds
- Total with freethreaded: 12 builds
Trigger: Manual (workflow_dispatch)
Runner: ubuntu-latest
Inputs:
- tag: Release tag (e.g.,
20260105) - sha: Commit SHA to release (must have successful vlinux/vmacos runs)
- dry-run: Boolean to test without publishing
What it does:
- Fetches build artifacts from the specified commit's workflow runs
- Creates
install_onlyarchives (removes static libs, test modules) - Creates
install_only_strippedarchives (also removes debug symbols) - Creates a GitHub release with the tag
- Uploads all artifacts to the release
- Generates build provenance attestations
Important: The release workflow uses the just build automation tool and the pythonbuild Rust CLI to orchestrate the release process.
We created vlinux.yml, vmacos.yml, and vrelease.yml as separate workflows instead of modifying the upstream workflows:
Benefits:
- Won't conflict with upstream when syncing
- Clear separation of Verkada-specific configs
- Easier to maintain and understand
Trade-off:
- Upstream workflows still exist (disabled via
workflow_dispatchonly) - Duplicate code between upstream and Verkada workflows
Instead of using ci-matrix.py with ci-targets.yaml, we hardcode the build matrices in the workflow files:
Benefits:
- No dependency on upstream config files
- Self-contained and easier to understand
- Won't break when upstream changes ci-targets.yaml
Trade-off:
- Adding new Python versions requires workflow updates
- Less flexible than dynamic matrix generation
We use Verkada's namespace runners instead of GitHub-hosted runners:
Benefits:
- Native ARM64 builds (faster, more reliable than cross-compilation)
- Larger disk space (avoid "no space left" errors)
- Caching support for better performance
- Cost control within Verkada infrastructure
Trade-off:
- Requires namespace runner infrastructure
- Less portable than GitHub-hosted runners
CRITICAL: Always create PRs against verkada/python-build-standalone, NOT the upstream repo.
# Correct - targets our fork
gh pr create --repo verkada/python-build-standalone --title "Title" --body "Body"
# WRONG - would target upstream astral-sh repo
gh pr create --title "Title" --body "Body"Since this is a fork, the default gh pr create may target the upstream repository. Always explicitly specify --repo verkada/python-build-standalone.
When syncing from upstream, these files will conflict:
| File | Why |
|---|---|
src/release.rs |
We removed most targets |
src/validation.rs |
Added FIPS file skipping |
cpython-unix/build-*.sh |
Added FIPS support |
.github/workflows/*.yml |
Modified upstream workflows |
Resolution strategy:
- Keep our FIPS changes in build scripts
- Keep our target reduction in release.rs
- Keep our workflow trigger changes
- Merge other upstream changes normally
The v* workflow files won't conflict (they're unique to our fork).
If you see ImageNotFound errors, check:
- Image job completed successfully
- Image artifacts were uploaded
- Build job downloaded the artifacts
- Docker images were loaded (check debug output)
Known issue: Docker Buildx with containerd snapshotter creates different image IDs. Our workflows capture the actual loaded ID from docker load output to fix this.
If OpenSSL FIPS builds fail:
- Check that target is glibc-based (not musl)
- Verify
enable-fipsandinstall_fipsare in build commands - Check that
fipsmodule.cnfwas generated
If builds fail with runner errors:
- Verify runner names in namespace configuration
- Check that runners have Docker installed
- Ensure runners have sufficient disk space (50GB+ recommended)
- Upstream repo: https://github.com/astral-sh/python-build-standalone
- Our fork: https://github.com/verkada/python-build-standalone
- OpenSSL FIPS: https://www.openssl.org/docs/fips.html
- Python release schedule: https://peps.python.org/pep-0719/