[{"data":1,"prerenderedAt":575},["ShallowReactive",2],{"mdc--mvy4dg-key":3,"mdc--2cv3ww-key":16,"mdc-nun1im-key":50,"mdc--tbmn8i-key":271,"mdc--i0g7ea-key":283,"mdc-7m3u98-key":293,"mdc--2u66rt-key":312,"mdc--vwp42z-key":327,"mdc--8xt4lo-key":348,"mdc-lh41vf-key":358},{"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","Six lessons, six mechanisms, all of them independent. This is the one where they compose — a single process wrapped in every layer at once, which is a container and nothing more than a container.",{"data":17,"body":18},{},{"type":6,"children":19},[20,27,32,45],{"type":9,"tag":21,"props":22,"children":24},"h2",{"id":23},"the-whole-setup-in-order",[25],{"type":14,"value":26},"The whole setup, in order",{"type":9,"tag":10,"props":28,"children":29},{},[30],{"type":14,"value":31},"Assuming a fresh machine, here is everything from the previous lessons in the order it has to happen:",{"type":9,"tag":33,"props":34,"children":38},"pre",{"className":35,"code":37,"language":14},[36],"language-text","CONTAINER_ID=\"my-container\"\nDIF=\"wlan0\"                    # your real uplink: ip route | grep default\nBRIDGE_IFACE=\"bridge0\"\n\n# --- 1. Copy-on-write root filesystem ------------------------------\ntruncate -s 5G ./btrfs-disk.img\nmkfs.btrfs -f ./btrfs-disk.img\nmkdir -p ./btrfs-mount\nsudo mount -o loop ./btrfs-disk.img ./btrfs-mount\n\nsudo btrfs subvolume create ./btrfs-mount/base-image\ncurl -o alpine.tar.gz \\\n  https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/x86_64/alpine-minirootfs-3.19.1-x86_64.tar.gz\nsudo tar -xf alpine.tar.gz -C ./btrfs-mount/base-image\n\nsudo btrfs subvolume snapshot \\\n  ./btrfs-mount/base-image \\\n  ./btrfs-mount/$CONTAINER_ID\n\n# --- 2. Bridge and veth pair ---------------------------------------\nsudo ip link add name bridge0 type bridge\nsudo ip addr add 10.0.0.1/24 dev bridge0\nsudo ip link set bridge0 up\n\nsudo ip link add dev veth_host type veth peer name veth_cont\nsudo ip link set veth_host master bridge0\nsudo ip link set veth_host up\n\n# --- 3. Network namespace ------------------------------------------\nsudo ip netns add netns_$CONTAINER_ID\nsudo ip link set veth_cont netns netns_$CONTAINER_ID\n\nsudo ip netns exec netns_$CONTAINER_ID ip link set dev lo up\nsudo ip netns exec netns_$CONTAINER_ID ip addr add 10.0.0.2/24 dev veth_cont\nsudo ip netns exec netns_$CONTAINER_ID ip link set dev veth_cont up\nsudo ip netns exec netns_$CONTAINER_ID ip route add default via 10.0.0.1\n\nsudo bash -c \"echo 'nameserver 8.8.8.8' > ./btrfs-mount/$CONTAINER_ID/etc/resolv.conf\"\n\n# --- 4. NAT ---------------------------------------------------------\nsudo sysctl -w net.ipv4.ip_forward=1\nsudo iptables -t nat -A POSTROUTING -o $DIF -j MASQUERADE\nsudo iptables -A FORWARD -i $BRIDGE_IFACE -o $DIF -j ACCEPT\nsudo iptables -A FORWARD -o $BRIDGE_IFACE -m state --state RELATED,ESTABLISHED -j ACCEPT\n\n# --- 5. cgroup ------------------------------------------------------\nsudo cgcreate -g cpu,memory:/$CONTAINER_ID\nsudo cgset -r cpu.weight=50 $CONTAINER_ID\nsudo cgset -r memory.max=536870912 $CONTAINER_ID\n",[39],{"type":9,"tag":40,"props":41,"children":43},"code",{"__ignoreMap":42},"",[44],{"type":14,"value":37},{"type":9,"tag":10,"props":46,"children":47},{},[48],{"type":14,"value":49},"Nothing new. Every line has appeared in a previous lesson.",{"data":51,"body":52},{},{"type":6,"children":53},[54,60,69,82,235],{"type":9,"tag":21,"props":55,"children":57},{"id":56},"the-command",[58],{"type":14,"value":59},"The command",{"type":9,"tag":33,"props":61,"children":64},{"className":62,"code":63,"language":14},[36],"sudo cgexec -g cpu,memory:$CONTAINER_ID \\\n  ip netns exec netns_$CONTAINER_ID \\\n  unshare --fork --pid --mount --uts --ipc --mount-proc \\\n  chroot ./btrfs-mount/$CONTAINER_ID \\\n  /bin/sh -c \"mount -t proc proc /proc && hostname $CONTAINER_ID && /bin/sh\"\n",[65],{"type":9,"tag":40,"props":66,"children":67},{"__ignoreMap":42},[68],{"type":14,"value":63},{"type":9,"tag":10,"props":70,"children":71},{},[72,74,80],{"type":14,"value":73},"Read it from the outside in. Each program does one thing to the environment and then executes the next one, so the layers nest — and by the time the innermost ",{"type":9,"tag":40,"props":75,"children":77},{"className":76},[],[78],{"type":14,"value":79},"/bin/sh",{"type":14,"value":81}," starts, every cage is already around it.",{"type":9,"tag":83,"props":84,"children":85},"table",{},[86,105],{"type":9,"tag":87,"props":88,"children":89},"thead",{},[90],{"type":9,"tag":91,"props":92,"children":93},"tr",{},[94,100],{"type":9,"tag":95,"props":96,"children":97},"th",{},[98],{"type":14,"value":99},"Layer",{"type":9,"tag":95,"props":101,"children":102},{},[103],{"type":14,"value":104},"What it adds",{"type":9,"tag":106,"props":107,"children":108},"tbody",{},[109,127,144,161,186,209],{"type":9,"tag":91,"props":110,"children":111},{},[112,122],{"type":9,"tag":113,"props":114,"children":115},"td",{},[116],{"type":9,"tag":40,"props":117,"children":119},{"className":118},[],[120],{"type":14,"value":121},"cgexec -g cpu,memory:...",{"type":9,"tag":113,"props":123,"children":124},{},[125],{"type":14,"value":126},"runs the process inside the CPU and memory cgroup",{"type":9,"tag":91,"props":128,"children":129},{},[130,139],{"type":9,"tag":113,"props":131,"children":132},{},[133],{"type":9,"tag":40,"props":134,"children":136},{"className":135},[],[137],{"type":14,"value":138},"ip netns exec netns_...",{"type":9,"tag":113,"props":140,"children":141},{},[142],{"type":14,"value":143},"moves it into the isolated network stack",{"type":9,"tag":91,"props":145,"children":146},{},[147,156],{"type":9,"tag":113,"props":148,"children":149},{},[150],{"type":9,"tag":40,"props":151,"children":153},{"className":152},[],[154],{"type":14,"value":155},"unshare --fork --pid --mount --uts --ipc",{"type":9,"tag":113,"props":157,"children":158},{},[159],{"type":14,"value":160},"fresh process tree, mount table, hostname, and IPC",{"type":9,"tag":91,"props":162,"children":163},{},[164,173],{"type":9,"tag":113,"props":165,"children":166},{},[167],{"type":9,"tag":40,"props":168,"children":170},{"className":169},[],[171],{"type":14,"value":172},"--mount-proc",{"type":9,"tag":113,"props":174,"children":175},{},[176,178,184],{"type":14,"value":177},"remounts ",{"type":9,"tag":40,"props":179,"children":181},{"className":180},[],[182],{"type":14,"value":183},"/proc",{"type":14,"value":185}," so the new PID view is visible",{"type":9,"tag":91,"props":187,"children":188},{},[189,198],{"type":9,"tag":113,"props":190,"children":191},{},[192],{"type":9,"tag":40,"props":193,"children":195},{"className":194},[],[196],{"type":14,"value":197},"chroot ./btrfs-mount/...",{"type":9,"tag":113,"props":199,"children":200},{},[201,203],{"type":14,"value":202},"makes the btrfs snapshot the new ",{"type":9,"tag":40,"props":204,"children":206},{"className":205},[],[207],{"type":14,"value":208},"/",{"type":9,"tag":91,"props":210,"children":211},{},[212,223],{"type":9,"tag":113,"props":213,"children":214},{},[215,217],{"type":14,"value":216},"the inner ",{"type":9,"tag":40,"props":218,"children":220},{"className":219},[],[221],{"type":14,"value":222},"/bin/sh -c",{"type":9,"tag":113,"props":224,"children":225},{},[226,228,233],{"type":14,"value":227},"mounts ",{"type":9,"tag":40,"props":229,"children":231},{"className":230},[],[232],{"type":14,"value":183},{"type":14,"value":234},", sets the hostname, starts the real shell",{"type":9,"tag":10,"props":236,"children":237},{},[238,240,246,248,253,255,261,263,269],{"type":14,"value":239},"That last line is worth naming: ",{"type":9,"tag":241,"props":242,"children":243},"strong",{},[244],{"type":14,"value":245},"it is the entrypoint",{"type":14,"value":247},". In Docker terms, everything above it is what the runtime does, and ",{"type":9,"tag":40,"props":249,"children":251},{"className":250},[],[252],{"type":14,"value":79},{"type":14,"value":254}," is your ",{"type":9,"tag":40,"props":256,"children":258},{"className":257},[],[259],{"type":14,"value":260},"ENTRYPOINT",{"type":14,"value":262}," plus ",{"type":9,"tag":40,"props":264,"children":266},{"className":265},[],[267],{"type":14,"value":268},"CMD",{"type":14,"value":270},".",{"data":272,"body":273},{},{"type":6,"children":274},[275],{"type":9,"tag":276,"props":277,"children":282},"quiz",{":answer":278,":options":279,"explanation":280,"question":281},"0","[\"So the mount of `/proc` inside the new root happens in the container's own mount namespace, not the host's\",\"Because chroot cannot run as a child of unshare\",\"Order does not matter; the layers are independent\"]","Mounting is the ordering constraint. If `chroot` ran first and the mount namespace were created afterwards, the `mount -t proc` would land in the host's mount table and stay there after the container exited. `unshare --mount` first means every mount the container makes is discarded with it.","Why must `chroot` come after `unshare --mount` rather than before it?",[],{"data":284,"body":285},{},{"type":6,"children":286},[287],{"type":9,"tag":21,"props":288,"children":290},{"id":289},"what-it-looks-like-from-inside",[291],{"type":14,"value":292},"What it looks like from inside",{"data":294,"body":295},{},{"type":6,"children":296},[297,302,307],{"type":9,"tag":298,"props":299,"children":301},"terminal-teaser",{":lines":300},"[{\"cmd\":\"hostname\",\"out\":\"my-container\"},{\"cmd\":\"ps -e\",\"out\":\"PID   USER     TIME  COMMAND\\n    1 root      0:00 /bin/sh\\n    8 root      0:00 ps -e\"},{\"cmd\":\"cat /etc/os-release\",\"out\":\"NAME=\\\"Alpine Linux\\\"\"},{\"cmd\":\"ip addr show veth_cont\",\"out\":\"inet 10.0.0.2/24 scope global veth_cont\"},{\"cmd\":\"ping -c1 8.8.8.8\",\"out\":\"64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=13.8 ms\"},{\"cmd\":\"free -m\",\"out\":\"Mem:  total 512   used 4   free 508\"}]",[],{"type":9,"tag":10,"props":303,"children":304},{},[305],{"type":14,"value":306},"Its own hostname. Two processes, its shell being PID 1. An Alpine userland on an Ubuntu kernel. Its own IP, with working internet through NAT. 512 MB of memory, on a host with far more.",{"type":9,"tag":10,"props":308,"children":309},{},[310],{"type":14,"value":311},"You have built a container. No Docker was installed at any point.",{"data":313,"body":314},{},{"type":6,"children":315},[316,322],{"type":9,"tag":21,"props":317,"children":319},{"id":318},"check-it-from-the-host",[320],{"type":14,"value":321},"Check it from the host",{"type":9,"tag":10,"props":323,"children":324},{},[325],{"type":14,"value":326},"The more convincing demonstration is from the other side:",{"data":328,"body":329},{},{"type":6,"children":330},[331,335],{"type":9,"tag":298,"props":332,"children":334},{":lines":333},"[{\"cmd\":\"ps -ef | grep '[/]bin/sh'\",\"out\":\"root  30412  30402  0 09:14 ?  00:00:00 /bin/sh\"},{\"cmd\":\"sudo ls -l /proc/30412/root\",\"out\":\"lrwxrwxrwx 1 root root 0 -> /home/you/btrfs-mount/my-container\"},{\"cmd\":\"sudo readlink /proc/30412/ns/pid /proc/self/ns/pid\",\"out\":\"pid:[4026532445]\\npid:[4026531836]\"},{\"cmd\":\"cat /proc/30412/cgroup\",\"out\":\"0::/my-container\"}]",[],{"type":9,"tag":10,"props":336,"children":337},{},[338,340,346],{"type":14,"value":339},"One process on the host, PID 30412, with a different root, a different PID namespace, and a cgroup. That is the entire difference between a container and any other process on the machine — and ",{"type":9,"tag":40,"props":341,"children":343},{"className":342},[],[344],{"type":14,"value":345},"/proc/PID/",{"type":14,"value":347}," is where you go to establish it, for a hand-built container and a Kubernetes pod alike.",{"data":349,"body":350},{},{"type":6,"children":351},[352],{"type":9,"tag":276,"props":353,"children":357},{":answer":278,":options":354,"explanation":355,"question":356},"[\"The container is sharing the host's network stack — no network isolation is in effect\",\"The container is stopped\",\"Both processes are in the same cgroup\"]","Identical namespace inodes mean the same namespace. For the network namespace that is `--network host` or, here, a missing `ip netns exec`. It also means the container can bind host ports and see host interfaces — worth confirming rather than assuming when something is reachable that should not be.","From the host, `readlink /proc/PID/ns/net` for a container process matches your own shell's. What does that tell you?",[],{"data":359,"body":360},{},{"type":6,"children":361},[362,570],{"type":9,"tag":363,"props":364,"children":366},"deep-dive",{"title":365},"What a real runtime does that this does not",[367,380,421,454,464,494,550,560],{"type":9,"tag":10,"props":368,"children":369},{},[370,372,378],{"type":14,"value":371},"The command above is a container by the definition we started with. It is not a ",{"type":9,"tag":373,"props":374,"children":375},"em",{},[376],{"type":14,"value":377},"safe",{"type":14,"value":379}," one, and the gap is worth naming precisely.",{"type":9,"tag":10,"props":381,"children":382},{},[383,388,390,396,398,404,406,412,413,419],{"type":9,"tag":241,"props":384,"children":385},{},[386],{"type":14,"value":387},"Capabilities.",{"type":14,"value":389}," Our process runs as full root with every capability. ",{"type":9,"tag":40,"props":391,"children":393},{"className":392},[],[394],{"type":14,"value":395},"runc",{"type":14,"value":397}," drops all but a small default set — no ",{"type":9,"tag":40,"props":399,"children":401},{"className":400},[],[402],{"type":14,"value":403},"CAP_SYS_ADMIN",{"type":14,"value":405},", no ",{"type":9,"tag":40,"props":407,"children":409},{"className":408},[],[410],{"type":14,"value":411},"CAP_SYS_MODULE",{"type":14,"value":405},{"type":9,"tag":40,"props":414,"children":416},{"className":415},[],[417],{"type":14,"value":418},"CAP_NET_ADMIN",{"type":14,"value":420},". Without that, the container can load kernel modules and reconfigure the host's network.",{"type":9,"tag":10,"props":422,"children":423},{},[424,429,431,437,439,445,446,452],{"type":9,"tag":241,"props":425,"children":426},{},[427],{"type":14,"value":428},"seccomp.",{"type":14,"value":430}," Docker's default profile blocks around 40 syscalls outright — ",{"type":9,"tag":40,"props":432,"children":434},{"className":433},[],[435],{"type":14,"value":436},"kexec_load",{"type":14,"value":438},", ",{"type":9,"tag":40,"props":440,"children":442},{"className":441},[],[443],{"type":14,"value":444},"mount",{"type":14,"value":438},{"type":9,"tag":40,"props":447,"children":449},{"className":448},[],[450],{"type":14,"value":451},"ptrace",{"type":14,"value":453},", and the rest of the kernel attack surface a normal workload never touches. We block none.",{"type":9,"tag":10,"props":455,"children":456},{},[457,462],{"type":9,"tag":241,"props":458,"children":459},{},[460],{"type":14,"value":461},"User namespaces.",{"type":14,"value":463}," Our root is the host's root. UID 0 inside is UID 0 outside, so a filesystem escape is a full host compromise.",{"type":9,"tag":10,"props":465,"children":466},{},[467,485,487,492],{"type":9,"tag":241,"props":468,"children":469},{},[470,476,478,484],{"type":9,"tag":40,"props":471,"children":473},{"className":472},[],[474],{"type":14,"value":475},"pivot_root",{"type":14,"value":477}," instead of ",{"type":9,"tag":40,"props":479,"children":481},{"className":480},[],[482],{"type":14,"value":483},"chroot",{"type":14,"value":270},{"type":14,"value":486}," As covered in lesson 3: our ",{"type":9,"tag":40,"props":488,"children":490},{"className":489},[],[491],{"type":14,"value":483},{"type":14,"value":493}," is escapable by a root process in a dozen lines of C.",{"type":9,"tag":10,"props":495,"children":496},{},[497,525,527,533,534,540,542,548],{"type":9,"tag":241,"props":498,"children":499},{},[500,502,508,510,516,518,523],{"type":14,"value":501},"A read-only ",{"type":9,"tag":40,"props":503,"children":505},{"className":504},[],[506],{"type":14,"value":507},"/sys",{"type":14,"value":509},", a proper ",{"type":9,"tag":40,"props":511,"children":513},{"className":512},[],[514],{"type":14,"value":515},"/dev",{"type":14,"value":517},", masked ",{"type":9,"tag":40,"props":519,"children":521},{"className":520},[],[522],{"type":14,"value":183},{"type":14,"value":524}," paths.",{"type":14,"value":526}," ",{"type":9,"tag":40,"props":528,"children":530},{"className":529},[],[531],{"type":14,"value":532},"/proc/kcore",{"type":14,"value":438},{"type":9,"tag":40,"props":535,"children":537},{"className":536},[],[538],{"type":14,"value":539},"/proc/sysrq-trigger",{"type":14,"value":541},", and ",{"type":9,"tag":40,"props":543,"children":545},{"className":544},[],[546],{"type":14,"value":547},"/sys/firmware",{"type":14,"value":549}," are masked or read-only in a real runtime. Ours are wide open.",{"type":9,"tag":10,"props":551,"children":552},{},[553,558],{"type":9,"tag":241,"props":554,"children":555},{},[556],{"type":14,"value":557},"Lifecycle.",{"type":14,"value":559}," Nothing here reaps zombies, forwards signals, restarts on failure, streams logs, or cleans up when the process exits.",{"type":9,"tag":10,"props":561,"children":562},{},[563,565],{"type":14,"value":564},"That list is what the OCI runtime specification is: not a different mechanism, but the same mechanisms plus every hardening step. Which is the honest summary of the whole course — ",{"type":9,"tag":241,"props":566,"children":567},{},[568],{"type":14,"value":569},"you have built the isolation, not the security.",{"type":9,"tag":10,"props":571,"children":572},{},[573],{"type":14,"value":574},"Next up: the cleanup, and the debugging map this all adds up to.",1787908868390]