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:
2026-08-15 15:17:43 -05:00
parent 826f199ebc
commit be3e5c2a4c
9 changed files with 666 additions and 13 deletions
@@ -0,0 +1,103 @@
# Incidente: SQLite en modo DELETE causando contención en Gitea (2026-08)
| | |
|---|---|
| **Ventana del incidente** | 2026-08-15, coincidiendo con el arranque del runner de Gitea Actions y el polling frecuente de tareas |
| **Servicio afectado** | `gitea` (base de datos SQLite propia, `gitea.db`) |
| **Impacto** | Errores intermitentes `database is locked (SQLITE_BUSY)` al escribir desde distintos componentes de Gitea (runner, web, cron) al mismo tiempo |
## Contexto: por qué Gitea usa SQLite acá
Este lab corre Gitea con `DB_TYPE = sqlite3` (no Postgres/MySQL) — una
sola base de datos, un solo archivo (`gitea.db`), sin servidor de base
de datos aparte. Es la opción correcta para un lab de un solo nodo,
pero SQLite tiene una particularidad importante: por defecto abre el
archivo en **modo DELETE** (el modo "clásico" de journaling).
## Qué es el modo DELETE y por qué molesta acá
En modo DELETE, cada transacción de escritura:
1. Crea un archivo de journal temporal (`gitea.db-journal`).
2. Toma un **lock exclusivo sobre el archivo completo** de la base de
datos mientras dura la escritura.
3. Borra el journal al terminar.
Ese lock exclusivo es el problema: **mientras una escritura está en
curso, nada más puede leer ni escribir** — ni siquiera otro proceso
que solo quiere leer una fila que no tiene nada que ver con la que se
está escribiendo.
!!! info "Por qué esto pega justo en Gitea Actions"
El runner de Gitea Actions hace *polling* constante contra la API
de Gitea (cada pocos segundos, según `fetch_interval` en su
`config.yaml`) para preguntar "¿hay una tarea nueva?", y además
escribe actualizaciones de estado de cada step casi en tiempo real
mientras un job corre. Sumale los cron jobs internos de Gitea y el
tráfico normal de la web UI. Con la base en modo DELETE, todo eso
compite por el mismo lock exclusivo — cuantas más cosas escriben
seguido, más chance de pisarse.
## Síntoma
`SQLITE_BUSY` intermitente en los logs de Gitea, típicamente en
operaciones de corta duración (actualizar el estado de un step,
registrar un heartbeat del runner) que no deberían tener motivo para
fallar por contención:
```text
[E] can't update runner status: database is locked (5) (SQLITE_BUSY)
```
## Causa raíz
Modo de journaling `DELETE` (el default de SQLite si no se configura
nada distinto) combinado con el patrón de acceso de Gitea Actions:
muchas escrituras cortas y frecuentes desde procesos distintos
(runner, servidor web, cron), cada una compitiendo por un lock
exclusivo de archivo completo.
## Fix aplicado
Se cambió el modo de journaling a **WAL** (Write-Ahead Logging) en
`app.ini`:
```ini
[database]
; journal_mode por defecto (DELETE) toma un lock exclusivo del archivo
; completo en cada escritura. WAL permite lectores concurrentes mientras
; hay una escritura en curso -- fix para "database is locked" bajo el
; polling frecuente de Actions + cron jobs (diagnosticado 2026-08-15).
SQLITE_JOURNAL_MODE = WAL
```
### Por qué WAL resuelve esto (y qué NO resuelve)
En modo WAL, las escrituras no se aplican directo al archivo principal
de la base: se van agregando a un archivo aparte (`gitea.db-wal`), y
los lectores siguen leyendo del estado consistente más reciente sin
bloquearse. La regla cambia de *"una escritura bloquea todo"* a
*"una escritura bloquea solo a otra escritura"* — los lectores dejan
de competir por el lock.
!!! warning "WAL reduce la contención, no la elimina"
WAL sigue permitiendo **una sola escritura a la vez** — dos
escrituras concurrentes todavía pueden chocar y devolver
`SQLITE_BUSY` si el proceso no reintenta. En una corrida de este
mismo playbook se vio, después de aplicado el fix, una única
ocurrencia aislada de `database is locked` coincidiendo con un
reinicio del contenedor de `gitea` (ver
[contención de recursos de Gitea/runner](incidente-contencion-recursos-gitea-runner-2026-08.md)) —
consistente con una escritura en curso justo en el momento del
reinicio, no con que WAL no esté funcionando. Frecuencia bajó de
"molesta con cierta regularidad" a "un caso aislado en varias
horas de uso intensivo".
## Validación
El cambio se aplicó directo en el `app.ini` montado como volumen del
contenedor de `gitea` (no en un `docker exec` puntual) — persiste
across reinicios del contenedor, confirmado leyendo el archivo montado
después de un reinicio real del contenedor (ver
[contención de recursos](incidente-contencion-recursos-gitea-runner-2026-08.md)),
no solo aplicado en caliente y perdido al reciclar el contenedor.