[{"data":1,"prerenderedAt":764},["ShallowReactive",2],{"mdc-iee8yq-key":3,"mdc--ghb20q-key":16,"mdc-u4n416-key":226,"mdc--351tyf-key":235,"mdc--cmclu7-key":246,"mdc--7ura8s-key":372,"mdc-tlm3dt-key":504,"mdc-uudq9r-key":514,"mdc--5q90gc-key":606,"mdc--lg9fgj-key":618},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13],{"type":14,"value":15},"text","The cheapest vulnerability to fix is one for a package you never installed. Every previous lesson made a container harder to attack; this one removes the things there was no reason to ship.",{"data":17,"body":18},{},{"type":6,"children":19},[20,27,221],{"type":9,"tag":21,"props":22,"children":24},"h2",{"id":23},"the-ladder-and-what-each-step-costs",[25],{"type":14,"value":26},"The ladder, and what each step costs",{"type":9,"tag":28,"props":29,"children":30},"table",{},[31,60],{"type":9,"tag":32,"props":33,"children":34},"thead",{},[35],{"type":9,"tag":36,"props":37,"children":38},"tr",{},[39,45,50,55],{"type":9,"tag":40,"props":41,"children":42},"th",{},[43],{"type":14,"value":44},"Base",{"type":9,"tag":40,"props":46,"children":47},{},[48],{"type":14,"value":49},"Packages",{"type":9,"tag":40,"props":51,"children":52},{},[53],{"type":14,"value":54},"Shell",{"type":9,"tag":40,"props":56,"children":57},{},[58],{"type":14,"value":59},"Trade-off",{"type":9,"tag":61,"props":62,"children":63},"tbody",{},[64,93,119,145,172,195],{"type":9,"tag":36,"props":65,"children":66},{},[67,78,83,88],{"type":9,"tag":68,"props":69,"children":70},"td",{},[71],{"type":9,"tag":72,"props":73,"children":75},"code",{"className":74},[],[76],{"type":14,"value":77},"ubuntu:24.04",{"type":9,"tag":68,"props":79,"children":80},{},[81],{"type":14,"value":82},"~100",{"type":9,"tag":68,"props":84,"children":85},{},[86],{"type":14,"value":87},"yes",{"type":9,"tag":68,"props":89,"children":90},{},[91],{"type":14,"value":92},"everything works, largest surface",{"type":9,"tag":36,"props":94,"children":95},{},[96,105,110,114],{"type":9,"tag":68,"props":97,"children":98},{},[99],{"type":9,"tag":72,"props":100,"children":102},{"className":101},[],[103],{"type":14,"value":104},"debian:13-slim",{"type":9,"tag":68,"props":106,"children":107},{},[108],{"type":14,"value":109},"~90",{"type":9,"tag":68,"props":111,"children":112},{},[113],{"type":14,"value":87},{"type":9,"tag":68,"props":115,"children":116},{},[117],{"type":14,"value":118},"docs and extras stripped",{"type":9,"tag":36,"props":120,"children":121},{},[122,131,136,140],{"type":9,"tag":68,"props":123,"children":124},{},[125],{"type":9,"tag":72,"props":126,"children":128},{"className":127},[],[129],{"type":14,"value":130},"alpine:3.22",{"type":9,"tag":68,"props":132,"children":133},{},[134],{"type":14,"value":135},"~15",{"type":9,"tag":68,"props":137,"children":138},{},[139],{"type":14,"value":87},{"type":9,"tag":68,"props":141,"children":142},{},[143],{"type":14,"value":144},"musl libc, not glibc",{"type":9,"tag":36,"props":146,"children":147},{},[148,153,158,167],{"type":9,"tag":68,"props":149,"children":150},{},[151],{"type":14,"value":152},"distroless",{"type":9,"tag":68,"props":154,"children":155},{},[156],{"type":14,"value":157},"~5",{"type":9,"tag":68,"props":159,"children":160},{},[161],{"type":9,"tag":162,"props":163,"children":164},"strong",{},[165],{"type":14,"value":166},"no",{"type":9,"tag":68,"props":168,"children":169},{},[170],{"type":14,"value":171},"runtime only; hard to debug",{"type":9,"tag":36,"props":173,"children":174},{},[175,180,185,190],{"type":9,"tag":68,"props":176,"children":177},{},[178],{"type":14,"value":179},"hardened (DHI)",{"type":9,"tag":68,"props":181,"children":182},{},[183],{"type":14,"value":184},"minimal",{"type":9,"tag":68,"props":186,"children":187},{},[188],{"type":14,"value":189},"varies",{"type":9,"tag":68,"props":191,"children":192},{},[193],{"type":14,"value":194},"non-root, signed, SBOM + VEX included",{"type":9,"tag":36,"props":196,"children":197},{},[198,207,212,216],{"type":9,"tag":68,"props":199,"children":200},{},[201],{"type":9,"tag":72,"props":202,"children":204},{"className":203},[],[205],{"type":14,"value":206},"scratch",{"type":9,"tag":68,"props":208,"children":209},{},[210],{"type":14,"value":211},"0",{"type":9,"tag":68,"props":213,"children":214},{},[215],{"type":14,"value":166},{"type":9,"tag":68,"props":217,"children":218},{},[219],{"type":14,"value":220},"static binaries only",{"type":9,"tag":10,"props":222,"children":223},{},[224],{"type":14,"value":225},"Each step down removes packages, and packages are what scanners find. Going from a distro base to distroless typically moves a CVE report from a hundred-odd findings to single digits — not because anything was patched, but because the software is not there.",{"data":227,"body":228},{},{"type":6,"children":229},[230],{"type":9,"tag":231,"props":232,"children":234},"terminal-teaser",{":lines":233},"[{\"cmd\":\"docker scout quickview node:22\",\"out\":\"node:22            1C  4H  38M  91L\"},{\"cmd\":\"docker scout quickview node:22-alpine\",\"out\":\"node:22-alpine     0C  1H   4M  11L\"},{\"cmd\":\"docker scout quickview gcr.io/distroless/nodejs22-debian12\",\"out\":\"distroless/nodejs22 0C  0H   1M   2L\"}]",[],{"data":236,"body":237},{},{"type":6,"children":238},[239],{"type":9,"tag":240,"props":241,"children":245},"quiz",{":answer":211,":options":242,"explanation":243,"question":244},"[\"It ships far fewer packages, so there is less installed software for a scanner to find problems in\",\"Its packages are patched more aggressively\",\"Scanners cannot analyse distroless images\"]","Nothing was fixed — the shell, package manager, and dozens of utilities simply are not present. Which is also the security argument: an attacker with code execution has no `curl`, no `apt`, and no shell to pivot with.","Why does a distroless image report far fewer CVEs than a Debian-based one running the same application?",[],{"data":247,"body":248},{},{"type":6,"children":249},[250,256,269,274,307,347],{"type":9,"tag":21,"props":251,"children":253},{"id":252},"distroless-in-practice",[254],{"type":14,"value":255},"Distroless in practice",{"type":9,"tag":257,"props":258,"children":264},"pre",{"className":259,"code":261,"language":262,"meta":263},[260],"language-dockerfile","# syntax=docker/dockerfile:1\n\nFROM node:22-alpine AS build\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci --omit=dev\nCOPY . .\nRUN npm run build\n\nFROM gcr.io/distroless/nodejs22-debian12\nWORKDIR /app\nCOPY --from=build /app/node_modules ./node_modules\nCOPY --from=build /app/dist ./dist\nUSER nonroot\nCMD [\"dist/server.js\"]\n","dockerfile","",[265],{"type":9,"tag":72,"props":266,"children":267},{"__ignoreMap":263},[268],{"type":14,"value":261},{"type":9,"tag":10,"props":270,"children":271},{},[272],{"type":14,"value":273},"Three things differ from a normal image and each catches people once.",{"type":9,"tag":10,"props":275,"children":276},{},[277,282,284,290,292,298,300,305],{"type":9,"tag":162,"props":278,"children":279},{},[280],{"type":14,"value":281},"There is no shell",{"type":14,"value":283},", so ",{"type":9,"tag":72,"props":285,"children":287},{"className":286},[],[288],{"type":14,"value":289},"CMD",{"type":14,"value":291}," cannot use shell form and there is no ",{"type":9,"tag":72,"props":293,"children":295},{"className":294},[],[296],{"type":14,"value":297},"sh -c",{"type":14,"value":299},". The entrypoint is the runtime itself, which is why ",{"type":9,"tag":72,"props":301,"children":303},{"className":302},[],[304],{"type":14,"value":289},{"type":14,"value":306}," is a script path rather than a command.",{"type":9,"tag":10,"props":308,"children":309},{},[310,321,323,329,331,337,339,345],{"type":9,"tag":162,"props":311,"children":312},{},[313,319],{"type":9,"tag":72,"props":314,"children":316},{"className":315},[],[317],{"type":14,"value":318},"docker exec",{"type":14,"value":320}," gets you nothing",{"type":14,"value":322},", because there is nothing to exec. Debugging is ",{"type":9,"tag":72,"props":324,"children":326},{"className":325},[],[327],{"type":14,"value":328},"docker debug",{"type":14,"value":330}," (Docker Desktop attaches a toolbox without altering the image), or ",{"type":9,"tag":72,"props":332,"children":334},{"className":333},[],[335],{"type":14,"value":336},"nsenter",{"type":14,"value":338}," from the host into the container's namespaces with binaries from the host — the technique from ",{"type":9,"tag":340,"props":341,"children":342},"em",{},[343],{"type":14,"value":344},"Containers From Scratch",{"type":14,"value":346},".",{"type":9,"tag":10,"props":348,"children":349},{},[350,370],{"type":9,"tag":162,"props":351,"children":352},{},[353,355,361,363,369],{"type":14,"value":354},"Healthchecks cannot use ",{"type":9,"tag":72,"props":356,"children":358},{"className":357},[],[359],{"type":14,"value":360},"CMD-SHELL",{"type":14,"value":362}," or ",{"type":9,"tag":72,"props":364,"children":366},{"className":365},[],[367],{"type":14,"value":368},"curl",{"type":14,"value":346},{"type":14,"value":371}," Either ship a small static healthcheck binary, or move the check outside the container entirely.",{"data":373,"body":374},{},{"type":6,"children":375},[376,382,387,460,465,474,493],{"type":9,"tag":21,"props":377,"children":379},{"id":378},"docker-hardened-images",[380],{"type":14,"value":381},"Docker Hardened Images",{"type":9,"tag":10,"props":383,"children":384},{},[385],{"type":14,"value":386},"Docker's own line of minimal images, aimed at the compliance side of the problem rather than only the size side. What they commit to:",{"type":9,"tag":388,"props":389,"children":390},"ul",{},[391,402,420,430,440,450],{"type":9,"tag":392,"props":393,"children":394},"li",{},[395,400],{"type":9,"tag":162,"props":396,"children":397},{},[398],{"type":14,"value":399},"Minimal surface",{"type":14,"value":401}," — distroless variants strip the majority of what a normal base carries.",{"type":9,"tag":392,"props":403,"children":404},{},[405,410,412,418],{"type":9,"tag":162,"props":406,"children":407},{},[408],{"type":14,"value":409},"Non-root by default",{"type":14,"value":411},", so the ",{"type":9,"tag":72,"props":413,"children":415},{"className":414},[],[416],{"type":14,"value":417},"USER",{"type":14,"value":419}," decision is made for you.",{"type":9,"tag":392,"props":421,"children":422},{},[423,428],{"type":9,"tag":162,"props":424,"children":425},{},[426],{"type":14,"value":427},"Continuously patched",{"type":14,"value":429},", targeting near-zero known CVEs.",{"type":9,"tag":392,"props":431,"children":432},{},[433,438],{"type":9,"tag":162,"props":434,"children":435},{},[436],{"type":14,"value":437},"Signed, with verifiable SBOMs and SLSA Build L3 provenance",{"type":14,"value":439}," on every image.",{"type":9,"tag":392,"props":441,"children":442},{},[443,448],{"type":9,"tag":162,"props":444,"children":445},{},[446],{"type":14,"value":447},"VEX statements included",{"type":14,"value":449},", so the findings that remain arrive with justifications attached.",{"type":9,"tag":392,"props":451,"children":452},{},[453,458],{"type":9,"tag":162,"props":454,"children":455},{},[456],{"type":14,"value":457},"Full, unsuppressed CVE visibility",{"type":14,"value":459}," — the findings are published rather than filtered, which is the part that makes the near-zero claim checkable.",{"type":9,"tag":10,"props":461,"children":462},{},[463],{"type":14,"value":464},"They are used like any other base:",{"type":9,"tag":257,"props":466,"children":469},{"className":467,"code":468,"language":262,"meta":263},[260],"FROM \u003Cyour-org>/dhi-node:22-alpine3.22 AS build\n...\nFROM \u003Cyour-org>/dhi-node:22-alpine3.22-runtime\n",[470],{"type":9,"tag":72,"props":471,"children":472},{"__ignoreMap":263},[473],{"type":14,"value":468},{"type":9,"tag":10,"props":475,"children":476},{},[477,479,484,486,491],{"type":14,"value":478},"The pattern to notice is the pairing: a ",{"type":9,"tag":162,"props":480,"children":481},{},[482],{"type":14,"value":483},"build",{"type":14,"value":485}," variant with a toolchain and a ",{"type":9,"tag":162,"props":487,"children":488},{},[489],{"type":14,"value":490},"runtime",{"type":14,"value":492}," variant without one. Mixing them up produces either a build that cannot compile or a runtime carrying a compiler, and the second is the one nobody notices.",{"type":9,"tag":10,"props":494,"children":495},{},[496,502],{"type":9,"tag":72,"props":497,"children":499},{"className":498},[],[500],{"type":14,"value":501},"docker dhi",{"type":14,"value":503}," is the CLI plugin for browsing and managing them. Docker Hardened Images are a paid product; the techniques in this lesson are not, and distroless plus the previous lesson's practices gets most of the way for free.",{"data":505,"body":506},{},{"type":6,"children":507},[508],{"type":9,"tag":240,"props":509,"children":513},{":answer":211,":options":510,"explanation":511,"question":512},"[\"A justification for why a remaining finding is not exploitable, which is auditable\",\"A guarantee the image has no vulnerabilities\",\"Automatic patching of the affected package\"]","A low count can be achieved by suppression. VEX publishes the finding *and* the reasoning — \"vulnerable code not in execute path\" — which is a claim a reviewer can check and disagree with. Suppression is a claim nobody can see.","What does a VEX statement add that a low CVE count does not?",[],{"data":515,"body":516},{},{"type":6,"children":517},[518,524,529,601],{"type":9,"tag":21,"props":519,"children":521},{"id":520},"choosing-honestly",[522],{"type":14,"value":523},"Choosing, honestly",{"type":9,"tag":10,"props":525,"children":526},{},[527],{"type":14,"value":528},"Match the base to how the service is actually operated:",{"type":9,"tag":388,"props":530,"children":531},{},[532,542,568,585],{"type":9,"tag":392,"props":533,"children":534},{},[535,540],{"type":9,"tag":162,"props":536,"children":537},{},[538],{"type":14,"value":539},"A service you deploy weekly and debug rarely",{"type":14,"value":541}," — distroless or hardened. The debugging cost is paid seldom and the surface reduction is permanent.",{"type":9,"tag":392,"props":543,"children":544},{},[545,550,552,558,560,566],{"type":9,"tag":162,"props":546,"children":547},{},[548],{"type":14,"value":549},"A service under active development",{"type":14,"value":551}," — Alpine or ",{"type":9,"tag":72,"props":553,"children":555},{"className":554},[],[556],{"type":14,"value":557},"-slim",{"type":14,"value":559},". Being able to ",{"type":9,"tag":72,"props":561,"children":563},{"className":562},[],[564],{"type":14,"value":565},"exec",{"type":14,"value":567}," in matters more right now, and you can tighten later.",{"type":9,"tag":392,"props":569,"children":570},{},[571,576,578,583],{"type":9,"tag":162,"props":572,"children":573},{},[574],{"type":14,"value":575},"A single static binary",{"type":14,"value":577}," — ",{"type":9,"tag":72,"props":579,"children":581},{"className":580},[],[582],{"type":14,"value":206},{"type":14,"value":584},", with the CA bundle copied in.",{"type":9,"tag":392,"props":586,"children":587},{},[588,593,594,599],{"type":9,"tag":162,"props":589,"children":590},{},[591],{"type":14,"value":592},"Something needing glibc, a distro package, or ancient dependencies",{"type":14,"value":577},{"type":9,"tag":72,"props":595,"children":597},{"className":596},[],[598],{"type":14,"value":104},{"type":14,"value":600},". Fighting musl to save 40 MB is not a good trade.",{"type":9,"tag":10,"props":602,"children":603},{},[604],{"type":14,"value":605},"The mistake worth avoiding is adopting distroless for a service the team debugs interactively every week. The image is more secure and the on-call experience is worse, and what happens next is somebody adds a shell back \"temporarily\".",{"data":607,"body":608},{},{"type":6,"children":609},[610],{"type":9,"tag":611,"props":612,"children":617},"fill-blank",{":answer":613,"hint":614,"placeholder":615,"prompt":616},"[\"docker scout quickview node:22-alpine\",\"docker scout quickview node:22-alpine --org acme\"]","The Scout subcommand that gives the three-row summary.","docker scout ...","Get a one-line vulnerability summary for the image `node:22-alpine`.",[],{"data":619,"body":620},{},{"type":6,"children":621},[622,759],{"type":9,"tag":623,"props":624,"children":626},"deep-dive",{"title":625},"Where the CA certificates went",[627,639,649,658,689,698,721,747],{"type":9,"tag":10,"props":628,"children":629},{},[630,632,637],{"type":14,"value":631},"The ",{"type":9,"tag":72,"props":633,"children":635},{"className":634},[],[636],{"type":14,"value":206},{"type":14,"value":638}," image is genuinely empty, and the failures that produces do not look like emptiness.",{"type":9,"tag":10,"props":640,"children":641},{},[642,647],{"type":9,"tag":162,"props":643,"children":644},{},[645],{"type":14,"value":646},"No CA bundle",{"type":14,"value":648},", so every outbound HTTPS call fails with a certificate verification error. The application looks broken; the network looks fine.",{"type":9,"tag":257,"props":650,"children":653},{"className":651,"code":652,"language":262,"meta":263},[260],"COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/\n",[654],{"type":9,"tag":72,"props":655,"children":656},{"__ignoreMap":263},[657],{"type":14,"value":652},{"type":9,"tag":10,"props":659,"children":660},{},[661,672,674,679,681,687],{"type":9,"tag":162,"props":662,"children":663},{},[664,666],{"type":14,"value":665},"No ",{"type":9,"tag":72,"props":667,"children":669},{"className":668},[],[670],{"type":14,"value":671},"/etc/passwd",{"type":14,"value":673},", so a numeric ",{"type":9,"tag":72,"props":675,"children":677},{"className":676},[],[678],{"type":14,"value":417},{"type":14,"value":680}," works and a named one does not, and any library calling ",{"type":9,"tag":72,"props":682,"children":684},{"className":683},[],[685],{"type":14,"value":686},"getpwuid()",{"type":14,"value":688}," fails. Synthesise one:",{"type":9,"tag":257,"props":690,"children":693},{"className":691,"code":692,"language":262,"meta":263},[260],"RUN echo 'app:x:10001:10001::/nonexistent:/sbin/nologin' > /etc/passwd.min\n# then, in the final stage:\nCOPY --from=build /etc/passwd.min /etc/passwd\n",[694],{"type":9,"tag":72,"props":695,"children":696},{"__ignoreMap":263},[697],{"type":14,"value":692},{"type":9,"tag":10,"props":699,"children":700},{},[701,711,713,719],{"type":9,"tag":162,"props":702,"children":703},{},[704,705],{"type":14,"value":665},{"type":9,"tag":72,"props":706,"children":708},{"className":707},[],[709],{"type":14,"value":710},"/tmp",{"type":14,"value":712},", and no timezone database, so anything formatting a local time gets UTC or an error. ",{"type":9,"tag":72,"props":714,"children":716},{"className":715},[],[717],{"type":14,"value":718},"tzdata",{"type":14,"value":720}," copies in the same way.",{"type":9,"tag":10,"props":722,"children":723},{},[724,729,731,737,739,745],{"type":9,"tag":162,"props":725,"children":726},{},[727],{"type":14,"value":728},"No DNS resolver configuration behaviour you expect",{"type":14,"value":730}," — Go binaries built with ",{"type":9,"tag":72,"props":732,"children":734},{"className":733},[],[735],{"type":14,"value":736},"CGO_ENABLED=0",{"type":14,"value":738}," use the pure-Go resolver and are fine; a cgo-linked binary wants ",{"type":9,"tag":72,"props":740,"children":742},{"className":741},[],[743],{"type":14,"value":744},"/etc/nsswitch.conf",{"type":14,"value":746}," and glibc's NSS modules, which are not there.",{"type":9,"tag":10,"props":748,"children":749},{},[750,752,757],{"type":14,"value":751},"The common thread: ",{"type":9,"tag":72,"props":753,"children":755},{"className":754},[],[756],{"type":14,"value":206},{"type":14,"value":758}," removes the parts of a userland that everything quietly assumes. It is excellent for a static Go or Rust binary that makes no outbound TLS calls, and a series of small archaeological discoveries for anything else. Distroless exists precisely to be the version of this that has already made those discoveries for you.",{"type":9,"tag":10,"props":760,"children":761},{},[762],{"type":14,"value":763},"Next up: signing and trust — proving an image is the one you built.",1787908868444]