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