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:
@@ -104,6 +104,74 @@ Si la imagen fue firmada por este pipeline, el comando termina con
|
||||
firmó, porque la firmó otra llave, o porque la imagen fue modificada
|
||||
después — termina con `exit 1` y un error explícito.
|
||||
|
||||
## Limitaciones conocidas y mejoras futuras
|
||||
|
||||
Dos limitaciones identificadas al validar el pipeline end-to-end,
|
||||
documentadas a propósito como TODO — ninguna de las dos se implementó
|
||||
todavía.
|
||||
|
||||
### 1. Se firma por tag, no por digest
|
||||
|
||||
Hoy el pipeline firma `ecommerce-frontend:v1.0.104` — un **tag**, no un
|
||||
**digest** (`sha256:...`). Un tag es una etiqueta mutable: nada impide
|
||||
que, después de firmada la imagen, alguien (o un bug en el propio
|
||||
pipeline) vuelva a subir contenido distinto bajo el mismo tag
|
||||
`v1.0.104`. La firma original seguiría "verificando" — porque Cosign,
|
||||
al verificar por tag, resuelve el tag al digest que tenga *en ese
|
||||
momento*, no al que tenía cuando se firmó. Si el tag fue reasignado,
|
||||
se está verificando una imagen distinta de la que realmente se firmó,
|
||||
sin que nada avise.
|
||||
|
||||
Esto no es hipotético: el propio Cosign lo advierte en cada firma de
|
||||
este pipeline (visto en los logs reales de cada run):
|
||||
|
||||
```text
|
||||
WARNING: Image reference gitea.cruzcloud.net/.../ecommerce-frontend:v1.0.102
|
||||
uses a tag, not a digest, to identify the image to sign.
|
||||
This can lead you to sign a different image than the intended one.
|
||||
```
|
||||
|
||||
**TODO:** capturar el digest exacto que devuelve `docker push` (o
|
||||
`docker buildx build --metadata-file`) y firmar/verificar contra ese
|
||||
digest en vez del tag —
|
||||
`gitea.cruzcloud.net/devops/ecommerce-frontend@sha256:...` en vez de
|
||||
`:v1.0.104`. No implementado todavía; requiere ajustar el step de
|
||||
build para exponer el digest como output y pasarlo a los steps de
|
||||
firma y verificación.
|
||||
|
||||
### 2. El transparency log (Rekor) está deshabilitado
|
||||
|
||||
Ya se explicó arriba, en la sección "Por qué `--tlog-upload=false`" de
|
||||
esta misma página, la decisión consciente de no publicar en el
|
||||
transparency log para este registry privado. Vale la pena nombrar en
|
||||
simple qué es lo que se está dejando afuera:
|
||||
|
||||
Un **transparency log** (Rekor, el de Sigstore) es un registro
|
||||
público, append-only, criptográficamente verificable, de "quién firmó
|
||||
qué imagen y cuándo" — pensalo como un libro contable público que
|
||||
nadie puede editar ni borrar después de escrito, solo agregar filas
|
||||
nuevas. Cualquiera puede consultar ese libro para confirmar de forma
|
||||
independiente (sin confiar en el propio proyecto, ni en la llave
|
||||
privada, ni en el registry) que una firma específica existió en un
|
||||
momento específico.
|
||||
|
||||
Sin transparency log, la garantía que queda es más débil: *"esta firma
|
||||
la generó quien tuviera la llave privada en el momento en que se
|
||||
verificó"* — pero no hay ningún registro externo e inmutable que
|
||||
demuestre *cuándo* se generó, ni una forma de detectar si alguien con
|
||||
acceso a la llave privada firmó algo por fuera del pipeline sin que
|
||||
quede rastro. Para un lab personal con un registry privado, ese
|
||||
trade-off es razonable (ver la advertencia más abajo en esta misma
|
||||
página). Pero es una limitación real, no solo un detalle de
|
||||
configuración — importa especialmente el día que este mismo patrón se
|
||||
use en un contexto con más de una persona firmando, o con un registry
|
||||
que deje de ser privado.
|
||||
|
||||
**TODO:** si en algún momento el registry deja de ser exclusivamente
|
||||
privado, o se suma más de una persona con acceso a la llave de firma,
|
||||
reevaluar habilitar el transparency log público de Sigstore (o correr
|
||||
uno privado propio) en vez de mantenerlo deshabilitado.
|
||||
|
||||
## Qué falta (a propósito, todavía)
|
||||
|
||||
Hoy la verificación de firma corre como smoke test **dentro del mismo
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
# DevSecOps
|
||||
|
||||
Fase 2 del lab: integrar tooling de seguridad al pipeline de Gitea
|
||||
Actions, empezando por un solo repo de referencia — el frontend de ARI
|
||||
Shopping (`workloads/ecommerce`, `.gitea/workflows/build.yaml`) — antes
|
||||
de replicarlo a `commerce-backend` y `docs-portal`.
|
||||
Actions. Se empezó por un solo repo de referencia — el frontend de ARI
|
||||
Shopping (`workloads/ecommerce`, `.gitea/workflows/build.yaml`) — y ya
|
||||
está replicado, con evidencia real de corridas en verde, a los tres
|
||||
componentes del monorepo:
|
||||
|
||||
| App | Workflow | Imagen | Adaptación de Semgrep |
|
||||
|---|---|---|---|
|
||||
| Frontend (Next.js) | `build.yaml` | `ecommerce-frontend` | `p/typescript` + `p/react` + `p/nextjs` + `p/security-audit` |
|
||||
| Backend (Medusa v2, API TypeScript) | `build-medusa.yaml` | `ecommerce-medusa` | `p/typescript` + `p/security-audit` + `p/owasp-top-ten` (sin React/Next.js — es una API, no SSR) |
|
||||
| Docs Portal (MkDocs, Python) | `deploy-docs.yaml` | `docs-portal` | `p/python` + `p/security-audit` (sin código de aplicación propio hoy) |
|
||||
|
||||
Cada app tiene su propia llave pública de Cosign commiteada
|
||||
(`cosign.pub` en su propio directorio), pero las tres comparten el
|
||||
mismo par de llaves de firma (mismos secrets `COSIGN_PRIVATE_KEY` /
|
||||
`COSIGN_PASSWORD` a nivel de repo) — una sola identidad de firma para
|
||||
todo el registry de este lab.
|
||||
|
||||
Cinco herramientas, cada una respondiendo una pregunta distinta:
|
||||
|
||||
@@ -18,6 +31,11 @@ Cinco herramientas, cada una respondiendo una pregunta distinta:
|
||||
|
||||
## El pipeline completo
|
||||
|
||||
El mismo patrón corre, con el mismo orden de etapas, en los tres
|
||||
workflows (`build.yaml`, `build-medusa.yaml`, `deploy-docs.yaml`) — lo
|
||||
que cambia entre ellos es el ruleset de Semgrep y qué manifiesto(s)
|
||||
escanea Trivy IaC (ver tabla arriba), no la estructura del pipeline.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph J1["job: gitleaks (push + pull_request)"]
|
||||
@@ -28,8 +46,8 @@ flowchart TD
|
||||
A2 -- "limpio" --> B0
|
||||
|
||||
subgraph J2["job: build (solo push a main, needs: gitleaks)"]
|
||||
B0[checkout + validaciones de código] --> B1["Trivy IaC<br/>(frontend.yaml)"]
|
||||
B1 -.informa.-> B2["Semgrep SAST<br/>(audit mode)"]
|
||||
B0[checkout + validaciones de código] --> B1["Trivy IaC<br/>(manifiesto K8s de la app)"]
|
||||
B1 -.informa.-> B2["Semgrep SAST<br/>(ruleset por stack, audit mode)"]
|
||||
B2 -.informa.-> B3[login registry]
|
||||
B3 --> B4["docker build<br/>(push: false, load: true)"]
|
||||
B4 --> B5["Trivy imagen<br/>CRITICAL"]
|
||||
@@ -40,7 +58,7 @@ flowchart TD
|
||||
B8 --> B9["cosign sign"]
|
||||
B9 --> B10["cosign verify<br/>(smoke test)"]
|
||||
B10 -- "firma inválida" --> XC["❌ detenido"]
|
||||
B10 -- "firma válida" --> B11["actualizar frontend.yaml<br/>(GitOps, Argo CD sincroniza)"]
|
||||
B10 -- "firma válida" --> B11["actualizar manifiesto de la app<br/>(GitOps, Argo CD sincroniza)"]
|
||||
end
|
||||
```
|
||||
|
||||
@@ -87,5 +105,15 @@ flowchart TD
|
||||
- Admission controller en el cluster que verifique la firma de Cosign
|
||||
antes de dejar correr un Pod (ver [cosign.md](cosign.md)) — hoy la
|
||||
verificación es solo un smoke test dentro del propio pipeline.
|
||||
- Replicar este mismo patrón a `commerce-backend` (Medusa) y
|
||||
`docs-portal`, adaptando lo que corresponda a cada stack.
|
||||
- Firmar por digest en vez de por tag, y reevaluar el transparency log
|
||||
de Sigstore — ver
|
||||
[limitaciones conocidas de Cosign](cosign.md#limitaciones-conocidas-y-mejoras-futuras).
|
||||
- `commerce-backend` tiene dos CVE CRITICAL documentados como excepción
|
||||
puntual en `workloads/commerce-backend/.trivyignore` (Go stdlib
|
||||
embebido en el binario de esbuild, sin fix compatible con la versión
|
||||
de Vite que usa el admin-sdk de Medusa hoy) — revisar en cada bump de
|
||||
`@medusajs/*` si ya deja de ser necesaria.
|
||||
- Contención de recursos entre Gitea y su runner bajo carga real —
|
||||
causa raíz confirmada, fix propuesto y documentado, aplicación
|
||||
manual pendiente (ver
|
||||
[playbook de contención de recursos](../playbooks/incidente-contencion-recursos-gitea-runner-2026-08.md)).
|
||||
|
||||
Reference in New Issue
Block a user