Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be3e5c2a4c | ||
|
|
826f199ebc | ||
|
|
5c47a39579 | ||
|
|
3ee7171178 | ||
|
|
81c0f99e00 | ||
|
|
38722b97a8 | ||
|
|
5fc86e6ef4 | ||
|
|
93f071c175 | ||
|
|
0c1076eda0 | ||
|
|
80d99f9f3f | ||
|
|
81f01c7ade | ||
|
|
45312fc189 | ||
|
|
8b49ac2bc5 |
@@ -1,5 +1,15 @@
|
|||||||
name: Build and Push Medusa
|
name: Build and Push Medusa
|
||||||
|
|
||||||
|
# Retrigger: el run 107 (con la remediacion de CVEs ya mergeada) fue
|
||||||
|
# cancelado a mitad de camino porque el stack de Gitea (gitea +
|
||||||
|
# gitea-runner) se reinicio solo durante el build -- no por el
|
||||||
|
# contenido del pipeline. Ver hallazgo de contencion de recursos en
|
||||||
|
# docs/playbooks.
|
||||||
|
|
||||||
|
# Sin anchors/aliases de YAML (&x / *x) a propósito -- el parser de
|
||||||
|
# workflows de Gitea Actions no los resuelve en el bloque "on:" y
|
||||||
|
# descarta el archivo completo con "unknown on type". Las dos listas de
|
||||||
|
# paths quedan duplicadas literalmente (mismo criterio que build.yaml).
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@@ -7,17 +17,76 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'workloads/commerce-backend/**'
|
- 'workloads/commerce-backend/**'
|
||||||
- '.gitea/workflows/build-medusa.yaml'
|
- '.gitea/workflows/build-medusa.yaml'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'workloads/commerce-backend/**'
|
||||||
|
- '.gitea/workflows/build-medusa.yaml'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# Corre en push y en pull_request, siempre antes que build. Mismo
|
||||||
|
# criterio que el frontend: si hay un secreto commiteado, el job build
|
||||||
|
# nunca arranca (needs: gitleaks).
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Escanear secretos en el árbol de archivos
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
/tmp/gitleaks detect \
|
||||||
|
--source=workloads/commerce-backend \
|
||||||
|
--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 publicar Medusa
|
name: Construir y publicar Medusa
|
||||||
|
needs: gitleaks
|
||||||
|
if: github.event_name == 'push'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
|
|
||||||
|
env:
|
||||||
|
APP_DIR: workloads/commerce-backend
|
||||||
|
MANIFEST_FILE: workloads/ecommerce/commerce/medusa.yaml
|
||||||
|
IMAGE_NAME: gitea.cruzcloud.net/devops/ecommerce-medusa
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout del código
|
- name: Checkout del código
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@@ -48,6 +117,82 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- 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 de Medusa (no la imagen).
|
||||||
|
# Informativo por ahora -- mismo criterio que el frontend, se
|
||||||
|
# revisan los hallazgos en conjunto antes de decidir qué bloquea.
|
||||||
|
- name: Escanear manifiestos Kubernetes (Trivy IaC)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
/tmp/trivy config \
|
||||||
|
--severity CRITICAL,HIGH,MEDIUM \
|
||||||
|
--exit-code 0 \
|
||||||
|
"${MANIFEST_FILE}"
|
||||||
|
|
||||||
|
# Mismo motivo que en build.yaml: el runner ejecuta el job ya
|
||||||
|
# dentro de un contenedor propio que habla con el daemon Docker
|
||||||
|
# del host (sibling containers, no Docker-in-Docker) -- correr
|
||||||
|
# Semgrep como container aparte falla montando rutas que no
|
||||||
|
# existen en el host real. Se instala nativo en un venv.
|
||||||
|
- name: Instalar Semgrep
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
python3 -m venv /tmp/semgrep-venv
|
||||||
|
/tmp/semgrep-venv/bin/pip install --quiet "semgrep==1.173.0"
|
||||||
|
/tmp/semgrep-venv/bin/semgrep --version
|
||||||
|
|
||||||
|
# Ruleset adaptado al stack real de este repo: commerce-backend es
|
||||||
|
# una API de Medusa v2 (Node.js/TypeScript sobre Express), no una
|
||||||
|
# app Next.js con SSR -- confirmado en package.json (sin
|
||||||
|
# dependencia de "next", react/react-dom solo llegan como
|
||||||
|
# dependencia transitiva del admin-sdk de Medusa, no hay código de
|
||||||
|
# UI propio en este directorio). Por eso se usa p/typescript en
|
||||||
|
# vez de p/typescript + p/react + p/nextjs del frontend.
|
||||||
|
# Modo auditoría, igual que el frontend: sin --error, primera
|
||||||
|
# vuelta para revisar hallazgos antes de decidir qué bloquea.
|
||||||
|
- name: Escaneo SAST (Semgrep) — modo auditoría, no bloquea
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
/tmp/semgrep-venv/bin/semgrep scan \
|
||||||
|
--config=p/typescript \
|
||||||
|
--config=p/security-audit \
|
||||||
|
--config=p/owasp-top-ten \
|
||||||
|
--json \
|
||||||
|
--output=semgrep-report.json \
|
||||||
|
"${APP_DIR}"
|
||||||
|
|
||||||
|
echo "=== Resumen Semgrep ==="
|
||||||
|
/tmp/semgrep-venv/bin/python -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:
|
||||||
@@ -56,15 +201,138 @@ jobs:
|
|||||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
logout: true
|
logout: true
|
||||||
|
|
||||||
- name: Construir y subir imagen
|
# push: false — igual que el frontend, la imagen se queda cargada
|
||||||
|
# en el daemon local 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/commerce-backend/
|
context: ${{ env.APP_DIR }}/
|
||||||
file: workloads/commerce-backend/Dockerfile
|
file: ${{ env.APP_DIR }}/Dockerfile
|
||||||
push: true
|
push: false
|
||||||
|
load: true
|
||||||
tags: |
|
tags: |
|
||||||
gitea.cruzcloud.net/devops/ecommerce-medusa:${{ steps.vars.outputs.VERSION }}
|
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
|
||||||
gitea.cruzcloud.net/devops/ecommerce-medusa:latest
|
${{ env.IMAGE_NAME }}:latest
|
||||||
|
|
||||||
|
# CRITICAL bloquea el pipeline: no se sube una imagen con una CVE
|
||||||
|
# crítica conocida y con fix disponible.
|
||||||
|
# --timeout 15m0s: el default de Trivy (5m) no alcanza para esta
|
||||||
|
# imagen -- a diferencia del frontend, commerce-backend arrastra
|
||||||
|
# el node_modules completo de Medusa v2 (framework + admin-sdk +
|
||||||
|
# cli), muchos más archivos que escanear tanto para vulnerabilidades
|
||||||
|
# como para el escaneo de secretos que Trivy corre por default
|
||||||
|
# dentro de la imagen. Visto en vivo: "context deadline exceeded"
|
||||||
|
# a los 4m52s con el timeout default, en un run donde el host
|
||||||
|
# venía de terminar el docker build (ver hallazgo de rendimiento
|
||||||
|
# de Gitea/runner en docs/playbooks).
|
||||||
|
# --ignorefile: excepciones puntuales y documentadas (ver
|
||||||
|
# workloads/commerce-backend/.trivyignore) para CVEs sin fix de
|
||||||
|
# bajo riesgo disponible hoy. No debilita el gate en general --
|
||||||
|
# cualquier otra CRITICAL sigue bloqueando igual.
|
||||||
|
- name: Escanear imagen (Trivy) — CRITICAL bloquea
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
/tmp/trivy image \
|
||||||
|
--severity CRITICAL \
|
||||||
|
--exit-code 1 \
|
||||||
|
--ignore-unfixed \
|
||||||
|
--timeout 15m0s \
|
||||||
|
--ignorefile "${APP_DIR}/.trivyignore" \
|
||||||
|
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
|
||||||
|
|
||||||
|
# HIGH solo informa por ahora — mismo criterio que el frontend.
|
||||||
|
- name: Escanear imagen (Trivy) — HIGH informativo
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
/tmp/trivy image \
|
||||||
|
--severity HIGH \
|
||||||
|
--exit-code 0 \
|
||||||
|
--ignore-unfixed \
|
||||||
|
--timeout 15m0s \
|
||||||
|
"${IMAGE_NAME}:${{ steps.vars.outputs.VERSION }}"
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
|
# Mismo par de llaves que el frontend (COSIGN_PRIVATE_KEY /
|
||||||
|
# COSIGN_PASSWORD ya existen como secrets a nivel de repo, no hace
|
||||||
|
# falta un secret nuevo por app): una sola identidad de firma para
|
||||||
|
# todo el registry de este lab. La llave pública se commitea en
|
||||||
|
# cada directorio de app (workloads/commerce-backend/cosign.pub)
|
||||||
|
# para que la verificación quede local a cada workflow, igual que
|
||||||
|
# el frontend.
|
||||||
|
- 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 }}"
|
||||||
|
|
||||||
|
- 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 }}"
|
||||||
|
|
||||||
- name: Verificar promoción segura
|
- name: Verificar promoción segura
|
||||||
id: promotion
|
id: promotion
|
||||||
@@ -91,8 +359,8 @@ jobs:
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${{ steps.vars.outputs.VERSION }}"
|
VERSION="${{ steps.vars.outputs.VERSION }}"
|
||||||
MANIFEST="workloads/ecommerce/commerce/medusa.yaml"
|
MANIFEST="${{ env.MANIFEST_FILE }}"
|
||||||
IMAGE="gitea.cruzcloud.net/devops/ecommerce-medusa"
|
IMAGE="${{ env.IMAGE_NAME }}"
|
||||||
|
|
||||||
git config user.name "gitea-actions"
|
git config user.name "gitea-actions"
|
||||||
git config user.email "[email protected]"
|
git config user.email "[email protected]"
|
||||||
@@ -115,3 +383,14 @@ jobs:
|
|||||||
-m "chore(gitops): deploy Medusa ${VERSION} [skip ci]"
|
-m "chore(gitops): deploy Medusa ${VERSION} [skip ci]"
|
||||||
|
|
||||||
git push origin HEAD:main
|
git push origin HEAD:main
|
||||||
|
|
||||||
|
- name: Resumen del pipeline
|
||||||
|
if: always()
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "========================================"
|
||||||
|
echo "ARI Shopping Commerce Backend (Medusa)"
|
||||||
|
echo "Versión: ${{ steps.vars.outputs.VERSION }}"
|
||||||
|
echo "Commit: ${{ github.sha }}"
|
||||||
|
echo "Promoción GitOps: ${{ steps.promotion.outputs.promote }}"
|
||||||
|
echo "========================================"
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Excepciones documentadas al gate CRITICAL de Trivy (imagen) para
|
||||||
|
# commerce-backend. Cada entrada requiere justificación y fecha -- no
|
||||||
|
# es un mecanismo para silenciar hallazgos sin revisar.
|
||||||
|
#
|
||||||
|
# CVE-2024-24790 / CVE-2025-68121 (golang stdlib, gobinary):
|
||||||
|
# Van embebidas en el binario precompilado de esbuild
|
||||||
|
# (app/node_modules/@esbuild/linux-x64/bin/esbuild), traído
|
||||||
|
# transitivamente por [email protected], que a su vez lo trae
|
||||||
|
# @medusajs/admin-sdk para bundlear el panel de admin en build time.
|
||||||
|
# No es un binario que se ejecute en runtime del contenedor (el CMD
|
||||||
|
# corre "node .../medusa/cli start", nunca esbuild).
|
||||||
|
#
|
||||||
|
# Se intentó el fix real (bump de esbuild a una versión compilada con
|
||||||
|
# un Go toolchain más nuevo, override en package.json) y rompió el
|
||||||
|
# build del admin: [email protected] declara "esbuild: ^0.21.3" como
|
||||||
|
# dependencia directa (no rango amplio), y esbuild >=0.24 cambió el
|
||||||
|
# manejo de la lista de "target" de transpilación que vite 5 pasa
|
||||||
|
# internamente -- build.yaml falló con
|
||||||
|
# "Transforming destructuring... not supported yet" /
|
||||||
|
# PLUGIN_ERROR en vite:esbuild-transpile. No existe un patch dentro
|
||||||
|
# de la propia serie 0.21.x (0.21.5 ya es la última) que incluya un
|
||||||
|
# Go toolchain con estas CVEs corregidas.
|
||||||
|
#
|
||||||
|
# Domesticar esto de verdad requiere subir @medusajs/admin-sdk (y por
|
||||||
|
# lo tanto vite) a una versión que dependa de un esbuild más nuevo --
|
||||||
|
# fuera de alcance de este pipeline de seguridad, queda como TODO de
|
||||||
|
# dependencias en un cambio aparte, no bloqueado por CI mientras
|
||||||
|
# tanto.
|
||||||
|
#
|
||||||
|
# Documentado: 2026-08-15. Revisar en cada bump de @medusajs/* por si
|
||||||
|
# ya arrastra una versión de vite/esbuild más nueva y esta excepción
|
||||||
|
# deja de ser necesaria.
|
||||||
|
CVE-2024-24790
|
||||||
|
CVE-2025-68121
|
||||||
@@ -59,6 +59,26 @@ ENV NODE_ENV=production \
|
|||||||
PORT=9000 \
|
PORT=9000 \
|
||||||
NPM_CONFIG_UPDATE_NOTIFIER=false
|
NPM_CONFIG_UPDATE_NOTIFIER=false
|
||||||
|
|
||||||
|
#
|
||||||
|
# libgnutls30 en la base node:22.18.0-bookworm-slim trae dos CVE
|
||||||
|
# CRITICAL con fix ya publicado por Debian (CVE-2026-33845,
|
||||||
|
# CVE-2026-42010) -- detectado por el gate de Trivy imagen del
|
||||||
|
# pipeline (run 138). Solo se actualiza este paquete puntual, no toda
|
||||||
|
# la imagen, para minimizar el diff de superficie de la base.
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get upgrade -y libgnutls30 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
#
|
||||||
|
# El CLI global de npm que trae la imagen base node:*-slim no se usa en
|
||||||
|
# runtime (el CMD invoca a Medusa directo con `node`, nunca `npm`) y
|
||||||
|
# arrastra su propia copia vendorizada de `tar` con un CVE CRITICAL
|
||||||
|
# (CVE-2026-59873, detectado por Trivy en
|
||||||
|
# usr/local/lib/node_modules/npm/node_modules/tar). Se elimina en vez
|
||||||
|
# de forzar una versión: no es una dependencia real de este proyecto,
|
||||||
|
# así que no hay nada que "actualizar" -- solo superficie sin uso.
|
||||||
|
RUN rm -rf /usr/local/lib/node_modules/npm
|
||||||
|
|
||||||
#
|
#
|
||||||
# Solo lo necesario para ejecutar Medusa
|
# Solo lo necesario para ejecutar Medusa
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhjg9/nC0u+iEANiHkVJY8iN+LZo+
|
||||||
|
VFMF7XG/oC64W3/SfwrPgt+ZIqF6t+ceyrNuEgugajvUdpigz1PHEqQKLw==
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -16,6 +16,14 @@ RUN mkdocs build --strict
|
|||||||
# --- Stage 2: sirve el sitio estático generado con nginx ---
|
# --- Stage 2: sirve el sitio estático generado con nginx ---
|
||||||
FROM nginx:1.27-alpine
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
# libssl3/libcrypto3 de la base nginx:1.27-alpine traen un CVE CRITICAL
|
||||||
|
# con fix ya publicado por Alpine (CVE-2026-31789, heap buffer overflow
|
||||||
|
# en OpenSSL) -- detectado por el gate de Trivy imagen del pipeline
|
||||||
|
# (run 144). Solo se actualizan estos dos paquetes puntuales.
|
||||||
|
RUN apk update \
|
||||||
|
&& apk upgrade --no-cache libssl3 libcrypto3 \
|
||||||
|
&& rm -rf /var/cache/apk/*
|
||||||
|
|
||||||
COPY --from=build /site/site /usr/share/nginx/html
|
COPY --from=build /site/site /usr/share/nginx/html
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
@@ -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.98
|
image: gitea.cruzcloud.net/devops/docs-portal:v1.0.110
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 80
|
- containerPort: 80
|
||||||
resources:
|
resources:
|
||||||
|
|||||||
@@ -104,6 +104,74 @@ Si la imagen fue firmada por este pipeline, el comando termina con
|
|||||||
firmó, porque la firmó otra llave, o porque la imagen fue modificada
|
firmó, porque la firmó otra llave, o porque la imagen fue modificada
|
||||||
después — termina con `exit 1` y un error explícito.
|
después — termina con `exit 1` y un error explícito.
|
||||||
|
|
||||||
|
## Limitaciones conocidas y mejoras futuras
|
||||||
|
|
||||||
|
Dos limitaciones identificadas al validar el pipeline end-to-end,
|
||||||
|
documentadas a propósito como TODO — ninguna de las dos se implementó
|
||||||
|
todavía.
|
||||||
|
|
||||||
|
### 1. Se firma por tag, no por digest
|
||||||
|
|
||||||
|
Hoy el pipeline firma `ecommerce-frontend:v1.0.104` — un **tag**, no un
|
||||||
|
**digest** (`sha256:...`). Un tag es una etiqueta mutable: nada impide
|
||||||
|
que, después de firmada la imagen, alguien (o un bug en el propio
|
||||||
|
pipeline) vuelva a subir contenido distinto bajo el mismo tag
|
||||||
|
`v1.0.104`. La firma original seguiría "verificando" — porque Cosign,
|
||||||
|
al verificar por tag, resuelve el tag al digest que tenga *en ese
|
||||||
|
momento*, no al que tenía cuando se firmó. Si el tag fue reasignado,
|
||||||
|
se está verificando una imagen distinta de la que realmente se firmó,
|
||||||
|
sin que nada avise.
|
||||||
|
|
||||||
|
Esto no es hipotético: el propio Cosign lo advierte en cada firma de
|
||||||
|
este pipeline (visto en los logs reales de cada run):
|
||||||
|
|
||||||
|
```text
|
||||||
|
WARNING: Image reference gitea.cruzcloud.net/.../ecommerce-frontend:v1.0.102
|
||||||
|
uses a tag, not a digest, to identify the image to sign.
|
||||||
|
This can lead you to sign a different image than the intended one.
|
||||||
|
```
|
||||||
|
|
||||||
|
**TODO:** capturar el digest exacto que devuelve `docker push` (o
|
||||||
|
`docker buildx build --metadata-file`) y firmar/verificar contra ese
|
||||||
|
digest en vez del tag —
|
||||||
|
`gitea.cruzcloud.net/devops/ecommerce-frontend@sha256:...` en vez de
|
||||||
|
`:v1.0.104`. No implementado todavía; requiere ajustar el step de
|
||||||
|
build para exponer el digest como output y pasarlo a los steps de
|
||||||
|
firma y verificación.
|
||||||
|
|
||||||
|
### 2. El transparency log (Rekor) está deshabilitado
|
||||||
|
|
||||||
|
Ya se explicó arriba, en la sección "Por qué `--tlog-upload=false`" de
|
||||||
|
esta misma página, la decisión consciente de no publicar en el
|
||||||
|
transparency log para este registry privado. Vale la pena nombrar en
|
||||||
|
simple qué es lo que se está dejando afuera:
|
||||||
|
|
||||||
|
Un **transparency log** (Rekor, el de Sigstore) es un registro
|
||||||
|
público, append-only, criptográficamente verificable, de "quién firmó
|
||||||
|
qué imagen y cuándo" — pensalo como un libro contable público que
|
||||||
|
nadie puede editar ni borrar después de escrito, solo agregar filas
|
||||||
|
nuevas. Cualquiera puede consultar ese libro para confirmar de forma
|
||||||
|
independiente (sin confiar en el propio proyecto, ni en la llave
|
||||||
|
privada, ni en el registry) que una firma específica existió en un
|
||||||
|
momento específico.
|
||||||
|
|
||||||
|
Sin transparency log, la garantía que queda es más débil: *"esta firma
|
||||||
|
la generó quien tuviera la llave privada en el momento en que se
|
||||||
|
verificó"* — pero no hay ningún registro externo e inmutable que
|
||||||
|
demuestre *cuándo* se generó, ni una forma de detectar si alguien con
|
||||||
|
acceso a la llave privada firmó algo por fuera del pipeline sin que
|
||||||
|
quede rastro. Para un lab personal con un registry privado, ese
|
||||||
|
trade-off es razonable (ver la advertencia más abajo en esta misma
|
||||||
|
página). Pero es una limitación real, no solo un detalle de
|
||||||
|
configuración — importa especialmente el día que este mismo patrón se
|
||||||
|
use en un contexto con más de una persona firmando, o con un registry
|
||||||
|
que deje de ser privado.
|
||||||
|
|
||||||
|
**TODO:** si en algún momento el registry deja de ser exclusivamente
|
||||||
|
privado, o se suma más de una persona con acceso a la llave de firma,
|
||||||
|
reevaluar habilitar el transparency log público de Sigstore (o correr
|
||||||
|
uno privado propio) en vez de mantenerlo deshabilitado.
|
||||||
|
|
||||||
## Qué falta (a propósito, todavía)
|
## Qué falta (a propósito, todavía)
|
||||||
|
|
||||||
Hoy la verificación de firma corre como smoke test **dentro del mismo
|
Hoy la verificación de firma corre como smoke test **dentro del mismo
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
# DevSecOps
|
# DevSecOps
|
||||||
|
|
||||||
Fase 2 del lab: integrar tooling de seguridad al pipeline de Gitea
|
Fase 2 del lab: integrar tooling de seguridad al pipeline de Gitea
|
||||||
Actions, empezando por un solo repo de referencia — el frontend de ARI
|
Actions. Se empezó por un solo repo de referencia — el frontend de ARI
|
||||||
Shopping (`workloads/ecommerce`, `.gitea/workflows/build.yaml`) — antes
|
Shopping (`workloads/ecommerce`, `.gitea/workflows/build.yaml`) — y ya
|
||||||
de replicarlo a `commerce-backend` y `docs-portal`.
|
está replicado, con evidencia real de corridas en verde, a los tres
|
||||||
|
componentes del monorepo:
|
||||||
|
|
||||||
|
| App | Workflow | Imagen | Adaptación de Semgrep |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Frontend (Next.js) | `build.yaml` | `ecommerce-frontend` | `p/typescript` + `p/react` + `p/nextjs` + `p/security-audit` |
|
||||||
|
| Backend (Medusa v2, API TypeScript) | `build-medusa.yaml` | `ecommerce-medusa` | `p/typescript` + `p/security-audit` + `p/owasp-top-ten` (sin React/Next.js — es una API, no SSR) |
|
||||||
|
| Docs Portal (MkDocs, Python) | `deploy-docs.yaml` | `docs-portal` | `p/python` + `p/security-audit` (sin código de aplicación propio hoy) |
|
||||||
|
|
||||||
|
Cada app tiene su propia llave pública de Cosign commiteada
|
||||||
|
(`cosign.pub` en su propio directorio), pero las tres comparten el
|
||||||
|
mismo par de llaves de firma (mismos secrets `COSIGN_PRIVATE_KEY` /
|
||||||
|
`COSIGN_PASSWORD` a nivel de repo) — una sola identidad de firma para
|
||||||
|
todo el registry de este lab.
|
||||||
|
|
||||||
Cinco herramientas, cada una respondiendo una pregunta distinta:
|
Cinco herramientas, cada una respondiendo una pregunta distinta:
|
||||||
|
|
||||||
@@ -18,6 +31,11 @@ Cinco herramientas, cada una respondiendo una pregunta distinta:
|
|||||||
|
|
||||||
## El pipeline completo
|
## El pipeline completo
|
||||||
|
|
||||||
|
El mismo patrón corre, con el mismo orden de etapas, en los tres
|
||||||
|
workflows (`build.yaml`, `build-medusa.yaml`, `deploy-docs.yaml`) — lo
|
||||||
|
que cambia entre ellos es el ruleset de Semgrep y qué manifiesto(s)
|
||||||
|
escanea Trivy IaC (ver tabla arriba), no la estructura del pipeline.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
subgraph J1["job: gitleaks (push + pull_request)"]
|
subgraph J1["job: gitleaks (push + pull_request)"]
|
||||||
@@ -28,8 +46,8 @@ flowchart TD
|
|||||||
A2 -- "limpio" --> B0
|
A2 -- "limpio" --> B0
|
||||||
|
|
||||||
subgraph J2["job: build (solo push a main, needs: gitleaks)"]
|
subgraph J2["job: build (solo push a main, needs: gitleaks)"]
|
||||||
B0[checkout + validaciones de código] --> B1["Trivy IaC<br/>(frontend.yaml)"]
|
B0[checkout + validaciones de código] --> B1["Trivy IaC<br/>(manifiesto K8s de la app)"]
|
||||||
B1 -.informa.-> B2["Semgrep SAST<br/>(audit mode)"]
|
B1 -.informa.-> B2["Semgrep SAST<br/>(ruleset por stack, audit mode)"]
|
||||||
B2 -.informa.-> B3[login registry]
|
B2 -.informa.-> B3[login registry]
|
||||||
B3 --> B4["docker build<br/>(push: false, load: true)"]
|
B3 --> B4["docker build<br/>(push: false, load: true)"]
|
||||||
B4 --> B5["Trivy imagen<br/>CRITICAL"]
|
B4 --> B5["Trivy imagen<br/>CRITICAL"]
|
||||||
@@ -40,7 +58,7 @@ flowchart TD
|
|||||||
B8 --> B9["cosign sign"]
|
B8 --> B9["cosign sign"]
|
||||||
B9 --> B10["cosign verify<br/>(smoke test)"]
|
B9 --> B10["cosign verify<br/>(smoke test)"]
|
||||||
B10 -- "firma inválida" --> XC["❌ detenido"]
|
B10 -- "firma inválida" --> XC["❌ detenido"]
|
||||||
B10 -- "firma válida" --> B11["actualizar frontend.yaml<br/>(GitOps, Argo CD sincroniza)"]
|
B10 -- "firma válida" --> B11["actualizar manifiesto de la app<br/>(GitOps, Argo CD sincroniza)"]
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -87,5 +105,15 @@ flowchart TD
|
|||||||
- Admission controller en el cluster que verifique la firma de Cosign
|
- Admission controller en el cluster que verifique la firma de Cosign
|
||||||
antes de dejar correr un Pod (ver [cosign.md](cosign.md)) — hoy la
|
antes de dejar correr un Pod (ver [cosign.md](cosign.md)) — hoy la
|
||||||
verificación es solo un smoke test dentro del propio pipeline.
|
verificación es solo un smoke test dentro del propio pipeline.
|
||||||
- Replicar este mismo patrón a `commerce-backend` (Medusa) y
|
- Firmar por digest en vez de por tag, y reevaluar el transparency log
|
||||||
`docs-portal`, adaptando lo que corresponda a cada stack.
|
de Sigstore — ver
|
||||||
|
[limitaciones conocidas de Cosign](cosign.md#limitaciones-conocidas-y-mejoras-futuras).
|
||||||
|
- `commerce-backend` tiene dos CVE CRITICAL documentados como excepción
|
||||||
|
puntual en `workloads/commerce-backend/.trivyignore` (Go stdlib
|
||||||
|
embebido en el binario de esbuild, sin fix compatible con la versión
|
||||||
|
de Vite que usa el admin-sdk de Medusa hoy) — revisar en cada bump de
|
||||||
|
`@medusajs/*` si ya deja de ser necesaria.
|
||||||
|
- Contención de recursos entre Gitea y su runner bajo carga real —
|
||||||
|
causa raíz confirmada, fix propuesto y documentado, aplicación
|
||||||
|
manual pendiente (ver
|
||||||
|
[playbook de contención de recursos](../playbooks/incidente-contencion-recursos-gitea-runner-2026-08.md)).
|
||||||
|
|||||||
@@ -81,8 +81,7 @@ estable ~0.37–0.4s sostenida por 24+ minutos sin un solo 504.
|
|||||||
servicios), **no relacionado al fix de protocolo del túnel** — no
|
servicios), **no relacionado al fix de protocolo del túnel** — no
|
||||||
se reabrió como parte de este incidente.
|
se reabrió como parte de este incidente.
|
||||||
|
|
||||||
**Pendiente:** vigilar si estas ventanas de degradación coinciden
|
**Actualización 2026-08-15:** sí fue recurrente — ver
|
||||||
sistemáticamente con builds/deploys de Gitea Actions. Si es
|
[contención de recursos entre Gitea y su runner](incidente-contencion-recursos-gitea-runner-2026-08.md)
|
||||||
recurrente, considerar mover el runner (`gitea-runner`) a otro host
|
para la causa raíz confirmada (ninguno de los dos contenedores
|
||||||
o limitarle recursos para evitar que compita con el resto de las
|
tiene límites de recursos reales) y la propuesta de fix.
|
||||||
apps del NAS.
|
|
||||||
|
|||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
# Incidente: contención de recursos entre Gitea y su runner (2026-08)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Ventana del incidente** | 2026-08-15, durante el primer intento real de correr el pipeline de seguridad completo en `commerce-backend` |
|
||||||
|
| **Servicio afectado** | `gitea` y `gitea-runner` (mismo host ZimaOS) |
|
||||||
|
| **Impacto** | Un job de CI en curso (build pesado de Medusa + Trivy) fue cancelado a mitad de camino porque ambos contenedores se reiniciaron solos |
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
Este hallazgo estaba **anotado como pendiente** en el playbook del
|
||||||
|
[504 del túnel](incidente-504-gitea-tunnel.md): una ventana de latencia
|
||||||
|
alta y algunos 502/530 habían coincidido, en su momento, con una
|
||||||
|
ejecución pesada de Gitea Actions — anotado como "contención de
|
||||||
|
recursos local, no relacionado al fix del túnel, vigilar si es
|
||||||
|
recurrente". Al triplicar la carga agregando el mismo pipeline de
|
||||||
|
seguridad a `commerce-backend` y `docs-portal`, se volvió a ver en
|
||||||
|
vivo, esta vez con evidencia suficiente para confirmar la causa.
|
||||||
|
|
||||||
|
## Síntoma
|
||||||
|
|
||||||
|
Un run de `build-medusa.yaml` (con Gitleaks ya en verde, build de
|
||||||
|
Docker en curso) quedó cancelado a mitad de camino. El log del runner
|
||||||
|
mostró, en la misma ventana de un par de minutos:
|
||||||
|
|
||||||
|
```text
|
||||||
|
level=error msg="failed to fetch task" error="unavailable: 502 Bad Gateway"
|
||||||
|
level=info msg="runner: ... shutdown initiated, waiting 0s for running jobs to complete before shutting down"
|
||||||
|
level=warning msg="runner: ... cancelled in progress jobs during shutdown"
|
||||||
|
level=info msg="Starting runner daemon"
|
||||||
|
```
|
||||||
|
|
||||||
|
El mismo patrón se repitió una segunda vez pocos minutos después. En
|
||||||
|
paralelo, `docker ps` mostró que el contenedor `gitea` también se
|
||||||
|
había reiniciado casi al mismo tiempo.
|
||||||
|
|
||||||
|
## Diagnóstico
|
||||||
|
|
||||||
|
`docker inspect` sobre ambos contenedores en el momento del incidente
|
||||||
|
mostró:
|
||||||
|
|
||||||
|
- `ExitCode=0` y `RestartCount=0` en los dos — **no fue un crash ni un
|
||||||
|
OOM-kill** (eso dejaría un `ExitCode` distinto de 0 o
|
||||||
|
`OOMKilled=true`, y el conteo de reinicios de la política de Docker
|
||||||
|
subiría). El `StartedAt` cambió igual, lo que indica un reinicio
|
||||||
|
limpio disparado por algo externo al propio proceso — muy
|
||||||
|
probablemente el supervisor de apps de CasaOS (`zimaos-app-management`),
|
||||||
|
aunque no se pudo confirmar por logs (`/var/log/casaos/log.log` es
|
||||||
|
de acceso root-only, sin sudo sin password disponible en esta
|
||||||
|
sesión).
|
||||||
|
- Memoria del host en el momento del incidente: **~1.3 GB libres de
|
||||||
|
15 GB totales**, con ~2 GB de swap en uso — el host estaba bajo
|
||||||
|
presión real de memoria, no solo de CPU.
|
||||||
|
- Ningún límite de recursos real en ninguno de los dos contenedores
|
||||||
|
(ver detalle abajo).
|
||||||
|
|
||||||
|
## Causa raíz
|
||||||
|
|
||||||
|
Ni `gitea` ni `gitea-runner` tienen aislamiento de recursos real
|
||||||
|
frente al resto del host:
|
||||||
|
|
||||||
|
| | `gitea` | `gitea-runner` |
|
||||||
|
|---|---|---|
|
||||||
|
| Límite de memoria | ~15.5 GB (≈ toda la RAM del host, no es un límite real) | **ninguno** |
|
||||||
|
| Límite de CPU (cuota dura) | ninguno | ninguno |
|
||||||
|
| Peso relativo de CPU (`cpu-shares`) | **90** (muy por debajo del default de Docker, 1024) | 0 → default Docker (**1024**) |
|
||||||
|
|
||||||
|
Con `gitea` en 90 y `gitea-runner` en el default de 1024, bajo
|
||||||
|
contención de CPU **el runner tiene ~11x más prioridad que el propio
|
||||||
|
servidor Gitea** — lo opuesto a lo deseable, ya que Gitea es el
|
||||||
|
servicio que necesita seguir respondiendo peticiones HTTP (incluidas
|
||||||
|
las del propio runner haciendo polling) mientras un build pesado corre.
|
||||||
|
|
||||||
|
Sumado a que `gitea-runner` no tiene techo de memoria: un job pesado
|
||||||
|
(build de Docker de una imagen con `node_modules` grande + escaneo de
|
||||||
|
Trivy con vuln + secret scanning) puede consumir memoria sin límite,
|
||||||
|
empujando al host entero a swap y dejando lento/no-responsivo a todo
|
||||||
|
lo demás — incluida la propia base de datos SQLite de Gitea (ver
|
||||||
|
[incidente de SQLite en modo DELETE](incidente-sqlite-modo-delete-2026-08.md),
|
||||||
|
que probablemente reaparezca con más frecuencia bajo este mismo tipo
|
||||||
|
de presión, aunque WAL reduce el impacto).
|
||||||
|
|
||||||
|
`gitea-runner` tampoco corre gestionado por el mismo `docker-compose.yml`
|
||||||
|
de Gitea — se crea con un `docker run` suelto desde
|
||||||
|
`scripts/deploy-lab.sh` (`create_gitea_runner_container()`), montando
|
||||||
|
`/var/run/docker.sock` directo (sibling containers, ver
|
||||||
|
[incidente de Docker-in-Docker](incidente-docker-in-docker-semgrep-2026-08.md)).
|
||||||
|
Esto no es la causa del incidente de recursos, pero significa que
|
||||||
|
cualquier ajuste de límites tiene que aplicarse en dos lugares
|
||||||
|
distintos: el `docker-compose.yml` de `gitea` y el script que crea
|
||||||
|
`gitea-runner`.
|
||||||
|
|
||||||
|
!!! info "La concurrencia del runner ya estaba bien"
|
||||||
|
`capacity: 1` en el `config.yaml` del runner ya limita la
|
||||||
|
ejecución a **un solo job a la vez** — no es un problema de
|
||||||
|
"demasiados jobs en paralelo". El problema es que incluso un solo
|
||||||
|
job pesado, sin ningún límite de recursos, puede acaparar
|
||||||
|
suficiente CPU/memoria como para dejar sin aire al resto del host.
|
||||||
|
|
||||||
|
## Fix propuesto (documentado, aplicación manual pendiente)
|
||||||
|
|
||||||
|
1. **`scripts/deploy-lab.sh`** (rama `fix/gitea-runner-resource-limits`,
|
||||||
|
pendiente de mergear): agrega `--cpu-shares 512 --memory 8g
|
||||||
|
--memory-swap 10g` a la creación de `gitea-runner`. Solo afecta a la
|
||||||
|
próxima vez que se cree el contenedor (la función es idempotente),
|
||||||
|
no al que ya está corriendo.
|
||||||
|
2. **Fix inmediato en caliente** (sin recrear contenedores), a aplicar
|
||||||
|
manualmente por fuera de este repo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker update --cpu-shares 1024 gitea
|
||||||
|
docker update --cpu-shares 512 --memory 8g --memory-swap 10g gitea-runner
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **`docker-compose.yml` de Gitea** (fuera de este repo, acceso
|
||||||
|
root-only): subir `cpu_shares` de `gitea` de 90 a 1024 — pendiente
|
||||||
|
de aplicar manualmente, no se implementó todavía.
|
||||||
|
4. **Mover `gitea-runner` a k3d** como job/pod separado: evaluado como
|
||||||
|
posible mejora de fondo (aislamiento real vía Kubernetes en vez de
|
||||||
|
límites de Docker sueltos), pero queda **solo como recomendación
|
||||||
|
documentada** — no implementado en esta vuelta.
|
||||||
|
|
||||||
|
## Validación
|
||||||
|
|
||||||
|
Con `gitea` y `gitea-runner` estables (sin reinicios) después del
|
||||||
|
incidente, se reintentó el mismo pipeline y corrió de punta a punta
|
||||||
|
sin interrupciones — pero eso confirma que el host se estabilizó
|
||||||
|
después del pico de carga, **no** que el fix de límites de recursos ya
|
||||||
|
esté aplicado (sigue pendiente, ver arriba). La correlación completa
|
||||||
|
carga-pesada → reinicio ya se había visto antes, en dos fechas
|
||||||
|
distintas (2026-07-30 y 2026-08-11) además de esta — no es un evento
|
||||||
|
aislado, es un patrón recurrente que ahora tiene una causa raíz
|
||||||
|
concreta y una propuesta de fix.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Incidente: el ingress del túnel rompió específicamente a Cosign, no al resto del pipeline (2026-08)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Ventana del incidente** | 2026-08-15, primer intento de firmar una imagen con Cosign en `build.yaml` |
|
||||||
|
| **Servicio afectado** | Cadena Cloudflare Tunnel → NPM (Nginx Proxy Manager) → Gitea, específicamente el endpoint del registry de contenedores |
|
||||||
|
| **Impacto** | `cosign sign` fallaba siempre, en cada intento — el resto del pipeline (Gitleaks, Trivy, Semgrep, build, push de la imagen) funcionaba normal contra el mismo registry |
|
||||||
|
|
||||||
|
## Síntoma
|
||||||
|
|
||||||
|
Todos los pasos anteriores del pipeline pasaban, incluido `docker push`
|
||||||
|
de la imagen al registry de Gitea. El paso de Cosign, inmediatamente
|
||||||
|
después, fallaba siempre con:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Error: signing [gitea.cruzcloud.net/***/ecommerce-frontend:v1.0.102]:
|
||||||
|
accessing entity: invalid realm in www-authenticate: realm scheme
|
||||||
|
"http" not allowed for a secure registry; use https
|
||||||
|
```
|
||||||
|
|
||||||
|
Lo llamativo: **`docker push` a ese mismo registry, un paso antes,
|
||||||
|
funcionaba sin problema.** Si el registry estuviera mal configurado en
|
||||||
|
general, se esperaría que fallara también el push, no solo la firma.
|
||||||
|
|
||||||
|
## Diagnóstico
|
||||||
|
|
||||||
|
El mensaje da la pista exacta: al autenticar contra el registry,
|
||||||
|
Cosign recibe un header `WWW-Authenticate` cuyo campo `realm` apunta a
|
||||||
|
una URL con esquema `http://` en vez de `https://`. Cosign rechaza
|
||||||
|
explícitamente seguir un realm `http` contra lo que él considera "un
|
||||||
|
registry seguro" (un dominio público, no `localhost`) — es una
|
||||||
|
protección intencional del lado de Cosign, no un bug.
|
||||||
|
|
||||||
|
`docker push`/`docker login` son más permisivos con esto (o cachean la
|
||||||
|
sesión de otra forma) y no chocan con el mismo problema, por eso solo
|
||||||
|
Cosign lo mostraba.
|
||||||
|
|
||||||
|
## Causa raíz
|
||||||
|
|
||||||
|
El realm que arma Gitea en su respuesta `WWW-Authenticate` depende de
|
||||||
|
que Gitea sepa correctamente que la petición le llegó por HTTPS
|
||||||
|
(típicamente vía el header `X-Forwarded-Proto` o el Server Name que ve
|
||||||
|
en la conexión TLS que termina antes de llegar a él). La cadena real
|
||||||
|
acá es **Cloudflare Tunnel → NPM → Gitea** — si el proxy host de NPM
|
||||||
|
para `gitea.cruzcloud.net` no tiene el *Origin Server Name* (SNI hacia
|
||||||
|
el origen) configurado correctamente, Gitea puede terminar creyendo
|
||||||
|
que la conexión entrante fue por HTTP plano, y arma el realm con ese
|
||||||
|
esquema equivocado.
|
||||||
|
|
||||||
|
## Fix aplicado
|
||||||
|
|
||||||
|
Corrección de la regla de ingress de `cloudflared`/NPM para
|
||||||
|
`gitea.cruzcloud.net`: HTTPS hacia el origen con el *Origin Server
|
||||||
|
Name* correcto. El fix fue enteramente de infraestructura — no hubo
|
||||||
|
ningún cambio en este repo más allá de un commit vacío para volver a
|
||||||
|
disparar el pipeline y confirmar el fix contra credenciales reales.
|
||||||
|
|
||||||
|
## La misma causa raíz de fondo, dos síntomas distintos
|
||||||
|
|
||||||
|
Este incidente **no es el mismo bug** que el
|
||||||
|
[504 Gateway Timeout intermitente del túnel](incidente-504-gitea-tunnel.md)
|
||||||
|
ya documentado — ese fue inestabilidad de QUIC/UDP en `cloudflared`,
|
||||||
|
resuelto forzando `TUNNEL_TRANSPORT_PROTOCOL=http2`. Pero **ambos
|
||||||
|
viven en la misma capa**: la cadena de ingress
|
||||||
|
Cloudflare Tunnel → NPM → Gitea, mal configurada de dos formas
|
||||||
|
distintas, descubiertas en momentos distintos:
|
||||||
|
|
||||||
|
- Una regla de **transporte** (QUIC vs HTTP/2) causando 504s
|
||||||
|
intermitentes en *cualquier* petición HTTP a los dominios detrás del
|
||||||
|
túnel.
|
||||||
|
- Una regla de **SNI/origin server name** causando que Gitea generara
|
||||||
|
un realm HTTP incorrecto, rompiendo específicamente a un cliente
|
||||||
|
(Cosign) que valida ese detalle de forma estricta.
|
||||||
|
|
||||||
|
La lección compartida: en una cadena de proxies con TLS terminando en
|
||||||
|
varios saltos, un solo campo de configuración mal puesto en cualquiera
|
||||||
|
de los saltos puede manifestarse como fallas completamente distintas
|
||||||
|
según qué tan estricto sea el cliente al otro lado — la mayoría de las
|
||||||
|
herramientas HTTP normales ni lo notan, pero una herramienta de
|
||||||
|
seguridad como Cosign, que valida explícitamente el esquema del
|
||||||
|
realm, sí.
|
||||||
|
|
||||||
|
## Validación
|
||||||
|
|
||||||
|
Confirmado en una corrida real posterior al fix: `cosign sign` y
|
||||||
|
`cosign verify` completaron sin error contra el registry real, con
|
||||||
|
credenciales reales, de punta a punta.
|
||||||
|
|
||||||
|
!!! note "Nota relacionada, no parte de este incidente"
|
||||||
|
El mismo log de este fallo mostró, además del error de realm, el
|
||||||
|
warning esperado de Cosign sobre firmar por tag en vez de por
|
||||||
|
digest — ver
|
||||||
|
[limitaciones conocidas de Cosign](../devsecops/cosign.md#limitaciones-conocidas-y-mejoras-futuras).
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# Incidente: Docker-in-Docker roto en el runner — por qué Semgrep corre nativo (2026-08)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Ventana del incidente** | 2026-08-15, primer intento de correr Semgrep como container aparte en `build.yaml` |
|
||||||
|
| **Servicio afectado** | Gitea Actions runner (`gitea-runner`, imagen `act_runner`) |
|
||||||
|
| **Impacto** | Cualquier step que intentara `docker run -v "$(pwd):/algo"` desde dentro de un job fallaba con `read-only file system` o rutas inexistentes |
|
||||||
|
|
||||||
|
## Docker-in-Docker vs. sibling containers, para quien recién arranca en CI/CD
|
||||||
|
|
||||||
|
Cuando un job de CI necesita usar Docker (por ejemplo, para correr una
|
||||||
|
herramienta empaquetada como imagen), hay dos formas distintas de
|
||||||
|
dárselo, y confundirlas rompe cosas de maneras difíciles de
|
||||||
|
diagnosticar:
|
||||||
|
|
||||||
|
- **Docker-in-Docker (DinD):** el contenedor del job tiene su **propio**
|
||||||
|
daemon Docker corriendo adentro, completamente aislado del daemon
|
||||||
|
del host. Cuando el job hace `docker run`, ese contenedor nuevo nace
|
||||||
|
*dentro* del contenedor del job, como una muñeca rusa. El
|
||||||
|
filesystem que ve ese daemon interno es el del contenedor del job,
|
||||||
|
no el del host real.
|
||||||
|
- **Sibling containers (contenedores hermanos):** el contenedor del
|
||||||
|
job **no** tiene su propio daemon — en cambio, monta el socket del
|
||||||
|
daemon Docker del **host** (`/var/run/docker.sock`) y habla
|
||||||
|
directamente con él. Cuando el job hace `docker run`, ese contenedor
|
||||||
|
nuevo nace como **hermano** del contenedor del job (mismo nivel,
|
||||||
|
mismo host, mismo daemon), no adentro suyo.
|
||||||
|
|
||||||
|
La imagen `act_runner` que usa este runner de Gitea Actions usa el
|
||||||
|
segundo modelo: **sibling containers**, montando el socket del daemon
|
||||||
|
del host (ver `create_gitea_runner_container()` en
|
||||||
|
`scripts/deploy-lab.sh`, que monta
|
||||||
|
`-v /var/run/docker.sock:/var/run/docker.sock`).
|
||||||
|
|
||||||
|
## Por qué eso rompe un `docker run -v` ingenuo
|
||||||
|
|
||||||
|
Con sibling containers, cuando un step dentro de un job pide
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -v "${{ github.workspace }}:/src" alguna-imagen
|
||||||
|
```
|
||||||
|
|
||||||
|
ese comando lo ejecuta el **daemon del host**, no el contenedor del
|
||||||
|
job. `${{ github.workspace }}` es una ruta que existe *dentro* del
|
||||||
|
contenedor del job (por ejemplo `/workspace/devops/apps-registry`) —
|
||||||
|
pero el daemon del host, al crear el contenedor hermano, intenta
|
||||||
|
montar esa misma ruta **desde el filesystem del host real**, donde
|
||||||
|
probablemente no existe (o existe otra cosa completamente distinta ahí).
|
||||||
|
|
||||||
|
## Síntoma
|
||||||
|
|
||||||
|
Al intentar correr Semgrep como container aparte (`docker run` desde
|
||||||
|
dentro del step), el error observado en el run real fue:
|
||||||
|
|
||||||
|
```text
|
||||||
|
mkdir /workspace: read-only file system
|
||||||
|
```
|
||||||
|
|
||||||
|
No un "no existe la ruta" limpio — un intento de crear el directorio
|
||||||
|
que falla porque, en el contexto real del daemon del host, esa ruta
|
||||||
|
cae en un punto de montaje de solo lectura (o simplemente no es un
|
||||||
|
lugar donde el daemon del host puede/debe escribir).
|
||||||
|
|
||||||
|
## Causa raíz
|
||||||
|
|
||||||
|
Sibling containers, no Docker-in-Docker: un `docker run -v
|
||||||
|
"${{ github.workspace }}:/src"` ejecutado desde dentro de un job
|
||||||
|
intenta montar, en el daemon del **host**, una ruta que solo tiene
|
||||||
|
sentido **dentro** del contenedor del job. El daemon del host no ve
|
||||||
|
esa ruta como el mismo directorio — la ve como una ruta arbitraria de
|
||||||
|
su propio filesystem.
|
||||||
|
|
||||||
|
## Fix aplicado
|
||||||
|
|
||||||
|
Se evita el problema de raíz: **Semgrep corre nativo**, instalado
|
||||||
|
directo en el contenedor del job (mismo criterio ya usado para
|
||||||
|
Gitleaks, Trivy, Syft y Cosign — todos se instalan como binario/paquete
|
||||||
|
dentro del job, ninguno corre como container aparte):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv /tmp/semgrep-venv
|
||||||
|
/tmp/semgrep-venv/bin/pip install --quiet "semgrep==1.173.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Por qué un venv y no `pip3 install` directo
|
||||||
|
|
||||||
|
La imagen del runner ya trae paquetes de Python instalados por `apt`
|
||||||
|
(por ejemplo `PyJWT`) sin metadata compatible con `pip`. Un
|
||||||
|
`pip3 install --break-system-packages` sobre esa base falla al
|
||||||
|
intentar reemplazarlos (`RECORD file not found`) porque `pip` no
|
||||||
|
encuentra el registro de archivos que necesita para saber qué está
|
||||||
|
reemplazando. Un venv aislado evita tocar los paquetes del sistema por
|
||||||
|
completo — Semgrep y sus dependencias quedan en su propio directorio,
|
||||||
|
sin interferir con nada que `apt` ya haya instalado.
|
||||||
|
|
||||||
|
## Validación
|
||||||
|
|
||||||
|
Validado localmente contra `docker.gitea.com/runner-images:ubuntu-latest`
|
||||||
|
(la misma imagen que usa el runner real) que `python3`/`pip3` están
|
||||||
|
disponibles, y confirmado en una corrida real posterior al fix que
|
||||||
|
Semgrep corre y reporta hallazgos sin ningún error de Docker de por
|
||||||
|
medio.
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Incidente: SQLite en modo DELETE causando contención en Gitea (2026-08)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Ventana del incidente** | 2026-08-15, coincidiendo con el arranque del runner de Gitea Actions y el polling frecuente de tareas |
|
||||||
|
| **Servicio afectado** | `gitea` (base de datos SQLite propia, `gitea.db`) |
|
||||||
|
| **Impacto** | Errores intermitentes `database is locked (SQLITE_BUSY)` al escribir desde distintos componentes de Gitea (runner, web, cron) al mismo tiempo |
|
||||||
|
|
||||||
|
## Contexto: por qué Gitea usa SQLite acá
|
||||||
|
|
||||||
|
Este lab corre Gitea con `DB_TYPE = sqlite3` (no Postgres/MySQL) — una
|
||||||
|
sola base de datos, un solo archivo (`gitea.db`), sin servidor de base
|
||||||
|
de datos aparte. Es la opción correcta para un lab de un solo nodo,
|
||||||
|
pero SQLite tiene una particularidad importante: por defecto abre el
|
||||||
|
archivo en **modo DELETE** (el modo "clásico" de journaling).
|
||||||
|
|
||||||
|
## Qué es el modo DELETE y por qué molesta acá
|
||||||
|
|
||||||
|
En modo DELETE, cada transacción de escritura:
|
||||||
|
|
||||||
|
1. Crea un archivo de journal temporal (`gitea.db-journal`).
|
||||||
|
2. Toma un **lock exclusivo sobre el archivo completo** de la base de
|
||||||
|
datos mientras dura la escritura.
|
||||||
|
3. Borra el journal al terminar.
|
||||||
|
|
||||||
|
Ese lock exclusivo es el problema: **mientras una escritura está en
|
||||||
|
curso, nada más puede leer ni escribir** — ni siquiera otro proceso
|
||||||
|
que solo quiere leer una fila que no tiene nada que ver con la que se
|
||||||
|
está escribiendo.
|
||||||
|
|
||||||
|
!!! info "Por qué esto pega justo en Gitea Actions"
|
||||||
|
El runner de Gitea Actions hace *polling* constante contra la API
|
||||||
|
de Gitea (cada pocos segundos, según `fetch_interval` en su
|
||||||
|
`config.yaml`) para preguntar "¿hay una tarea nueva?", y además
|
||||||
|
escribe actualizaciones de estado de cada step casi en tiempo real
|
||||||
|
mientras un job corre. Sumale los cron jobs internos de Gitea y el
|
||||||
|
tráfico normal de la web UI. Con la base en modo DELETE, todo eso
|
||||||
|
compite por el mismo lock exclusivo — cuantas más cosas escriben
|
||||||
|
seguido, más chance de pisarse.
|
||||||
|
|
||||||
|
## Síntoma
|
||||||
|
|
||||||
|
`SQLITE_BUSY` intermitente en los logs de Gitea, típicamente en
|
||||||
|
operaciones de corta duración (actualizar el estado de un step,
|
||||||
|
registrar un heartbeat del runner) que no deberían tener motivo para
|
||||||
|
fallar por contención:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[E] can't update runner status: database is locked (5) (SQLITE_BUSY)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Causa raíz
|
||||||
|
|
||||||
|
Modo de journaling `DELETE` (el default de SQLite si no se configura
|
||||||
|
nada distinto) combinado con el patrón de acceso de Gitea Actions:
|
||||||
|
muchas escrituras cortas y frecuentes desde procesos distintos
|
||||||
|
(runner, servidor web, cron), cada una compitiendo por un lock
|
||||||
|
exclusivo de archivo completo.
|
||||||
|
|
||||||
|
## Fix aplicado
|
||||||
|
|
||||||
|
Se cambió el modo de journaling a **WAL** (Write-Ahead Logging) en
|
||||||
|
`app.ini`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[database]
|
||||||
|
; journal_mode por defecto (DELETE) toma un lock exclusivo del archivo
|
||||||
|
; completo en cada escritura. WAL permite lectores concurrentes mientras
|
||||||
|
; hay una escritura en curso -- fix para "database is locked" bajo el
|
||||||
|
; polling frecuente de Actions + cron jobs (diagnosticado 2026-08-15).
|
||||||
|
SQLITE_JOURNAL_MODE = WAL
|
||||||
|
```
|
||||||
|
|
||||||
|
### Por qué WAL resuelve esto (y qué NO resuelve)
|
||||||
|
|
||||||
|
En modo WAL, las escrituras no se aplican directo al archivo principal
|
||||||
|
de la base: se van agregando a un archivo aparte (`gitea.db-wal`), y
|
||||||
|
los lectores siguen leyendo del estado consistente más reciente sin
|
||||||
|
bloquearse. La regla cambia de *"una escritura bloquea todo"* a
|
||||||
|
*"una escritura bloquea solo a otra escritura"* — los lectores dejan
|
||||||
|
de competir por el lock.
|
||||||
|
|
||||||
|
!!! warning "WAL reduce la contención, no la elimina"
|
||||||
|
WAL sigue permitiendo **una sola escritura a la vez** — dos
|
||||||
|
escrituras concurrentes todavía pueden chocar y devolver
|
||||||
|
`SQLITE_BUSY` si el proceso no reintenta. En una corrida de este
|
||||||
|
mismo playbook se vio, después de aplicado el fix, una única
|
||||||
|
ocurrencia aislada de `database is locked` coincidiendo con un
|
||||||
|
reinicio del contenedor de `gitea` (ver
|
||||||
|
[contención de recursos de Gitea/runner](incidente-contencion-recursos-gitea-runner-2026-08.md)) —
|
||||||
|
consistente con una escritura en curso justo en el momento del
|
||||||
|
reinicio, no con que WAL no esté funcionando. Frecuencia bajó de
|
||||||
|
"molesta con cierta regularidad" a "un caso aislado en varias
|
||||||
|
horas de uso intensivo".
|
||||||
|
|
||||||
|
## Validación
|
||||||
|
|
||||||
|
El cambio se aplicó directo en el `app.ini` montado como volumen del
|
||||||
|
contenedor de `gitea` (no en un `docker exec` puntual) — persiste
|
||||||
|
across reinicios del contenedor, confirmado leyendo el archivo montado
|
||||||
|
después de un reinicio real del contenedor (ver
|
||||||
|
[contención de recursos](incidente-contencion-recursos-gitea-runner-2026-08.md)),
|
||||||
|
no solo aplicado en caliente y perdido al reciclar el contenedor.
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# Incidente: anchors YAML no soportados por Gitea Actions — y un diagnóstico equivocado en el camino (2026-08)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Ventana del incidente** | 2026-08-15, desde el primer commit que agregó Gitleaks a `build.yaml` |
|
||||||
|
| **Servicio afectado** | Gitea Actions — el workflow `build.yaml` (frontend) |
|
||||||
|
| **Impacto** | `build.yaml` no generaba **ningún** `action_run`, ni en `push` ni en `pull_request` — el pipeline entero era invisible para Gitea, sin ningún error visible en la UI |
|
||||||
|
|
||||||
|
## Qué es un anchor/alias YAML, para quien no lo conoce
|
||||||
|
|
||||||
|
YAML tiene una forma de evitar repetir el mismo bloque dos veces:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
paths: &frontend_paths
|
||||||
|
- 'workloads/ecommerce/**'
|
||||||
|
- '.gitea/workflows/build.yaml'
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths: *frontend_paths
|
||||||
|
pull_request:
|
||||||
|
paths: *frontend_paths
|
||||||
|
```
|
||||||
|
|
||||||
|
`&frontend_paths` es un **anchor** (marca ese nodo con un nombre).
|
||||||
|
`*frontend_paths` es un **alias** (dice "poné acá una copia de lo que
|
||||||
|
marcó ese anchor"). Es YAML 100% estándar — cualquier parser que siga
|
||||||
|
la especificación lo resuelve sin problema, y es una forma común de no
|
||||||
|
duplicar listas idénticas en el mismo archivo.
|
||||||
|
|
||||||
|
## Síntoma
|
||||||
|
|
||||||
|
`build.yaml` (con Gitleaks recién agregado, usando anchors para no
|
||||||
|
repetir la lista de `paths` entre `push:` y `pull_request:`) no
|
||||||
|
generaba ningún run en Gitea Actions. Ni en el push directo a la rama
|
||||||
|
del PR, ni al abrir el pull request. Sin mensaje de error visible en
|
||||||
|
la UI de Gitea — el workflow simplemente no aparecía en la lista de
|
||||||
|
Actions, como si no existiera.
|
||||||
|
|
||||||
|
## Primer diagnóstico (equivocado)
|
||||||
|
|
||||||
|
La primera sospecha fue una indentación rota específicamente en el
|
||||||
|
step de Semgrep: el bloque `python3 -c "..."` embebido dentro de un
|
||||||
|
`run: |` tenía código pegado a columna 0, por debajo de la
|
||||||
|
indentación esperada del block scalar de YAML. Eso *sí* era un
|
||||||
|
problema real — cortaba el block scalar ahí mismo y en teoría podía
|
||||||
|
hacer que Gitea rechazara el archivo con un error de parseo genérico
|
||||||
|
(`could not find expected ':'`).
|
||||||
|
|
||||||
|
Se corrigió la indentación, se validó con un parser YAML real y
|
||||||
|
`bash -n` sobre los 20 steps del archivo, se mergeó a `main` como fix
|
||||||
|
(`fix(ci): corregir indentación YAML rota en el step de Semgrep`, PR
|
||||||
|
#19)... y `build.yaml` **siguió sin generar runs**.
|
||||||
|
|
||||||
|
!!! warning "Por qué vale la pena contar el diagnóstico que no era"
|
||||||
|
El primer fix no estaba mal — la indentación efectivamente estaba
|
||||||
|
rota y corregirla era necesario. El error fue asumir que esa era
|
||||||
|
*la única* causa sin confirmarlo con una corrida real después del
|
||||||
|
fix. La lección de proceso: un fix que "tiene sentido" y que pasa
|
||||||
|
la validación local (parser YAML, `bash -n`) todavía necesita una
|
||||||
|
confirmación en vivo contra el sistema real antes de darlo por
|
||||||
|
cerrado — sobre todo cuando el síntoma es "no pasa nada" en vez de
|
||||||
|
un error explícito, que es exactamente el tipo de fallo más fácil
|
||||||
|
de dar por resuelto sin verificar.
|
||||||
|
|
||||||
|
## Causa raíz real
|
||||||
|
|
||||||
|
`deploy-docs.yaml` (sin anchors) corría normal. `build.yaml` (con
|
||||||
|
anchors, agregados junto con Gitleaks) no generaba ningún run — ni
|
||||||
|
en `push` ni en `pull_request`, desde el primer commit que los
|
||||||
|
introdujo. El log del propio Gitea lo confirmó:
|
||||||
|
|
||||||
|
```text
|
||||||
|
unknown on type: &yaml.Node{...Value:"frontend_paths"...}
|
||||||
|
```
|
||||||
|
|
||||||
|
El parser propio de Gitea Actions para el bloque `on:` de un workflow
|
||||||
|
**no resuelve `&anchor`/`*alias` antes de inspeccionar el tipo de
|
||||||
|
nodo** — encuentra un anchor donde esperaba un valor ya resuelto, no
|
||||||
|
sabe qué tipo de dato es, y descarta el archivo completo en
|
||||||
|
silencio. `pyyaml` y cualquier parser YAML estándar sí resuelven el
|
||||||
|
alias primero (por eso el archivo "parseaba bien" en cualquier
|
||||||
|
herramienta de validación genérica) — es una limitación específica del
|
||||||
|
parser de workflows de Gitea Actions, no un YAML inválido.
|
||||||
|
|
||||||
|
Esto explica por qué el síntoma no tenía ningún error visible: no es
|
||||||
|
que el job fallara, es que **Gitea nunca llegaba a registrar el
|
||||||
|
workflow como existente**.
|
||||||
|
|
||||||
|
## Fix aplicado
|
||||||
|
|
||||||
|
Se eliminaron los anchors/alias. Las dos listas de `paths` (una para
|
||||||
|
`push:`, otra para `pull_request:`) quedan **duplicadas
|
||||||
|
literalmente**, sin referencia compartida:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'workloads/ecommerce/**'
|
||||||
|
- '.gitea/workflows/build.yaml'
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'workloads/ecommerce/**'
|
||||||
|
- '.gitea/workflows/build.yaml'
|
||||||
|
```
|
||||||
|
|
||||||
|
Es más repetitivo, pero es el precio de que el parser de Gitea
|
||||||
|
Actions lo entienda. Este mismo criterio (sin anchors, paths
|
||||||
|
duplicados) se replicó a propósito en `build-medusa.yaml` y
|
||||||
|
`deploy-docs.yaml` al agregarles el resto del pipeline de seguridad,
|
||||||
|
para no repetir el mismo problema.
|
||||||
|
|
||||||
|
## Validación
|
||||||
|
|
||||||
|
Confirmado en vivo: tras mergear el fix, `build.yaml` generó su
|
||||||
|
primer `action_run` real. Validado también con un parser YAML +
|
||||||
|
`bash -n` sobre los 20 steps del archivo, igual que en el intento
|
||||||
|
anterior — la diferencia esta vez fue confirmarlo contra una corrida
|
||||||
|
real antes de cerrar el incidente.
|
||||||
@@ -93,6 +93,11 @@ nav:
|
|||||||
- 504 túnel Gitea: playbooks/incidente-504-gitea-tunnel.md
|
- 504 túnel Gitea: playbooks/incidente-504-gitea-tunnel.md
|
||||||
- Precios Medusa: playbooks/incidente-precios-medusa.md
|
- Precios Medusa: playbooks/incidente-precios-medusa.md
|
||||||
- Imágenes NPM: playbooks/incidente-imagenes-npm.md
|
- Imágenes NPM: playbooks/incidente-imagenes-npm.md
|
||||||
|
- SQLite modo DELETE: playbooks/incidente-sqlite-modo-delete-2026-08.md
|
||||||
|
- Anchors YAML en Gitea Actions: playbooks/incidente-yaml-anchors-gitea-actions-2026-08.md
|
||||||
|
- Docker-in-Docker y Semgrep: playbooks/incidente-docker-in-docker-semgrep-2026-08.md
|
||||||
|
- Realm HTTP y Cosign: playbooks/incidente-cosign-realm-http-tunnel-2026-08.md
|
||||||
|
- Contención de recursos Gitea/runner: playbooks/incidente-contencion-recursos-gitea-runner-2026-08.md
|
||||||
- Aprendizajes:
|
- Aprendizajes:
|
||||||
- Notas sueltas: aprendizajes/notas-sueltas.md
|
- Notas sueltas: aprendizajes/notas-sueltas.md
|
||||||
- Guía del estudiante:
|
- Guía del estudiante:
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ spec:
|
|||||||
memory: 64Mi
|
memory: 64Mi
|
||||||
|
|
||||||
- name: migrations
|
- name: migrations
|
||||||
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.92
|
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.108
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- npx
|
- npx
|
||||||
@@ -73,7 +73,7 @@ spec:
|
|||||||
|
|
||||||
containers:
|
containers:
|
||||||
- name: medusa
|
- name: medusa
|
||||||
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.92
|
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.108
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
|
|||||||
Reference in New Issue
Block a user