Guía9 min

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.

Docker
Contenedor del agente sin privilegios nuevos tras exec

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

Flag de no-new-privileges junto a USER

KnobQué evita
USER 1000root por default
cap_drop ALL + cap_add NET_BIND_SERVICECAP_SYS_ADMIN etc.
no-new-privilegessetuid en exec
read_onlywrites al rootfs

Errores comunes

Escalada setuid en el helper

SíntomaCausaFix
sigue rootsin USERUSER primero
gosu fallaflag + gosuno uses gosu; USER en imagen
“permission denied” en toolsetuid helperquita el helper
Swarm ignorecopy malsecurity_opt en Compose v2
K8s yamlcopy DockerallowPrivilegeEscalation: false

Relación con el resto

Checklist

  • no-new-privileges:true en 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):

FlagQué hace
--security-opt no-new-privileges=trueel proceso no gana privilegios nuevos
--security-opt no-new-privilegesigual, sin =true
--security-opt label=disableapaga confinement de labels
--security-opt apparmor=PROFILEperfil AppArmor
--security-opt seccomp=unconfinedapaga seccomp
--security-opt seccomp=builtinperfil built-in aunque el daemon tenga otro default
--security-opt seccomp=profile.jsonallowlist custom
--security-opt systempaths=unconfinedapaga 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.