no-new-privileges: el agente no sube de uid aunque el binario pida setuid
Resumen
Cómo no dejar que un binario setuid escape el USER 1000: security_opt no-new-privileges en Compose. Distinto de cap_drop. El spec acepta flag, =true y :true. En K8s el campo es allowPrivilegeEscalation false. CLI Docker, curl el 3 de septiembre de 2026.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
USER 1000 no basta si un helper setuid o un sudo interno pide más. no-new-privileges:true bloquea bits setuid/setgid en exec. Distinto de cap_drop: caps recortan capacidades; este flag recorta escalada en exec. Fuentes oficiales consultadas el 3 de septiembre de 2026.
La regla
security_opt:
- no-new-privileges:true
en agent (y en caddy si aplica). Postgres de imagen oficial rara vez lo necesita para el bot; no lo copies a ciegas si el entrypoint de la DB se queja.
Usuario no-root + este flag + cap_drop es el trío mínimo. No es AppArmor custom.
Qué cubre
Un npm postinstall raro, un binario vendored, un gosu mal puesto. El kernel niega el privilegio extra. El proceso sigue 1000.
No sustituye secrets ni readonly.
Tabla

| Knob | Qué evita |
|---|---|
| USER 1000 | root por default |
| cap_drop ALL + cap_add NET_BIND_SERVICE | CAP_SYS_ADMIN etc. |
| no-new-privileges | setuid en exec |
| read_only | writes al rootfs |
Errores comunes

