When to Use
Invoke this skill when a container spec is about to be applied to a host that can send
orders, and you need to know whether the code inside it can still be altered after
deployment. The failure it prevents is specific: an operator SSHes into a production
node during an incident, edits strategy.py in place, and the fix survives until the
next restart silently reverts it — or does not survive, and nobody knows which build is
actually running when the P&L diverges.
NIST SP 800-190 states the principle directly: containers "should be operated as stateless entities that are deployed but not changed", and when one needs updating it "is simply destroyed and replaced with a new container that has the updates." This skill audits whether your spec actually delivers that, or only claims to.
Use it in two places: as a CI/CD gate before an artifact is promoted, and again in the deploy job on the host, against the spec that will really be applied.
When NOT to Use
- You want the signature actually verified. This engine reads a boolean your
pipeline sets. It runs no registry call and no cryptography. The real check is
cosign verify <image>@<digest> --certificate-identity=... --certificate-oidc-issuer=..., and this audit is downstream of it, not a substitute for it. - You need admission control that cannot be bypassed. A Python function in your
deploy job is advisory — anyone with cluster access can
kubectl runaround it. Enforce at the admission layer (Sigstore policy-controller, Kyverno, Gatekeeper) and use this to fail fast and explain why, before the cluster refuses the manifest. - You are comparing two environments' configuration. This audits one spec against
fixed immutability policy; it does not diff DEV against PROD. See
configuration-drift-detection-across-environments. - You are sequencing the rollout itself. Whether to shift traffic gradually, and how
to roll back, belongs to
blue-green-deployment-for-live-strategy-updatesandcanary-releases-for-strategy-code-changes. This skill only decides whether the artifact is fit to be rolled out at all. - The build is not reproducible in the first place. Pinning a digest freezes
whatever went into that build, including an unpinned transitive dependency resolved on
build day. See
dependency-pinning-and-reproducible-builds.
Prerequisites
- A deployment spec you can read before it is applied — Compose service, Kubernetes
PodSpec, or the arguments your deploy script passes todocker run. - The image content digest, resolved once in CI (
docker buildx imagetools inspect, or thedigestoutput ofdocker/build-push-action) and threaded through unchanged. - A signing and verification step already in the pipeline. Cosign (Sigstore) or Notation
(Notary Project) are the current options; Docker Content Trust and Notary v1 are
not — the upstream Notary v1 server "is no longer maintained" and Docker has
scheduled full shutdown of
notary.docker.iofor 8 December 2026. - Optionally, the image's
org.opencontainers.image.revisionannotation read back from the manifest, so the commit you claim to have deployed is a property of the artifact rather than a string your pipeline typed into a report.
Workflow
-
Pin to a digest, and treat the tag as a label.
ImmutableContainerSpecrequiresimage_sha256_digest; the audit validates it against the OCI descriptor grammar (sha256:plus exactly 64 lowercase hex). A malformed or absent digest isUNPINNED_DIGEST_REJECTED.- Kubernetes documents why: "Tags can be moved to point to different images, but digests are fixed", and deploying by tag means "you might end up with a mix of Pods running the old and new code."
- A floating tag name (
latest,prod,main, …) additionally raisesMUTABLE_TAG_REJECTED. This is hygiene, not the control — the digest is the control. Overridemutable_tag_namesif your convention differs.
-
Bind the artifact to a reviewed commit — do not assert it.
git_commit_shamust be a full Git object id: 40 hex under SHA-1, or 64 hex under the SHA-256 object format. Abbreviated ids are rejected because a deployment record must name exactly one commit.- Set
source_revision_annotationfrom the image's ownorg.opencontainers.image.revisionannotation ("source control revision identifier for the packaged software"). A mismatch isSOURCE_REVISION_MISMATCH_REJECTED— the artifact was not built from the commit under review. - With no annotation available the audit warns and proceeds, because
git_commit_shais then unverified pipeline metadata. Once your builder sets the label, construct the engine withrequire_source_revision_annotation=Trueto turn that warning into a gate. - There is no cryptographic check of a digest "against" a Git SHA — those are hashes of different things. The annotation, and the provenance attestation behind it, is what actually links them.
-
Close the rootfs, then close the mounts that reopen it.
read_only_rootfs=FalseisMUTABLE_ROOTFS_REJECTED.- A read-only rootfs alone is not enough. Docker's own reference says
--read-onlyprohibits "writes to locations other than the specified volumes". A single-v /opt/bot:/apprestores in-place editing completely, with the read-only flag still set and still reported as set. List every read-write bind mount and volume inwritable_volumes; a non-empty list isWRITABLE_HOST_MOUNT_REJECTED. NIST SP 800-190: "Very rarely should containers mount local file systems on a host."
-
Record signature verification as an attestation, and label it as one.
is_image_signed_cosign=FalseisUNSIGNED_IMAGE_REJECTED. The report field is namedis_signature_attested, not..._verified, because that is all this engine can honestly claim.- Set the flag from the exit status of a real
cosign verifythat named an expected identity and issuer. Cosign 2.0 made those flags mandatory: "it's critical to specify who you trust to generate a signature for identity-based signing." A pipeline that hard-codesTruehas built a gate that always opens.
-
Harden privileges, or the read-only flag is decorative.
no_new_privilegesandrun_as_non_root_usermust both hold, orPRIVILEGE_HARDENING_REJECTED. A root process able to escalate can remount the rootfs read-write.- On Kubernetes this is
allowPrivilegeEscalation: falseplusrunAsNonRoot: true. Note that Kubernetes forcesallowPrivilegeEscalationto true for a privileged container or one holdingCAP_SYS_ADMIN, so setting the field is not sufficient if those are also present.
-
Give the process scratch space it cannot execute from.
- A read-only rootfs leaves nowhere to write. Declare
tmpfs_mounts(/tmp,/run); a missing required path is a warning, because it crashes the bot at first write rather than weakening immutability. - Declare the options per mount in
tmpfs_options. A declared mount missingnoexecornosuidisEXECUTABLE_TMPFS_REJECTED— writable and executable scratch space is somewhere to drop and run a payload despite the read-only rootfs. A mount with no declared options warns instead of failing: the engine cannot see the real runtime, and unknown is not the same as unsafe.
- A read-only rootfs leaves nowhere to write. Declare
-
Act on the whole report, not the first line.
- Branch on
report.approved.report.violationsholds every breach, ordered most severe first, each with aseverityand aremediationnaming the exact flag. Print all of them — a gate that reveals one violation per rebuild burns a deployment window per finding. report.statusis the code of that first, most severe violation, so it is safe to alert on: it never reports aHIGHwhile aCRITICALbreach is further down the list. Logreport.violation_codesfor the full picture.- A malformed spec raises
ImmutableSpecErrorrather than returning a report. Catch it as a failure, never as a pass: an unevaluable spec is not an approved spec.
- Branch on
Full step-by-step procedure and CI/CD wiring: see
references/workflows.md. Control-by-control sources: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Bind-mounting the code directory into a
--read-onlycontainer. The most common way an immutable deployment turns out to be mutable. The flag is set, the audit field reads True, andvi /opt/bot/strategy.pyon the host still edits live code. Only thewritable_volumescheck catches this. - Hard-coding
is_image_signed_cosign=True. A signature field that is never derived from a verification exit status is a comment, not a control. Equally: runningcosign verifywithout--certificate-identityand--certificate-oidc-issuer, which Cosign 2.0 refuses in keyless mode for exactly this reason. - Verifying a tag and then deploying that tag. Verification resolves the tag to a digest at that instant; the pull resolves it again later. Verify and deploy the same digest string.
- Treating "no digest" as "digest not required". An empty or malformed digest field
previously produced an image URI ending in a bare
@and an approved report. Absence of a pin is a rejection, not a default. - Believing a read-only rootfs stops privilege escalation. A root process with
CAP_SYS_ADMINcan remount it. Read-only, non-root, and no-new-privileges are one control in three parts. - Mounting
/tmpas tmpfs and stopping there. Withoutnoexec,nosuidyou have replaced a read-only filesystem with a writable, executable one and called it hardening. - Assuming
latestis pinned because it was pulled once. NIST SP 800-190 warns the tag "is only a label attached to the image and not a guarantee of freshness." - Passing a YAML-loaded spec straight in.
read_only_rootfs: "false"parses as a truthy string. The spec now rejects non-boolvalues rather than approving them. - Patching the container instead of the image. Any fix applied to a running trading container is lost on restart and invisible to the next audit. Rebuild, re-sign, redeploy — even for a one-line hotfix, and especially during an incident.
Verification
- Run the unit suite:
python -m unittest discover -s skills/immutable-infrastructure-for-trading-bots/scripts— all tests must pass. - Audit a fully compliant spec (digest-pinned,
read_only_rootfs=True,is_image_signed_cosign=True,writable_volumes=[],tmpfs_optionscarryingnoexec,nosuid, annotation matching the commit) and confirmstatus == "IMMUTABLE_SPEC_APPROVED"with bothviolationsandwarningsempty. - Audit a spec that is both mutable and unsigned and confirm
violation_codescontains bothMUTABLE_ROOTFS_REJECTEDandUNSIGNED_IMAGE_REJECTEDfrom one call. - Combine a
HIGHbreach evaluated early (an invalidgit_commit_sha) with aCRITICALone evaluated later (writable_volumes=["/app"]) and confirmstatusreports theCRITICALcode, not the first one checked. - Take an approved spec, add
writable_volumes=["/app"], and confirm it flips toWRITABLE_HOST_MOUNT_REJECTEDwhileis_read_only_rootfs_enforcedstays True — this is the case a rootfs-only audit misses. - Set
tmpfs_options={"/tmp": ["rw", "size=64m"]}and confirmEXECUTABLE_TMPFS_REJECTED; remove the key entirely and confirm a warning and approval instead. - Pass
read_only_rootfs="false"and confirmImmutableSpecErroris raised at construction rather than a report being returned. - Against your real pipeline: change the deployed tag to point at a different image, re-resolve the digest, and confirm the audit input changes. If it does not, your deploy path is not digest-pinned regardless of what the spec says.