Aurora Libros funciona, està ben organitzada i és reproduïble. El que no està és endurida. En aquesta lliçó recorres les quatre superfícies d'atac d'una plataforma de contenidors i hi apliques, una a una, les defenses: usuari sense privilegis, sistema de fitxers de només lectura, capacitats del kernel retallades, seccomp, escaneig de vulnerabilitats i signatura d'imatges.
Advertència imprescindible. Tot el que segueix són bones pràctiques tècniques, no una política de seguretat. Les decisions concretes —què s'exposa, quin CVE s'accepta, quins controls són obligatoris— s'han de validar amb el responsable de seguretat o de compliance de la teva organització. Cap configuració d'aquest curs no substitueix una auditoria professional ni el compliment normatiu que t'apliqui.
Contingut
- Model d'amenaces: què aïlla un contenidor i què no
- El daemon: el grup
dockerés root - Docker rootless
--userns-remap, l'alternativa intermèdia- La imatge: base mínima i fixada per digest
distrolessiscratchdavant d'Alpine- Secrets en capes: la prova amb
docker history - Runtime:
--read-onlyi--tmpfs - Capacitats del kernel
no-new-privileges, seccomp i AppArmor- Recursos com a defensa i per què
--privilegedés un error - Escaneig de vulnerabilitats: Scout i Trivy
- Cadena de subministrament: SBOM, Cosign i procedència
- El
compose.prod.yamlendurit - Llista de comprovació de seguretat
- Model d'amenaces: què aïlla un contenidor i què no
Un contenidor no és una màquina virtual. Comparteix el kernel del host, i aquell kernel és l'única frontera real:
| Un contenidor sí que aïlla | Un contenidor NO aïlla |
|---|---|
| Processos (no veus els del host) | El kernel: una fallada del kernel afecta tothom |
| Sistema de fitxers (llevat del que muntis) | El maquinari, el rellotge i els paràmetres globals |
| Xarxa, nom de host, IPC, usuaris | El que li donis tu: sockets, muntatges, capacitats |
flowchart TB
A["1 · IMATGE<br/>CVE a la base, dependències<br/>vulnerables, secrets en capes"]
B["2 · RUNTIME<br/>root a dins, capacitats de més,<br/>escriptura lliure, sense límits"]
C["3 · DAEMON<br/>grup docker = root, socket<br/>exposat, --privileged"]
D["4 · REGISTRE D'IMATGES<br/>imatge suplantada, sense signatura,<br/>etiqueta mutable"]
A --> P(("Aurora<br/>Libros"))
B --> P
C --> P
D --> P
Les quatre superfícies es defensen amb eines diferents i cap no cobreix les altres: una imatge sense CVE executada com a root amb --privileged continua sent un desastre.
- El daemon: el grup
docker és root
docker és rootAquesta és l'afirmació que més resistència genera i la més fàcil de demostrar. Pertànyer al grup docker equival a tenir root al host, sense sudo i sense registre als logs de sudo.
id | tr ',' '\n' | grep docker
docker run --rm -v /:/host alpine:3 sh -c 'cat /host/etc/shadow | head -2'Acabes de llegir els hash de contrasenyes del host sense ser root. I es pot anar més lluny: muntant / en mode escriptura, un usuari del grup docker pot afegir-se a /etc/sudoers, instal·lar una clau SSH a /root/.ssh/authorized_keys o modificar una unitat de systemd. El motiu és estructural: el client docker només envia peticions a /var/run/docker.sock, i el daemon corre com a root. Qui pugui parlar amb aquell socket mana sobre el daemon, i el daemon mana sobre el host. Tres conseqüències operatives:
- Afegir algú al grup
dockerés concedir-li root. Tracta-ho amb aquest criteri a la teva gestió d'accessos. - Mai no muntis
/var/run/docker.sockdins d'un contenidor "per comoditat". És la via d'escapada més explotada que existeix. Si una eina ho necessita (un agent de CI, Portainer), fes servir un proxy de socket que filtri les peticions permeses. - Si l'equip no necessita root al host, la resposta correcta és el mode rootless.
- Docker rootless
En mode rootless el daemon corre com el teu usuari, no com a root, recolzant-se en espais de noms d'usuari (lliçó 05-07). Si algú s'escapa del contenidor, apareix com un usuari sense privilegis del host.
sudo apt-get install -y uidmap && dockerd-rootless-setuptool.sh install
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
systemctl --user enable --now docker
docker info --format 'rootless={{.SecurityOptions}} | dir: {{.DockerRootDir}}'
docker run --rm -v /:/host alpine:3 sh -c 'cat /host/etc/shadow' 2>&1 | tail -1rootless=[name=seccomp,profile=builtin name=rootless name=cgroupns] | dir: /home/joan/.local/share/docker
cat: can't open '/host/etc/shadow': Permission deniedEl muntatge es fa, però el fitxer és il·legible: dins del contenidor ets root, i aquell root està remapat al teu UID real del host, que no pot llegir /etc/shadow. L'escalada deixa d'existir.
| Limitació del mode rootless | Detall |
|---|---|
| Ports < 1024 | No es publiquen sense CAP_NET_BIND_SERVICE o net.ipv4.ip_unprivileged_port_start |
| Rendiment de xarxa | La sortida passa per slirp4netns/rootlesskit: una mica més lenta |
| Storage driver | overlay2 requereix kernel ≥ 5.13; si no, fuse-overlayfs, més lent |
| Límits de recursos | Requereixen cgroups v2 amb delegació per systemd |
--network host, macvlan, AppArmor |
No disponibles o restringits: no hi ha privilegis per tocar la xarxa del host |
Per a Aurora Libros en un servidor compartit, rootless és l'opció per defecte. Per a un servidor dedicat amb accés restringit, el mode clàssic endurit és defensable; decideix-ho amb el teu responsable de seguretat.
--userns-remap, l'alternativa intermèdia
--userns-remap, l'alternativa intermèdiaSi no pots migrar a rootless, --userns-remap manté el daemon com a root però remapa els usuaris dels contenidors a un rang sense privilegis del host. S'activa amb {"userns-remap": "default"} a /etc/docker/daemon.json:
sudo systemctl restart docker
docker run -d --name t alpine:3 sleep 300 && docker exec t id -u
ps -o user,pid,cmd -C sleep --no-headersA dins el procés és root (uid 0); al host corre com l'UID 165536, que no té cap privilegi. El cost: els volums existents canvien de propietari efectiu, i no és compatible amb --network host ni amb contenidors que comparteixin namespaces d'usuari. És un pas intermedi raonable, no un substitut de rootless.
- La imatge: base mínima i fixada per digest
Cada paquet de la imatge és un CVE en potència. Les regles, per ordre d'impacte: base oficial i mínima (node:22-alpine en comptes de node:22 passa d'uns 1,1 GB a uns 142 MB i amb això centenars de paquets); fixar per digest, perquè una etiqueta és mutable i un digest no; no instal·lar el que és innecessari (res de curl, vim, git o build-essential a la imatge final); USER sense privilegis (lliçó 02-04); i actualitzar la base periòdicament, perquè fixar per digest sense renovar-lo és congelar vulnerabilitats.
# Reproduïble i verificable: el digest identifica un contingut exacte
FROM node:22-alpine@sha256:3f1c8d9e7a4b2c5f6e0d1a8b7c4e2f9d0a3b6c5e8f1d2a7b4c9e0f3a6b5d8c1eEl digest s'obté amb docker image inspect node:22-alpine --format '{{index .RepoDigests 0}}' i és l'única referència que garanteix que la imatge d'avui és exactament la que vas auditar. En desenvolupament pots fer servir etiquetes; a compose.prod.yaml, digest.
distroless i scratch davant d'Alpine
distroless i scratch davant d'Alpine| Base | Mida | Shell | Paquets | Superfície | Depuració |
|---|---|---|---|---|---|
node:22 (Debian) |
~1,1 GB | sí | apt | Molt gran | Còmoda |
node:22-slim |
~230 MB | sí | apt | Mitjana | Còmoda |
node:22-alpine |
~142 MB | sí (BusyBox) | apk | Petita | Acceptable |
distroless/nodejs22 |
~110 MB | no | no | Mínima | Difícil: :debug o nsenter |
scratch |
0 MB | no | no | Nul·la | Molt difícil; només binaris estàtics |
distroless conté l'intèrpret i les seves biblioteques, i res més: ni sh, ni apk, ni wget. L'atacant que aconsegueixi execució de comandes no hi troba eines amb què treballar. El preu és real: no pots fer docker exec ... sh, i el HEALTHCHECK amb wget d'aurora-api deixa de funcionar (caldria sondejar des de fora o incloure un petit binari de salut). scratch només serveix per a binaris estàtics —Go, Rust—, no per a Node.js. Per a aurora-api, Alpine és l'equilibri correcte entre superfície i operabilitat; distroless és l'esglaó següent si la teva organització ho exigeix.
- Secrets en capes: la prova amb
docker history
docker historyUn secret que passa per una capa queda a la imatge per sempre, encara que després l'esborris.
# ❌ Les tres maneres de filtrar un secret
ARG NPM_TOKEN
ENV DB_PASSWORD=aurora_pass_2026
RUN echo "$NPM_TOKEN" > /root/.npmrc && npm ci && rm /root/.npmrcdocker build --build-arg NPM_TOKEN=npm_s3cr3t0 -t fuita:1.0 .
docker history --no-trunc fuita:1.0 | grep -o 'NPM_TOKEN=[^ ]*' | head -1
docker image inspect fuita:1.0 --format '{{json .Config.Env}}'
# NPM_TOKEN=npm_s3cr3t0
# ["PATH=/usr/local/bin:...","DB_PASSWORD=aurora_pass_2026"]Les tres errades: l'ARG queda a les metadades de construcció, l'ENV a la configuració de la imatge, i el fitxer esborrat continua a la capa on es va crear sota un whiteout (lliçó 05-02). Qualsevol que descarregui la imatge els recupera.
Les solucions, segons el moment: durant el build, RUN --mount=type=secret de BuildKit (lliçó 05-05); en execució, fitxers muntats a /run/secrets/ amb el patró _FILE (lliçó 04-05); dins de la imatge, mai.
Recorda el patró que Aurora Libros ja fa servir: DB_PASSWORD_FILE=/run/secrets/db_password, i l'aplicació llegeix el fitxer en arrencar. El secret viu al host, no a la imatge ni a l'entorn.
- Runtime:
--read-only i --tmpfs
--read-only i --tmpfsSi el sistema de fitxers del contenidor és de només lectura, un atacant no hi pot deixar cap binari ni modificar el codi.
Gairebé cap aplicació no funciona sense escriure en algun lloc: cal donar tmpfs per a aquells punts concrets.
docker run -d --name api-ro --read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m auroralibros/aurora-api:1.2.0
docker exec api-ro sh -c 'touch /tmp/ok && echo "tmp escrivible"; touch /app/x 2>&1'Fixa't en noexec i nosuid: encara que l'atacant aconsegueixi escriure un binari a /tmp, no el podrà executar. Per esbrinar quines rutes necessita escriure una aplicació, arrenca-la en només lectura i llegeix els errors; sol n'hi haver prou amb /tmp i algun directori de memòria cau.
- Capacitats del kernel
Ser root dins d'un contenidor no és ser root del host: Linux divideix els privilegis en capacitats, i Docker en concedeix per defecte un subconjunt d'unes catorze. La bona pràctica és treure-les totes i tornar només les necessàries.
docker run --rm --cap-drop=ALL alpine:3 chown 1000 /tmp
docker run --rm --cap-drop=ALL --cap-add=CHOWN alpine:3 sh -c 'chown 1000 /tmp && echo "chown OK"'| Capacitat | Permet | La necessita Aurora Libros? |
|---|---|---|
NET_BIND_SERVICE |
Escoltar en ports < 1024 | Només aurora-web (nginx al 80) |
CHOWN, FOWNER, DAC_OVERRIDE |
Canviar propietaris i saltar-se permisos | aurora-db en inicialitzar el volum |
SETUID, SETGID |
Canviar d'usuari (típic de su-exec) |
aurora-db i aurora-web en arrencar |
NET_RAW |
Sockets crus: ping, i també spoofing |
No. Treu-la sempre |
SYS_ADMIN |
Muntar, canviar namespaces… gairebé root | Mai. Equival a --privileged |
SYS_PTRACE |
Depurar altres processos, llegir-ne la memòria | Només en depuració puntual |
SYS_MODULE |
Carregar mòduls del kernel | Mai. Compromet el host sencer |
Per a aurora-api, que escolta al 3000 i corre com l'usuari node, la resposta és neta: cap capacitat, és a dir, cap_drop: [ALL] sense cap cap_add.
no-new-privileges, seccomp i AppArmor
no-new-privileges, seccomp i AppArmorno-new-privileges:true a security_opt impedeix que un procés guanyi privilegis mitjançant binaris setuid, i talla en sec tota una família d'escalades locals.
seccomp filtra les crides al sistema. El perfil per defecte de Docker en bloqueja unes 40 de les més de 300 disponibles, entre elles les més perilloses: mount, reboot, kexec_load, init_module, ptrace (en kernels antics), clone amb banderes de namespace noves i bpf.
docker run --rm alpine:3 sh -c 'mount -t tmpfs none /mnt' 2>&1 | tail -1
docker run --rm --security-opt seccomp=unconfined --cap-add=SYS_ADMIN alpine:3 \
sh -c 'mount -t tmpfs none /mnt && echo "muntat: perfil desactivat"'Un perfil propi és un JSON amb una acció per defecte i una llista de permeses:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [{
"names": ["read","write","openat","close","fstat","mmap","brk","epoll_wait",
"accept4","socket","bind","listen","futex","clock_gettime","exit_group"],
"action": "SCMP_ACT_ALLOW"
}]
}S'aplica amb security_opt: [seccomp:./seguretat/aurora-api-seccomp.json]. Construir un perfil a mida és feina fina: cal traçar l'aplicació (amb strace o auditd) per descobrir quines crides fa realment, i una crida oblidada produeix fallades intermitents difícils de diagnosticar. Comença pel perfil per defecte —que ja és una bona defensa— i aborda'n un de propi només si el teu context ho exigeix.
AppArmor (Debian/Ubuntu) i SELinux (RHEL/Fedora) actuen a un altre nivell: controlen quins fitxers i operacions pot tocar un procés. Docker aplica el perfil docker-default si AppArmor està actiu. A SELinux, l'opció :z / :Z als muntatges reetiqueta el contingut perquè el contenidor hi pugui accedir (-v dades:/dades:Z), i ometre-la és la causa del clàssic Permission denied a Fedora amb permisos aparentment correctes. El que mai no s'ha de fer: seccomp=unconfined o apparmor=unconfined per "arreglar" un error.
- Recursos com a defensa i per què
--privileged és un error
--privileged és un errorEls límits de la lliçó 03-07 no són només de capacitat: són defensa davant de denegació de servei. Un contenidor compromès que mini criptomonedes, exhaureixi la memòria o llanci una fork bomb queda contingut pel seu pids_limit: 200 i els seus deploy.resources.limits.
I --privileged, en una línia: desactiva gairebé totes les proteccions alhora. Concedeix totes les capacitats, desactiva seccomp i AppArmor, i dona accés a tots els dispositius del host.
docker run --rm --privileged alpine:3 sh -c 'ls /dev/sda*'
# /dev/sda /dev/sda1 /dev/sda2 <- els discos del host, visibles i muntablesSi alguna cosa necessita un dispositiu concret, concedeix-l'hi amb --device=/dev/xxx; si necessita una capacitat concreta, amb --cap-add. --privileged és l'equivalent a treure la porta perquè costava trobar la clau.
- Escaneig de vulnerabilitats: Scout i Trivy
✗ HIGH CVE-2025-31842 [Improper Input Validation]
Affected range : <4.19.3 Fixed version : 4.19.3
Package : pkg:npm/[email protected]
✗ HIGH CVE-2025-47101 [Out-of-bounds Write]
Affected range : <3.20.4-r2 Fixed version : 3.20.4-r2
Package : pkg:apk/alpine/[email protected]
2 vulnerabilities found in 2 packagesCom es llegeix un informe, per ordre de decisió:
| Camp | Què cal fer |
|---|---|
| Severitat (CRITICAL/HIGH/MEDIUM/LOW) | Comença per les dues primeres |
| Fixed version | Si hi ha correcció, actualitza. És la dada decisiva |
| Paquet | Dependència teva: npm update. De la base: canvia de base o de digest |
| Vector d'atac | Un CVE en codi que no executes és menys urgent, no inexistent |
Trivy dona el mateix des de la línia de comandes i serveix per automatitzar:
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:latest \
image --severity HIGH,CRITICAL --exit-code 1 auroralibros/aurora-api:1.2.0
# Total: 2 (HIGH: 2, CRITICAL: 0) -> la comanda acaba amb codi 1--exit-code 1 és la clau de la integració: fa que la comanda falli i, amb ella, el pipeline (lliçó 06-02). El flux de treball sensat són tres punts de control: en construir en local, a cada pull request i de manera periòdica sobre les imatges ja desplegades. I una nota sobre el socket: muntar docker.sock a l'escàner és exactament el que l'apartat 2 desaconsella; és acceptable a la teva màquina, però a CI convé escanejar el tarball de la imatge o fer servir un escàner sense accés al daemon.
- Cadena de subministrament: SBOM, Cosign i procedència
Un SBOM (Software Bill of Materials) és l'inventari de tot el que hi ha dins d'una imatge. Sense ell, davant d'un CVE nou no pots respondre la pregunta més bàsica: m'afecta?
docker sbom auroralibros/aurora-api:1.2.0 --format syft-json > sbom-aurora-1.2.0.json
docker sbom auroralibros/aurora-api:1.2.0 | head -4Cosign signa imatges amb criptografia i verifica l'origen:
cosign generate-key-pair # cosign.key (privada) i cosign.pub
cosign sign --key cosign.key auroralibros/aurora-api:1.2.0
cosign verify --key cosign.pub auroralibros/aurora-api:1.2.0Verification for auroralibros/aurora-api:1.2.0 --
- The cosign claims were validated
- The signatures were verified against the specified public keyLa signatura es desa com un artefacte addicional al registre d'imatges mateix. Amb keyless signing pots signar fent servir la identitat OIDC del pipeline, sense gestionar claus privades —cosa que, a la pràctica, és el gran avantatge: la clau que ningú no custodia és la que ningú no filtra.
Les attestations de procedència responen a "qui ha construït això, des de quin commit i amb quin Dockerfile", i es generen durant el build: docker buildx build --provenance=mode=max --sbom=true -t auroralibros/aurora-api:1.2.0 --push ./api, consultables amb docker buildx imagetools inspect.
Sobre el mecanisme antic: Docker Content Trust (DOCKER_CONTENT_TRUST=1) i Notary v1 van ser el primer intent de signatura a Docker Hub. Encara funcionen, però estan en desús: la indústria s'ha mogut a Cosign i a l'ecosistema Sigstore. Si comences avui, comença amb Cosign.
- El
compose.prod.yaml endurit
compose.prod.yaml endurit# compose.prod.yaml — Aurora Libros S.L., servidor endurit
services:
aurora-api:
# Digest, no etiqueta: contingut exacte i auditat
image: auroralibros/aurora-api:${AURORA_API_VERSION:?fixa la versió}@${AURORA_API_DIGEST:?fixa el digest}
build: !reset null # el servidor no construeix: només descarrega
user: "1000:1000" # sense privilegis, encara que la imatge ja ho fixi
read_only: true # sistema de fitxers immutable
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
cap_drop: [ALL] # cap capacitat: escolta al 3000
security_opt: [no-new-privileges:true] # sense escalada via binaris setuid
pids_limit: 200 # contenció davant de fork bomb
environment:
NODE_ENV: production
LOG_NIVELL: warn
DB_PASSWORD_FILE: /run/secrets/db_password # mai el valor a l'entorn
secrets: [db_password]
ports: !reset [] # sense accés directe: tot passa pel proxy
restart: always
deploy:
resources: { limits: { memory: 512M, cpus: "2.0" }, reservations: { memory: 128M } }
logging:
driver: json-file
options: { max-size: "50m", max-file: "5" }
aurora-db:
image: postgres:16-alpine@sha256:9c1f2b8e5a7d...
user: "999:999"
read_only: true
tmpfs: ["/tmp:rw,noexec,nosuid,size=64m", "/run/postgresql:rw,nosuid,size=16m"]
cap_drop: [ALL]
cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, SETGID, SETUID] # les mínimes de postgres
security_opt: [no-new-privileges:true]
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets: [db_password]
restart: always
deploy:
resources: { limits: { memory: 2G, cpus: "2.0" } }
aurora-cache:
image: redis:7-alpine@sha256:4b2e9c7f1a3d...
user: "999:999"
read_only: true
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
command: ["redis-server","--maxmemory","200mb","--maxmemory-policy","allkeys-lru","--save",""]
restart: always
aurora-web:
image: nginx:alpine@sha256:7e3a1c9d0b5f...
read_only: true
tmpfs: ["/var/cache/nginx:rw,nosuid,size=64m", "/var/run:rw,nosuid,size=8m"]
cap_drop: [ALL]
cap_add: [NET_BIND_SERVICE, CHOWN, SETGID, SETUID] # nginx escolta al 80
security_opt: [no-new-privileges:true]
ports: ["80:80"]
restart: always
secrets:
db_password:
file: ./secrets/db_password.txt # fora de Git; permisos 0400Verifica sempre la fusió abans de desplegar: docker compose -f compose.yaml -f compose.prod.yaml config | grep -E 'read_only|cap_drop|no-new'.
- Llista de comprovació de seguretat
| # | Control | Com es verifica |
|---|---|---|
| 1 | El daemon no és accessible a usuaris sense confiança | getent group docker |
| 2 | Mode rootless valorat o justificat per escrit | docker info | grep -i rootless |
| 3 | Cap contenidor no munta docker.sock |
docker ps -q | xargs docker inspect | grep docker.sock |
| 4 | Cap --privileged en producció |
grep -r privileged compose*.yaml |
| 5 | Totes les imatges fixades per digest | docker compose config | grep image: |
| 6 | Cap contenidor no corre com a root | docker inspect --format '{{.Config.User}}' |
| 7 | read_only: true amb els seus tmpfs mínims |
{{.HostConfig.ReadonlyRootfs}} |
| 8 | cap_drop: [ALL] i addicions justificades |
Revisió del compose.prod.yaml |
| 9 | no-new-privileges, i seccomp/AppArmor sense unconfined |
{{.HostConfig.SecurityOpt}} |
| 10 | Límits de memòria, CPU i PID a tots | docker stats --no-stream |
| 11 | Escaneig sense CVE crítics amb correcció disponible | docker scout cves --only-severity critical |
| 12 | Escaneig periòdic del que ja està desplegat | Tasca programada setmanal |
| 13 | Secrets per fitxer, mai a ENV ni en capes |
docker history | grep -iE 'pass|token' |
| 14 | SBOM generat i arxivat per versió | Artefacte del pipeline |
| 15 | Imatges signades i verificades abans de desplegar | cosign verify |
| 16 | Xarxes segmentades i dades sense sortida a Internet | internal: true a posterior |
| 17 | Ports publicats només els imprescindibles | docker compose ps, iptables -t nat -S DOCKER |
Repassa aquesta llista abans de cada desplegament i revisa-la amb el teu responsable de seguretat: és un punt de partida tècnic, no un certificat de compliment.
Errors Habituals i Consells
Creure que un contenidor aïlla com una VM. Comparteix kernel: una fallada del kernel afecta tothom.
Afegir gent al grup docker com si fos un permís menor, o muntar /var/run/docker.sock en un contenidor. El primer és concedir root sense traces a sudo; el segon és la via d'escapada més explotada que existeix.
Passar secrets amb ARG o ENV al Dockerfile. Queden a les capes i a les metadades. docker history els revela.
Fer servir --privileged per resoldre un error de permisos, o desactivar seccomp/AppArmor perquè alguna cosa funcioni. Concedeix el que falti en concret (--cap-add, --device); l'altra cosa és apagar l'alarma en comptes d'atendre l'incendi.
Escanejar una vegada i oblidar-se'n, o fixar per digest i no renovar-lo mai. Els CVE apareixen després de publicar; una imatge neta al març pot ser vulnerable al juny sense que ningú no la toqui.
Consell: aplica l'enduriment per servei i mesurant. Afegeix read_only, arrenca, llegeix l'error, afegeix el tmpfs just; després cap_drop: [ALL], arrenca, llegeix l'error, afegeix la capacitat justa. És més lent que copiar una plantilla, i és l'única manera de saber per què hi ha cada línia.
Exercicis
Exercici 1. Demostra l'escalada de privilegis des del grup docker: llegeix un fitxer del host que el teu usuari no pugui llegir i explica exactament per què funciona. Després raona quines dues configuracions ho impedirien.
Exercici 2. Endureix aurora-api de manera incremental: user, read_only, cap_drop: [ALL] i no-new-privileges. Documenta què falla a cada pas, quin tmpfs cal i verifica al final que /salut i /llibres continuen responent.
Exercici 3. Demostra que un secret passat amb ARG queda a la imatge: construeix-la, recupera'l amb docker history encara que el fitxer s'hagi esborrat, i explica per què el patró _FILE d'Aurora Libros no té aquest problema.
Solucions
Solució 1.
El mateix fitxer, el mateix usuari, dos resultats oposats. La raó és en qui executa l'operació: la comanda docker no llegeix res, només envia una petició al socket del daemon; el daemon corre com a root i munta / dins del contenidor amb els seus propis privilegis. A dins, el procés també és root, així que llegeix el que vulgui. El teu usuari mai no ha necessitat permís perquè mai no ha estat ell qui ha llegit el fitxer.
Les dues configuracions que ho impedeixen són el mode rootless (el daemon corre com el teu usuari i el root de dins es remapa al teu UID real, que no pot llegir /etc/shadow) i --userns-remap (el root del contenidor es remapa a un UID sense privilegis, amb el mateix efecte sobre el muntatge).
La conclusió de gestió és la que importa: usermod -aG docker algu és una concessió de root i ha de passar pel mateix procés d'aprovació que concedir sudo sense contrasenya.
Solució 2.
docker run -d --name h1 -u 1000:1000 auroralibros/aurora-api:1.2.0 && docker exec h1 id -u
docker run --rm --read-only auroralibros/aurora-api:1.2.0 2>&1 | tail -1 # pas 2: falla# Passos 3 i 4: tmpfs mínim, sense capacitats i sense escalada
docker run -d --name h4 -u 1000:1000 --read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--cap-drop=ALL --security-opt no-new-privileges:true \
--pids-limit 200 -p 3000:3000 auroralibros/aurora-api:1.2.0
sleep 3
curl -s http://localhost:3000/salut | jq -c '{estat, bd, cache}'
docker inspect h4 --format 'ro={{.HostConfig.ReadonlyRootfs}} caps={{.HostConfig.CapDrop}} opts={{.HostConfig.SecurityOpt}}'
docker exec h4 sh -c 'touch /app/porta.js' 2>&1{"estat":"ok","bd":"connectada","cache":"connectada"}
ro=true caps=[ALL] opts=[no-new-privileges:true]
touch: /app/porta.js: Read-only file systemL'únic pas que ha fallat ha estat el segon, per un motiu concret: l'aplicació escriu el seu fitxer de PID a /tmp. Un tmpfs de 64 MB amb noexec,nosuid ho resol sense tornar escriptura a la resta del sistema de fitxers. cap_drop: [ALL] no ha trencat res perquè aurora-api escolta al 3000 (no privilegiat) i ja corria com l'usuari node: aquí es recull el fruit de la decisió que vas prendre a la lliçó 02-04.
El resultat és un contenidor on un atacant amb execució de codi no pot escriure un binari, no el pot executar si l'escriu a /tmp, no té cap capacitat del kernel, no pot escalar per setuid i no pot llançar més de 200 processos. L'aplicació, mentrestant, respon exactament igual.
Solució 3.
mkdir -p /tmp/fuita && cd /tmp/fuita
printf 'FROM alpine:3\nARG API_TOKEN\nRUN echo "$API_TOKEN" > /token.txt && rm /token.txt\n' > Dockerfile
docker build --build-arg API_TOKEN=aur_tok_9f3c1 -t fuita:1.0 . >/dev/null
docker run --rm fuita:1.0 ls /token.txt 2>&1 # el fitxer ja no hi és
docker history --no-trunc fuita:1.0 | grep -o 'API_TOKEN=[^ |]*' | head -1
docker save fuita:1.0 | tar -xO --wildcards '*/layer.tar' 2>/dev/null | strings | grep -o 'aur_tok_[a-z0-9]*' | head -1Tres fets encadenats: el fitxer no existeix al sistema final, el token apareix a l'historial de construcció, i apareix també dins de les dades de la capa exportada. El primer explica per què tanta gent creu que esborrar el fitxer n'hi ha prou; el segon i el tercer, per què no és així: l'ARG queda a les metadades i el contingut esborrat continua a la seva capa, ocult només per un whiteout (lliçó 05-02).
El patró _FILE d'Aurora Libros no té aquest problema perquè el secret no entra mai a la imatge. La imatge conté només DB_PASSWORD_FILE=/run/secrets/db_password, que és una ruta, no un valor:
docker image inspect auroralibros/aurora-api:1.2.0 --format '{{json .Config.Env}}' | tr ',' '\n' | grep -iE 'pass|token'
docker compose exec aurora-api sh -c 'mount | grep run/secrets'"DB_PASSWORD_FILE=/run/secrets/db_password"
/dev/sda2 on /run/secrets/db_password type ext4 (ro,relatime)A les metadades només hi ha una ruta; el valor arriba per un muntatge de només lectura que existeix únicament mentre el contenidor viu. Si demà publiques aquella imatge a Docker Hub, no hi viatja cap secret a dins. Per als secrets que es necessiten durant la construcció —un token d'npm privat—, la solució equivalent és RUN --mount=type=secret de BuildKit, i és justament el que veuràs a la lliçó 05-05.
Conclusió
Aurora Libros està endurida i, més important, saps per què ho està cada línia. Vas partir del model d'amenaces: un contenidor aïlla processos, fitxers i xarxa, però comparteix kernel, i les quatre superfícies —imatge, runtime, daemon i registre d'imatges— exigeixen defenses diferents que no se substitueixen entre si. Has demostrat, llegint /etc/shadow sense ser root, que pertànyer al grup docker és tenir root al host, i coneixes les dues respostes: el mode rootless amb les seves limitacions reals (ports baixos, xarxa una mica més lenta, sense --network host) i --userns-remap com a pas intermedi.
A la imatge apliques base mínima fixada per digest, la comparativa entre Alpine, distroless i scratch amb el seu cost d'operabilitat, i la prova irrefutable que un secret passat per ARG o ENV queda a les capes per sempre. Al runtime, --read-only amb tmpfs noexec,nosuid per al just, cap_drop: [ALL] amb la taula de capacitats que de debò importen —i aurora-api sense cap—, no-new-privileges, el perfil seccomp per defecte i quan escriure'n un de propi, AppArmor i SELinux amb el seu :Z, els límits de recursos com a contenció de denegació de servei, i el perquè que --privileged sigui gairebé sempre un error. Escaneges amb Docker Scout i Trivy sabent llegir un informe —la versió corregida és la dada decisiva— i integrant-lo amb --exit-code 1; i assegures la cadena de subministrament amb SBOM, signatura amb Cosign i attestations de procedència, deixant Content Trust com el mecanisme antic que és. Tot condensat en un compose.prod.yaml comentat línia a línia i en una llista de comprovació de disset controls verificables. I l'advertència inicial continua dempeus: això és un terra tècnic sòlid, no una política, així que valida cada decisió amb el responsable de seguretat o de compliance de la teva organització.
A la lliçó 05-04 canviem d'objectiu i tornem a la imatge, ara amb la bàscula al davant: per què la mida importa (descàrrega, cost, superfície d'atac, velocitat d'escalat), com mesurar amb history i dive per trobar la capa grassa, quina base triar amb xifres reals, i sobretot les construccions multietapa, amb les quals aquells 142 MB d'aurora-api baixaran fins a una xifra concreta sense perdre res del que acabes d'endurir.
Docker: De Principiant a Avançat
Mòdul 1: Introducció a Docker
- Què és Docker?
- Instal·lant Docker
- Arquitectura de Docker
- Comandes Bàsiques de Docker
- Entenent les Imatges de Docker
- Creant el teu Primer Contenidor Docker
- El Projecte del Curs: la Plataforma Aurora Libros
Mòdul 2: Treballant amb Imatges Docker
- Docker Hub i Repositoris
- Construint Imatges Docker
- Conceptes Bàsics de Dockerfile
- Instruccions Avançades del Dockerfile
- Gestionant Imatges Docker
- Etiquetatge i Publicació d'Imatges
Mòdul 3: Contenidors Docker
- Executant Contenidors
- Cicle de Vida del Contenidor
- Gestionant Contenidors
- Inspecció i Depuració de Contenidors
- Xarxes a Docker
- Persistència de Dades amb Volums
- Límits de Recursos i Polítiques de Reinici
Mòdul 4: Docker Compose
- Introducció a Docker Compose
- Definint Serveis a Docker Compose
- Comandes de Docker Compose
- Aplicacions Multi-Contenidor
- Variables d'Entorn a Docker Compose
- Perfils, Overrides i Múltiples Entorns
- Desenvolupament Local amb Docker Compose
Mòdul 5: Conceptes Avançats de Docker
- Aprofundiment en Xarxes Docker
- Opcions d'Emmagatzematge Docker
- Millors Pràctiques de Seguretat a Docker
- Optimitzant Imatges Docker
- Builds Avançades amb BuildKit i Buildx
- Registre i Monitoratge a Docker
- El Runtime per Dins: Namespaces, Cgroups i Capes
Mòdul 6: Docker en Producció
- Preparar una Imatge per a Producció
- CI/CD amb Docker
- Orquestrant Contenidors amb Docker Swarm
- Introducció a Kubernetes
- Desplegant Contenidors Docker a Kubernetes
- Escalat i Balanceig de Càrrega
- Estratègies de Desplegament i Rollback
