5 playbooks nuevos en docs/playbooks/ del portal:
- SQLite en modo DELETE causando SQLITE_BUSY, por que WAL lo resuelve.
- Anchors YAML no soportados por el parser de Gitea Actions, incluido
el diagnostico equivocado inicial (indentacion) que se corrigio
despues -- documentado con honestidad como leccion de proceso.
- Docker-in-Docker vs sibling containers, y por que Semgrep corre
nativo en un venv en vez de como container aparte.
- El SNI/realm HTTP del tunel rompiendo especificamente a Cosign
(docker push funcionaba, cosign sign no), conectado con el
incidente ya documentado del 504 -- misma capa de ingress, dos
fallas distintas.
- Contencion de recursos entre Gitea y su runner: causa raiz real
confirmada en vivo durante esta misma sesion (ambos contenedores
reiniciados a mitad de un build, sin limites de CPU/memoria reales),
con fix propuesto (ver rama fix/gitea-runner-resource-limits en
scripts/) pendiente de aplicacion manual.
Se actualiza el playbook del 504 para linkear hacia el nuevo hallazgo
de contencion, cerrando el loop que habia quedado abierto ahi.
cosign.md: nueva seccion "Limitaciones conocidas y mejoras futuras"
con los dos TODO reales -- firma por tag en vez de digest (con el
warning real de Cosign visto en los logs) y transparency log de Rekor
deshabilitado, explicado en simple.
index.md: tabla resumen de los 3 pipelines (imagen + adaptacion de
Semgrep por stack), diagrama generalizado para reflejar que es el
mismo patron en los 3 workflows, y "que falta" actualizado.
Validado con mkdocs build --strict (0 errores, 0 warnings) antes de
commitear.
Run 144 de deploy-docs.yaml encontro un CVE CRITICAL real
(CVE-2026-31789, heap buffer overflow en OpenSSL) en libssl3/libcrypto3
de la base nginx:1.27-alpine, con fix ya publicado por Alpine
(3.3.3-r0 -> 3.3.7-r0). Se agrega apk upgrade puntual de esos dos
paquetes en el Dockerfile. Verificado localmente: docker build OK +
Trivy real en exit 0 (0 vulnerabilidades).
Run 138 de build-medusa.yaml encontro 5 CVE CRITICAL reales (no falsos
positivos) al escanear la imagen con Trivy:
Remediados en el Dockerfile (verificado localmente con Trivy real,
exit 0):
- CVE-2026-33845 / CVE-2026-42010 (libgnutls30, base Debian): apt
upgrade puntual del paquete.
- CVE-2026-59873 (tar vendorizado dentro del npm CLI global de la
imagen base node:*-slim): se elimina /usr/local/lib/node_modules/npm
completo -- no es una dependencia real del proyecto (el CMD nunca
invoca npm en runtime), asi que no hay nada que "actualizar".
Documentados como excepcion en workloads/commerce-backend/.trivyignore
(con --ignorefile solo en este workflow, no afecta a build.yaml ni
deploy-docs.yaml):
- CVE-2024-24790 / CVE-2025-68121 (Go stdlib embebido en el binario de
esbuild que trae [email protected], dependencia interna de
@medusajs/admin-sdk). Se probo el fix real (override de esbuild a
una version mas nueva) y rompio el build del admin panel --
[email protected] fija "esbuild: ^0.21.3" como dependencia directa, y
esbuild >=0.24 cambio el manejo de targets de transpilacion que vite
5 espera. No existe un patch dentro de la serie 0.21.x con un Go
toolchain mas nuevo. Requiere subir @medusajs/admin-sdk/vite en un
cambio aparte, fuera de alcance de este pipeline de seguridad.
Agrega a deploy-docs.yaml las mismas 6 etapas ya validadas en
build.yaml (run 134, frontend): Gitleaks, Trivy IaC, Semgrep (SAST),
Trivy imagen, Syft (SBOM) y Cosign (firma + verificación).
Adaptaciones de stack:
- Trivy IaC escanea todo workloads/docs-portal/ (varios manifiestos
K8s separados: deployment/service/ingress), no un MANIFEST_FILE
único como el frontend.
- Semgrep usa p/python + p/security-audit (no hay TypeScript/React
acá) -- docs-portal no tiene código de aplicación propio hoy (solo
Markdown + config de MkDocs), así que 0 hallazgos es el resultado
esperado; el stage queda listo para el día que se agregue un
hook/plugin en Python.
Mismos umbrales que el resto: Trivy bloquea en CRITICAL, Semgrep en
modo auditoría. Reutiliza el mismo par de llaves Cosign (secrets ya
existentes a nivel de repo); se agrega workloads/docs-portal/cosign.pub.
Agrega a build-medusa.yaml las mismas 6 etapas ya validadas en
build.yaml (run 134, frontend): Gitleaks, Trivy IaC, Semgrep (SAST),
Trivy imagen, Syft (SBOM) y Cosign (firma + verificación).
Ruleset de Semgrep adaptado: p/typescript + p/security-audit +
p/owasp-top-ten, sin p/react/p/nextjs -- commerce-backend es la API de
Medusa v2 (Node/TypeScript), no una app con SSR. Mismos umbrales que
el frontend: Trivy bloquea en CRITICAL, Semgrep en modo auditoría
(no bloquea todavía).
Reutiliza el mismo par de llaves Cosign del frontend (secrets ya
existentes a nivel de repo); se agrega workloads/commerce-backend/cosign.pub
con la misma llave pública para que la verificación quede local a
este workflow.
docs/devsecops/index.md documenta el pipeline completo (job gitleaks +
job build) con un diagrama Mermaid del orden real de ejecución, tabla
de qué bloquea vs qué solo informa, y qué pasa si cada herramienta
falla. Nav actualizado: "Resumen" como landing page de la sección
DevSecOps.
Con esto quedan las 5 herramientas de la Fase 2 (Gitleaks, Trivy,
Semgrep, Syft, Cosign) integradas y documentadas para
workloads/ecommerce, listas para replicar a commerce-backend y
docs-portal en una próxima vuelta.
Firma la imagen (solo el tag versionado, no :latest) después del push
exitoso, usando la llave privada desde secrets de Gitea Actions
(COSIGN_PRIVATE_KEY/COSIGN_PASSWORD, nunca en el repo ni en disco).
Verifica la firma como smoke test en el mismo pipeline contra
workloads/ecommerce/cosign.pub (pública, commiteada a propósito).
Firma solo con el par de llaves propio, sin publicar en el
transparency log público de Sigstore (--use-signing-config=false
--tlog-upload=false / --insecure-ignore-tlog=true) — registry privado,
no tiene sentido esa fuga de metadata.
Par de llaves generado y validado end-to-end (firma + verificación,
incluyendo que verificar con la llave incorrecta falla como se espera)
contra un registry local descartable antes de tocar nada real. Docs en
docs/devsecops/cosign.md, incluyendo qué falta para que un admission
controller verifique esto en el cluster (no implementado todavía).
Corre después del gate de CRITICAL de Trivy, sobre la imagen ya
construida. Genera CycloneDX y SPDX, publicados como artifact
sbom-v1.0.X en cada build.
Validado contra la imagen real: 3528 componentes CycloneDX / 228
paquetes SPDX. Documenta en docs/devsecops/sbom.md el propósito
(trazabilidad tipo "log4shell"), diferencia entre formatos, y un
hallazgo real (yarn empaquetado sin usarse, mismo patrón que el npm
sacado en el fix de Trivy).
Escanea workloads/ecommerce con rulesets públicos del registro de
Semgrep (p/typescript, p/react, p/nextjs, p/security-audit) vía la
imagen oficial semgrep/semgrep:1.173.0. Sin --error a propósito: siempre
termina en exit 0, solo reporta — primera vuelta para revisar juntos qué
reglas deberían pasar a bloquear más adelante.
Validado contra el código real: 0 hallazgos en 91 reglas / 72 archivos.
Documenta la herramienta en docs/devsecops/sast.md, incluyendo la
diferencia con gitleaks/trivy y qué significa (y qué no) un scan limpio.
trivy config sobre frontend.yaml (CRITICAL/HIGH/MEDIUM, informativo) y
trivy image sobre la imagen recién construida (CRITICAL bloquea con
--ignore-unfixed, HIGH solo informa), entre build (push:false, load:true)
y el push real al registry.
Fix real encontrado al validar contra la imagen real: el stage runner
heredaba npm/npx/corepack completos de la imagen base de Node sin
necesitarlos en runtime, trayendo CVE-2026-59873 (CRITICAL, node-tar
empaquetado en npm). Sacarlos del stage final baja CRITICAL de 1 a 0 y
HIGH fixable de 21 a 15 (las que quedan son de la propia app, ej. next
desactualizado, documentadas como pendiente sin bloquear).
Documenta ambos scans en docs/devsecops/trivy.md con los hallazgos
reales de este repo.
Corre en push y pull_request, antes del job build (needs: gitleaks).
Si detecta un secreto, exit-code=1 detiene el pipeline antes de
construir la imagen. Scan histórico completo del repo (249 commits)
confirmado limpio, corrido aparte de forma manual.
Documenta el step en docs/devsecops/gitleaks.md: qué es secret
scanning, por qué corre antes del build, cómo leer un hallazgo y
cómo manejar falsos positivos con allowlist.
mkdocs build --strict aborta ante CUALQUIER WARNING, no solo ante links
rotos. El plugin, al no tener .git real en el contexto de build
(workloads/docs-portal/, sin .git de la raíz del repo), cae siempre en
fallback_to_build_date y emite un WARNING por página — suficiente para
tumbar el build bajo --strict (confirmado en run 96, job 108: 33
warnings, todos de este plugin, cero links rotos).
Se deshabilita el plugin (comentado en mkdocs.yml y requirements.txt,
con TODO fase 2: mover el build a la raíz del repo + fetch-depth:0 para
darle historial real) y se revierte la instalación de git en el
Dockerfile, que ya no hace falta. index.md actualizado para no prometer
fechas reales que hoy no se muestran.
mkdocs build --strict (run 95, job 107) falló: el link a '#deuda-técnica-pendiente'
no coincide con el anchor real que genera mkdocs, porque el slugify por
defecto de Python-Markdown normaliza tildes (NFKD + strip de combining marks)
antes de generar el id del heading. El heading '## Deuda técnica pendiente'
genera 'deuda-tecnica-pendiente' (sin tilde), no 'deuda-técnica-pendiente'.
El plugin depende de GitPython, que necesita el binario git para
importarse (no solo para leer fechas) — sin él, mkdocs build --strict
falla en el import del plugin antes de llegar al fallback_to_build_date
configurado en mkdocs.yml.
Detectado en el primer intento real de build en Gitea Actions (run 94,
job 106).
Dispara el primer build real del portal via .gitea/workflows/deploy-docs.yaml
ahora que el Deployment, Service, Ingress y el secret de registry ya existen
en el namespace docs-portal.
Agrega lo que faltaba para desplegar workloads/docs-portal/ (ya existente
sin commitear): deployment/service/ingress + kustomization siguiendo el
patrón de workloads/nginx, Applications de workload y gobernanza
separadas, y el workflow de Gitea Actions (build+push a Gitea Registry,
bump de versión en el manifiesto) siguiendo el mismo patrón que
build.yaml/build-medusa.yaml.
El subscriber revalidate-storefront.ts (PR #9) solo escuchaba
product.updated y los eventos de variante, asi que un producto nuevo
(product.created) o eliminado (product.deleted) no disparaba
revalidateTag: el storefront tardaba hasta el ciclo normal de cache
(~60s por pod) en reflejar altas/bajas de catalogo, a diferencia de
precio/descripcion/imagenes en productos existentes, que ya viajan por
product.updated.
El pipeline build-medusa.yaml (run #90) construyo y publico
ecommerce-medusa:v1.0.90 con exito, pero su paso "Actualizar
manifiesto Medusa" quedo skipped: el chequeo de promocion segura
comparo github.sha contra origin/main y encontro que el commit
"chore: release v1.0.91" del frontend ya habia cambiado el HEAD, asi
que se abstuvo de sobrescribirlo. medusa.yaml se quedo en v1.0.88, sin
el subscriber revalidate-storefront agregado en el PR #9.
Bump manual del tag en ambos containers (migrations y medusa) para
que Argo CD despliegue la imagen que ya esta en el registry.
Antes el precio actualizado en Medusa tardaba hasta ~60s (o más, con
varias réplicas del frontend sin cache handler compartido) en verse en
el front, por el revalidate:60 del fetch a /store/products.
Ahora:
- Nueva ruta app/api/revalidate en el storefront que llama a
revalidateTag("medusa-products") al recibir un POST autenticado con
el header x-revalidate-secret.
- Nuevo subscriber en el backend Medusa (product.updated,
product-variant.updated/created/deleted) que llama a esa ruta usando
STOREFRONT_URL (ya existe en commerce-config) y un REVALIDATE_SECRET
compartido entre ambos servicios.
- secrets.template.yaml documenta la nueva clave REVALIDATE_SECRET en
commerce-secrets y commerce-storefront (mismo valor en ambos).
Requiere reaplicar commerce-secrets y commerce-storefront con el script
actualizado en el repo scripts (fix/revalidate-secret-scripts) para que
el REVALIDATE_SECRET exista en el cluster.
Product detail pages were generateStaticParams'd at Docker build time,
where MEDUSA_* env vars aren't set, so the static snapshot baked in the
mock catalog (price: null for all products). With revalidate=60 and no
shared ISR cache handler across the 2 frontend replicas, each pod healed
its own cache independently on next visit past staleness — so a product
could show "Consultar precio" on one pod and the real price on the other,
while the fully-dynamic catalog listing always hit Medusa fresh. Verified
against the live Medusa API that pricing/region data itself was correct
for the reported failing products.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DXqBoNfYQNwFg65mbx2FWM
Todos los Ingress del lab terminan en Traefik por HTTP plano (entrypoint
web, sin TLS); Cloudflare es quien atiende HTTPS de cara al navegador.
Con secure:true (default en produccion), express-session nunca emite
Set-Cookie porque no detecta la conexion como HTTPS, dejando el
dashboard admin con 401 en /admin/users/me pese a loguear bien.
generateStaticParams usa el catálogo mock en build time porque
HEADLESS_PROVIDER y MEDUSA_* no existen como build args del Dockerfile.
Sin un revalidate explícito, esa ruta queda estática para siempre con
price:null. El listado no sufre esto porque su ruta es dinámica por
searchParams.
El validador de /store/products (StoreGetProductsParams, .strict()) solo
acepta region_id/country_code/province/cart_id para resolver contexto de
precio -- currency_code como campo plano no existe y rompía con 400
"Unrecognized fields: 'currency_code'" en cada carga de /catalog.
country_code tampoco alcanza: probado en vivo, devuelve "Missing required
pricing context to calculate prices - region_id". Se necesita region_id
explícito.
Se agrega MEDUSA_REGION_ID (env var) apuntando a la región "Colombia"
(COP) creada vía Admin API, ya que no existía ninguna región configurada
en el backend.
El Deployment frontend-deploy no tenía ninguna variable de entorno, así
que HEADLESS_PROVIDER quedaba undefined y el provider caía siempre en
mockCatalogProvider (data/products.json estático), aunque
medusaCatalogProvider ya estaba implementado y listo en
lib/headless/providers/medusa.ts.
Se agrega HEADLESS_PROVIDER=medusa, MEDUSA_BACKEND_URL apuntando al
Service interno medusa-svc:9000 (evita depender de Cloudflare/NPM en
cada render SSR), y envFrom hacia el secret commerce-storefront
(MEDUSA_PUBLISHABLE_KEY) ya creado en el namespace ecommerce.
No se bumpea el tag de imagen: build.yaml excluye explícitamente los
cambios de frontend.yaml del trigger de build, y la versión la gestiona
la propia CI vía sed sobre este archivo.
El init container wait-for-postgres pasaba $DATABASE_URL completo a
pg_isready. Al agregarle ?sslmode=disable (via create-commerce-secrets.sh),
pg_isready fallaba con "no attempt" (exit 3, conninfo invalida para su
parser de URI) y el pod quedaba atascado en Init indefinidamente, sin
llegar nunca al CrashLoopBackOff visible. Postgres, DNS, Endpoints y
NetworkPolicy estaban sanos; el fallo era puramente del parseo de la URI.
Se reemplaza por -h/-p/-U/-d explicitos (host y puerto fijos del Service,
usuario y db ya disponibles via envFrom), asi el init container queda
inmune a query params futuros en DATABASE_URL.
Diagnostico: pg_isready y el cliente pg de Medusa fallan de forma
consistente (100%, 186/186 intentos) cuando el pod queda agendado en un
nodo distinto al de commerce-postgres-0. TCP handshake (nc/ping) funciona
cruzando nodos, pero el primer paquete de datos real de la sesion nunca
llega - blackhole de PMTU Discovery entre los nodos del cluster k3d
(flannel VXLAN / MTU del bridge Docker subyacente).
Se agrega podAffinity requiredDuringSchedulingIgnoredDuringExecution
(no nodeSelector fijo, mantiene portabilidad) para que medusa-deploy
siempre comparta nodo con commerce-postgres-0 y evite cruzar el
blackhole. Es un workaround, no el fix de raiz: documentado en
docs/known-issues.md junto con el TODO de ajustar el MTU del cluster
a 1400.