[{"data":1,"prerenderedAt":602},["ShallowReactive",2],{"mdc--t8l774-key":3,"mdc-16o3lq-key":28,"mdc-3j4zrn-key":126,"mdc-tjw0ug-key":135,"mdc--o4phxq-key":211,"mdc-z8b6z1-key":267,"mdc-7hj7y0-key":279,"mdc-avcl6y-key":371,"mdc-egef80-key":425,"mdc--am80rk-key":435,"mdc-t9k5bn-key":466,"mdc--bfvzxj-key":478},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8,23],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13,16,21],{"type":14,"value":15},"text","A container's default security posture is better than a bare process and considerably worse than most people assume. It runs as ",{"type":9,"tag":17,"props":18,"children":19},"strong",{},[20],{"type":14,"value":6},{"type":14,"value":22}," — the host's root, with a reduced but still substantial capability set — with a permissive seccomp filter and a writable filesystem.",{"type":9,"tag":10,"props":24,"children":25},{},[26],{"type":14,"value":27},"None of that is required by anything you are likely to run. This lesson is turning it off.",{"data":29,"body":30},{},{"type":6,"children":31},[32,39,53,81,90],{"type":9,"tag":33,"props":34,"children":36},"h2",{"id":35},"do-not-run-as-root",[37],{"type":14,"value":38},"Do not run as root",{"type":9,"tag":40,"props":41,"children":47},"pre",{"className":42,"code":44,"language":45,"meta":46},[43],"language-dockerfile","FROM node:22-alpine\nWORKDIR /app\nCOPY --chown=node:node . .\nUSER node\nCMD [\"node\", \"server.js\"]\n","dockerfile","",[48],{"type":9,"tag":49,"props":50,"children":51},"code",{"__ignoreMap":46},[52],{"type":14,"value":44},{"type":9,"tag":10,"props":54,"children":55},{},[56,58,64,66,72,73,79],{"type":14,"value":57},"Most official images ship a suitable non-root user already — ",{"type":9,"tag":49,"props":59,"children":61},{"className":60},[],[62],{"type":14,"value":63},"node",{"type":14,"value":65},", ",{"type":9,"tag":49,"props":67,"children":69},{"className":68},[],[70],{"type":14,"value":71},"postgres",{"type":14,"value":65},{"type":9,"tag":49,"props":74,"children":76},{"className":75},[],[77],{"type":14,"value":78},"nginx",{"type":14,"value":80},". Where one does not, make it, and use a high fixed UID so it cannot collide with a host account that means something:",{"type":9,"tag":40,"props":82,"children":85},{"className":83,"code":84,"language":45,"meta":46},[43],"RUN adduser -D -u 10001 app\nUSER 10001\n",[86],{"type":9,"tag":49,"props":87,"children":88},{"__ignoreMap":46},[89],{"type":14,"value":84},{"type":9,"tag":10,"props":91,"children":92},{},[93,95,100,102,108,110,116,118,124],{"type":14,"value":94},"Prefer the ",{"type":9,"tag":17,"props":96,"children":97},{},[98],{"type":14,"value":99},"numeric",{"type":14,"value":101}," form in ",{"type":9,"tag":49,"props":103,"children":105},{"className":104},[],[106],{"type":14,"value":107},"USER",{"type":14,"value":109},". Kubernetes' ",{"type":9,"tag":49,"props":111,"children":113},{"className":112},[],[114],{"type":14,"value":115},"runAsNonRoot",{"type":14,"value":117}," check reads the image config and cannot tell whether a ",{"type":9,"tag":119,"props":120,"children":121},"em",{},[122],{"type":14,"value":123},"name",{"type":14,"value":125}," resolves to UID 0, so a named user is rejected by some policies even when it is fine.",{"data":127,"body":128},{},{"type":6,"children":129},[130],{"type":9,"tag":131,"props":132,"children":134},"terminal-teaser",{":lines":133},"[{\"cmd\":\"docker run --rm alpine id\",\"out\":\"uid=0(root) gid=0(root) groups=0(root)\"},{\"cmd\":\"docker run --rm --user 10001:10001 alpine id\",\"out\":\"uid=10001 gid=10001\"},{\"cmd\":\"docker run --rm alpine capsh --print | head -2\",\"out\":\"Current: cap_chown,cap_dac_override,cap_fowner,cap_setgid,cap_setuid,\\ncap_net_bind_service,cap_net_raw,cap_sys_chroot,...\"}]",[],{"data":136,"body":137},{},{"type":6,"children":138},[139,145,150,160,171,184],{"type":9,"tag":33,"props":140,"children":142},{"id":141},"drop-capabilities",[143],{"type":14,"value":144},"Drop capabilities",{"type":9,"tag":10,"props":146,"children":147},{},[148],{"type":14,"value":149},"Root inside a container has around fourteen of Linux's forty-odd capabilities. A web application needs none of them.",{"type":9,"tag":40,"props":151,"children":155},{"className":152,"code":154,"language":14},[153],"language-text","docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp\n",[156],{"type":9,"tag":49,"props":157,"children":158},{"__ignoreMap":46},[159],{"type":14,"value":154},{"type":9,"tag":40,"props":161,"children":166},{"className":162,"code":164,"language":165,"meta":46},[163],"language-yaml","services:\n  api:\n    cap_drop: [ALL]\n    cap_add: [NET_BIND_SERVICE]\n","yaml",[167],{"type":9,"tag":49,"props":168,"children":169},{"__ignoreMap":46},[170],{"type":14,"value":164},{"type":9,"tag":10,"props":172,"children":173},{},[174,176,182],{"type":14,"value":175},"Drop everything, then add back only what actually fails. In practice most services need nothing at all — ",{"type":9,"tag":49,"props":177,"children":179},{"className":178},[],[180],{"type":14,"value":181},"NET_BIND_SERVICE",{"type":14,"value":183}," only matters for binding below port 1024, and the better answer there is to listen on 8080 and publish it as 80.",{"type":9,"tag":10,"props":185,"children":186},{},[187,189,198,200,209],{"type":14,"value":188},"Two that deserve naming because they are the ones granted casually and they are the ones that end badly. ",{"type":9,"tag":17,"props":190,"children":191},{},[192],{"type":9,"tag":49,"props":193,"children":195},{"className":194},[],[196],{"type":14,"value":197},"SYS_ADMIN",{"type":14,"value":199}," is close to root on the host — mounting filesystems, manipulating namespaces — and is the usual \"just add this to make it work\" for anything that mounts. ",{"type":9,"tag":17,"props":201,"children":202},{},[203],{"type":9,"tag":49,"props":204,"children":206},{"className":205},[],[207],{"type":14,"value":208},"SYS_PTRACE",{"type":14,"value":210}," lets a process inspect and manipulate others in its namespace; useful for a debugger, not for a service.",{"data":212,"body":213},{},{"type":6,"children":214},[215,227,236,253,258],{"type":9,"tag":33,"props":216,"children":218},{"id":217},"privileged-is-not-a-debugging-step",[219,225],{"type":9,"tag":49,"props":220,"children":222},{"className":221},[],[223],{"type":14,"value":224},"--privileged",{"type":14,"value":226}," is not a debugging step",{"type":9,"tag":40,"props":228,"children":231},{"className":229,"code":230,"language":14},[153],"docker run --privileged myapp        # don't\n",[232],{"type":9,"tag":49,"props":233,"children":234},{"__ignoreMap":46},[235],{"type":14,"value":230},{"type":9,"tag":10,"props":237,"children":238},{},[239,244,246,251],{"type":9,"tag":49,"props":240,"children":242},{"className":241},[],[243],{"type":14,"value":224},{"type":14,"value":245}," drops ",{"type":9,"tag":17,"props":247,"children":248},{},[249],{"type":14,"value":250},"all",{"type":14,"value":252}," of it: every capability granted, seccomp and AppArmor disabled, all host devices exposed. A privileged container can load kernel modules, read raw disks, and reach the host trivially. It is the single largest security decision available in one flag, and it is most often typed by someone narrowing down a permission error.",{"type":9,"tag":10,"props":254,"children":255},{},[256],{"type":14,"value":257},"The right move when something needs a privilege is to find the specific one:",{"type":9,"tag":40,"props":259,"children":262},{"className":260,"code":261,"language":14},[153],"docker run --cap-add SYS_TIME myapp          # not --privileged\ndocker run --device /dev/ttyUSB0 myapp       # not --privileged\n",[263],{"type":9,"tag":49,"props":264,"children":265},{"__ignoreMap":46},[266],{"type":14,"value":261},{"data":268,"body":269},{},{"type":6,"children":270},[271],{"type":9,"tag":272,"props":273,"children":278},"quiz",{":answer":274,":options":275,"explanation":276,"question":277},"0","[\"Identify the specific capability or device needed and grant only that\",\"Add `--privileged` and revisit later\",\"Run the container as root instead\"]","`--privileged` grants every capability and disables seccomp and AppArmor at once, and \"revisit later\" reliably means never. `--cap-add SYS_ADMIN`, or a `--device`, is narrower — and if the answer really is SYS_ADMIN, that is worth knowing rather than hiding.","A container fails with \"operation not permitted\" on a mount. A colleague suggests `--privileged`. What is the better response?",[],{"data":280,"body":281},{},{"type":6,"children":282},[283,289,330,335,344,349,358],{"type":9,"tag":33,"props":284,"children":286},{"id":285},"seccomp-apparmor-and-no-new-privileges",[287],{"type":14,"value":288},"Seccomp, AppArmor, and no-new-privileges",{"type":9,"tag":10,"props":290,"children":291},{},[292,294,299,301,307,308,314,315,321,323,328],{"type":14,"value":293},"Docker applies a ",{"type":9,"tag":17,"props":295,"children":296},{},[297],{"type":14,"value":298},"default seccomp profile",{"type":14,"value":300}," blocking around forty syscalls that no normal workload uses — ",{"type":9,"tag":49,"props":302,"children":304},{"className":303},[],[305],{"type":14,"value":306},"kexec_load",{"type":14,"value":65},{"type":9,"tag":49,"props":309,"children":311},{"className":310},[],[312],{"type":14,"value":313},"mount",{"type":14,"value":65},{"type":9,"tag":49,"props":316,"children":318},{"className":317},[],[319],{"type":14,"value":320},"ptrace",{"type":14,"value":322},", and the rest of the kernel's attack surface. It is on unless you disable it, which ",{"type":9,"tag":49,"props":324,"children":326},{"className":325},[],[327],{"type":14,"value":224},{"type":14,"value":329}," does silently.",{"type":9,"tag":10,"props":331,"children":332},{},[333],{"type":14,"value":334},"A custom profile, when you know what your workload calls:",{"type":9,"tag":40,"props":336,"children":339},{"className":337,"code":338,"language":14},[153],"docker run --security-opt seccomp=./profile.json myapp\n",[340],{"type":9,"tag":49,"props":341,"children":342},{"__ignoreMap":46},[343],{"type":14,"value":338},{"type":9,"tag":10,"props":345,"children":346},{},[347],{"type":14,"value":348},"And the flag worth adding to essentially everything:",{"type":9,"tag":40,"props":350,"children":353},{"className":351,"code":352,"language":14},[153],"docker run --security-opt no-new-privileges myapp\n",[354],{"type":9,"tag":49,"props":355,"children":356},{"__ignoreMap":46},[357],{"type":14,"value":352},{"type":9,"tag":10,"props":359,"children":360},{},[361,363,369],{"type":14,"value":362},"That sets the kernel's ",{"type":9,"tag":49,"props":364,"children":366},{"className":365},[],[367],{"type":14,"value":368},"no_new_privs",{"type":14,"value":370}," bit: the process and its children can never gain privileges through a setuid binary. It costs nothing and closes the most common privilege-escalation path inside a container.",{"data":372,"body":373},{},{"type":6,"children":374},[375,381,390,399,420],{"type":9,"tag":33,"props":376,"children":378},{"id":377},"read-only-root-filesystem",[379],{"type":14,"value":380},"Read-only root filesystem",{"type":9,"tag":40,"props":382,"children":385},{"className":383,"code":384,"language":14},[153],"docker run --read-only --tmpfs /tmp --tmpfs /run myapp\n",[386],{"type":9,"tag":49,"props":387,"children":388},{"__ignoreMap":46},[389],{"type":14,"value":384},{"type":9,"tag":40,"props":391,"children":394},{"className":392,"code":393,"language":165,"meta":46},[163],"services:\n  api:\n    read_only: true\n    tmpfs: [/tmp, /run]\n",[395],{"type":9,"tag":49,"props":396,"children":397},{"__ignoreMap":46},[398],{"type":14,"value":393},{"type":9,"tag":10,"props":400,"children":401},{},[402,404,410,412,418],{"type":14,"value":403},"Most services never write outside ",{"type":9,"tag":49,"props":405,"children":407},{"className":406},[],[408],{"type":14,"value":409},"/tmp",{"type":14,"value":411},". Making the root filesystem read-only means an attacker who achieves code execution cannot drop a binary, modify configuration, or persist. The ",{"type":9,"tag":49,"props":413,"children":415},{"className":414},[],[416],{"type":14,"value":417},"tmpfs",{"type":14,"value":419}," mounts give back the few directories that genuinely need writing, in RAM, discarded on exit.",{"type":9,"tag":10,"props":421,"children":422},{},[423],{"type":14,"value":424},"Turning this on usually surfaces one or two surprises — a framework writing a cache, a library writing a lockfile — and each one is worth knowing about anyway.",{"data":426,"body":427},{},{"type":6,"children":428},[429],{"type":9,"tag":272,"props":430,"children":434},{":answer":274,":options":431,"explanation":432,"question":433},"[\"A process gaining privileges by executing a setuid binary\",\"The container from running as root\",\"New capabilities being added after start\"]","It sets the kernel's `no_new_privs` bit, so `execve` can never grant more privilege than the caller had. It closes the standard escalation path from a compromised unprivileged process to root inside the container, and it breaks almost nothing.","What does `--security-opt no-new-privileges` prevent?",[],{"data":436,"body":437},{},{"type":6,"children":438},[439,445,454],{"type":9,"tag":33,"props":440,"children":442},{"id":441},"a-hardened-service",[443],{"type":14,"value":444},"A hardened service",{"type":9,"tag":40,"props":446,"children":449},{"className":447,"code":448,"language":165,"meta":46},[163],"services:\n  api:\n    image: ghcr.io/acme/api@sha256:9f2a1c...\n    user: \"10001:10001\"\n    read_only: true\n    tmpfs: [/tmp]\n    cap_drop: [ALL]\n    security_opt:\n      - no-new-privileges:true\n    pids_limit: 200\n    mem_limit: 512m\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD\", \"/healthcheck\"]\n      interval: 30s\n      start_period: 20s\n",[450],{"type":9,"tag":49,"props":451,"children":452},{"__ignoreMap":46},[453],{"type":14,"value":448},{"type":9,"tag":10,"props":455,"children":456},{},[457,459,464],{"type":14,"value":458},"Every line is a decision this course has argued for. Note the image is pinned by ",{"type":9,"tag":17,"props":460,"children":461},{},[462],{"type":14,"value":463},"digest",{"type":14,"value":465},", not tag — a later lesson makes that case.",{"data":467,"body":468},{},{"type":6,"children":469},[470],{"type":9,"tag":471,"props":472,"children":477},"fill-blank",{":answer":473,"hint":474,"placeholder":475,"prompt":476},"[\"docker run --cap-drop ALL myapp\",\"docker run --cap-drop=ALL myapp\",\"docker run --cap-drop all myapp\"]","One flag, and the value that means everything.","docker run ...","Run `myapp` dropping all Linux capabilities.",[],{"data":479,"body":480},{},{"type":6,"children":481},[482,597],{"type":9,"tag":483,"props":484,"children":486},"deep-dive",{"title":485},"Rootless mode and user namespaces",[487,499,509,518,531,545,556,561,585],{"type":9,"tag":10,"props":488,"children":489},{},[490,492,497],{"type":14,"value":491},"Everything above hardens the container. Two options harden the ",{"type":9,"tag":119,"props":493,"children":494},{},[495],{"type":14,"value":496},"daemon",{"type":14,"value":498},", and they address the fact that a container escape normally lands you as real root on the host.",{"type":9,"tag":10,"props":500,"children":501},{},[502,507],{"type":9,"tag":17,"props":503,"children":504},{},[505],{"type":14,"value":506},"Rootless mode",{"type":14,"value":508}," runs the whole daemon as an unprivileged user:",{"type":9,"tag":40,"props":510,"children":513},{"className":511,"code":512,"language":14},[153],"dockerd-rootless-setuptool.sh install\nexport DOCKER_HOST=unix:///run/user/1000/docker.sock\n",[514],{"type":9,"tag":49,"props":515,"children":516},{"__ignoreMap":46},[517],{"type":14,"value":512},{"type":9,"tag":10,"props":519,"children":520},{},[521,523,529],{"type":14,"value":522},"A breakout gets you the user's privileges and nothing more. The trade-offs are real: binding ports below 1024 needs extra configuration, some storage drivers and network features are unavailable, and there is a small performance cost. Engine 29.5 improved the networking side by switching the rootless default to the ",{"type":9,"tag":49,"props":524,"children":526},{"className":525},[],[527],{"type":14,"value":528},"gvisor-tap-vsock",{"type":14,"value":530}," driver, replacing slirp4netns.",{"type":9,"tag":10,"props":532,"children":533},{},[534,543],{"type":9,"tag":17,"props":535,"children":536},{},[537],{"type":9,"tag":49,"props":538,"children":540},{"className":539},[],[541],{"type":14,"value":542},"userns-remap",{"type":14,"value":544}," keeps a normal root daemon but maps container UIDs to an unprivileged host range:",{"type":9,"tag":40,"props":546,"children":551},{"className":547,"code":549,"language":550,"meta":46},[548],"language-json","{ \"userns-remap\": \"default\" }\n","json",[552],{"type":9,"tag":49,"props":553,"children":554},{"__ignoreMap":46},[555],{"type":14,"value":549},{"type":9,"tag":10,"props":557,"children":558},{},[559],{"type":14,"value":560},"Root inside the container is UID 100000 outside. Cheaper to adopt than rootless and covers the main risk.",{"type":9,"tag":10,"props":562,"children":563},{},[564,569,571,576,578,583],{"type":9,"tag":17,"props":565,"children":566},{},[567],{"type":14,"value":568},"The catch, and it is current:",{"type":14,"value":570}," daemons using ",{"type":9,"tag":49,"props":572,"children":574},{"className":573},[],[575],{"type":14,"value":542},{"type":14,"value":577}," do ",{"type":9,"tag":17,"props":579,"children":580},{},[581],{"type":14,"value":582},"not",{"type":14,"value":584}," get the containerd image store, because of an unresolved interaction between the two. So on Engine 29 you are choosing between UID remapping and the new store's multi-platform and attestation support. Worth knowing before you find out during a migration.",{"type":9,"tag":10,"props":586,"children":587},{},[588,590,595],{"type":14,"value":589},"The honest summary: ",{"type":9,"tag":17,"props":591,"children":592},{},[593],{"type":14,"value":594},"a container is an isolation boundary, and hardening is what moves it toward being a security boundary.",{"type":14,"value":596}," For genuinely untrusted code — running someone else's build, executing user-submitted programs — the answer is a VM boundary: Firecracker, gVisor, Kata. Docker was not designed to be the last line of defence against code that is actively trying to escape.",{"type":9,"tag":10,"props":598,"children":599},{},[600],{"type":14,"value":601},"Next up: resource limits — capping what a container can consume before it takes the host with it.",1787908868422]