Thin, hardened, version-pinned images for CI jobs and tooling: built from pinned inputs on Docker Hardened Images, scanned before and after publishing, signed, and published to GHCR and Docker Hub from the same digest.
- Overview
- Quick start
- Images
- Choosing an image
- Tags, versions and support
- Usage
- Configuration reference
- Adding an organisation's CA bundle
- Pinning by digest or date tag
- Security and supply chain
- Changing the base image
- Troubleshooting
- Developing
- Contributing
- Licence
Five images, each a thin layer of pinned tools on a hardened base:
python: CPython and uv, with one tag for every supported CPython from 3.12 upwards.azure-cli: the Azure CLI in its own virtual environment, andldo-cli. The current release, plus 2.72.0 as a long-lived line.terraform: tenv with a default Terraform, TFLint, terraform-docs and Conftest. A repository's.terraform-versionwins over the default. Also as an Alpine variant (-alpinetags), about a third smaller.powershell: PowerShell LTS with PSScriptAnalyzer, Pester, Az and LibreDevOpsHelpers.toolbox: every tool above in one image, assembled from the same build stages.
What every image guarantees:
| Property | How |
|---|---|
| Hardened base | Docker Hardened Images on Debian 13 (Alpine 3.24 for -alpine tags), pinned by digest |
| No known fixable vulnerabilities at publication | Scanned on every pull request, at release (the exact digest pushed) and daily after; fixes are applied at the source, not excused |
| Every input pinned | Bases by digest; downloads by version and SHA-256 per platform; Go modules through the Go checksum database; Python packages by hash |
| Reproducible references | Date tags are never overwritten; every tag has the same digest in GHCR and Docker Hub |
| Verifiable | cosign keyless signature, SBOM and SLSA provenance on every published digest |
| Non-root | Runs as app (uid 10001) in /work; works rootless under Podman, and as an arbitrary uid in group 0 |
| Multi-platform | linux/amd64 and linux/arm64, each built natively |
| Portable | The same build runs on RHEL (UBI), Wolfi and Alpine bases, proven on every pull request |
| Current | Rebuilt every Monday, on every merge, and whenever a published image can be fixed by a rebuild |
# Pull from either registry; the digest is the same.
docker pull ghcr.io/libre-devops/terraform:1
docker pull docker.io/libredevops/terraform:1
# Run against the current directory.
docker run --rm -v "$PWD:/work" ghcr.io/libre-devops/terraform:1 terraform fmt -check
# Verify the signature before you trust it.
cosign verify ghcr.io/libre-devops/terraform:1 \
--certificate-identity https://github.com/libre-devops/container-images/.github/workflows/release.yml@refs/heads/main \
--certificate-oidc-issuer https://token.actions.githubusercontent.com| Image | Tags | Base | Contents | Pull |
|---|---|---|---|---|
python |
3.123.12-YYYYMMDD |
dhi.io/python:3.12-debian13-dev |
CPython 3.12, uv 0.13.7 | ghcr.io/libre-devops/python:3.12docker.io/libredevops/python:3.12 |
python |
3.133.13-YYYYMMDD |
dhi.io/python:3.13-debian13-dev |
CPython 3.13, uv 0.13.7 | ghcr.io/libre-devops/python:3.13docker.io/libredevops/python:3.13 |
python |
3.143.14-YYYYMMDD |
dhi.io/python:3.14-debian13-dev |
CPython 3.14, uv 0.13.7 | ghcr.io/libre-devops/python:3.14docker.io/libredevops/python:3.14 |
python |
3.15 3 latest3.15-YYYYMMDD |
dhi.io/python:3.15-debian13-dev |
CPython 3.15, uv 0.13.7 | ghcr.io/libre-devops/python:3.15docker.io/libredevops/python:3.15 |
azure-cli |
2.92.0 latest2.92.0-py3.12-YYYYMMDD |
dhi.io/python:3.12-debian13-dev |
Azure CLI 2.92.0 on Python 3.12, ldo-cli 0.8.1 | ghcr.io/libre-devops/azure-cli:2.92.0docker.io/libredevops/azure-cli:2.92.0 |
azure-cli |
2.72.02.72.0-py3.12-YYYYMMDD |
dhi.io/python:3.12-debian13-dev |
Azure CLI 2.72.0 on Python 3.12, ldo-cli 0.8.1 | ghcr.io/libre-devops/azure-cli:2.72.0docker.io/libredevops/azure-cli:2.72.0 |
terraform |
1.17.1 1.17 1 latest1.17.1-YYYYMMDD |
dhi.io/debian-base:trixie-debian13-dev |
Terraform 1.17.1 by default (tenv 4.15.1; a repository's .terraform-version wins), TFLint 0.65.0, terraform-docs 0.24.0, Conftest 0.71.1 | ghcr.io/libre-devops/terraform:1.17.1docker.io/libredevops/terraform:1.17.1 |
terraform |
1.17.1-alpine 1.17-alpine 1-alpine alpine1.17.1-alpine-YYYYMMDD |
dhi.io/alpine-base:3.24-alpine3.24-dev |
Terraform 1.17.1 by default (tenv 4.15.1; a repository's .terraform-version wins), TFLint 0.65.0, terraform-docs 0.24.0, Conftest 0.71.1 | ghcr.io/libre-devops/terraform:1.17.1-alpinedocker.io/libredevops/terraform:1.17.1-alpine |
powershell |
7.6.9 7.6 7 lts latest7.6.9-YYYYMMDD |
dhi.io/debian-base:trixie-debian13-dev |
PowerShell 7.6.9, Az 16.5.0, Pester 6.2.0, PSScriptAnalyzer 1.25.0, LibreDevOpsHelpers 3.0.1 | ghcr.io/libre-devops/powershell:7.6.9docker.io/libredevops/powershell:7.6.9 |
toolbox |
1 latest1-YYYYMMDD |
dhi.io/python:3.12-debian13-dev |
Python 3.12 and uv 0.13.7, Azure CLI 2.92.0, ldo-cli 0.8.1, Terraform 1.17.1 by default (tenv 4.15.1), TFLint 0.65.0, terraform-docs 0.24.0, Conftest 0.71.1, PowerShell 7.6.9 with Az 16.5.0, Pester 6.2.0, PSScriptAnalyzer 1.25.0 and LibreDevOpsHelpers 3.0.1 | ghcr.io/libre-devops/toolbox:1docker.io/libredevops/toolbox:1 |
Every image also has bash, git, curl, ca-certificates, ShellCheck 0.11.0, shfmt 3.14.1, jq 1.8.2, runs as app (uid 10001) in /work, and is a multi-platform image for linux/amd64 and linux/arm64. The Go tools are built from source with Go 1.27.3.
| If you need | Use | Why |
|---|---|---|
| One tool family in a CI job | python, azure-cli, terraform or powershell |
Smaller pulls, smaller attack surface, fewer reasons to rebuild |
| Several tool families in one job, or an interactive shell | toolbox |
Everything together; the largest image (the Az PowerShell modules and the Azure CLI dominate its size) |
| A specific CPython | python:<major.minor> |
One tag per supported CPython |
| The smallest Terraform image | terraform:alpine |
About a third smaller than the Debian image. musl, so not for Azure Pipelines container jobs, which need glibc |
| The Azure CLI behind TLS inspection that rejects Python 3.13's stricter checks | azure-cli:2.72.0 |
Long-lived line on Python 3.12 |
| Your organisation's CA, mirrors or internal tools | A thin layer FROM a pinned digest of one of these |
See Adding an organisation's CA bundle |
| Tag | Example | Moves? |
|---|---|---|
| Version and build date | python:3.12-20261107, azure-cli:2.72.0-py3.12-20261107 |
Never. A second build on the same day gets .2, .3 and so on |
| Version | python:3.12, azure-cli:2.72.0 |
Yes, to the newest build of that version |
| Major or major.minor | terraform:1, terraform:1.17, python:3, toolbox:1 |
Yes |
| Alias | powershell:lts |
Yes |
| Flavour | terraform:1.17.1-alpine, terraform:1-alpine, terraform:alpine |
As above, with the flavour as a suffix; the flavour's own name is its newest version |
latest |
python:latest |
Yes, to the newest supported version |
Every release records its digests in this README and in
releases.json.
What is built. Every version listed in versions.yml:
| Image | Versions |
|---|---|
python |
Every CPython release from 3.12 upwards that upstream still supports |
azure-cli |
The current release, and 2.72.0 as a long-lived line for networks whose TLS inspection fails the stricter certificate checks in Python 3.13 and later |
terraform |
The current Terraform 1.x as the default, on Debian and on Alpine; any other version through .terraform-version |
powershell |
The current PowerShell LTS |
toolbox |
The latest version of each of the above, with the Azure CLI's Python |
Support. A version is supported while it is listed in versions.yml: its moving tags are
rebuilt weekly and whenever a fix is available. When a version leaves versions.yml (for
example when upstream support ends), its tags stop moving and receive no further fixes, but
nothing published is ever deleted or re-tagged, so pinned references keep working.
The examples use moving tags for brevity. For anything you need to reproduce, pin a date tag or a digest (see Pinning by digest or date tag).
As a container job. The runner mounts the workspace and HOME as uid 1001, so run the job as
that uid in group 0; the images' home, work and tool directories are writable by group 0.
jobs:
plan:
runs-on: ubuntu-24.04
container:
image: ghcr.io/libre-devops/terraform:1
options: --user 1001:0
steps:
- uses: actions/checkout@v7
- run: terraform init -backend=false && terraform validate
- run: tflint --init && tflinttest:
image: ghcr.io/libre-devops/python:3.13
script:
- uv sync --locked
- uv run pytestThe Azure Pipelines agent adds its own user inside a container job, which needs root, so start
the container as root. useradd and groupadd are in every image for this.
resources:
containers:
- container: azure
image: ghcr.io/libre-devops/azure-cli:2.92.0
options: --user 0:0
jobs:
- job: deploy
container: azure
steps:
- script: az versionOr run a single command with docker run, as your own uid so that files written to the
workspace stay yours:
steps:
- script: >-
docker run --rm --user "$(id -u):0" -v "$(Build.SourcesDirectory):/work"
ghcr.io/libre-devops/powershell:lts pwsh -File ./build.ps1--userns=keep-id maps your user to the image's uid 10001, so files in the mounted directory
are owned by you on both sides:
podman run --rm -it --userns=keep-id:uid=10001,gid=10001 \
-v "$PWD:/work:Z" ghcr.io/libre-devops/terraform:1 terraform fmt -checkThe images run as a non-root numeric uid, so runAsNonRoot: true is satisfied as is. An
arbitrary uid (OpenShift's default) works too, because the writable directories belong to
group 0:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]| Path | Purpose | Writable by |
|---|---|---|
/home/app |
HOME for every user the image runs as |
app, and any uid in group 0 |
/work |
Working directory; mount your project here | app, and any uid in group 0 |
/opt/az |
The Azure CLI's virtual environment (azure-cli, toolbox) |
root |
/opt/ldo-cli |
ldo-cli's virtual environment (azure-cli, toolbox) |
root |
/opt/microsoft/powershell/7 |
PowerShell (powershell, toolbox) |
root |
/usr/local/share/powershell/Modules |
Pinned PowerShell modules, for all users | root |
/home/app/.tenv |
tenv's Terraform installs (TENV_ROOT) |
app, and any uid in group 0 |
Set by the images; override any of them with -e or your CI system's environment settings.
| Variable | Value | Image | Effect |
|---|---|---|---|
HOME |
/home/app |
all | Consistent home for any uid |
PYTHONUNBUFFERED |
1 |
python, azure-cli, toolbox |
Unbuffered output in CI logs |
AZURE_CORE_COLLECT_TELEMETRY |
no |
azure-cli, toolbox |
No Azure CLI telemetry |
TENV_ROOT |
/home/app/.tenv |
terraform, toolbox |
Where tenv keeps Terraform versions |
TENV_AUTO_INSTALL |
true |
terraform, toolbox |
A .terraform-version installs on demand; set false offline |
CHECKPOINT_DISABLE |
1 |
terraform, toolbox |
No HashiCorp update checks |
POWERSHELL_TELEMETRY_OPTOUT |
1 |
powershell, toolbox |
No PowerShell telemetry |
POWERSHELL_UPDATECHECK |
Off |
powershell, toolbox |
No update notices |
DOTNET_CLI_TELEMETRY_OPTOUT, DOTNET_NOLOGO |
1 |
powershell, toolbox |
No .NET telemetry or banners |
git trusts every directory (safe.directory = * in the system configuration), because CI
systems mount workspaces owned by other uids.
Networks with TLS inspection need their root CA in the image. Build a thin layer FROM a
pinned digest of these images, add the certificate, and switch back to the app user:
FROM ghcr.io/libre-devops/azure-cli:2.92.0@sha256:<digest>
USER root
COPY organisation-root-ca.crt /usr/local/share/ca-certificates/organisation-root-ca.crt
RUN update-ca-certificates
USER 10001:10001That covers curl, git, Terraform and the Go tools, PowerShell and .NET, and Python's ssl
module. Some tools carry their own bundle and need pointing at the system one:
| Tool | Setting |
|---|---|
Azure CLI, Python requests |
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt |
| pip | PIP_CERT=/etc/ssl/certs/ca-certificates.crt |
| uv | UV_NATIVE_TLS=true |
On a RHEL base the certificate goes in /etc/pki/ca-trust/source/anchors/, the command is
update-ca-trust, and the bundle is /etc/pki/tls/certs/ca-bundle.crt.
Moving tags (3.12, 1, latest) follow each rebuild. If you cannot refresh a tag you
already pulled, or need a build to be reproducible, pin a date tag, which is never
overwritten, or a digest, which is the same in GHCR and Docker Hub:
ghcr.io/libre-devops/python:3.12-20261107
docker.io/libredevops/python@sha256:<digest>
Renovate and Dependabot both update digest pins in your own repositories.
The newest digest of every version, as of the last release:
| Image | Version | Newest date tag | Digest | Released |
|---|---|---|---|---|
python |
3.12 |
3.12-20261107.3 |
sha256:f555821b9a3babc68de6f99b5e934585e8a7d9df2b88cd2e59cc3ec6aa2818eb |
2026-11-07 |
python |
3.13 |
3.13-20261107.3 |
sha256:207f4af99756c2c17c8030b3737d29db060d01f2136f5c865fb61eaefc349eb7 |
2026-11-07 |
python |
3.14 |
3.14-20261107.3 |
sha256:9f175fd369e00defef21b3f11d0542b490cb62a6e807383f7601508f183f7497 |
2026-11-07 |
python |
3.15 |
3.15-20261107.3 |
sha256:7f770a1d81d75accd566030297accea7162373ec8ee977700b90efba1737ac7b |
2026-11-07 |
azure-cli |
2.92.0 |
2.92.0-py3.12-20261107.3 |
sha256:8eccedf3cc48b847459e9ce16b4f57ccb769e75215d675341a9ea31f11089497 |
2026-11-07 |
azure-cli |
2.72.0 |
2.72.0-py3.12-20261107.3 |
sha256:8fbd8cc8c5b15f57e6769b6adb80e6b249462db2d416607bed6a1473c9985978 |
2026-11-07 |
terraform |
1.17.1 |
1.17.1-20261107.2 |
sha256:7bcb52b1aa98955459c09db0f657beb970fc863626a8d6da33401ed3f18a07e4 |
2026-11-07 |
terraform |
1.17.1-alpine |
1.17.1-alpine-20261107.2 |
sha256:26eb825c197b1a4bdca134a505ae9d11e5634462efeef39561846cad9282bf6a |
2026-11-07 |
powershell |
7.6.9 |
7.6.9-20261107.3 |
sha256:fe7a6b84036bd629906ea07c8228f4ac09431c4e1bba131df8274e76994fc73e |
2026-11-07 |
toolbox |
1 |
1-20261107.3 |
sha256:95fcf9a6a2b533802eb327576deedc34843ac62b7c5c437e56f877b52503c13b |
2026-11-07 |
Security problems are found as early as possible and fixed at the source.
flowchart LR
subgraph pr["Every pull request"]
check["Validate pins, lint, zizmor, actionlint, Trivy repository scan"] --> build["Build every version, amd64 and arm64"]
build --> test["Smoke tests"] --> gate["Trivy gate"]
check --> port["Build on UBI, Wolfi and Alpine bases"]
end
subgraph release["Release: merges, Mondays, on demand"]
push["Build once per platform, push by digest"] --> verify["Smoke test and scan that digest"]
verify --> tag["Tag GHCR and Docker Hub from one manifest"] --> sign["Check digests match, sign with cosign"]
end
subgraph daily["Every day"]
pins["Pins: base digests, OSV patches, Python locks"] --> prs["Pull request, built and tested"]
scan["Scan every published image"] --> tab["Security tab"]
end
gate --> push
prs --> push
scan -- "fails the gate" --> push
- Before anything is built. Every pull request lints the workflows for security problems with zizmor (template injection, excessive permissions, persisted credentials, unpinned actions) and for correctness with actionlint, scans the repository for secrets and misconfigurations with Trivy, and lints the Containerfile and every script.
- Least privilege in CI. Every workflow starts from
permissions: {}and grants each job only what it needs. Every action is pinned by commit SHA. No checkout persists credentials. - Go tools built from source. shfmt, tenv, TFLint, terraform-docs and Conftest are built
with the pinned, current Go rather than downloaded, so they carry no old toolchain's
vulnerabilities.
just lockchecks every dependency of each against OSV and records the fixed version of any that is vulnerable; the build raises it, never lowering anything. Sources and dependencies are verified against the Go checksum database. - Python dependencies hash-locked and patched. The Azure CLI and ldo-cli install from
locks in
images/locks/that pin every package by version and SHA-256, with any package OSV knows to be vulnerable raised to a fixed version. - Scanning before publishing. Trivy scans every image on every pull request and scans the exact pushed digest on every release. A critical finding, or a high one with a fix available, fails the build and nothing is tagged.
- Scanning after publishing. Every day, the Scan workflow rescans every published image, reports every finding in the repository's Security tab, and starts a rebuild if a published image fails the gate.
- Fixes as soon as they exist. Every day, the Pins workflow re-resolves the base images, re-audits every pinned dependency and opens a pull request with any fix. Renovate raises security updates immediately, outside its weekly schedule.
A finding is resolved by removing it: a newer base, a patched dependency, or a newer tool. Where a scanner reports something that does not apply, it is set aside only by a VEX statement that says why:
| Source | What it covers |
|---|---|
| The base image's publisher | Docker Hardened Images attach signed OpenVEX statements to every image, recording packages they have patched in place or that are not affected. The scan applies the statements attached to the pinned base digest, and no others. |
This repository, vex/libre-devops.openvex.json |
Findings examined here and shown not to apply, each with its justification. Reviewed in pull requests like any other change. |
Time-boxed exceptions, if ever needed, go in .trivyignore.yaml with a reason and an expiry
date, after which the finding fails the gate again. There are none.
Every published digest is signed by the Release workflow on main, in both registries, and
carries an SBOM and SLSA provenance as attestations. The signature covers the image index, and
with it the attestations it references.
# Signature
cosign verify ghcr.io/libre-devops/python:3.13 \
--certificate-identity https://github.com/libre-devops/container-images/.github/workflows/release.yml@refs/heads/main \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
# SBOM and provenance
docker buildx imagetools inspect ghcr.io/libre-devops/python:3.13 --format '{{ json .SBOM }}'
docker buildx imagetools inspect ghcr.io/libre-devops/python:3.13 --format '{{ json .Provenance }}'
# The same digest in both registries
for r in ghcr.io/libre-devops docker.io/libredevops; do
docker buildx imagetools inspect "$r/python:3.13" --format '{{ json .Manifest }}' | jq -r .digest
doneTo report a vulnerability, see SECURITY.md.
The published images build on Docker Hardened Images, but nothing in the build depends on that. A base needs only:
- glibc or musl. On musl (Alpine), uv and PowerShell use their musl builds and everything
else is a static binary, so every image builds, with one exception: Microsoft publishes no
musl build of PowerShell for arm64, so
powershellandtoolboxare amd64 only on musl, - a POSIX shell at
/bin/sh, - one of
apt-get,dnf,microdnforapk.
Every image is a target in one multi-stage images/Containerfile.
Package installs go through images/common/os-packages.sh, which maps logical names to each
package manager's; the user is created by writing the account files directly; tools are pinned
downloads or built from source; and if the base lacks the requested CPython, uv installs it.
The CPython uv installs comes without the pip it bundles, whose vendored libraries lag behind
their security fixes: use uv pip, and virtual environments get a current pip of their own.
To build on another base locally, name a key from versions.yml or give a full reference:
just test terraform "" rhel-minimal
just build python 3.13 registry.access.redhat.com/ubi10/ubi:10.1@sha256:<digest>To publish on another base, change the entries under bases in versions.yml and run
just pin. To publish an extra variant on another base, as the -alpine tags are, add a
version line with a flavour (see versions.yml). The portability bases (UBI 10, UBI 10
minimal, Wolfi and Docker Hardened Images Alpine) are built and smoke tested on every pull
request, so either edit stays safe. If a hardened variant you need does not exist,
docker.io/library/debian:13-slim, pinned by digest, is a drop-in fallback.
| Symptom | Cause | Fix |
|---|---|---|
Permission denied writing to the workspace in a GitHub Actions container job |
The runner's workspace belongs to uid 1001 | Add options: --user 1001:0 to the container |
| Files created in a mounted directory are owned by uid 10001 on the host | The container ran as app |
docker run --user "$(id -u):0", or podman run --userns=keep-id:uid=10001,gid=10001 |
certificate verify failed or self-signed certificate in certificate chain |
TLS inspection with a CA the image does not trust | Add your organisation's CA bundle |
A .terraform-version fails to install in a network without internet access |
tenv downloads it from HashiCorp | Use the default Terraform with TENV_AUTO_INSTALL=false, or point tenv at an internal mirror |
toomanyrequests pulling from Docker Hub |
Docker Hub's anonymous pull limits | Pull the same digest from ghcr.io/libre-devops |
| A container job in Azure Pipelines fails before the first step | The agent could not add its user | Start the container as root: options: --user 0:0 |
An -alpine image fails as an Azure Pipelines container job |
The agent's Node.js needs glibc, and Alpine uses musl | Use the Debian tags (the ones without -alpine) |
Import-Module Az is slow |
Az loads every service module | Import only what you use, for example Import-Module Az.Accounts, Az.Resources |
You need Docker or Podman, just and uv.
just tools installs the pinned linters and Trivy into .tools.
| Recipe | Does |
|---|---|
just build <image> [version] [base] |
Build for this machine, with CI's build arguments (a flavour is a version: just build terraform 1.17.1-alpine) |
just test <image> [version] [base] |
Build, then run the smoke tests |
just scan <image> [version] [base] |
Build, then scan with the release gate |
just docs / just docs-check |
Write README.md, or fail if it is out of date |
just lock |
Pin new downloads and patch known vulnerabilities |
just pin |
Re-resolve every base image tag to its current digest |
just validate |
Check versions.yml, the lock and the base pins |
just lint |
hadolint, ShellCheck, shfmt, actionlint, zizmor, Trivy and the text rules |
just snapshot <dir> |
Record every package and tag in both registries |
| Workflow | Runs on | Does | Publishes |
|---|---|---|---|
| Build | Every pull request; started by Pins and Release | Check, build every version for both platforms, smoke test, scan; portability builds | No |
| Release | Merges to main, Mondays 03:00 UTC, on demand, and Scan |
Build once per platform, test and scan the pushed digest, tag both registries, sign, open a pull request with the new digests | Yes |
| Pins | Pull requests that change versions.yml; daily 02:00 UTC |
Repin and patch on the branch and re-run Build; daily, refresh base digests and patches as a pull request | No |
| Scan | Daily 06:00 UTC | Rescan every published image, report to the Security tab, rebuild if needed | No |
Add one line under the image's versions in versions.yml (and, for a new CPython, a line
under bases), then run just lock pin docs, or push the branch and let the Pins workflow do
it. The CI matrix, tags, tests and README follow from versions.yml.
| Pin | Lives in | Kept current by |
|---|---|---|
| Tool and image versions | versions.yml |
Renovate: weekly, and at once for a security fix |
| SHA-256 of every download, per platform | versions.lock.json |
The Pins workflow, on every pull request branch in this repository that changes versions.yml |
| Fixed versions of vulnerable Go dependencies | versions.lock.json |
The Pins workflow, on those branches and every day |
| Hash-locked Python dependencies | images/locks/ |
The Pins workflow, on those branches and every day |
| Base image digests | versions.yml |
The Pins workflow, every day, as a pull request |
| GitHub Actions | the workflows, by commit SHA | Renovate |
A download pin, once written, is never re-fetched: an upstream asset that changes after it was
pinned fails the build rather than being trusted. Vulnerability patches, by contrast, are
recomputed on every run, so a newly published advisory is acted on the next day. Because a
push made with GITHUB_TOKEN does not trigger workflows, the Pins workflow starts the Build
workflow on the commit it pushes.
versions.yml The single source: images, versions, tags, bases
versions.lock.json Generated: download SHA-256s, Go dependency fixes, Python locks
releases.json Generated by releases: the newest digest of every version
images/Containerfile One multi-stage build; every image is a target
images/common/ Distribution-neutral build steps shared by every image
images/<image>/ An image's own install steps
images/locks/ Generated: hash-locked Python requirements
vex/ VEX statements for findings shown not to apply
tests/ Smoke tests, run in every image as the app user
scripts/ Matrix, tags, pins, VEX and docs (images.py), and CI helpers
.github/workflows/ Build, Release, Pins and Scan
Contributions are welcome. See CONTRIBUTING.md for the workflow and conventions, and SECURITY.md to report a vulnerability privately.