[{"data":1,"prerenderedAt":492},["ShallowReactive",2],{"mdc-5n8i5l-key":3,"mdc-7yz43a-key":29,"mdc-5tiebj-key":91,"mdc--gvrfhp-key":103,"mdc-5z7ipx-key":164,"mdc-v4b5rx-key":227,"mdc-ez37vy-key":237,"mdc--2clk1m-key":277,"mdc-ezy1iu-key":347,"mdc-9rmw1p-key":359},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8,24],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13,16,22],{"type":14,"value":15},"text","Attestations say what an image contains and where it came from. Nothing so far establishes that ",{"type":9,"tag":17,"props":18,"children":19},"strong",{},[20],{"type":14,"value":21},"the attestation is genuine",{"type":14,"value":23}," — an attacker who can push to your registry can push an image with a lovely SBOM describing something entirely different.",{"type":9,"tag":10,"props":25,"children":26},{},[27],{"type":14,"value":28},"Signatures close that gap, and Docker's own answer changed recently enough that most documentation is wrong about it.",{"data":30,"body":31},{},{"type":6,"children":32},[33,40,69,79],{"type":9,"tag":34,"props":35,"children":37},"h2",{"id":36},"docker-content-trust-is-gone",[38],{"type":14,"value":39},"Docker Content Trust is gone",{"type":9,"tag":10,"props":41,"children":42},{},[43,45,50,52,59,61,67],{"type":14,"value":44},"If you have read about image signing before, you have read about ",{"type":9,"tag":17,"props":46,"children":47},{},[48],{"type":14,"value":49},"Docker Content Trust",{"type":14,"value":51}," — ",{"type":9,"tag":53,"props":54,"children":56},"code",{"className":55},[],[57],{"type":14,"value":58},"DOCKER_CONTENT_TRUST=1",{"type":14,"value":60},", Notary, ",{"type":9,"tag":53,"props":62,"children":64},{"className":63},[],[65],{"type":14,"value":66},"docker trust sign",{"type":14,"value":68},".",{"type":9,"tag":10,"props":70,"children":71},{},[72,77],{"type":9,"tag":17,"props":73,"children":74},{},[75],{"type":14,"value":76},"Engine 29 removed it from the CLI.",{"type":14,"value":78}," It survives only as a separate plugin. Notary v1 had well-known operational problems — key management that people got wrong, a hard dependency on a Notary server, and a trust model that did not compose with anything else — and the ecosystem moved on.",{"type":9,"tag":10,"props":80,"children":81},{},[82,84,89],{"type":14,"value":83},"So: ",{"type":9,"tag":53,"props":85,"children":87},{"className":86},[],[88],{"type":14,"value":58},{"type":14,"value":90}," is not the answer any more, and instructions telling you to set it are describing a version of Docker you are probably not running.",{"data":92,"body":93},{},{"type":6,"children":94},[95],{"type":9,"tag":96,"props":97,"children":102},"quiz",{":answer":98,":options":99,"explanation":100,"question":101},"0","[\"Docker Content Trust was removed from the CLI in 29, so the runbook needs replacing with a Sigstore-based flow\",\"It silently signs the image with your account key\",\"It fails the push with a clear error naming the replacement\"]","The feature is gone from the CLI and available only as a separate plugin. The failure is not self-explanatory, which is exactly why an old runbook can look like it is still working when nothing is being signed.","A runbook says to set `DOCKER_CONTENT_TRUST=1` before pushing. On Engine 29, what happens?",[],{"data":104,"body":105},{},{"type":6,"children":106},[107,113,147,152],{"type":9,"tag":34,"props":108,"children":110},{"id":109},"sigstore-and-cosign",[111],{"type":14,"value":112},"Sigstore and cosign",{"type":9,"tag":10,"props":114,"children":115},{},[116,118,123,125,131,133,138,140,145],{"type":14,"value":117},"The current answer is ",{"type":9,"tag":17,"props":119,"children":120},{},[121],{"type":14,"value":122},"Sigstore",{"type":14,"value":124},", used through ",{"type":9,"tag":53,"props":126,"children":128},{"className":127},[],[129],{"type":14,"value":130},"cosign",{"type":14,"value":132},". Its important property is ",{"type":9,"tag":17,"props":134,"children":135},{},[136],{"type":14,"value":137},"keyless signing",{"type":14,"value":139},": instead of a long-lived private key someone has to protect, the signer authenticates via OIDC — a CI workload identity, a GitHub Actions token — and gets a short-lived certificate. The signature and certificate are recorded in ",{"type":9,"tag":17,"props":141,"children":142},{},[143],{"type":14,"value":144},"Rekor",{"type":14,"value":146},", a public append-only transparency log.",{"type":9,"tag":10,"props":148,"children":149},{},[150],{"type":14,"value":151},"No key to leak, and a public record that a signature was made, by whom, at what time.",{"type":9,"tag":153,"props":154,"children":158},"pre",{"className":155,"code":157,"language":14},[156],"language-text","cosign sign ghcr.io/acme/app@sha256:9f2a1c8b7e...\n",[159],{"type":9,"tag":53,"props":160,"children":162},{"__ignoreMap":161},"",[163],{"type":14,"value":157},{"data":165,"body":166},{},{"type":6,"children":167},[168,173,191],{"type":9,"tag":169,"props":170,"children":172},"terminal-teaser",{":lines":171},"[{\"cmd\":\"cosign sign ghcr.io/acme/app@sha256:9f2a1c8b7e\",\"out\":\"Generating ephemeral keys...\\nRetrieving signed certificate...\\ntlog entry created with index: 84729103\"},{\"cmd\":\"cosign verify --certificate-identity-regexp 'https://github.com/acme/.+' --certificate-oidc-issuer https://token.actions.githubusercontent.com ghcr.io/acme/app@sha256:9f2a1c8b7e\",\"out\":\"Verification for ghcr.io/acme/app@sha256:9f2a1c8b7e --\\nThe signatures were verified against the specified certificate identity\"}]",[],{"type":9,"tag":10,"props":174,"children":175},{},[176,181,183,189],{"type":9,"tag":17,"props":177,"children":178},{},[179],{"type":14,"value":180},"Sign the digest, never the tag.",{"type":14,"value":182}," Signing a tag signs a pointer that can be moved; the signature would still verify against different bytes tomorrow. Every command above names ",{"type":9,"tag":53,"props":184,"children":186},{"className":185},[],[187],{"type":14,"value":188},"@sha256:…",{"type":14,"value":190}," deliberately.",{"type":9,"tag":10,"props":192,"children":193},{},[194,196,202,204,210,212,217,219,225],{"type":14,"value":195},"The verification arguments are the substance. ",{"type":9,"tag":53,"props":197,"children":199},{"className":198},[],[200],{"type":14,"value":201},"--certificate-identity-regexp",{"type":14,"value":203}," and ",{"type":9,"tag":53,"props":205,"children":207},{"className":206},[],[208],{"type":14,"value":209},"--certificate-oidc-issuer",{"type":14,"value":211}," say ",{"type":9,"tag":17,"props":213,"children":214},{},[215],{"type":14,"value":216},"who is allowed to have signed this",{"type":14,"value":218},". Without them you are verifying that ",{"type":9,"tag":220,"props":221,"children":222},"em",{},[223],{"type":14,"value":224},"somebody",{"type":14,"value":226}," signed it, which is not a security property — anyone can sign anything.",{"data":228,"body":229},{},{"type":6,"children":230},[231],{"type":9,"tag":96,"props":232,"children":236},{":answer":98,":options":233,"explanation":234,"question":235},"[\"Otherwise it only proves that someone signed the image, not that a party you trust did\",\"Because keyless signatures have no signer\",\"To locate the signature in the registry\"]","Anyone can sign any public image. The check that matters is that the certificate binds to an identity you expect — your CI workflow, in your repository, from your OIDC issuer. A bare `cosign verify` passing is close to meaningless.","Why must `cosign verify` be given an expected identity and issuer?",[],{"data":238,"body":239},{},{"type":6,"children":240},[241,247,258],{"type":9,"tag":34,"props":242,"children":244},{"id":243},"in-a-pipeline",[245],{"type":14,"value":246},"In a pipeline",{"type":9,"tag":153,"props":248,"children":253},{"className":249,"code":251,"language":252,"meta":161},[250],"language-yaml","permissions:\n  contents: read\n  packages: write\n  id-token: write            # required for keyless signing\n\njobs:\n  build:\n    steps:\n      - uses: docker/setup-buildx-action@v3\n      - uses: docker/build-push-action@v6\n        id: build\n        with:\n          push: true\n          tags: ghcr.io/acme/app:${{ github.sha }}\n          sbom: true\n          provenance: mode=max\n\n      - uses: sigstore/cosign-installer@v3\n      - run: cosign sign --yes ghcr.io/acme/app@${{ steps.build.outputs.digest }}\n","yaml",[254],{"type":9,"tag":53,"props":255,"children":256},{"__ignoreMap":161},[257],{"type":14,"value":251},{"type":9,"tag":10,"props":259,"children":260},{},[261,267,269,275],{"type":9,"tag":53,"props":262,"children":264},{"className":263},[],[265],{"type":14,"value":266},"id-token: write",{"type":14,"value":268}," is the permission that makes keyless signing possible, and its absence is the usual cause of a confusing OIDC failure. ",{"type":9,"tag":53,"props":270,"children":272},{"className":271},[],[273],{"type":14,"value":274},"build-push-action",{"type":14,"value":276}," outputs the digest, which is what gets signed.",{"data":278,"body":279},{},{"type":6,"children":280},[281,287,292,335],{"type":9,"tag":34,"props":282,"children":284},{"id":283},"enforcing-it",[285],{"type":14,"value":286},"Enforcing it",{"type":9,"tag":10,"props":288,"children":289},{},[290],{"type":14,"value":291},"A signature nobody checks is decoration. The check belongs at admission — the moment before something runs:",{"type":9,"tag":293,"props":294,"children":295},"ul",{},[296,307,325],{"type":9,"tag":297,"props":298,"children":299},"li",{},[300,305],{"type":9,"tag":17,"props":301,"children":302},{},[303],{"type":14,"value":304},"Kubernetes",{"type":14,"value":306}," — an admission controller such as Sigstore Policy Controller, Kyverno, or Connaisseur rejects unsigned or wrongly-signed images.",{"type":9,"tag":297,"props":308,"children":309},{},[310,315,317,323],{"type":9,"tag":17,"props":311,"children":312},{},[313],{"type":14,"value":314},"A plain Docker host",{"type":14,"value":316}," — verify in the deploy script before ",{"type":9,"tag":53,"props":318,"children":320},{"className":319},[],[321],{"type":14,"value":322},"docker run",{"type":14,"value":324},", and pin by digest so the verified bytes are the ones that start.",{"type":9,"tag":297,"props":326,"children":327},{},[328,333],{"type":9,"tag":17,"props":329,"children":330},{},[331],{"type":14,"value":332},"Registry-side",{"type":14,"value":334}," — some registries can require signatures for a repository.",{"type":9,"tag":10,"props":336,"children":337},{},[338,340,345],{"type":14,"value":339},"The order to adopt it in matters, because doing this in the wrong order takes production down: ",{"type":9,"tag":17,"props":341,"children":342},{},[343],{"type":14,"value":344},"sign everything first, observe for a while, then enforce.",{"type":14,"value":346}," Turning on enforcement before every image is signed means the next deploy fails, usually at the worst moment.",{"data":348,"body":349},{},{"type":6,"children":350},[351],{"type":9,"tag":352,"props":353,"children":358},"fill-blank",{":answer":354,"hint":355,"placeholder":356,"prompt":357},"[\"cosign sign --yes ghcr.io/acme/app@sha256:9f2a1c\",\"cosign sign -y ghcr.io/acme/app@sha256:9f2a1c\"]","Sign the digest, not a tag, and skip the confirmation.","cosign sign ...","Sign the image at digest `sha256:9f2a1c` in `ghcr.io/acme/app` with cosign, without an interactive prompt.",[],{"data":360,"body":361},{},{"type":6,"children":362},[363,487],{"type":9,"tag":364,"props":365,"children":367},"deep-dive",{"title":366},"What signing does and does not prove",[368,378,390,395,447,460],{"type":9,"tag":10,"props":369,"children":370},{},[371,373],{"type":14,"value":372},"A verified signature proves one narrow thing: ",{"type":9,"tag":17,"props":374,"children":375},{},[376],{"type":14,"value":377},"these exact bytes were signed by that identity.",{"type":9,"tag":10,"props":379,"children":380},{},[381,383,388],{"type":14,"value":382},"It does ",{"type":9,"tag":17,"props":384,"children":385},{},[386],{"type":14,"value":387},"not",{"type":14,"value":389}," prove the image is safe, that the code was reviewed, that the build was not compromised, or that the signer meant to endorse it for production. A compromised CI pipeline signs malware perfectly.",{"type":9,"tag":10,"props":391,"children":392},{},[393],{"type":14,"value":394},"So signing is one link. The chain that is actually load-bearing:",{"type":9,"tag":396,"props":397,"children":398},"ol",{},[399,409,427,437],{"type":9,"tag":297,"props":400,"children":401},{},[402,407],{"type":9,"tag":17,"props":403,"children":404},{},[405],{"type":14,"value":406},"Source integrity",{"type":14,"value":408}," — protected branches, required reviews, signed commits.",{"type":9,"tag":297,"props":410,"children":411},{},[412,417,419,425],{"type":9,"tag":17,"props":413,"children":414},{},[415],{"type":14,"value":416},"Build integrity",{"type":14,"value":418}," — an ephemeral, isolated builder whose provenance records the source commit. This is what SLSA levels grade, and why ",{"type":9,"tag":53,"props":420,"children":422},{"className":421},[],[423],{"type":14,"value":424},"provenance=mode=max",{"type":14,"value":426}," matters.",{"type":9,"tag":297,"props":428,"children":429},{},[430,435],{"type":9,"tag":17,"props":431,"children":432},{},[433],{"type":14,"value":434},"Artifact integrity",{"type":14,"value":436}," — the signature, over the digest.",{"type":9,"tag":297,"props":438,"children":439},{},[440,445],{"type":9,"tag":17,"props":441,"children":442},{},[443],{"type":14,"value":444},"Deployment integrity",{"type":14,"value":446}," — admission control verifying identity, and deploying by digest.",{"type":9,"tag":10,"props":448,"children":449},{},[450,452,458],{"type":14,"value":451},"Break any link and the rest is theatre. The most commonly broken one is the fourth: organisations that sign diligently and then deploy ",{"type":9,"tag":53,"props":453,"children":455},{"className":454},[],[456],{"type":14,"value":457},":latest",{"type":14,"value":459}," with no verification, so the signature is generated and never consulted.",{"type":9,"tag":10,"props":461,"children":462},{},[463,465,470,472,477,479,485],{"type":14,"value":464},"Two habits worth taking from this lesson regardless of how far you go. ",{"type":9,"tag":17,"props":466,"children":467},{},[468],{"type":14,"value":469},"Deploy by digest",{"type":14,"value":471},", because it is free and it makes \"what is running\" answerable exactly. And ",{"type":9,"tag":17,"props":473,"children":474},{},[475],{"type":14,"value":476},"write down what your verification actually asserts",{"type":14,"value":478}," — which identity, which issuer, which repository — because a ",{"type":9,"tag":53,"props":480,"children":482},{"className":481},[],[483],{"type":14,"value":484},"cosign verify",{"type":14,"value":486}," with the wrong regex passes happily against an image signed by someone else entirely.",{"type":9,"tag":10,"props":488,"children":489},{},[490],{"type":14,"value":491},"Next up: observability — logs, events, and metrics from containers at scale.",1787908868456]