Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
376eb6d46a |
+5
-245
@@ -4,7 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths: &frontend_paths
|
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,69 +29,14 @@ 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
|
||||||
|
|
||||||
@@ -235,76 +180,6 @@ jobs:
|
|||||||
|
|
||||||
echo "OK: navegación real del catálogo validada."
|
echo "OK: navegación real del catálogo validada."
|
||||||
|
|
||||||
- name: Instalar Trivy
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
TRIVY_VERSION="0.74.0"
|
|
||||||
curl -sSfL \
|
|
||||||
"https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" \
|
|
||||||
-o /tmp/trivy.tar.gz
|
|
||||||
tar -xzf /tmp/trivy.tar.gz -C /tmp trivy
|
|
||||||
chmod +x /tmp/trivy
|
|
||||||
/tmp/trivy version
|
|
||||||
|
|
||||||
# Escanea el manifiesto Kubernetes real del frontend (no la imagen):
|
|
||||||
# contenedores como root, falta de resource limits, falta de
|
|
||||||
# readiness/liveness probes, etc. Informativo por ahora — no bloquea
|
|
||||||
# el pipeline mientras revisamos juntos qué hallazgos son reales.
|
|
||||||
- name: Escanear manifiestos Kubernetes (Trivy IaC)
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
/tmp/trivy config \
|
|
||||||
--severity CRITICAL,HIGH,MEDIUM \
|
|
||||||
--exit-code 0 \
|
|
||||||
"${MANIFEST_FILE}"
|
|
||||||
|
|
||||||
# Modo auditoría: sin --error a propósito, Semgrep siempre termina
|
|
||||||
# con exit 0 aunque reporte hallazgos. Es la primera vuelta — se
|
|
||||||
# revisan los resultados en conjunto antes de decidir qué reglas
|
|
||||||
# deberían pasar a bloquear el pipeline más adelante.
|
|
||||||
- name: Escaneo SAST (Semgrep) — modo auditoría, no bloquea
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
SEMGREP_IMAGE="semgrep/semgrep:1.173.0"
|
|
||||||
|
|
||||||
docker run --rm \
|
|
||||||
-v "${{ github.workspace }}:/src" \
|
|
||||||
-w /src \
|
|
||||||
"${SEMGREP_IMAGE}" \
|
|
||||||
semgrep scan \
|
|
||||||
--config=p/typescript \
|
|
||||||
--config=p/react \
|
|
||||||
--config=p/nextjs \
|
|
||||||
--config=p/security-audit \
|
|
||||||
--json \
|
|
||||||
--output=semgrep-report.json \
|
|
||||||
"${APP_DIR}"
|
|
||||||
|
|
||||||
echo "=== Resumen Semgrep ==="
|
|
||||||
docker run --rm \
|
|
||||||
-v "${{ github.workspace }}:/src" \
|
|
||||||
-w /src \
|
|
||||||
"${SEMGREP_IMAGE}" \
|
|
||||||
python3 -c "
|
|
||||||
import json
|
|
||||||
data = json.load(open('semgrep-report.json'))
|
|
||||||
results = data.get('results', [])
|
|
||||||
print(f'Hallazgos: {len(results)}')
|
|
||||||
for r in results:
|
|
||||||
print(f\" [{r['extra']['severity']}] {r['check_id']} - {r['path']}:{r['start']['line']}\")
|
|
||||||
"
|
|
||||||
|
|
||||||
- name: Publicar reporte de Semgrep
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: semgrep-report
|
|
||||||
path: semgrep-report.json
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
- name: Login en Gitea Registry
|
- name: Login en Gitea Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
@@ -313,129 +188,14 @@ jobs:
|
|||||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
logout: true
|
logout: true
|
||||||
|
|
||||||
# push: false — la imagen se queda cargada en el daemon local (load:
|
- name: Construir y Subir Imagen
|
||||||
# true) para poder escanearla con Trivy antes de subirla al registry.
|
|
||||||
- name: Construir Imagen
|
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: workloads/ecommerce/
|
context: workloads/ecommerce/
|
||||||
push: false
|
push: true
|
||||||
load: true
|
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
|
gitea.cruzcloud.net/devops/ecommerce-frontend:${{ steps.vars.outputs.VERSION }}
|
||||||
${{ env.IMAGE_NAME }}:latest
|
gitea.cruzcloud.net/devops/ecommerce-frontend:latest
|
||||||
|
|
||||||
# CRITICAL bloquea el pipeline: no se sube una imagen con una CVE
|
|
||||||
# crítica conocida y con fix disponible.
|
|
||||||
- name: Escanear imagen (Trivy) — CRITICAL bloquea
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
/tmp/trivy image \
|
|
||||||
--severity CRITICAL \
|
|
||||||
--exit-code 1 \
|
|
||||||
--ignore-unfixed \
|
|
||||||
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
|
|
||||||
|
|
||||||
# HIGH solo informa por ahora — no bloquea mientras aprendemos a leer
|
|
||||||
# los reportes y decidimos, con calma, qué reglas deben bloquear.
|
|
||||||
- name: Escanear imagen (Trivy) — HIGH informativo
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
/tmp/trivy image \
|
|
||||||
--severity HIGH \
|
|
||||||
--exit-code 0 \
|
|
||||||
--ignore-unfixed \
|
|
||||||
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
|
|
||||||
|
|
||||||
# SBOM de la imagen que ya pasó el gate de CRITICAL — describe
|
|
||||||
# exactamente qué paquetes (y en qué versión) quedaron adentro.
|
|
||||||
# CycloneDX: formato más usado por herramientas de consulta/alertas
|
|
||||||
# de CVEs (ej. cruzar el SBOM contra un aviso nuevo tipo log4shell).
|
|
||||||
- name: Instalar Syft
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
SYFT_VERSION="1.51.0"
|
|
||||||
curl -sSfL \
|
|
||||||
"https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}/syft_${SYFT_VERSION}_linux_amd64.tar.gz" \
|
|
||||||
-o /tmp/syft.tar.gz
|
|
||||||
tar -xzf /tmp/syft.tar.gz -C /tmp syft
|
|
||||||
chmod +x /tmp/syft
|
|
||||||
/tmp/syft version
|
|
||||||
|
|
||||||
- name: Generar SBOM (Syft)
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
/tmp/syft "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}" \
|
|
||||||
-o cyclonedx-json=sbom.cdx.json \
|
|
||||||
-o spdx-json=sbom.spdx.json
|
|
||||||
|
|
||||||
- name: Publicar SBOM
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: sbom-${{ steps.vars.outputs.VERSION }}
|
|
||||||
path: |
|
|
||||||
sbom.cdx.json
|
|
||||||
sbom.spdx.json
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
# Login ya se hizo arriba; recién acá se sube, después de que la
|
|
||||||
# imagen pasó el gate de CRITICAL.
|
|
||||||
- name: Subir Imagen al Registry
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
docker push "${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
|
|
||||||
docker push "${IMAGE_NAME}:latest"
|
|
||||||
|
|
||||||
- name: Instalar Cosign
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
COSIGN_VERSION="3.1.3"
|
|
||||||
curl -sSfL \
|
|
||||||
"https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}/cosign-linux-amd64" \
|
|
||||||
-o /tmp/cosign
|
|
||||||
chmod +x /tmp/cosign
|
|
||||||
/tmp/cosign version
|
|
||||||
|
|
||||||
# Firma solo la versión (no :latest, que es un tag mutable y firmarlo
|
|
||||||
# pierde sentido en cuanto se vuelve a mover). --use-signing-config=false
|
|
||||||
# --tlog-upload=false: firma solo con el par de llaves propio, sin
|
|
||||||
# publicar metadata en el transparency log público de Sigstore — este
|
|
||||||
# registry es privado, no tiene sentido anunciar públicamente qué se
|
|
||||||
# firmó y cuándo.
|
|
||||||
- name: Firmar Imagen (Cosign)
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
|
||||||
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
/tmp/cosign sign \
|
|
||||||
--key env://COSIGN_PRIVATE_KEY \
|
|
||||||
--use-signing-config=false \
|
|
||||||
--tlog-upload=false \
|
|
||||||
--yes \
|
|
||||||
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
|
|
||||||
|
|
||||||
# Smoke test: confirma en el propio pipeline que la firma que se
|
|
||||||
# acaba de crear valida contra la llave pública commiteada en el
|
|
||||||
# repo (workloads/ecommerce/cosign.pub). Es la misma verificación
|
|
||||||
# que, más adelante, podría correr un admission controller en el
|
|
||||||
# cluster antes de dejar desplegar la imagen (ver docs/devsecops/cosign.md).
|
|
||||||
- name: Verificar Firma (smoke test)
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
/tmp/cosign verify \
|
|
||||||
--key "${APP_DIR}/cosign.pub" \
|
|
||||||
--insecure-ignore-tlog=true \
|
|
||||||
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
|
|
||||||
|
|
||||||
# Evita que una ejecución antigua actualice frontend.yaml después de que
|
# Evita que una ejecución antigua actualice frontend.yaml después de que
|
||||||
# ya exista un commit más reciente en main.
|
# ya exista un commit más reciente en main.
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ spec:
|
|||||||
- name: gitea-registry-secret
|
- name: gitea-registry-secret
|
||||||
containers:
|
containers:
|
||||||
- name: docs-portal
|
- name: docs-portal
|
||||||
image: gitea.cruzcloud.net/devops/docs-portal:v1.0.97
|
image: gitea.cruzcloud.net/devops/docs-portal:v1.0.0
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 80
|
- containerPort: 80
|
||||||
resources:
|
resources:
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
# Cosign — firma de imágenes
|
|
||||||
|
|
||||||
!!! info "Qué problema resuelve"
|
|
||||||
Todo lo anterior en esta sección (Gitleaks, Trivy, Semgrep, SBOM)
|
|
||||||
responde a la pregunta *"¿esta imagen es segura de construir?"*.
|
|
||||||
Cosign responde una pregunta distinta, y que pasa **después**: *"la
|
|
||||||
imagen que está corriendo ahora mismo en el cluster, ¿es
|
|
||||||
exactamente la que armó el pipeline — o pudo haber sido reemplazada,
|
|
||||||
modificada, o subida por otra vía?"*.
|
|
||||||
|
|
||||||
## Analogía simple
|
|
||||||
|
|
||||||
Pensalo como el sello de cera en un sobre antiguo. Cualquiera puede leer
|
|
||||||
la carta (la imagen es pública, cualquiera puede bajarla del registry) —
|
|
||||||
eso Cosign no lo esconde. Lo que el sello garantiza es otra cosa: que la
|
|
||||||
carta salió exactamente de donde dice que salió, y que nadie la abrió y
|
|
||||||
volvió a cerrar en el camino.
|
|
||||||
|
|
||||||
- **La llave privada** (guardada como secret de Gitea, nunca en el repo)
|
|
||||||
es el sello físico — solo el pipeline de CI puede estampar una firma
|
|
||||||
válida, porque solo él tiene el sello.
|
|
||||||
- **La llave pública** (`workloads/ecommerce/cosign.pub`, commiteada sin
|
|
||||||
problema — es pública a propósito) es la forma de reconocer el sello:
|
|
||||||
cualquiera puede mirar la carta, ver el sello, y confirmar "sí, esto lo
|
|
||||||
selló quien tiene la llave privada" — sin necesitar la llave privada
|
|
||||||
para verificarlo.
|
|
||||||
|
|
||||||
Si alguien sube una imagen distinta con el mismo tag, o modifica un solo
|
|
||||||
byte de la imagen original, la firma deja de coincidir. No es que Cosign
|
|
||||||
"detecte" la alteración activamente — es que la verificación
|
|
||||||
simplemente falla, porque la firma fue calculada sobre el digest exacto
|
|
||||||
de la imagen original.
|
|
||||||
|
|
||||||
## Cómo funciona en este pipeline
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
A["docker push"] --> B["cosign sign<br/>(llave privada, secret)"]
|
|
||||||
B --> C["cosign verify<br/>(llave pública, repo)"]
|
|
||||||
C -- "firma válida" --> D["✅ pipeline termina OK"]
|
|
||||||
C -- "firma inválida/ausente" --> X["❌ pipeline falla"]
|
|
||||||
```
|
|
||||||
|
|
||||||
1. **Par de llaves**: generado una vez con `cosign generate-key-pair`,
|
|
||||||
protegido por password. La privada (`cosign.key`) se subió como
|
|
||||||
secret de Gitea Actions (`COSIGN_PRIVATE_KEY` + `COSIGN_PASSWORD`) —
|
|
||||||
nunca se commiteó al repo, ni existe en el disco de este equipo
|
|
||||||
después de subirla. La pública (`cosign.pub`) sí vive commiteada en
|
|
||||||
`workloads/ecommerce/cosign.pub`, porque su función es poder
|
|
||||||
compartirse.
|
|
||||||
2. **Firma**: después de subir la imagen al registry, el pipeline la
|
|
||||||
firma con la llave privada (leída desde el secret vía
|
|
||||||
`--key env://COSIGN_PRIVATE_KEY`, sin escribirla nunca a disco).
|
|
||||||
3. **Verificación (smoke test)**: en el mismo pipeline, inmediatamente
|
|
||||||
después, se verifica la firma recién creada contra la llave pública
|
|
||||||
del repo. Si algo salió mal (llave incorrecta, imagen corrupta), el
|
|
||||||
pipeline falla ahí mismo — antes de que nadie más intente confiar en
|
|
||||||
esa imagen.
|
|
||||||
|
|
||||||
## Por qué `--tlog-upload=false`
|
|
||||||
|
|
||||||
Cosign, por defecto, publica cada firma en el *transparency log* público
|
|
||||||
de Sigstore (Rekor) — un registro público, auditable, de "quién firmó
|
|
||||||
qué y cuándo", pensado para proyectos open source donde esa
|
|
||||||
transparencia es el punto. Este registry (`gitea.cruzcloud.net`) es
|
|
||||||
privado; no tiene sentido — y sería una fuga de metadata innecesaria —
|
|
||||||
anunciar públicamente que este lab construyó una imagen `v1.0.97` en tal
|
|
||||||
fecha. Por eso el pipeline firma solo con el par de llaves propio,
|
|
||||||
localmente, sin tocar el transparency log público
|
|
||||||
(`--use-signing-config=false --tlog-upload=false` al firmar,
|
|
||||||
`--insecure-ignore-tlog=true` al verificar).
|
|
||||||
|
|
||||||
!!! warning "Trade-off consciente, no gratis"
|
|
||||||
Sin transparency log, la garantía es "esta firma la generó quien
|
|
||||||
tiene la llave privada" — pero no hay un registro público e
|
|
||||||
inmutable de *cuándo* se generó cada firma. Para un registry privado
|
|
||||||
de un lab personal, ese trade-off tiene sentido. Para un proyecto
|
|
||||||
open source con más de una persona firmando, seguramente no.
|
|
||||||
|
|
||||||
## Qué NO se firma
|
|
||||||
|
|
||||||
Solo se firma el tag versionado (`ecommerce-frontend:v1.0.X`), no
|
|
||||||
`:latest`. `:latest` es un tag mutable — se re-apunta a una imagen
|
|
||||||
distinta en cada build — así que firmarlo no significa nada útil: la
|
|
||||||
firma quedaría asociada al digest de turno, y la siguiente build la
|
|
||||||
volvería a mover. Cualquier verificación real de firma debería apuntar
|
|
||||||
siempre a un tag de versión específico (o, mejor todavía, al digest
|
|
||||||
exacto).
|
|
||||||
|
|
||||||
## Verificar manualmente
|
|
||||||
|
|
||||||
Con la llave pública del repo, cualquiera puede confirmar la firma de
|
|
||||||
una imagen sin necesitar acceso a nada privado:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cosign verify \
|
|
||||||
--key workloads/ecommerce/cosign.pub \
|
|
||||||
--insecure-ignore-tlog=true \
|
|
||||||
gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.97
|
|
||||||
```
|
|
||||||
|
|
||||||
Si la imagen fue firmada por este pipeline, el comando termina con
|
|
||||||
`exit 0` y muestra el detalle de la firma. Si no — sea porque nunca se
|
|
||||||
firmó, porque la firmó otra llave, o porque la imagen fue modificada
|
|
||||||
después — termina con `exit 1` y un error explícito.
|
|
||||||
|
|
||||||
## Qué falta (a propósito, todavía)
|
|
||||||
|
|
||||||
Hoy la verificación de firma corre como smoke test **dentro del mismo
|
|
||||||
pipeline que la creó** — útil para confirmar que el mecanismo funciona,
|
|
||||||
pero no impide que alguien despliegue manualmente una imagen sin firmar
|
|
||||||
en el cluster. El siguiente paso natural, que **no** se implementó en
|
|
||||||
esta primera vuelta, sería un *admission controller* en el cluster
|
|
||||||
(ej. [Sigstore's policy-controller](https://docs.sigstore.dev/policy-controller/overview/)
|
|
||||||
o [Kyverno](https://kyverno.io/policies/other/verify-images/verify-images/)
|
|
||||||
con una política de verificación de imágenes) que rechace cualquier Pod
|
|
||||||
cuya imagen no tenga una firma válida de `cosign.pub` — momento en el
|
|
||||||
que Argo CD dejaría de poder desplegar una imagen sin firmar, no solo el
|
|
||||||
pipeline de CI.
|
|
||||||
|
|
||||||
## Si la llave privada se compromete
|
|
||||||
|
|
||||||
1. Generar un par nuevo (`cosign generate-key-pair`).
|
|
||||||
2. Reemplazar `COSIGN_PRIVATE_KEY` y `COSIGN_PASSWORD` en los secrets de
|
|
||||||
Gitea Actions del repo.
|
|
||||||
3. Reemplazar `workloads/ecommerce/cosign.pub` con la nueva llave
|
|
||||||
pública, en un commit normal (no es secreto, no hace falta
|
|
||||||
reescribir historial).
|
|
||||||
4. Las imágenes ya firmadas con la llave vieja **siguen verificando
|
|
||||||
contra la llave vieja** — no se "invalidan" solas. Si se sospecha
|
|
||||||
compromiso real, hay que decidir explícitamente qué imágenes ya
|
|
||||||
desplegadas se consideran no confiables, no asumir que rotar la
|
|
||||||
llave alcanza.
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# 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`.
|
|
||||||
|
|
||||||
Cinco herramientas, cada una respondiendo una pregunta distinta:
|
|
||||||
|
|
||||||
| Herramienta | Pregunta que responde | Modo |
|
|
||||||
|---|---|---|
|
|
||||||
| [Gitleaks](gitleaks.md) | ¿Hay un secreto commiteado? | **Bloquea** |
|
|
||||||
| [Trivy — imagen](trivy.md) | ¿La imagen tiene una CVE conocida? | CRITICAL **bloquea**, HIGH informa |
|
|
||||||
| [Trivy — IaC](trivy.md) | ¿El manifiesto de Kubernetes es inseguro? | Informa |
|
|
||||||
| [Semgrep (SAST)](sast.md) | ¿El código tiene un patrón inseguro conocido? | Informa (auditoría) |
|
|
||||||
| [Syft (SBOM)](sbom.md) | ¿Qué paquetes exactos quedaron en la imagen? | Informa (inventario) |
|
|
||||||
| [Cosign](cosign.md) | ¿Esta imagen es exactamente la que armó el pipeline? | **Bloquea** (smoke test) |
|
|
||||||
|
|
||||||
## El pipeline completo
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
subgraph J1["job: gitleaks (push + pull_request)"]
|
|
||||||
A1[checkout] --> A2["gitleaks detect --no-git"]
|
|
||||||
end
|
|
||||||
|
|
||||||
A2 -- "secreto encontrado" --> XA["❌ pipeline detenido<br/>build nunca arranca"]
|
|
||||||
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)"]
|
|
||||||
B2 -.informa.-> B3[login registry]
|
|
||||||
B3 --> B4["docker build<br/>(push: false, load: true)"]
|
|
||||||
B4 --> B5["Trivy imagen<br/>CRITICAL"]
|
|
||||||
B5 -- "CRITICAL con fix" --> XB["❌ detenido<br/>no se sube la imagen"]
|
|
||||||
B5 -- "sin CRITICAL" --> B6["Trivy imagen<br/>HIGH (informa)"]
|
|
||||||
B6 --> B7["Syft → SBOM<br/>(CycloneDX + SPDX)"]
|
|
||||||
B7 --> B8["docker push"]
|
|
||||||
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)"]
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Por qué este orden
|
|
||||||
|
|
||||||
- **Gitleaks corre en un job aparte, antes que todo lo demás** — incluso
|
|
||||||
antes que el checkout completo del job de build. Si hay un secreto,
|
|
||||||
no tiene sentido gastar minutos de build en un commit que hay que
|
|
||||||
rechazar igual. Es el único check que corre también en `pull_request`,
|
|
||||||
no solo en push a `main`.
|
|
||||||
- **Trivy IaC y Semgrep corren antes del build de Docker.** Ninguno de
|
|
||||||
los dos necesita la imagen construida — analizan manifiestos y código
|
|
||||||
fuente respectivamente — así que si algo llamativo apareciera ahí, se
|
|
||||||
sabe temprano, sin esperar el build (que es el paso más lento del
|
|
||||||
pipeline).
|
|
||||||
- **Trivy imagen corre después del build pero antes del push.** Por eso
|
|
||||||
el build usa `push: false, load: true`: la imagen queda en el daemon
|
|
||||||
local del runner, escaneable, pero no sale hacia el registry hasta
|
|
||||||
pasar el gate de CRITICAL.
|
|
||||||
- **Syft (SBOM) corre sobre la imagen ya validada**, antes del push —
|
|
||||||
documenta exactamente lo que se está por publicar.
|
|
||||||
- **Cosign firma después del push exitoso** (no tiene sentido firmar
|
|
||||||
algo que no llegó al registry) y se verifica en el mismo pipeline como
|
|
||||||
smoke test.
|
|
||||||
|
|
||||||
## Qué pasa si cada uno falla
|
|
||||||
|
|
||||||
| Si falla... | El pipeline... |
|
|
||||||
|---|---|
|
|
||||||
| Gitleaks | Se detiene ahí mismo. El job `build` nunca arranca (`needs: gitleaks`). Nada se construye. |
|
|
||||||
| Trivy IaC | No se detiene — el hallazgo queda en el log, informativo. |
|
|
||||||
| Semgrep | No se detiene — mismo criterio: primera vuelta en modo auditoría. |
|
|
||||||
| Trivy imagen (CRITICAL) | Se detiene después del build, antes del push. La imagen con la CVE nunca llega al registry. |
|
|
||||||
| Trivy imagen (HIGH) | No se detiene — se reporta para revisar en conjunto. |
|
|
||||||
| Syft | Si el propio comando falla (no si "encuentra algo" — un SBOM no tiene hallazgos que bloqueen), el pipeline se detiene por error real de la herramienta. |
|
|
||||||
| Cosign sign/verify | Se detiene. Si la imagen se firmó pero no verifica, algo está mal con las llaves o con la imagen — no se continúa con la promoción GitOps. |
|
|
||||||
|
|
||||||
## Qué falta después de esta primera vuelta
|
|
||||||
|
|
||||||
- Revisar en conjunto los hallazgos de Trivy IaC y Semgrep (hoy
|
|
||||||
informativos) y decidir cuáles pasan a bloquear.
|
|
||||||
- Decidir si Trivy imagen sube el umbral de bloqueo a HIGH una vez que
|
|
||||||
el backlog de CVEs conocidas esté bajo control.
|
|
||||||
- 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.
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
# SAST (Semgrep) — análisis estático de código
|
|
||||||
|
|
||||||
!!! info "Qué problema resuelve"
|
|
||||||
SAST significa *Static Application Security Testing*: analizar el
|
|
||||||
**código fuente** en busca de patrones de programación inseguros, sin
|
|
||||||
ejecutar la aplicación. Semgrep lee cada archivo `.ts`/`.tsx` y lo
|
|
||||||
compara contra un catálogo de reglas — cada regla describe una forma
|
|
||||||
de escribir código que suele terminar en una vulnerabilidad conocida
|
|
||||||
(inyección, XSS, uso inseguro de una API, etc.).
|
|
||||||
|
|
||||||
## En qué se diferencia de Gitleaks y Trivy
|
|
||||||
|
|
||||||
Las tres herramientas ya integradas a este pipeline analizan cosas
|
|
||||||
completamente distintas — vale la pena tenerlo claro porque a primera
|
|
||||||
vista "escaneo de seguridad" suena como una sola categoría:
|
|
||||||
|
|
||||||
| Herramienta | Qué mira | Pregunta que responde |
|
|
||||||
|---|---|---|
|
|
||||||
| [Gitleaks](gitleaks.md) | El texto de los archivos y el historial de git | "¿Hay una credencial commiteada?" |
|
|
||||||
| [Trivy](trivy.md) | Paquetes instalados (imagen) y configuración (YAML) | "¿Alguna dependencia tiene una CVE conocida? ¿El manifiesto de Kubernetes es inseguro?" |
|
|
||||||
| **Semgrep (SAST)** | La **lógica** del código que escribimos nosotros | "¿Esta función, tal como está escrita, abre una vulnerabilidad?" |
|
|
||||||
|
|
||||||
Ninguna de las tres reemplaza a las otras. Una dependencia puede estar
|
|
||||||
100% al día (sin CVEs, Trivy contento) y aun así el código propio puede
|
|
||||||
construir una URL con un string sin sanitizar y quedar abierto a SSRF —
|
|
||||||
eso solo lo detecta un análisis de la lógica del código, que es
|
|
||||||
exactamente lo que hace Semgrep.
|
|
||||||
|
|
||||||
## Qué tipo de bugs detecta (ejemplos genéricos)
|
|
||||||
|
|
||||||
Los rulesets usados (`p/typescript`, `p/react`, `p/nextjs`,
|
|
||||||
`p/security-audit`) cubren, entre otras cosas:
|
|
||||||
|
|
||||||
- **Inyección**: construir queries, comandos de shell o URLs concatenando
|
|
||||||
strings con datos que vienen del usuario, en vez de usar una API
|
|
||||||
parametrizada.
|
|
||||||
- **XSS en React/Next.js**: usar `dangerouslySetInnerHTML` con contenido
|
|
||||||
que no pasó por un sanitizador.
|
|
||||||
- **SSRF**: hacer un `fetch()`/request server-side hacia una URL que
|
|
||||||
construye el propio usuario, sin validar el host de destino.
|
|
||||||
- **Criptografía insegura**: algoritmos de hash débiles (`md5`, `sha1`)
|
|
||||||
usados para contraseñas o tokens, en vez de un KDF diseñado para eso.
|
|
||||||
- **`eval` / `new Function()`** sobre datos no confiables.
|
|
||||||
- **ReDoS**: expresiones regulares con backtracking exponencial que un
|
|
||||||
input malicioso puede usar para colgar el proceso.
|
|
||||||
- **Prototype pollution**: merges/asignaciones dinámicas de objetos que
|
|
||||||
permiten sobrescribir `__proto__`.
|
|
||||||
|
|
||||||
## Resultado real de esta primera corrida
|
|
||||||
|
|
||||||
```text
|
|
||||||
Scanning 72 files tracked by git with 292 Code rules:
|
|
||||||
ts 87 rules 39 files
|
|
||||||
js 81 rules 1 file
|
|
||||||
json 1 rule 6 files
|
|
||||||
|
|
||||||
Ran 91 rules on 72 files: 0 findings.
|
|
||||||
```
|
|
||||||
|
|
||||||
`workloads/ecommerce` salió limpio con estos rulesets: **0 hallazgos**.
|
|
||||||
|
|
||||||
!!! warning "0 hallazgos no significa 'código perfecto'"
|
|
||||||
Significa que ningún patrón conocido de estos 91 rules hizo match —
|
|
||||||
no es una garantía de ausencia de bugs, solo de ausencia de *estos*
|
|
||||||
patrones específicos. SAST tiene falsos negativos por naturaleza (un
|
|
||||||
bug de lógica de negocio nuevo, específico de esta app, no está en
|
|
||||||
ningún ruleset genérico). El valor de correr esto en cada build no es
|
|
||||||
"una vez limpio, siempre limpio" — es que si alguien introduce a
|
|
||||||
futuro uno de estos patrones conocidos (por ejemplo, un
|
|
||||||
`dangerouslySetInnerHTML` sin sanitizar en un componente nuevo), el
|
|
||||||
pipeline lo va a marcar en ese mismo push.
|
|
||||||
|
|
||||||
## Por qué modo auditoría (no bloquea) en esta primera integración
|
|
||||||
|
|
||||||
El step corre **sin** la flag `--error` a propósito — Semgrep siempre
|
|
||||||
termina con `exit 0`, reporta lo que encuentra pero nunca frena el
|
|
||||||
pipeline. Es la primera vez que esta herramienta corre sobre el repo: la
|
|
||||||
idea es revisar juntos qué reglas de los 91 activos generan ruido (falsos
|
|
||||||
positivos específicos de este código) antes de decidir cuáles deberían
|
|
||||||
pasar a bloquear.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
A[Semgrep scan] --> B{hallazgos?}
|
|
||||||
B -- "sí" --> C["se reportan en el log<br/>+ artifact JSON"]
|
|
||||||
B -- "no" --> C
|
|
||||||
C --> D["✅ pipeline sigue<br/>(exit 0 siempre)"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Una vez que se decida qué reglas son suficientemente confiables para
|
|
||||||
esta app, el paso natural es agregar `--error` **con un subconjunto**
|
|
||||||
de reglas (no las 91 completas) usando `--config` más específico o
|
|
||||||
`.semgrepignore` / reglas individuales marcadas como bloqueantes —
|
|
||||||
todavía no se hizo ese recorte.
|
|
||||||
|
|
||||||
## Los rulesets elegidos
|
|
||||||
|
|
||||||
- `p/typescript` — patrones generales de TypeScript/JavaScript.
|
|
||||||
- `p/react` — específico de componentes React (hooks mal usados, XSS vía
|
|
||||||
props/render, etc.).
|
|
||||||
- `p/nextjs` — patrones propios del framework (App Router, API routes,
|
|
||||||
middlewares).
|
|
||||||
- `p/security-audit` — catálogo transversal de seguridad (inyección,
|
|
||||||
criptografía débil, deserialización insegura) sin atarse a un
|
|
||||||
framework específico.
|
|
||||||
|
|
||||||
Son rulesets **públicos y gratuitos** del registro de Semgrep — no
|
|
||||||
requieren cuenta ni login (`semgrep login` solo hace falta para acceder a
|
|
||||||
reglas adicionales de pago, que no se usan acá).
|
|
||||||
|
|
||||||
## Dónde ver el resultado
|
|
||||||
|
|
||||||
El JSON completo (`semgrep-report.json`) se publica como artifact del
|
|
||||||
pipeline (`semgrep-report`) en cada corrida, tenga o no hallazgos —
|
|
||||||
mismo patrón que Gitleaks y Trivy.
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
# SBOM (Syft) — inventario de software
|
|
||||||
|
|
||||||
!!! info "Qué es un SBOM"
|
|
||||||
SBOM = *Software Bill of Materials* — literalmente, una "lista de
|
|
||||||
materiales" del software, igual que la lista de ingredientes de un
|
|
||||||
producto. Es un documento (JSON, en este caso) que enumera **cada
|
|
||||||
paquete que terminó dentro de la imagen final**: nombre, versión
|
|
||||||
exacta, de dónde viene (`npm`, `deb`, etc.) y, cuando aplica, su
|
|
||||||
licencia.
|
|
||||||
|
|
||||||
[Syft](https://github.com/anchore/syft) genera ese inventario
|
|
||||||
inspeccionando la imagen Docker ya construida — no necesita acceso al
|
|
||||||
código fuente ni a `package.json`, lee directamente lo que quedó
|
|
||||||
instalado en los layers de la imagen.
|
|
||||||
|
|
||||||
## Por qué importa: el escenario "log4shell"
|
|
||||||
|
|
||||||
En diciembre de 2021 apareció una vulnerabilidad crítica en Log4j (una
|
|
||||||
librería de logging de Java) usada, directa o indirectamente, en una
|
|
||||||
cantidad enorme de software. La pregunta que todo equipo tuvo que
|
|
||||||
responder en horas, no en días, fue: **"¿nosotros usamos esto, en algún
|
|
||||||
lugar, aunque sea una dependencia de una dependencia?"**
|
|
||||||
|
|
||||||
Sin un SBOM, esa respuesta implica revisar manualmente cada
|
|
||||||
`package.json`, cada imagen Docker, cada servicio — y confiar en que no
|
|
||||||
se te escapó una dependencia transitiva de tres niveles de profundidad.
|
|
||||||
Con un SBOM generado en cada build y guardado como artifact, la respuesta
|
|
||||||
es una búsqueda de texto sobre un archivo:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
grep -i "nombre-del-paquete-afectado" sbom.cdx.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Si aparece, sabés exactamente en qué versión, y podés cruzarlo contra el
|
|
||||||
aviso de seguridad para saber si tu versión específica está afectada —
|
|
||||||
en minutos, no en una auditoría manual del repo completo.
|
|
||||||
|
|
||||||
## Qué genera este pipeline
|
|
||||||
|
|
||||||
Un step de Syft corre sobre la imagen ya construida (la misma que pasó
|
|
||||||
el gate de CRITICAL de Trivy) y produce **dos formatos** del mismo
|
|
||||||
inventario, publicados como artifact del pipeline:
|
|
||||||
|
|
||||||
- `sbom.cdx.json` — [CycloneDX](https://cyclonedx.org/), el formato con
|
|
||||||
mejor soporte en herramientas de consulta/alertas automáticas de CVEs.
|
|
||||||
- `sbom.spdx.json` — [SPDX](https://spdx.dev/), el estándar ISO, más
|
|
||||||
orientado a cumplimiento de licencias y trazabilidad legal.
|
|
||||||
|
|
||||||
No hay una razón fuerte para elegir solo uno en esta etapa — generar
|
|
||||||
ambos cuesta segundos y cada formato es mejor para un caso de uso
|
|
||||||
distinto, así que se publican los dos.
|
|
||||||
|
|
||||||
## Resultado real de este build
|
|
||||||
|
|
||||||
Corriendo Syft contra la imagen real de `workloads/ecommerce`:
|
|
||||||
|
|
||||||
| Formato | Paquetes listados |
|
|
||||||
|---|---|
|
|
||||||
| CycloneDX | 3528 componentes |
|
|
||||||
| SPDX | 228 paquetes |
|
|
||||||
|
|
||||||
!!! tip "¿Por qué el número es tan distinto entre formatos?"
|
|
||||||
No es un error — cada formato tiene un nivel de detalle distinto.
|
|
||||||
CycloneDX de Syft incluye entradas más granulares (variantes,
|
|
||||||
sub-paquetes, entradas sin versión resuelta marcadas `UNKNOWN`)
|
|
||||||
mientras que SPDX agrupa a un nivel más alto. Para "¿tengo este
|
|
||||||
paquete, sí o no?" cualquiera de los dos sirve; para conteos exactos,
|
|
||||||
hay que saber cuál se está mirando.
|
|
||||||
|
|
||||||
Ejemplo de una entrada real (`sbom.cdx.json`, recortado):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "next",
|
|
||||||
"version": "15.5.10",
|
|
||||||
"licenses": [{ "license": { "id": "MIT" } }],
|
|
||||||
"purl": "pkg:npm/[email protected]",
|
|
||||||
"properties": [
|
|
||||||
{ "name": "syft:package:language", "value": "javascript" },
|
|
||||||
{ "name": "syft:package:type", "value": "npm" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
El campo `purl` (*Package URL*) es el identificador estándar que usan
|
|
||||||
Trivy, Syft, GitHub Advisories y la mayoría de las bases de datos de
|
|
||||||
CVEs para referirse a "este paquete, en este ecosistema, en esta
|
|
||||||
versión" — es lo que hace posible cruzar un SBOM contra un aviso de
|
|
||||||
seguridad de forma automática, sin depender de que el nombre coincida
|
|
||||||
exactamente en texto libre.
|
|
||||||
|
|
||||||
!!! example "El SBOM también sirve para encontrar cosas que sobran"
|
|
||||||
Revisando el inventario real apareció `[email protected]` — viene
|
|
||||||
empaquetado en la imagen base de Node igual que el `npm` que ya se
|
|
||||||
sacó del stage final en el fix de [Trivy](trivy.md). No es una
|
|
||||||
vulnerabilidad activa hoy, pero es exactamente el tipo de hallazgo
|
|
||||||
que un SBOM hace visible para revisar después: herramientas que
|
|
||||||
viajan en la imagen de producción sin que el runtime las necesite.
|
|
||||||
|
|
||||||
## Cómo se consulta
|
|
||||||
|
|
||||||
El SBOM se publica como artifact del pipeline (`sbom-v1.0.X`, con ambos
|
|
||||||
archivos) en cada build. Para consultarlo:
|
|
||||||
|
|
||||||
1. Descargar el artifact de la corrida del pipeline que te interesa
|
|
||||||
(o del último build de `main`, para saber qué corre en producción
|
|
||||||
ahora mismo).
|
|
||||||
2. Buscar el paquete en cuestión:
|
|
||||||
```bash
|
|
||||||
python3 -c "
|
|
||||||
import json
|
|
||||||
d = json.load(open('sbom.cdx.json'))
|
|
||||||
for c in d['components']:
|
|
||||||
if c['name'] == 'next':
|
|
||||||
print(c['name'], c['version'])
|
|
||||||
"
|
|
||||||
```
|
|
||||||
(o `grep -A3 '"name": "next"' sbom.cdx.json` si no hay Python a mano).
|
|
||||||
3. Si el paquete aparece, confirmar la versión contra el aviso de
|
|
||||||
seguridad para saber si aplica.
|
|
||||||
|
|
||||||
No hace falta memorizar el formato — el punto de tener el SBOM ya
|
|
||||||
generado es no depender de reconstruir esta información bajo presión
|
|
||||||
el día que aparezca la próxima CVE grande.
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
# Trivy — vulnerabilidades de imagen e IaC
|
|
||||||
|
|
||||||
!!! info "Qué problema resuelve"
|
|
||||||
Trivy es un escáner de seguridad multipropósito. En este pipeline se usa
|
|
||||||
para dos cosas **distintas**, con dos comandos distintos:
|
|
||||||
|
|
||||||
- `trivy image`: busca CVEs conocidas en los paquetes que terminan
|
|
||||||
dentro de la imagen Docker final (el sistema operativo base, y las
|
|
||||||
librerías de Node.js instaladas).
|
|
||||||
- `trivy config`: busca **misconfiguraciones** en los manifiestos de
|
|
||||||
Kubernetes (YAML) — no vulnerabilidades de código, sino configuración
|
|
||||||
insegura (contenedor como root, sin límites de recursos, etc.).
|
|
||||||
|
|
||||||
Son preguntas distintas: "¿esta imagen tiene código con bugs de
|
|
||||||
seguridad conocidos?" contra "¿esta manera de desplegar el contenedor
|
|
||||||
es insegura, aunque el código adentro esté perfecto?".
|
|
||||||
|
|
||||||
## Dónde corre cada uno en el pipeline
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
A[checkout] --> B["trivy config<br/>(frontend.yaml)"]
|
|
||||||
B --> C[docker login]
|
|
||||||
C --> D["docker build<br/>(push: false, load: true)"]
|
|
||||||
D --> E["trivy image --severity CRITICAL<br/>--exit-code 1"]
|
|
||||||
E -- "CRITICAL con fix" --> X["❌ pipeline detenido<br/>no se sube la imagen"]
|
|
||||||
E -- "sin CRITICAL" --> F["trivy image --severity HIGH<br/>--exit-code 0 (informativo)"]
|
|
||||||
F --> G["docker push"]
|
|
||||||
```
|
|
||||||
|
|
||||||
El escaneo de imagen corre **después de construir la imagen, antes de
|
|
||||||
subirla al registry** — por eso el build ahora usa
|
|
||||||
`push: false, load: true` (la imagen queda en el daemon Docker del runner,
|
|
||||||
pero no sale de ahí hasta pasar el gate de CRITICAL). El escaneo de
|
|
||||||
manifiestos (`trivy config`) no depende de la imagen, así que corre antes,
|
|
||||||
junto a las otras validaciones del código.
|
|
||||||
|
|
||||||
## Umbral de severidad (y por qué)
|
|
||||||
|
|
||||||
| Severidad | Comportamiento | Por qué |
|
|
||||||
|---|---|---|
|
|
||||||
| `CRITICAL` | Bloquea (`--exit-code 1`) | Si existe un fix disponible para una CVE crítica, no tiene sentido publicar la imagen igual |
|
|
||||||
| `HIGH` | Informa, no bloquea (`--exit-code 0`) | Mientras aprendemos a leer los reportes, HIGH se revisa pero no frena el flujo — bloquear de entrada en HIGH hubiera parado el pipeline en el primer intento real (ver ejemplo abajo) |
|
|
||||||
|
|
||||||
Ambos steps usan `--ignore-unfixed`: si Trivy no tiene un `FixedVersion`
|
|
||||||
para reportar, bloquear o hasta advertir no ayuda en nada — no hay acción
|
|
||||||
posible más que esperar a que el mantenedor del paquete publique un
|
|
||||||
parche.
|
|
||||||
|
|
||||||
!!! tip "Por qué no escanear todo junto con un solo umbral"
|
|
||||||
Trivy permite pedir `--severity CRITICAL,HIGH` en una sola corrida,
|
|
||||||
pero el `--exit-code` se aplica igual a toda la corrida — no se puede
|
|
||||||
decir "bloqueá en CRITICAL, pero en HIGH solo avisá" en un solo
|
|
||||||
comando. Por eso son dos steps separados, cada uno con su propio
|
|
||||||
umbral y su propio `--exit-code`.
|
|
||||||
|
|
||||||
## Ejemplo real: un CRITICAL que sí bloqueaba
|
|
||||||
|
|
||||||
Antes de integrar este step, construimos la imagen real de
|
|
||||||
`workloads/ecommerce` y corrimos Trivy contra ella para validar el
|
|
||||||
pipeline. Encontró esto:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Node.js (node-pkg)
|
|
||||||
Total: 1 (CRITICAL: 1)
|
|
||||||
|
|
||||||
tar (7.5.15 → 7.5.19) CVE-2026-59873 CRITICAL
|
|
||||||
tar: node-tar: Denial of Service via crafted gzip bomb
|
|
||||||
```
|
|
||||||
|
|
||||||
**El detalle importante no era el CVE en sí, sino dónde vivía:**
|
|
||||||
`/usr/local/lib/node_modules/npm/node_modules/tar/` — ese `tar` no es una
|
|
||||||
dependencia de ARI Shopping, es el que trae **empaquetado el propio
|
|
||||||
`npm`** dentro de la imagen base `node:24.18.0-bookworm-slim`. El
|
|
||||||
`Dockerfile` original hacía `FROM ${NODE_IMAGE} AS runner` para el stage
|
|
||||||
final, heredando el Node.js completo — con `npm`, `npx` y `corepack`
|
|
||||||
incluidos — aunque en producción el contenedor solo ejecuta
|
|
||||||
`node server.js` y **nunca** invoca `npm`.
|
|
||||||
|
|
||||||
!!! danger "No se arregla solo subiendo la versión de Node"
|
|
||||||
Antes de tocar el Dockerfile probamos si un patch más nuevo de la
|
|
||||||
imagen base ya traía el `tar` corregido: `node:24.19.0-bookworm-slim`
|
|
||||||
trae `npm` con `[email protected]` — sigue por debajo del `7.5.19` con el
|
|
||||||
fix. El problema no es "Node desactualizado", es que el runtime de
|
|
||||||
producción no necesita `npm` para nada.
|
|
||||||
|
|
||||||
**Fix aplicado** (`workloads/ecommerce/Dockerfile`, stage `runner`):
|
|
||||||
|
|
||||||
```diff
|
|
||||||
+ RUN rm -rf \
|
|
||||||
+ /usr/local/lib/node_modules/npm \
|
|
||||||
+ /usr/local/lib/node_modules/corepack \
|
|
||||||
+ /usr/local/bin/npm \
|
|
||||||
+ /usr/local/bin/npx \
|
|
||||||
+ /usr/local/bin/corepack
|
|
||||||
```
|
|
||||||
|
|
||||||
Después del fix: **0 CRITICAL**, y de paso las HIGH fixable bajaron de 21
|
|
||||||
a 15 (varias venían de dependencias de ese mismo `npm` empaquetado, no de
|
|
||||||
la app). Se validó que la imagen sigue arrancando y respondiendo
|
|
||||||
`GET /api/health` con `200` después de sacar `npm`.
|
|
||||||
|
|
||||||
**Lección:** sacar herramientas que la imagen de producción no necesita
|
|
||||||
en runtime no es solo "buena práctica" en abstracto — reduce
|
|
||||||
directamente la superficie que Trivy (y un atacante) tienen para
|
|
||||||
encontrar algo.
|
|
||||||
|
|
||||||
## Las HIGH que quedan (ejemplo real, sin arreglar todavía)
|
|
||||||
|
|
||||||
Después del fix, `trivy image --severity HIGH` sigue reportando (de
|
|
||||||
forma informativa, no bloqueante) CVEs reales en dependencias que sí son
|
|
||||||
de la app — la mayoría en `next` (15.5.10, con fixes disponibles en
|
|
||||||
15.5.16+ y 15.5.21+ según el CVE), además de `nanoid`, `postcss` y
|
|
||||||
`sharp`. Esto queda pendiente de revisar como una actualización de
|
|
||||||
dependencias normal, no como una emergencia de seguridad — es exactamente
|
|
||||||
para eso que HIGH no bloquea en esta primera vuelta: da visibilidad sin
|
|
||||||
frenar el flujo mientras se decide cuándo priorizar el bump.
|
|
||||||
|
|
||||||
## Cómo priorizar qué arreglar primero
|
|
||||||
|
|
||||||
1. **CRITICAL con fix disponible** — ya bloquea el pipeline, así que en
|
|
||||||
la práctica no se acumulan.
|
|
||||||
2. **HIGH en una dependencia que el runtime realmente carga** (como
|
|
||||||
`next`, que corre en cada request) — más prioridad que una HIGH en
|
|
||||||
una herramienta de build que ni siquiera llega a la imagen final.
|
|
||||||
3. **HIGH sin ruta de explotación realista** (ej. una librería que solo
|
|
||||||
se usa en un script de generación, no en el server) — se puede
|
|
||||||
posponer con criterio, documentando por qué.
|
|
||||||
4. **MEDIUM/LOW** — se revisan en lote, no una por una.
|
|
||||||
|
|
||||||
La pregunta que más ayuda a priorizar no es "¿qué tan grave dice la
|
|
||||||
CVSS que es?", sino "¿este paquete corre en el proceso que atiende
|
|
||||||
tráfico real, o es una herramienta de build que ni siquiera debería estar
|
|
||||||
en la imagen final?" — el propio ejemplo de arriba (`npm` dentro de la
|
|
||||||
imagen de producción) es el caso de manual del segundo.
|
|
||||||
|
|
||||||
## Escaneo de manifiestos Kubernetes (`trivy config`)
|
|
||||||
|
|
||||||
Corre contra `workloads/ecommerce/frontend.yaml` (el `Deployment` +
|
|
||||||
`Service` real del frontend) con `--severity CRITICAL,HIGH,MEDIUM`, en
|
|
||||||
modo informativo (`--exit-code 0`) por ahora.
|
|
||||||
|
|
||||||
Hallazgos reales de este manifiesto, hoy:
|
|
||||||
|
|
||||||
| Severidad | Regla | Qué significa |
|
|
||||||
|---|---|---|
|
|
||||||
| HIGH | [KSV-0014](https://avd.aquasec.com/misconfig/ksv-0014) | El filesystem raíz del contenedor no es de solo lectura |
|
|
||||||
| HIGH | [KSV-0118](https://avd.aquasec.com/misconfig/ksv-0118) | No se define `securityContext` — Kubernetes usa el default, que permite privilegios de root |
|
|
||||||
| MEDIUM | [KSV-0012](https://avd.aquasec.com/misconfig/ksv-0012) | El contenedor puede correr como root (aunque la imagen ya defina `USER nextjs` en el Dockerfile, Kubernetes no lo está *forzando* vía `runAsNonRoot`) |
|
|
||||||
| MEDIUM | [KSV-0001](https://avd.aquasec.com/misconfig/ksv-0001) | El contenedor puede escalar sus propios privilegios (falta `allowPrivilegeEscalation: false`) |
|
|
||||||
| MEDIUM | [KSV-0104](https://avd.aquasec.com/misconfig/ksv-0104) | No hay perfil de Seccomp configurado |
|
|
||||||
| MEDIUM | [KSV-0117](https://avd.aquasec.com/misconfig/ksv-0117) | El `containerPort: 80` es un puerto privilegiado (<1024) |
|
|
||||||
| MEDIUM | [KSV-0125](https://avd.aquasec.com/misconfig/ksv-0125) | La imagen viene de un registry que Trivy no reconoce como "de confianza" por defecto (es autoalojado: `gitea.cruzcloud.net`) |
|
|
||||||
|
|
||||||
!!! warning "Lo que este scan NO detecta todavía"
|
|
||||||
El pedido original incluía "falta de resource limits" y "falta de
|
|
||||||
readiness/liveness probes" como ejemplos de misconfiguración a
|
|
||||||
buscar. En la práctica, `trivy config` sí tiene reglas para límites
|
|
||||||
de recursos (`KSV-0011` CPU, `KSV-0018` memoria) pero las clasifica
|
|
||||||
como **LOW**, por debajo del piso `MEDIUM` que usa este step — y no
|
|
||||||
tiene ninguna regla propia para probes de liveness/readiness (eso lo
|
|
||||||
cubren otras herramientas, como `kube-score` o `kube-linter`, que no
|
|
||||||
forman parte de esta primera integración). Si más adelante se quiere
|
|
||||||
cubrir ese hueco específico, es una herramienta aparte, no una opción
|
|
||||||
de configuración de Trivy.
|
|
||||||
|
|
||||||
Ninguno de estos hallazgos bloquea el pipeline todavía — son reales, pero
|
|
||||||
corregirlos (agregar `securityContext`, `resources.limits`, etc. a
|
|
||||||
`frontend.yaml`) es un cambio de GitOps que conviene revisar con calma,
|
|
||||||
no como reacción automática a un scan.
|
|
||||||
|
|
||||||
## Falsos positivos y excepciones
|
|
||||||
|
|
||||||
Cuando un hallazgo de Trivy no aplica (por ejemplo, KSV-0125 marcando el
|
|
||||||
registry propio como "no confiable" — que es exactamente lo esperado en
|
|
||||||
un lab self-hosted), se documenta con un archivo `.trivyignore` en la
|
|
||||||
raíz del repo:
|
|
||||||
|
|
||||||
```text
|
|
||||||
# KSV-0125: gitea.cruzcloud.net es nuestro registry self-hosted,
|
|
||||||
# no un registry público de terceros. Excepción intencional.
|
|
||||||
KSV-0125
|
|
||||||
```
|
|
||||||
|
|
||||||
Igual que con Gitleaks, cada línea de `.trivyignore` debe poder
|
|
||||||
justificarse — no es un lugar para silenciar hallazgos incómodos sin
|
|
||||||
revisarlos primero.
|
|
||||||
@@ -44,22 +44,9 @@ que cada una pueda entrar por su propia puerta:
|
|||||||
|
|
||||||
## Estado de este sitio
|
## Estado de este sitio
|
||||||
|
|
||||||
Este portal se mantiene vivo junto con el lab. La idea es que cada
|
Este portal se mantiene vivo junto con el lab — cada página muestra su
|
||||||
página muestre su última fecha de modificación real vía
|
última fecha de modificación real (`git-revision-date-localized`). Si
|
||||||
`git-revision-date-localized` — por ahora ese plugin está deshabilitado
|
una página dice "TODO", es contenido pendiente de una fase posterior,
|
||||||
(ver TODO en `mkdocs.yml`): el build corre con `mkdocs build --strict`,
|
no una promesa incumplida: la estructura completa se construyó primero
|
||||||
que aborta ante cualquier `WARNING`, y el plugin no tiene historial git
|
a propósito, para que el contenido se llene sección por sección con el
|
||||||
real que leer desde el contexto de build actual, así que se queda fuera
|
mismo criterio que el resto del lab.
|
||||||
hasta que ese contexto incluya `.git` de verdad. Si una página dice
|
|
||||||
"TODO", es contenido pendiente de una fase posterior, no una promesa
|
|
||||||
incumplida: la estructura completa se construyó primero a propósito,
|
|
||||||
para que el contenido se llene sección por sección con el mismo
|
|
||||||
criterio que el resto del lab.
|
|
||||||
|
|
||||||
Desde este commit, el sitio se construye y publica solo: cada push a
|
|
||||||
`main` que toca `docs/` o `mkdocs.yml` dispara
|
|
||||||
`.gitea/workflows/deploy-docs.yaml`, que hace `mkdocs build --strict`,
|
|
||||||
publica la imagen en el Registry de Gitea y Argo CD sincroniza el
|
|
||||||
Deployment en el cluster (namespace `docs-portal`). Este párrafo es la
|
|
||||||
prueba: si lo estás leyendo servido desde el pod real, el pipeline
|
|
||||||
funcionó de punta a punta.
|
|
||||||
|
|||||||
@@ -43,18 +43,10 @@ theme:
|
|||||||
plugins:
|
plugins:
|
||||||
- search:
|
- search:
|
||||||
lang: es
|
lang: es
|
||||||
# TODO (fase 2): git-revision-date-localized deshabilitado a propósito.
|
- git-revision-date-localized:
|
||||||
# El Dockerfile construye con context: workloads/docs-portal/, que no
|
enable_creation_date: true
|
||||||
# incluye .git (vive en la raíz del repo) — el plugin no tiene historial
|
type: date
|
||||||
# real que leer y cae siempre en fallback_to_build_date, emitiendo un
|
fallback_to_build_date: true
|
||||||
# WARNING por página. mkdocs build --strict aborta ante CUALQUIER
|
|
||||||
# WARNING, así que mientras no se le dé contexto de build con .git real
|
|
||||||
# (mover el build a la raíz del repo + fetch-depth:0 en el workflow),
|
|
||||||
# este plugin debe quedar fuera para no romper el pipeline.
|
|
||||||
# - git-revision-date-localized:
|
|
||||||
# enable_creation_date: true
|
|
||||||
# type: date
|
|
||||||
# fallback_to_build_date: true
|
|
||||||
|
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
- admonition
|
- admonition
|
||||||
@@ -98,13 +90,6 @@ 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:
|
|
||||||
- Resumen: devsecops/index.md
|
|
||||||
- Gitleaks (secretos): devsecops/gitleaks.md
|
|
||||||
- Trivy (imagen + IaC): devsecops/trivy.md
|
|
||||||
- SAST (Semgrep): devsecops/sast.md
|
|
||||||
- SBOM (Syft): devsecops/sbom.md
|
|
||||||
- Cosign (firma de imágenes): devsecops/cosign.md
|
|
||||||
|
|
||||||
extra:
|
extra:
|
||||||
social:
|
social:
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
mkdocs==1.6.*
|
mkdocs==1.6.*
|
||||||
mkdocs-material==9.5.*
|
mkdocs-material==9.5.*
|
||||||
# mkdocs-git-revision-date-localized-plugin==1.2.* # deshabilitado, ver TODO en mkdocs.yml
|
mkdocs-git-revision-date-localized-plugin==1.2.*
|
||||||
|
|||||||
@@ -305,18 +305,6 @@ RUN apt-get update \
|
|||||||
'cap_net_bind_service=+ep' \
|
'cap_net_bind_service=+ep' \
|
||||||
/usr/local/bin/node
|
/usr/local/bin/node
|
||||||
|
|
||||||
# El runtime solo ejecuta "node server.js" (standalone output de Next.js);
|
|
||||||
# nunca invoca npm/npx/corepack. Sacarlos del stage final reduce el árbol
|
|
||||||
# de dependencias escaneado por Trivy a lo que realmente corre en
|
|
||||||
# producción, en vez de arrastrar el npm completo de la imagen base
|
|
||||||
# (con sus propias deps y CVEs, ej. CVE-2026-59873 en node-tar).
|
|
||||||
RUN rm -rf \
|
|
||||||
/usr/local/lib/node_modules/npm \
|
|
||||||
/usr/local/lib/node_modules/corepack \
|
|
||||||
/usr/local/bin/npm \
|
|
||||||
/usr/local/bin/npx \
|
|
||||||
/usr/local/bin/corepack
|
|
||||||
|
|
||||||
COPY --from=builder \
|
COPY --from=builder \
|
||||||
--chown=nextjs:nodejs \
|
--chown=nextjs:nodejs \
|
||||||
/app/public \
|
/app/public \
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
-----BEGIN PUBLIC KEY-----
|
|
||||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhjg9/nC0u+iEANiHkVJY8iN+LZo+
|
|
||||||
VFMF7XG/oC64W3/SfwrPgt+ZIqF6t+ceyrNuEgugajvUdpigz1PHEqQKLw==
|
|
||||||
-----END PUBLIC KEY-----
|
|
||||||
Reference in New Issue
Block a user