docs(devsecops): documentar 4+1 hallazgos de infraestructura y TODOs de Cosign
5 playbooks nuevos en docs/playbooks/ del portal: - SQLite en modo DELETE causando SQLITE_BUSY, por que WAL lo resuelve. - Anchors YAML no soportados por el parser de Gitea Actions, incluido el diagnostico equivocado inicial (indentacion) que se corrigio despues -- documentado con honestidad como leccion de proceso. - Docker-in-Docker vs sibling containers, y por que Semgrep corre nativo en un venv en vez de como container aparte. - El SNI/realm HTTP del tunel rompiendo especificamente a Cosign (docker push funcionaba, cosign sign no), conectado con el incidente ya documentado del 504 -- misma capa de ingress, dos fallas distintas. - Contencion de recursos entre Gitea y su runner: causa raiz real confirmada en vivo durante esta misma sesion (ambos contenedores reiniciados a mitad de un build, sin limites de CPU/memoria reales), con fix propuesto (ver rama fix/gitea-runner-resource-limits en scripts/) pendiente de aplicacion manual. Se actualiza el playbook del 504 para linkear hacia el nuevo hallazgo de contencion, cerrando el loop que habia quedado abierto ahi. cosign.md: nueva seccion "Limitaciones conocidas y mejoras futuras" con los dos TODO reales -- firma por tag en vez de digest (con el warning real de Cosign visto en los logs) y transparency log de Rekor deshabilitado, explicado en simple. index.md: tabla resumen de los 3 pipelines (imagen + adaptacion de Semgrep por stack), diagrama generalizado para reflejar que es el mismo patron en los 3 workflows, y "que falta" actualizado. Validado con mkdocs build --strict (0 errores, 0 warnings) antes de commitear.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# Incidente: Docker-in-Docker roto en el runner — por qué Semgrep corre nativo (2026-08)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Ventana del incidente** | 2026-08-15, primer intento de correr Semgrep como container aparte en `build.yaml` |
|
||||
| **Servicio afectado** | Gitea Actions runner (`gitea-runner`, imagen `act_runner`) |
|
||||
| **Impacto** | Cualquier step que intentara `docker run -v "$(pwd):/algo"` desde dentro de un job fallaba con `read-only file system` o rutas inexistentes |
|
||||
|
||||
## Docker-in-Docker vs. sibling containers, para quien recién arranca en CI/CD
|
||||
|
||||
Cuando un job de CI necesita usar Docker (por ejemplo, para correr una
|
||||
herramienta empaquetada como imagen), hay dos formas distintas de
|
||||
dárselo, y confundirlas rompe cosas de maneras difíciles de
|
||||
diagnosticar:
|
||||
|
||||
- **Docker-in-Docker (DinD):** el contenedor del job tiene su **propio**
|
||||
daemon Docker corriendo adentro, completamente aislado del daemon
|
||||
del host. Cuando el job hace `docker run`, ese contenedor nuevo nace
|
||||
*dentro* del contenedor del job, como una muñeca rusa. El
|
||||
filesystem que ve ese daemon interno es el del contenedor del job,
|
||||
no el del host real.
|
||||
- **Sibling containers (contenedores hermanos):** el contenedor del
|
||||
job **no** tiene su propio daemon — en cambio, monta el socket del
|
||||
daemon Docker del **host** (`/var/run/docker.sock`) y habla
|
||||
directamente con él. Cuando el job hace `docker run`, ese contenedor
|
||||
nuevo nace como **hermano** del contenedor del job (mismo nivel,
|
||||
mismo host, mismo daemon), no adentro suyo.
|
||||
|
||||
La imagen `act_runner` que usa este runner de Gitea Actions usa el
|
||||
segundo modelo: **sibling containers**, montando el socket del daemon
|
||||
del host (ver `create_gitea_runner_container()` en
|
||||
`scripts/deploy-lab.sh`, que monta
|
||||
`-v /var/run/docker.sock:/var/run/docker.sock`).
|
||||
|
||||
## Por qué eso rompe un `docker run -v` ingenuo
|
||||
|
||||
Con sibling containers, cuando un step dentro de un job pide
|
||||
|
||||
```bash
|
||||
docker run -v "${{ github.workspace }}:/src" alguna-imagen
|
||||
```
|
||||
|
||||
ese comando lo ejecuta el **daemon del host**, no el contenedor del
|
||||
job. `${{ github.workspace }}` es una ruta que existe *dentro* del
|
||||
contenedor del job (por ejemplo `/workspace/devops/apps-registry`) —
|
||||
pero el daemon del host, al crear el contenedor hermano, intenta
|
||||
montar esa misma ruta **desde el filesystem del host real**, donde
|
||||
probablemente no existe (o existe otra cosa completamente distinta ahí).
|
||||
|
||||
## Síntoma
|
||||
|
||||
Al intentar correr Semgrep como container aparte (`docker run` desde
|
||||
dentro del step), el error observado en el run real fue:
|
||||
|
||||
```text
|
||||
mkdir /workspace: read-only file system
|
||||
```
|
||||
|
||||
No un "no existe la ruta" limpio — un intento de crear el directorio
|
||||
que falla porque, en el contexto real del daemon del host, esa ruta
|
||||
cae en un punto de montaje de solo lectura (o simplemente no es un
|
||||
lugar donde el daemon del host puede/debe escribir).
|
||||
|
||||
## Causa raíz
|
||||
|
||||
Sibling containers, no Docker-in-Docker: un `docker run -v
|
||||
"${{ github.workspace }}:/src"` ejecutado desde dentro de un job
|
||||
intenta montar, en el daemon del **host**, una ruta que solo tiene
|
||||
sentido **dentro** del contenedor del job. El daemon del host no ve
|
||||
esa ruta como el mismo directorio — la ve como una ruta arbitraria de
|
||||
su propio filesystem.
|
||||
|
||||
## Fix aplicado
|
||||
|
||||
Se evita el problema de raíz: **Semgrep corre nativo**, instalado
|
||||
directo en el contenedor del job (mismo criterio ya usado para
|
||||
Gitleaks, Trivy, Syft y Cosign — todos se instalan como binario/paquete
|
||||
dentro del job, ninguno corre como container aparte):
|
||||
|
||||
```bash
|
||||
python3 -m venv /tmp/semgrep-venv
|
||||
/tmp/semgrep-venv/bin/pip install --quiet "semgrep==1.173.0"
|
||||
```
|
||||
|
||||
### Por qué un venv y no `pip3 install` directo
|
||||
|
||||
La imagen del runner ya trae paquetes de Python instalados por `apt`
|
||||
(por ejemplo `PyJWT`) sin metadata compatible con `pip`. Un
|
||||
`pip3 install --break-system-packages` sobre esa base falla al
|
||||
intentar reemplazarlos (`RECORD file not found`) porque `pip` no
|
||||
encuentra el registro de archivos que necesita para saber qué está
|
||||
reemplazando. Un venv aislado evita tocar los paquetes del sistema por
|
||||
completo — Semgrep y sus dependencias quedan en su propio directorio,
|
||||
sin interferir con nada que `apt` ya haya instalado.
|
||||
|
||||
## Validación
|
||||
|
||||
Validado localmente contra `docker.gitea.com/runner-images:ubuntu-latest`
|
||||
(la misma imagen que usa el runner real) que `python3`/`pip3` están
|
||||
disponibles, y confirmado en una corrida real posterior al fix que
|
||||
Semgrep corre y reporta hallazgos sin ningún error de Docker de por
|
||||
medio.
|
||||
Reference in New Issue
Block a user