feat(devsecops): agregar Gitleaks al pipeline del frontend

Corre en push y pull_request, antes del job build (needs: gitleaks).
Si detecta un secreto, exit-code=1 detiene el pipeline antes de
construir la imagen. Scan histórico completo del repo (249 commits)
confirmado limpio, corrido aparte de forma manual.

Documenta el step en docs/devsecops/gitleaks.md: qué es secret
scanning, por qué corre antes del build, cómo leer un hallazgo y
cómo manejar falsos positivos con allowlist.
This commit is contained in:
2026-08-14 19:48:44 -05:00
parent 66c44a7dad
commit 422e9d257d
3 changed files with 195 additions and 1 deletions
+56 -1
View File
@@ -4,7 +4,7 @@ on:
push: push:
branches: branches:
- main - main
paths: paths: &frontend_paths
# Código y configuración real del frontend. # Código y configuración real del frontend.
# Los cambios exclusivos de GitOps (frontend.yaml, ingress, patches, etc.) # Los cambios exclusivos de GitOps (frontend.yaml, ingress, patches, etc.)
# no vuelven a construir la imagen. # no vuelven a construir la imagen.
@@ -29,14 +29,69 @@ on:
- 'workloads/ecommerce/public/**' - 'workloads/ecommerce/public/**'
- 'workloads/ecommerce/styles/**' - 'workloads/ecommerce/styles/**'
- '.gitea/workflows/build.yaml' - '.gitea/workflows/build.yaml'
pull_request:
branches:
- main
paths: *frontend_paths
permissions: permissions:
contents: write contents: write
packages: write packages: write
jobs: 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: build:
name: Construir y Subir Imagen name: Construir y Subir Imagen
needs: gitleaks
if: github.event_name == 'push'
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 20 timeout-minutes: 20
@@ -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<br/>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.
+2
View File
@@ -98,6 +98,8 @@ nav:
- Guía del estudiante: - Guía del estudiante:
- Inicio: guia-estudiante/README.md - Inicio: guia-estudiante/README.md
- Conceptos básicos: guia-estudiante/conceptos-basicos.md - Conceptos básicos: guia-estudiante/conceptos-basicos.md
- DevSecOps:
- Gitleaks (secretos): devsecops/gitleaks.md
extra: extra:
social: social: