[{"data":1,"prerenderedAt":452},["ShallowReactive",2],{"mdc-bed0cq-key":3,"mdc--ib79gy-key":27,"mdc-77r634-key":91,"mdc--c64y8b-key":132,"mdc-yxcpzg-key":144,"mdc--1vwusj-key":228,"mdc-k800ka-key":238,"mdc-bpbwrp-key":282,"mdc--pwvwq-key":294},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8,16],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13],{"type":14,"value":15},"text","\"What is in this image, and where did it come from?\" is a question production eventually asks, usually urgently, usually about a CVE announced that morning. Answering it by rebuilding and guessing does not scale past a handful of images.",{"type":9,"tag":10,"props":17,"children":18},{},[19,25],{"type":9,"tag":20,"props":21,"children":22},"strong",{},[23],{"type":14,"value":24},"Attestations",{"type":14,"value":26}," are signed statements attached to an image that answer it in advance.",{"data":28,"body":29},{},{"type":6,"children":30},[31,38,56,73,78],{"type":9,"tag":32,"props":33,"children":35},"h2",{"id":34},"the-two-that-matter",[36],{"type":14,"value":37},"The two that matter",{"type":9,"tag":10,"props":39,"children":40},{},[41,46,48,54],{"type":9,"tag":20,"props":42,"children":43},{},[44],{"type":14,"value":45},"SBOM",{"type":14,"value":47}," — Software Bill of Materials. Every package in the image, with version and license. Answers ",{"type":9,"tag":49,"props":50,"children":51},"em",{},[52],{"type":14,"value":53},"what is in it",{"type":14,"value":55},".",{"type":9,"tag":10,"props":57,"children":58},{},[59,64,66,71],{"type":9,"tag":20,"props":60,"children":61},{},[62],{"type":14,"value":63},"Provenance",{"type":14,"value":65}," — how the image was built: which Dockerfile, which source commit, which builder, which base image, when. Answers ",{"type":9,"tag":49,"props":67,"children":68},{},[69],{"type":14,"value":70},"where it came from",{"type":14,"value":72},". It follows the SLSA framework, whose levels describe how hard the record is to forge.",{"type":9,"tag":10,"props":74,"children":75},{},[76],{"type":14,"value":77},"BuildKit generates both:",{"type":9,"tag":79,"props":80,"children":84},"pre",{"className":81,"code":83,"language":14},[82],"language-text","docker buildx build \\\n  --sbom=true \\\n  --provenance=mode=max \\\n  -t ghcr.io/acme/app:1.0 --push .\n",[85],{"type":9,"tag":86,"props":87,"children":89},"code",{"__ignoreMap":88},"",[90],{"type":14,"value":83},{"data":92,"body":93},{},{"type":6,"children":94},[95,100,113],{"type":9,"tag":96,"props":97,"children":99},"terminal-teaser",{":lines":98},"[{\"cmd\":\"docker buildx imagetools inspect ghcr.io/acme/app:1.0\",\"out\":\"Manifests:\\n  Platform: linux/amd64\\n  Platform: unknown/unknown\\n    Annotations:\\n      vnd.docker.reference.type: attestation-manifest\"},{\"cmd\":\"docker buildx imagetools inspect ghcr.io/acme/app:1.0 --format '{{json .Provenance}}' | head -5\",\"out\":\"{\\n  \\\"SLSA\\\": {\\n    \\\"buildType\\\": \\\"https://mobyproject.org/buildkit@v1\\\",\\n    \\\"materials\\\": [ ... ]\"}]",[],{"type":9,"tag":10,"props":101,"children":102},{},[103,105,111],{"type":14,"value":104},"That ",{"type":9,"tag":86,"props":106,"children":108},{"className":107},[],[109],{"type":14,"value":110},"unknown/unknown",{"type":14,"value":112}," platform is not a bug. Attestations ride in the image index as extra manifests with a platform nothing matches, so they are stored and distributed alongside the image but never pulled as one.",{"type":9,"tag":10,"props":114,"children":115},{},[116,122,124,130],{"type":9,"tag":86,"props":117,"children":119},{"className":118},[],[120],{"type":14,"value":121},"mode=max",{"type":14,"value":123}," on provenance records the full build — every instruction, every source and material. ",{"type":9,"tag":86,"props":125,"children":127},{"className":126},[],[128],{"type":14,"value":129},"mode=min",{"type":14,"value":131}," records only the essentials. Max is what an audit wants.",{"data":133,"body":134},{},{"type":6,"children":135},[136],{"type":9,"tag":137,"props":138,"children":143},"quiz",{":answer":139,":options":140,"explanation":141,"question":142},"0","[\"Where the image came from — which source, which builder, which base, and when\",\"Which packages are installed and at what versions\",\"Whether any of the packages have known CVEs\"]","SBOM is the inventory, provenance is the chain of custody. You need both: the inventory tells you a vulnerable package is present, and the provenance tells you which repository and commit to fix so the next build does not reintroduce it.","What question does a provenance attestation answer that an SBOM does not?",[],{"data":145,"body":146},{},{"type":6,"children":147},[148,154,167,176,186,195,207,212,223],{"type":9,"tag":32,"props":149,"children":151},{"id":150},"tags-lie-digests-do-not",[152],{"type":14,"value":153},"Tags lie; digests do not",{"type":9,"tag":10,"props":155,"children":156},{},[157,159,165],{"type":14,"value":158},"A tag is a mutable pointer. ",{"type":9,"tag":86,"props":160,"children":162},{"className":161},[],[163],{"type":14,"value":164},"acme/app:1.0",{"type":14,"value":166}," can be moved to different bytes at any time by anyone with push access, and nothing about the name changes.",{"type":9,"tag":79,"props":168,"children":171},{"className":169,"code":170,"language":14},[82],"ghcr.io/acme/app:1.0                       # whatever this points at today\nghcr.io/acme/app@sha256:9f2a1c8b7e...      # exactly these bytes, forever\n",[172],{"type":9,"tag":86,"props":173,"children":174},{"__ignoreMap":88},[175],{"type":14,"value":170},{"type":9,"tag":10,"props":177,"children":178},{},[179,184],{"type":9,"tag":20,"props":180,"children":181},{},[182],{"type":14,"value":183},"Deploy by digest.",{"type":14,"value":185}," It is the only way to know that what you tested is what you shipped, it removes an entire class of supply-chain attack, and it makes rollback exact rather than approximate.",{"type":9,"tag":79,"props":187,"children":190},{"className":188,"code":189,"language":14},[82],"docker buildx imagetools inspect ghcr.io/acme/app:1.0 --format '{{.Manifest.Digest}}'\n",[191],{"type":9,"tag":86,"props":192,"children":193},{"__ignoreMap":88},[194],{"type":14,"value":189},{"type":9,"tag":10,"props":196,"children":197},{},[198,200,205],{"type":14,"value":199},"The usual workflow: CI builds and pushes a tag, resolves it to a digest, and puts the ",{"type":9,"tag":20,"props":201,"children":202},{},[203],{"type":14,"value":204},"digest",{"type":14,"value":206}," in the deployment manifest. The tag stays for humans; the digest is what runs.",{"type":9,"tag":10,"props":208,"children":209},{},[210],{"type":14,"value":211},"Base images too:",{"type":9,"tag":79,"props":213,"children":218},{"className":214,"code":216,"language":217,"meta":88},[215],"language-dockerfile","FROM node:22-alpine@sha256:1a3f9c...\n","dockerfile",[219],{"type":9,"tag":86,"props":220,"children":221},{"__ignoreMap":88},[222],{"type":14,"value":216},{"type":9,"tag":10,"props":224,"children":225},{},[226],{"type":14,"value":227},"The objection to this is real — a pinned base does not receive security patches, so you need automation (Renovate, Dependabot) that raises a pull request when the upstream digest moves. The pull request is the point: the update becomes a reviewed change instead of something that happens silently on the next build.",{"data":229,"body":230},{},{"type":6,"children":231},[232],{"type":9,"tag":137,"props":233,"children":237},{":answer":139,":options":234,"explanation":235,"question":236},"[\"A tag can be moved to different bytes at any time, so it does not identify what you tested\",\"Digests pull faster\",\"Tags are not supported by all registries\"]","Immutability. A digest is the content hash — it cannot point at anything else. It also means a compromised registry account cannot swap your image out from under a deployment that names the digest.","Why deploy by digest rather than by tag, even for a tag you control?",[],{"data":239,"body":240},{},{"type":6,"children":241},[242,248,257,270],{"type":9,"tag":32,"props":243,"children":245},{"id":244},"verifying-what-you-were-given",[246],{"type":14,"value":247},"Verifying what you were given",{"type":9,"tag":79,"props":249,"children":252},{"className":250,"code":251,"language":14},[82],"docker buildx imagetools inspect ghcr.io/acme/app:1.0 --format '{{json .SBOM}}'\ndocker buildx imagetools inspect ghcr.io/acme/app:1.0 --format '{{json .Provenance}}'\n",[253],{"type":9,"tag":86,"props":254,"children":255},{"__ignoreMap":88},[256],{"type":14,"value":251},{"type":9,"tag":10,"props":258,"children":259},{},[260,262,268],{"type":14,"value":261},"Engine 29 also added a dedicated API endpoint, ",{"type":9,"tag":86,"props":263,"children":265},{"className":264},[],[266],{"type":14,"value":267},"GET /images/{name}/attestations",{"type":14,"value":269},", so tooling can read them without shelling out to buildx.",{"type":9,"tag":10,"props":271,"children":272},{},[273,275,280],{"type":14,"value":274},"Reading them is the easy half. ",{"type":9,"tag":20,"props":276,"children":277},{},[278],{"type":14,"value":279},"Verifying",{"type":14,"value":281}," — establishing that the attestation was produced by a builder you trust and has not been altered — needs signatures, which is the next lesson.",{"data":283,"body":284},{},{"type":6,"children":285},[286],{"type":9,"tag":287,"props":288,"children":293},"fill-blank",{":answer":289,"hint":290,"placeholder":291,"prompt":292},"[\"docker buildx build --sbom=true --provenance=mode=max -t ghcr.io/acme/app:1.0 --push .\",\"docker buildx build --provenance=mode=max --sbom=true -t ghcr.io/acme/app:1.0 --push .\"]","Two attestation flags, a tag, and an output flag.","docker buildx build ...","Build and push `ghcr.io/acme/app:1.0` with an SBOM and full provenance attached.",[],{"data":295,"body":296},{},{"type":6,"children":297},[298,447],{"type":9,"tag":299,"props":300,"children":302},"deep-dive",{"title":301},"VEX, and the difference between present and exploitable",[303,308,313,323,391,396,408,413],{"type":9,"tag":10,"props":304,"children":305},{},[306],{"type":14,"value":307},"Scan any real image and you get a list of CVEs. Most of them are not exploitable in your context: the vulnerable function is never called, the affected component is a build-time dependency, the attack needs a configuration you do not use.",{"type":9,"tag":10,"props":309,"children":310},{},[311],{"type":14,"value":312},"The naive response is to chase the number to zero, which means either rebuilding constantly against unrelated patches or suppressing findings in a spreadsheet nobody trusts.",{"type":9,"tag":10,"props":314,"children":315},{},[316,321],{"type":9,"tag":20,"props":317,"children":318},{},[319],{"type":14,"value":320},"VEX",{"type":14,"value":322}," — Vulnerability Exploitability eXchange — is the machine-readable answer. A VEX document is a signed statement about a specific CVE in a specific artifact, with a status:",{"type":9,"tag":324,"props":325,"children":326},"ul",{},[327,362,373,382],{"type":9,"tag":328,"props":329,"children":330},"li",{},[331,337,339,344,346,352,354,360],{"type":9,"tag":86,"props":332,"children":334},{"className":333},[],[335],{"type":14,"value":336},"not_affected",{"type":14,"value":338},", with a ",{"type":9,"tag":20,"props":340,"children":341},{},[342],{"type":14,"value":343},"justification",{"type":14,"value":345}," — ",{"type":9,"tag":86,"props":347,"children":349},{"className":348},[],[350],{"type":14,"value":351},"vulnerable_code_not_present",{"type":14,"value":353},", ",{"type":9,"tag":86,"props":355,"children":357},{"className":356},[],[358],{"type":14,"value":359},"vulnerable_code_not_in_execute_path",{"type":14,"value":361},", and so on",{"type":9,"tag":328,"props":363,"children":364},{},[365,371],{"type":9,"tag":86,"props":366,"children":368},{"className":367},[],[369],{"type":14,"value":370},"affected",{"type":14,"value":372},", with an action statement",{"type":9,"tag":328,"props":374,"children":375},{},[376],{"type":9,"tag":86,"props":377,"children":379},{"className":378},[],[380],{"type":14,"value":381},"fixed",{"type":9,"tag":328,"props":383,"children":384},{},[385],{"type":9,"tag":86,"props":386,"children":388},{"className":387},[],[389],{"type":14,"value":390},"under_investigation",{"type":9,"tag":10,"props":392,"children":393},{},[394],{"type":14,"value":395},"The justification is what makes it different from suppression. \"We looked, and here is why it does not apply\" is auditable; a filter rule is not.",{"type":9,"tag":10,"props":397,"children":398},{},[399,401,406],{"type":14,"value":400},"VEX is one of the things a hardened base image supplies, and it is why \"near-zero CVEs\" claims are worth reading carefully — the honest version publishes ",{"type":9,"tag":20,"props":402,"children":403},{},[404],{"type":14,"value":405},"full, unsuppressed CVE visibility",{"type":14,"value":407}," alongside VEX statements explaining the ones that remain, rather than filtering them out of the report.",{"type":9,"tag":10,"props":409,"children":410},{},[411],{"type":14,"value":412},"Three practical consequences:",{"type":9,"tag":414,"props":415,"children":416},"ol",{},[417,427,437],{"type":9,"tag":328,"props":418,"children":419},{},[420,425],{"type":9,"tag":20,"props":421,"children":422},{},[423],{"type":14,"value":424},"Scan the image you ship, not the base.",{"type":14,"value":426}," A multi-stage build's final image has a fraction of the surface, and scanning the wrong one produces alarm about a compiler you did not ship.",{"type":9,"tag":328,"props":428,"children":429},{},[430,435],{"type":9,"tag":20,"props":431,"children":432},{},[433],{"type":14,"value":434},"Attach attestations at build time.",{"type":14,"value":436}," Generating an SBOM months later means guessing.",{"type":9,"tag":328,"props":438,"children":439},{},[440,445],{"type":9,"tag":20,"props":441,"children":442},{},[443],{"type":14,"value":444},"Consume VEX where your scanner supports it.",{"type":14,"value":446}," It turns a list of 200 findings into a list of 3 that matter, with reasons attached to the other 197.",{"type":9,"tag":10,"props":448,"children":449},{},[450],{"type":14,"value":451},"Next up: Docker Scout — the CLI that reads all of this and tells you what to do about it.",1787908868433]