diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index b48a783..11a6078 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -4,7 +4,7 @@ on: push: branches: - main - paths: + paths: &frontend_paths # Código y configuración real del frontend. # Los cambios exclusivos de GitOps (frontend.yaml, ingress, patches, etc.) # no vuelven a construir la imagen. @@ -29,14 +29,69 @@ on: - 'workloads/ecommerce/public/**' - 'workloads/ecommerce/styles/**' - '.gitea/workflows/build.yaml' + pull_request: + branches: + - main + paths: *frontend_paths permissions: contents: write packages: write jobs: + # Corre en push y en pull_request, siempre antes que build. Si encuentra + # un secreto commiteado, el step termina con exit code distinto de 0 y, + # por el "needs" del job build, la imagen nunca se construye ni se sube. + gitleaks: + name: Escaneo de Secretos (Gitleaks) + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout del código + uses: actions/checkout@v3 + with: + fetch-depth: 1 + + - name: Instalar Gitleaks + shell: bash + run: | + set -euo pipefail + GITLEAKS_VERSION="8.21.2" + curl -sSfL \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + -o /tmp/gitleaks.tar.gz + tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks + chmod +x /tmp/gitleaks + /tmp/gitleaks version + + # --no-git: escanea el árbol de archivos del checkout (fetch-depth: 1, + # sin historial), no el log de commits. El scan histórico completo del + # repo se corre aparte, manualmente, no en cada push/PR. + - name: Escanear secretos en el árbol de archivos + shell: bash + run: | + set -euo pipefail + /tmp/gitleaks detect \ + --source=workloads/ecommerce \ + --no-git \ + --redact \ + --report-format=json \ + --report-path=gitleaks-report.json \ + --exit-code=1 + + - name: Publicar reporte de Gitleaks + if: always() + uses: actions/upload-artifact@v3 + with: + name: gitleaks-report + path: gitleaks-report.json + if-no-files-found: ignore + build: name: Construir y Subir Imagen + needs: gitleaks + if: github.event_name == 'push' runs-on: ubuntu-latest timeout-minutes: 20 diff --git a/workloads/docs-portal/docs/devsecops/gitleaks.md b/workloads/docs-portal/docs/devsecops/gitleaks.md new file mode 100644 index 0000000..501f253 --- /dev/null +++ b/workloads/docs-portal/docs/devsecops/gitleaks.md @@ -0,0 +1,137 @@ +# Gitleaks — detección de secretos + +!!! info "Qué problema resuelve" + Gitleaks busca patrones de credenciales (API keys, tokens, contraseñas, + llaves privadas) dentro del código fuente. No sabe si una credencial es + "real" — detecta **formas** que parecen credenciales (una API key de + AWS siempre empieza con `AKIA`, una llave privada siempre tiene el + encabezado `-----BEGIN PRIVATE KEY-----`, etc.) y también cadenas con + entropía alta (aleatoriedad), que suelen ser tokens generados. + +## Por qué esto importa + +Un secreto commiteado a git **nunca deja de estar ahí**, aunque lo borres +en el siguiente commit. Sigue existiendo en el historial, en cualquier +fork, en cualquier clon local que alguien ya haya hecho. La única manera +real de "revocar" un secreto filtrado es rotarlo (generar uno nuevo e +invalidar el viejo) — borrar el commit no alcanza. + +Por eso el objetivo de gitleaks no es "arreglar" el secreto después de que +se filtró, sino **evitar que el commit con el secreto llegue a existir en +el repo remoto**. + +## Por qué corre antes del build + +En `.gitea/workflows/build.yaml`, el job `gitleaks` corre **antes** que el +job `build` (que compila la imagen Docker y la sube al registry). El job +`build` tiene `needs: gitleaks` — si el scan falla, `build` ni siquiera +arranca. + +```mermaid +flowchart LR + A[push / pull_request] --> B[gitleaks] + B -- "sin hallazgos" --> C[build] + B -- "secreto detectado" --> X["❌ pipeline detenido
build no corre"] +``` + +La lógica es simple: no tiene sentido gastar tiempo de build y minutos de +runner compilando una imagen a partir de un commit que de todas formas hay +que rechazar. Fallar rápido, fallar barato. + +También corre en **pull request**, no solo en push a `main` — así un +secreto se detecta antes de que el PR se mergee, que es el punto donde +todavía es más fácil corregirlo (basta con un `git commit --amend` o un +nuevo commit en la misma rama, sin tocar `main`). + +## Alcance de este scan + +El step de CI escanea el árbol de archivos ya *checked out* del commit +(`gitleaks detect --no-git`), no el historial completo — el checkout del +pipeline es superficial (`fetch-depth: 1`, solo el último commit), así que +no hay historial que recorrer en ese punto. + +Antes de integrar esto al pipeline corrimos un **scan histórico completo** +del repo `apps-registry` (los 249 commits, con `gitleaks detect` en modo +git normal, sin `--no-git`) para confirmar que no había secretos ya +commiteados en el pasado. Resultado: **sin hallazgos**. Ese scan histórico +es una tarea puntual, no algo que corra en cada push — si alguna vez se +sospecha una filtración vieja, se repite manualmente. + +## Cómo leer un hallazgo + +Un hallazgo de gitleaks (en el `gitleaks-report.json` que el pipeline +publica como artifact) se ve así: + +```json +{ + "Description": "AWS Access Key", + "StartLine": 14, + "File": "workloads/ecommerce/lib/config.ts", + "Match": "REDACTED", + "Secret": "REDACTED", + "RuleID": "aws-access-token", + "Commit": "a1b2c3d" +} +``` + +Campos clave: + +| Campo | Qué significa | +|---|---| +| `RuleID` | Qué tipo de secreto detectó (la regla que hizo match) | +| `File` / `StartLine` | Dónde está, exactamente | +| `Match` / `Secret` | El valor detectado — el pipeline usa `--redact`, así que en el reporte real aparece censurado, no en texto plano | +| `Commit` | En qué commit se introdujo (solo aplica al scan histórico, no al scan `--no-git` del pipeline) | + +!!! danger "Si el hallazgo es real" + 1. **No lo borres del código y listo** — el secreto sigue "filtrado" + aunque ya no esté en el archivo actual. + 2. **Rota la credencial primero** en el sistema que la emitió (AWS, + Gitea, Medusa, lo que sea). Un secreto que ya se vio en un log de + CI o en un diff de PR se trata como comprometido. + 3. Después de rotarla, sí, saca el valor viejo del código y usa una + variable de entorno / secret de Gitea Actions en su lugar. + 4. Si el secreto llegó a estar en `main` (no solo en una rama de PR), + avisa antes de reescribir historial — reescribir historial en un + repo compartido tiene sus propios riesgos y hay que decidirlo con + calma, no como reacción automática del pipeline. + +## Falsos positivos: cómo hacer allowlist + +Gitleaks detecta *formas*, no intención. Cosas que típicamente generan +falsos positivos en este proyecto: + +- Placeholders como `CAMBIAR` en `commerce/secrets.template.yaml` — **no** + deberían disparar nada porque no tienen la forma de un secreto real + (baja entropía, texto plano legible), pero si algún día se usa un + placeholder con más pinta de secreto real (ej. un UUID de ejemplo), sí + puede hacer match. +- Hashes largos o IDs opacos que no son secretos (ej. `MEDUSA_REGION_ID` + en `frontend.yaml`), si tienen entropía suficientemente alta. + +Cuando gitleaks marca algo que **no es** un secreto real, se agrega una +regla de allowlist en un archivo `.gitleaks.toml` en la raíz del repo +(todavía no existe — se crea la primera vez que haga falta): + +```toml +[allowlist] +description = "Falsos positivos conocidos del lab" +regexes = [ + '''MEDUSA_REGION_ID''', +] +paths = [ + '''workloads/ecommerce/commerce/secrets\.template\.yaml''', +] +``` + +!!! warning "No es una vía rápida para ignorar hallazgos reales" + Cada entrada de allowlist debe quedar documentada (por qué es un falso + positivo, no solo "molestaba") y revisada antes de mergear, porque una + allowlist mal escrita (una regex demasiado amplia) puede silenciar un + secreto real futuro sin que nadie se dé cuenta. + +## Dónde ver el resultado + +El job `gitleaks` publica el reporte JSON como artifact del pipeline +(`gitleaks-report`) en cada ejecución, tenga o no hallazgos — así queda +disponible para inspección incluso cuando el scan pasa limpio. diff --git a/workloads/docs-portal/mkdocs.yml b/workloads/docs-portal/mkdocs.yml index b6e71fe..e63b6ed 100644 --- a/workloads/docs-portal/mkdocs.yml +++ b/workloads/docs-portal/mkdocs.yml @@ -98,6 +98,8 @@ nav: - Guía del estudiante: - Inicio: guia-estudiante/README.md - Conceptos básicos: guia-estudiante/conceptos-basicos.md + - DevSecOps: + - Gitleaks (secretos): devsecops/gitleaks.md extra: social: