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:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user