| Síntoma | Causa | Fix |
|---|---|---|
| sigue root | sin USER | USER primero |
| gosu falla | flag + gosu | no uses gosu; USER en imagen |
| “permission denied” en tool | setuid helper | quita el helper |
| Swarm ignore | copy mal | security_opt en Compose v2 |
| K8s yaml | copy Docker | allowPrivilegeEscalation: false |
Relación con el resto
Checklist
-
no-new-privileges:trueen agent - USER no 0
- cap_drop alineado
- Ensayo: no gosu/sudo en la imagen
- K8s equivalente si migras
- CI no pisa security_opt
FAQ
¿Workers? Isolates; no hay setuid.
¿Fly? docker extra: el flag va en el Dockerfile/fly.toml processes, no siempre Compose.
¿Rompe Node? No. Node no es setuid.
El ensayo: imagen con un binario setuid dummy; con el flag, exec no sube uid. id sigue 1000.
Un flag no es seccomp custom. Si necesitas syscall filter fino, eso es otro PR. Este es el default barato.
Qué no hacer
No uses privileged: true “para que el debugger entre” en prod. Privileged anula casi todo. No dejes security_opt: apparmor:unconfined copiado de un gist.
No combines user: "0" con el flag y llames eso hardening. El orden es USER no-root primero.
gosu/su-exec en entrypoint: con no-new-privileges el drop de root→1000 falla. Fija USER en el Dockerfile y listo.
Techos oficiales (curl 2026-09-03)
Compose security_opt (spec de servicios, hoy): override del labeling scheme default de cada contenedor. Acepta option=value u option:value. En booleanos como no-new-privileges el valor se puede omitir y cuenta como enabled. Las tres formas son equivalentes según la doc:
security_opt:
- no-new-privileges
- no-new-privileges=true
- no-new-privileges:true
(El HTML de docs parte el hyphen; en el yaml es un solo string.) El mismo bloque documenta label=user:USER y label=role:ROLE. Más esquemas: “Security configuration”.
CLI docker container run (hoy):
| Flag | Qué hace |
|---|---|
--security-opt no-new-privileges=true | el proceso no gana privilegios nuevos |
--security-opt no-new-privileges | igual, sin =true |
--security-opt label=disable | apaga confinement de labels |
--security-opt apparmor=PROFILE | perfil AppArmor |
--security-opt seccomp=unconfined | apaga seccomp |
--security-opt seccomp=builtin | perfil built-in aunque el daemon tenga otro default |
--security-opt seccomp=profile.json | allowlist custom |
--security-opt systempaths=unconfined | apaga masked/read-only paths |
Ejemplo oficial: docker run --security-opt no-new-privileges -it ubuntu bash. Efecto documentado: su / sudo dejan de servir. Los filtros seccomp se aplican después de dropear privilegios, así que puedes ser más restrictivo. Detalle: kernel docs, no un número de Compose.
label=disable es lo contrario de endurecer. No lo copies de un gist de Fedora. label=level:s0:c100,c200 comparte contenido entre contenedores (MLS); no es el flag del bot.
Compose privileged (hoy): privilegios elevados, impacto de plataforma. Engine: --privileged da todas las caps y relaja LSM. Privileged anula la idea de no-new-privileges. Cero en el agente.
Ensayo extra
compose exec agent id = uid 1000. Un binario setuid de prueba no cambia euid. CI: docker inspect muestra NoNewPrivileges: true.
pid limits y ulimit son otro eje (DoS), no escalada.
Seccomp default
El perfil default de Docker ya bloquea syscalls raras. no-new-privileges es ortogonal: un setuid permitido por seccomp igual no escala. No desactives seccomp (seccomp=unconfined) “porque un tool falló”.
Tools en la imagen
El agente no necesita sudo, passwd, su. Quítalos en el stage runner (multistage). Menos bits setuid = menos superficie aunque olvides el flag.
Healthcheck del compose no corre como root si el servicio no es root. No pongas healthcheck que llame a un helper privilegiado.
El ensayo de imagen: find / -perm -4000 en el runner; lista vacía o justificada.
Seccomp default vs este flag
Engine seccomp (hoy): al correr un contenedor usa el perfil default salvo --security-opt. El default es allowlist. seccomp=unconfined corre sin ese perfil; el ejemplo oficial usa unshare --map-root-user. Eso es debug, no prod. SYS_PTRACE ya está bloqueado por drop de CAP_SYS_PTRACE; no desactives seccomp “porque un tool de tracing falló”.
no-new-privileges es ortogonal: un setuid que seccomp dejaría pasar igual no escala uid. El orden documentado en el CLI: primero no-new-privs, después el filtro seccomp.
Engine security (hoy): Docker arranca con un conjunto restringido de capabilities, no con cero. User namespaces existen desde 1.10 y no vienen enabled by default: mapean root del contenedor a un uid ≠ 0 en el host. Rootless corre daemon y contenedor sin root, salvo newuidmap/newgidmap. Ninguno de los dos sustituye el flag en un daemon rootful.
Kubernetes: el bool es al revés
Security Context (hoy): allowPrivilegeEscalation controla si un proceso puede ganar más privilegios que su padre. El bool controla el flag no_new_privs del proceso. Queda siempre true (escalada permitida, o sea no_new_privs off) si el contenedor es privileged o tiene CAP_SYS_ADMIN. readOnlyRootFilesystem es el primo de readonly.
Pod Security Standards, Restricted (Linux, no Windows): allowPrivilegeEscalation solo false. También exige non-root (runAsNonRoot: true) y, desde v1.23, runAsUser ≠ 0. Restricted además pide drop que incluye ALL; lo único addable de vuelta es NET_BIND_SERVICE. Un agente en Restricted es exactamente: no-root + no escalada + cap_drop ALL. No copies el yaml de Compose al manifest: el campo se llama allowPrivilegeEscalation: false.
Cómo verificar
docker inspect --format '{{.HostConfig.SecurityOpt}}' "$(docker compose ps -q agent)"
docker compose exec agent id
SecurityOpt debe listar no-new-privileges (con o sin =true). id = uid 1000. Un binario setuid dummy no cambia euid. Si el inspect está vacío, el yaml no se aplicó (override, Swarm mal copiado, servicio distinto).
Railway y Vercel no exponen este flag: el aislamiento es de plataforma. En VPS Compose sí. Fly: el flag va en el runtime Docker, no siempre en fly.toml. No copies el yaml sin leer el campo equivalente.
Siguiente paso: si uid 1000 y el flag on y igual hay root files, readonly. Sin runtime: curso.
Lecturas relacionadas
Sigue explorando Deploy y otras piezas para builders.



