Skip to content

feat(s130): the hardened headless image and the evidence about it - #117

Merged
HackTuah merged 6 commits into
mainfrom
slice/130-image-hardening-iron-bank
Oct 8, 2026
Merged

HackTuah merged 6 commits into
mainfrom
slice/130-image-hardening-iron-bank

Conversation

@HackTuah

@HackTuah HackTuah commented Oct 8, 2026

Copy link
Copy Markdown
Member

Slice 130's hardening half. The slice stays in_progress: release composition (dropping the neural group, the Hex-tree and cargo vendor resources) waits on slice 133, and AC7 is the owner's submission review.

What lands

  • ci/ironbank/, the repo1 project root:
    • Dockerfile: OTP from source with FIPS on UBI9, the release copied onto UBI9 micro. The base comes through ARG BASE_*, every input is a manifest resource with its digest, there is no network except dnf, it runs as UID 10001, and Erlang distribution is off with no baked cookie.
    • hardening_manifest.yaml, LICENSE, README.md, scripts/entrypoint.sh.
  • ci/headless/: digest pins, justifications.yaml, image_allowances.yaml, stig_dispositions.yaml and submission_holds.yaml (three holds, each with an owner, a date and a lift condition). The slice 061 Debian Containerfile is removed, so there is one recipe.
  • Four tasks: trinity.ironbank.lint, trinity.image.inspect, trinity.image.findings and trinity.image.stig.
  • Scripts: scripts/headless_image.sh and scripts/stig_scan.sh.
  • The headless-image workflow: on a path filter, nightly, and by hand.
  • Docs: docs/regulated/headless-image.md and stig-applicability.md.

Criteria

AC State
AC1 non-root, asserted on the built image proven; an image with USER root fails
AC2 base through ARG, no literal registry proven; reds: a literal host, a tag-not-digest base, main's own Containerfile
AC2a no network in the build proven statically, and measured with --network=none on every stage but the two dnf ones
AC3 scan fails on an unjustified High or Critical proven by a planted finding; the live image is red, see below
AC4 stale justifications fail proven by a planted row
AC5 STIG applicability, derived proven: 477 rules, 65 met, 410 not applicable with OpenSCAP's reason, 2 the deployment's
AC6 layer contents proven: 0 violations, 168 allowed with reasons; reds: SUID, 0777, dnf, and a key added and then deleted
AC6a layout proven against Iron Bank's own semgrep rules
AC7 submission review manual, owner
AC8 gate green

Expect the headless-image workflow to be red

The live scan fails on 14 OTP findings, 1 of them Critical (ssl). All 14 are fixed in OTP 28.5.0.7. Moving the toolchain needs the owner's approval, so they are deliberately left unjustified rather than excused. The workflow is not a required check.

Evidence

  • New tests: 80, passing.
  • Reviewer re-run before commit: mix format --check-formatted OK; mix credo --strict --all found no issues; mix trinity.ironbank.lint OK.
  • Author's gate: exit 0, 1044 passed (20 properties, 1024 tests), 18 excluded, prod_check: PASS, coverage 80.51% against 80.31%.
  • Red evidence: R1 to R11, with their logs, are in the slice's private records.

The hardening half of slice 130. Release composition waits on slice 133.

One recipe, two artifacts. ci/ironbank/ is laid out as the repo1 project
root (Dockerfile, hardening_manifest.yaml, LICENSE, README.md,
scripts/entrypoint.sh): OTP built from source with FIPS on UBI9, the
release copied onto UBI9 micro, the base through ARG BASE_*, every input a
manifest resource with its digest and no network except dnf, UID 10001,
Erlang distribution off and no baked cookie. ci/headless/ keeps what
belongs to our published image: digest pins, justifications, allowances,
STIG dispositions and the submission holds. The Debian Containerfile from
slice 061 is removed.

Four tasks carry the checks:
- trinity.ironbank.lint covers AC2, AC2a, AC6 and AC6a on the tree.
- trinity.image.inspect covers AC1 and AC6 on the built image.
- trinity.image.findings covers AC3 and AC4, keyed the way Iron Bank's
  tracker keys a finding.
- trinity.image.stig covers AC5 from OpenSCAP's own results.
The scanner is grype, chosen by measurement over Trivy: only grype found
OTP inside the release.

Red evidence R1 to R11 is in the slice NOTES with the logs in proof/. The
live image's scan is red on 14 OTP findings, fixed in 28.5.0.7; the
toolchain bump is the owner's to approve, so they stay unjustified.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
scripts/headless_image.sh build TAG, with no directory, set an EXIT trap
that read the function's local `dir`. By exit the local was gone, so
under set -u the trap failed with "dir: unbound variable", the script
exited 1 after a successful build, and the 453 MB context stayed in /tmp.
The headless-image workflow calls it exactly that way, so its image job
would have failed on every run. The path is now expanded into the trap
when it is set.

Found by slice 131 (F-131-2) and reproduced here in isolation before the
fix.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Run 37764956008 built the image and then failed two checks, neither in the image:

- `mix trinity.image.inspect` ran `docker save` on the base, which BuildKit keeps in its own
  cache and not in the image store ("reference does not exist"). The task now pulls the base
  when it is missing. The ref is pinned by digest, so the pull gets the bytes the build used.
- grype's `db update` ran as a non-root user with no writable TMPDIR. The scan run already set
  one; the update now sets the same one.

Both were reproduced here first: with the base untagged, `docker save` gives the runner's
error and the task pulls the same image id and passes; the old update command fails with the
runner's "permission denied" and the new one exits 0.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Signed-off-by: Ayla Croft <aylacroft@proton.me>

# Conflicts:
#	CHANGELOG.md
….6 with a TLS 1.2 clamp

Owner decision D2 (2026-10-08). The container images build OTP 28.5.0.7 from source
(ci/container.tool-versions; the Iron Bank manifest, the Dockerfile and the FIPS Containerfile
read it; the source sha256 matches the release's SHA256.txt). The desktop stays on 28.5.0.6,
because Burrito's CDN has no 28.5.0.7 ERTS for Linux or macOS, and holds every outbound TLS
client to TLS 1.2 while the loaded ssl carries CVE-2026-89422 (Trinity.TLS: the ssl application
default, Req's default Finch pool, ReqLLM's pools). The clamp lifts itself when ssl reaches
11.6.0.6. ADR-0014 records the window; a daily canary opens a pull request when Burrito can
fetch the container's OTP for every desktop target.

The ironbank lint holds the manifest to the container pin, both pins to one major, and the
container pin no older than the desktop's. THIRD_PARTY_LICENSES --check accepts OTP application
versions differing, and only on the container's runtime. Built and scanned locally: the image
on 28.5.0.7 has 0 of the 14 findings; inspect 0 violations; STIG every rule disposed.

Signed-off-by: Ayla Croft <aylacroft@proton.me>
Signed-off-by: Ayla Croft <aylacroft@proton.me>
@HackTuah
HackTuah merged commit ea3fa35 into main Oct 8, 2026
17 checks passed
@HackTuah
HackTuah deleted the slice/130-image-hardening-iron-bank branch October 8, 2026 16:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant