From a3c022c024a9745643dc644a2a7772d20b4151d6 Mon Sep 17 00:00:00 2001 From: Cristian Felipe Cruz Buitron Date: Mon, 17 Aug 2026 00:23:24 -0500 Subject: [PATCH] docs: READMEs completos, estructura final de arquetipos Mueve los reusable workflows a .gitea/workflows/ (restriccion real de Gitea: workflow_call no puede vivir en una ruta arbitraria, confirmado en vivo). Corrige todos los 'uses:' cross-repo a URL absoluta (https://gitea.cruzcloud.net/...) -- la forma corta de GitHub se resuelve contra github.com en esta instancia, confirmado en vivo con un test real (reusable workflow + composite action ejecutando gitleaks de punta a punta). Limpia los archivos de prueba. --- .gitea/workflows/_test-caller.yaml | 12 -- .gitea/workflows/_test-reusable.yaml | 17 -- .../workflows/archetype-nextjs-frontend.yaml | 12 +- .../workflows/archetype-node-api.yaml | 12 +- .../workflows/archetype-static-site.yaml | 12 +- README.md | 204 ++++++++++++++++++ devsecops-snippets/README.md | 66 ++++++ .../trivy-image-step/action.yml | 3 +- pipelines/archetype-nextjs-frontend/README.md | 94 ++++++++ pipelines/archetype-node-api/README.md | 66 ++++++ pipelines/archetype-static-site/README.md | 65 ++++++ 11 files changed, 514 insertions(+), 49 deletions(-) delete mode 100644 .gitea/workflows/_test-caller.yaml delete mode 100644 .gitea/workflows/_test-reusable.yaml rename pipelines/archetype-nextjs-frontend/workflow-template.yaml => .gitea/workflows/archetype-nextjs-frontend.yaml (90%) rename pipelines/archetype-node-api/workflow-template.yaml => .gitea/workflows/archetype-node-api.yaml (90%) rename pipelines/archetype-static-site/workflow-template.yaml => .gitea/workflows/archetype-static-site.yaml (90%) create mode 100644 devsecops-snippets/README.md create mode 100644 pipelines/archetype-nextjs-frontend/README.md create mode 100644 pipelines/archetype-node-api/README.md create mode 100644 pipelines/archetype-static-site/README.md diff --git a/.gitea/workflows/_test-caller.yaml b/.gitea/workflows/_test-caller.yaml deleted file mode 100644 index 42b5e26..0000000 --- a/.gitea/workflows/_test-caller.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: TEST - verificacion de reusable workflow + composite action - -on: - push: - branches: - - main - -jobs: - call-reusable: - uses: https://gitea.cruzcloud.net/devops/platform-templates/.gitea/workflows/_test-reusable.yaml@main - with: - greeting: "hola desde el caller" diff --git a/.gitea/workflows/_test-reusable.yaml b/.gitea/workflows/_test-reusable.yaml deleted file mode 100644 index f1f3cab..0000000 --- a/.gitea/workflows/_test-reusable.yaml +++ /dev/null @@ -1,17 +0,0 @@ -on: - workflow_call: - inputs: - greeting: - type: string - required: true - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/gitleaks-step@main - with: - source: devsecops-snippets - - shell: bash - run: echo "Reusable workflow input recibido -> '${{ inputs.greeting }}'" diff --git a/pipelines/archetype-nextjs-frontend/workflow-template.yaml b/.gitea/workflows/archetype-nextjs-frontend.yaml similarity index 90% rename from pipelines/archetype-nextjs-frontend/workflow-template.yaml rename to .gitea/workflows/archetype-nextjs-frontend.yaml index 6ab5d47..debc6a9 100644 --- a/pipelines/archetype-nextjs-frontend/workflow-template.yaml +++ b/.gitea/workflows/archetype-nextjs-frontend.yaml @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 1 - - uses: devops/platform-templates/devsecops-snippets/gitleaks-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/gitleaks-step@main with: source: ${{ inputs.app_dir }} @@ -89,11 +89,11 @@ jobs: test -n "${REGISTRY_USER}" || { echo "ERROR: REGISTRY_USER no está configurado."; exit 1; } test -n "${REGISTRY_PASSWORD}" || { echo "ERROR: REGISTRY_PASSWORD no está configurado."; exit 1; } - - uses: devops/platform-templates/devsecops-snippets/trivy-iac-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/trivy-iac-step@main with: target: ${{ env.MANIFEST_FILE }} - - uses: devops/platform-templates/devsecops-snippets/semgrep-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/semgrep-step@main with: target: ${{ env.APP_DIR }} configs: ${{ inputs.semgrep_configs }} @@ -118,12 +118,12 @@ jobs: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} ${{ env.IMAGE_NAME }}:latest - - uses: devops/platform-templates/devsecops-snippets/trivy-image-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/trivy-image-step@main with: image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} ignorefile: ${{ inputs.trivy_ignorefile }} - - uses: devops/platform-templates/devsecops-snippets/syft-sbom-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/syft-sbom-step@main with: image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} artifact_suffix: ${{ steps.vars.outputs.VERSION }} @@ -140,7 +140,7 @@ jobs: test -n "${DIGEST}" || { echo "ERROR: no se pudo extraer el digest del push."; exit 1; } echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" - - uses: devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main with: image_name: ${{ env.IMAGE_NAME }} digest: ${{ steps.push.outputs.digest }} diff --git a/pipelines/archetype-node-api/workflow-template.yaml b/.gitea/workflows/archetype-node-api.yaml similarity index 90% rename from pipelines/archetype-node-api/workflow-template.yaml rename to .gitea/workflows/archetype-node-api.yaml index 7ba3cfb..1529b70 100644 --- a/pipelines/archetype-node-api/workflow-template.yaml +++ b/.gitea/workflows/archetype-node-api.yaml @@ -47,7 +47,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 1 - - uses: devops/platform-templates/devsecops-snippets/gitleaks-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/gitleaks-step@main with: source: ${{ inputs.app_dir }} @@ -87,11 +87,11 @@ jobs: test -n "${REGISTRY_USER}" || { echo "ERROR: REGISTRY_USER no está configurado."; exit 1; } test -n "${REGISTRY_PASSWORD}" || { echo "ERROR: REGISTRY_PASSWORD no está configurado."; exit 1; } - - uses: devops/platform-templates/devsecops-snippets/trivy-iac-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/trivy-iac-step@main with: target: ${{ env.MANIFEST_FILE }} - - uses: devops/platform-templates/devsecops-snippets/semgrep-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/semgrep-step@main with: target: ${{ env.APP_DIR }} configs: ${{ inputs.semgrep_configs }} @@ -118,12 +118,12 @@ jobs: # commerce-backend (Medusa v2) es exactamente el caso real que # justificó ese default (node_modules completo del framework + # admin-sdk + cli, mucho más que escanear que una API chica). - - uses: devops/platform-templates/devsecops-snippets/trivy-image-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/trivy-image-step@main with: image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} ignorefile: ${{ inputs.trivy_ignorefile }} - - uses: devops/platform-templates/devsecops-snippets/syft-sbom-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/syft-sbom-step@main with: image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} artifact_suffix: ${{ steps.vars.outputs.VERSION }} @@ -140,7 +140,7 @@ jobs: test -n "${DIGEST}" || { echo "ERROR: no se pudo extraer el digest del push."; exit 1; } echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" - - uses: devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main with: image_name: ${{ env.IMAGE_NAME }} digest: ${{ steps.push.outputs.digest }} diff --git a/pipelines/archetype-static-site/workflow-template.yaml b/.gitea/workflows/archetype-static-site.yaml similarity index 90% rename from pipelines/archetype-static-site/workflow-template.yaml rename to .gitea/workflows/archetype-static-site.yaml index e21d592..fa7fc06 100644 --- a/pipelines/archetype-static-site/workflow-template.yaml +++ b/.gitea/workflows/archetype-static-site.yaml @@ -53,7 +53,7 @@ jobs: - uses: actions/checkout@v3 with: fetch-depth: 1 - - uses: devops/platform-templates/devsecops-snippets/gitleaks-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/gitleaks-step@main with: source: ${{ inputs.app_dir }} @@ -93,11 +93,11 @@ jobs: test -n "${REGISTRY_USER}" || { echo "ERROR: REGISTRY_USER no está configurado."; exit 1; } test -n "${REGISTRY_PASSWORD}" || { echo "ERROR: REGISTRY_PASSWORD no está configurado."; exit 1; } - - uses: devops/platform-templates/devsecops-snippets/trivy-iac-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/trivy-iac-step@main with: target: ${{ env.APP_DIR }} - - uses: devops/platform-templates/devsecops-snippets/semgrep-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/semgrep-step@main with: target: ${{ env.APP_DIR }} configs: ${{ inputs.semgrep_configs }} @@ -120,12 +120,12 @@ jobs: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} ${{ env.IMAGE_NAME }}:latest - - uses: devops/platform-templates/devsecops-snippets/trivy-image-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/trivy-image-step@main with: image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} ignorefile: ${{ inputs.trivy_ignorefile }} - - uses: devops/platform-templates/devsecops-snippets/syft-sbom-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/syft-sbom-step@main with: image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} artifact_suffix: ${{ steps.vars.outputs.VERSION }} @@ -142,7 +142,7 @@ jobs: test -n "${DIGEST}" || { echo "ERROR: no se pudo extraer el digest del push."; exit 1; } echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" - - uses: devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main with: image_name: ${{ env.IMAGE_NAME }} digest: ${{ steps.push.outputs.digest }} diff --git a/README.md b/README.md index 2af540a..f15a73d 100644 --- a/README.md +++ b/README.md @@ -1 +1,205 @@ # platform-templates + +Patrones reutilizables para dar de alta una app nueva en el lab +CruzCloud (ZimaOS + k3d + Gitea + Argo CD) sin escribir el pipeline y +los manifiestos K8s desde cero cada vez. Copiás una plantilla, completás +unos pocos valores, y tenés: pipeline de CI con el gate completo de +DevSecOps (Gitleaks, Trivy, Semgrep, Syft, Cosign) + manifiestos K8s con +buenas prácticas por default (resource limits, probes, non-root). + +**Para quién es esto:** si estás dando de alta una app nueva en el lab, +seguí el ejemplo de abajo. Si estás migrando una app existente, mirá el +punto "Migración de apps existentes" más abajo, y andá con más cuidado +— tocás algo que ya está en producción. + +## Mapa del repo + +``` +pipelines/ -- documentación de cada arquetipo de pipeline + (el archivo real que se invoca vive en + .gitea/workflows/, ver por qué más abajo) +.gitea/workflows/ -- los reusable workflows reales (workflow_call) +dockerfiles/ -- Dockerfiles genéricos, un ARG por cosa a cambiar +k8s-manifests/base/ -- Kustomize base: Deployment+Service+Ingress + genéricos, con securityContext/probes/resources + ya puestos +k8s-manifests/overlays-ejemplo/ -- el ejemplo de abajo, ya armado +devsecops-snippets/ -- los 6 composite actions que arman cada pipeline +``` + +## Hallazgo importante sobre Gitea Actions (leer antes de copiar nada) + +Este repo se armó verificando en vivo, no asumiendo que Gitea Actions es +100% igual a GitHub Actions. Dos diferencias reales, confirmadas +corriendo pipelines de verdad contra esta instancia: + +1. **Los reusable workflows (`workflow_call`) tienen que vivir bajo + `.gitea/workflows/`** — no en una carpeta cualquiera. Por eso + `pipelines/archetype-*/` solo tiene un `README.md`, el archivo + invocable real está en `.gitea/workflows/archetype-*.yaml`. +2. **Toda referencia cross-repo en un `uses:` necesita la URL completa** + (`https://gitea.cruzcloud.net/owner/repo/path@ref`), no la forma + corta de GitHub (`owner/repo/path@ref`) — esa forma corta se resuelve + contra **github.com** por default en esta instancia (confirmado: + intentar usarla hace que el runner intente clonar + `github.com/devops/platform-templates`, que no existe, y falle con + "Repository not found"). `actions/checkout@v3` sigue andando con la + forma corta porque ESE sí existe en github.com de verdad. + +## Ejemplo end-to-end: dar de alta "mi-app-ejemplo" + +Pensado para seguirse sin conocer nada más del lab que "existe Gitea en +gitea.cruzcloud.net y un cluster k3d con Argo CD sincronizando desde +ahí". Asumimos una API Node (arquetipo `node-api`) — para Next.js o un +sitio estático es el mismo procedimiento, cambiando el arquetipo. + +### 1. Creá el repo de tu app en Gitea + +Vía la UI (`https://gitea.cruzcloud.net/repo/create`) o `gh`/API si +tenés token con el scope correcto. Nombre: `mi-app-ejemplo`. + +### 2. Copiá el Dockerfile + +```bash +curl -o Dockerfile \ + https://gitea.cruzcloud.net/devops/platform-templates/raw/branch/main/dockerfiles/node-api.Dockerfile +``` + +Ajustá los `ARG` del inicio del archivo a tu caso (puerto real, comando +de arranque) — están documentados inline, no hace falta adivinar. + +### 3. Agregá el pipeline + +`.gitea/workflows/build.yaml` en tu repo: + +```yaml +name: Build and Push + +on: + push: + branches: [main] + paths: + - 'src/**' + - 'package.json' + - 'package-lock.json' + - 'Dockerfile' + - '.gitea/workflows/build.yaml' + +jobs: + build: + uses: https://gitea.cruzcloud.net/devops/platform-templates/.gitea/workflows/archetype-node-api.yaml@main + with: + app_dir: . + image_name: gitea.cruzcloud.net/devops/mi-app-ejemplo + manifest_file: k8s/deployment.yaml + secrets: + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} + COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} +``` + +Los 4 secrets tienen que existir en tu repo (Settings → Actions → +Secrets). Si tu lab ya los tiene a nivel de organización/compartidos, +puede que no haga falta redeclararlos — depende de cómo esté armado tu +Gitea. + +### 4. Generá tu par de llaves de Cosign (una vez) + +```bash +cosign generate-key-pair +# genera cosign.key (privada) y cosign.pub (pública) +``` + +- `cosign.pub` → commiteala en tu repo (es pública a propósito). +- `cosign.key` → **no la commitees**. Subila como secret + `COSIGN_PRIVATE_KEY` (contenido del archivo) y la password que + elegiste como secret `COSIGN_PASSWORD`. + +### 5. Manifiestos K8s: copiá el base y armá tu overlay + +```bash +mkdir -p k8s/patches +curl -o k8s/kustomization.yaml \ + https://gitea.cruzcloud.net/devops/platform-templates/raw/branch/main/k8s-manifests/overlays-ejemplo/mi-app-ejemplo/kustomization.yaml +curl -o k8s/patches/deployment.yaml \ + https://gitea.cruzcloud.net/devops/platform-templates/raw/branch/main/k8s-manifests/overlays-ejemplo/mi-app-ejemplo/patches/deployment.yaml +curl -o k8s/patches/service.yaml \ + https://gitea.cruzcloud.net/devops/platform-templates/raw/branch/main/k8s-manifests/overlays-ejemplo/mi-app-ejemplo/patches/service.yaml +curl -o k8s/patches/ingress.yaml \ + https://gitea.cruzcloud.net/devops/platform-templates/raw/branch/main/k8s-manifests/overlays-ejemplo/mi-app-ejemplo/patches/ingress.yaml +``` + +Esto ya viene armado con el patrón real (ver +`k8s-manifests/overlays-ejemplo/mi-app-ejemplo/` en este mismo repo) — +solo tenés que: + +1. Cambiar `mi-app-ejemplo` por el nombre real de tu app en los 3 + `patches/*.yaml` y en el `kustomization.yaml` (nombre del + Deployment/Service/Ingress, `host:`, `namespace:`). +2. Ajustar imagen, puerto, y ruta del healthcheck a los reales de tu + app. +3. **Leer los comentarios de `patches/deployment.yaml` sobre + `$patch: replace`/JSON6902** antes de tocar `ports` — Kustomize NO + reemplaza esa lista con un patch de merge normal, la duplica + (confirmado en vivo armando este repo). Si tu puerto real es + distinto al 8080 del base, el rename+ports va en un patch JSON6902 + aparte en tu `kustomization.yaml`, exactamente como en el ejemplo. +4. **Si tu app escucha en un puerto <1024** (80, por ejemplo): el base + saca TODAS las capabilities del contenedor por default — + necesitás agregar `capabilities.add: [NET_BIND_SERVICE]` en tu + patch, o el bind falla. Ver el comentario en + `k8s-manifests/overlays-ejemplo/mi-app-ejemplo/kustomization.yaml` + si tu imagen corre como root en vez de un usuario propio (necesitás + además `runAsNonRoot: false` a nivel pod). + +### 6. Validá el resultado ANTES de commitear + +```bash +kubectl kustomize k8s/ +``` + +Revisá que el YAML que sale tenga sentido — nombre, imagen, puerto, +host correctos, sin recursos duplicados. Si algo se ve raro (dos +`ports:`, dos contenedores), revisá el punto 3 de arriba. + +### 7. Creá la Application de Argo CD + +Sigue el patrón de este lab: una Application de workload +(`syncPolicy` con `prune: true, selfHeal: true`) apuntando al +`k8s/` de tu repo. Preguntale a quien mantiene `apps-registry` cómo se +registra — no es parte de este repo. + +### 8. Push, mirá el pipeline correr, y listo + +Un `git push` a `main` dispara Gitleaks → build → Trivy → Semgrep → Syft +→ Cosign → push al registry → Argo CD sincroniza. Si algo bloquea +(CRITICAL de Trivy, firma de Cosign inválida), el log del job te dice +exactamente cuál. + +## Migración de apps existentes + +Si estás moviendo una app que **ya está corriendo en producción** hacia +este patrón base+overlay: no es lo mismo que dar de alta una nueva. +Antes de tocar nada: + +1. Guardá el manifest actual desplegado (`kubectl get deploy + -n -o yaml`) para comparar. +2. Armá el overlay apuntando **al mismo nombre** que ya existe (usando + los patches JSON6902 de rename, como en el ejemplo) — no dejes que + Kustomize invente un nombre nuevo, rompe el tracking de Argo CD y + la resolución DNS de otros servicios que dependan del nombre viejo. +3. **Revisá con qué usuario corre tu imagen actual.** Si no tiene un + `USER` no-root en su Dockerfile, el `runAsNonRoot: true` del base + rompe el pod (`CreateContainerConfigError`) — hace falta un override + explícito hasta que la imagen se actualice aparte. Pasó exactamente + esto migrando `docs-portal` en este mismo lab. +4. Corré `kubectl kustomize` sobre tu overlay y compará el resultado + contra el manifest guardado en el paso 1 antes de dejar que Argo CD + sincronice nada. + +## Documentación ampliada + +`docs.cruzcloud.net` tiene una sección dedicada a este repo con un +diagrama del flujo completo y el detalle de la migración real que se +hizo sobre `ecommerce-frontend`, `commerce-backend` y `docs-portal`. diff --git a/devsecops-snippets/README.md b/devsecops-snippets/README.md new file mode 100644 index 0000000..58c6296 --- /dev/null +++ b/devsecops-snippets/README.md @@ -0,0 +1,66 @@ +# DevSecOps snippets — composite actions + +Cada subcarpeta es un *composite action* real de Gitea Actions +(`action.yml`), no un fragmento de YAML para copiar/pegar — se invocan +con `uses:` como cualquier action de GitHub. + +**Nota sobre la estructura:** el pedido original de este repo era una +lista plana (`gitleaks-step.yaml`, `trivy-image-step.yaml`, etc.). Se +ajustó a `/action.yml` porque es el único layout que Gitea (y +GitHub) reconocen como composite action invocable — un archivo con +nombre libre no se puede referenciar con `uses:`, tiene que ser +literalmente `action.yml` (o `action.yaml`) en su propia carpeta. + +## Cómo invocar uno desde tu pipeline + +```yaml +steps: + - uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/gitleaks-step@main + with: + source: mi-app/ +``` + +**La URL absoluta (`https://gitea.cruzcloud.net/...`) no es opcional.** +La forma corta de GitHub (`devops/platform-templates/...@main`) se +resuelve contra **github.com** por default en esta instancia de Gitea +(`DEFAULT_ACTIONS_URL` sin configurar = `github`), no contra el propio +Gitea — confirmado en vivo armando este repo: con la forma corta, el +runner intentó clonar `https://github.com/devops/platform-templates` +(no existe) y falló con "Repository not found". Con la URL absoluta +`https://gitea.cruzcloud.net/...` sí resuelve correctamente contra la +instancia local. Ver +`../pipelines/archetype-nextjs-frontend/README.md` para más contexto. + +## Los 6 snippets + +| Snippet | Qué hace | Bloquea por default | +|---|---|---| +| `gitleaks-step` | Secretos commiteados en el árbol de archivos | Sí | +| `trivy-iac-step` | Misconfiguraciones en manifiestos K8s | No (auditoría) | +| `trivy-image-step` | CVEs de la imagen construida | CRITICAL sí, HIGH no | +| `semgrep-step` | SAST, ruleset configurable por `with.configs` | No (auditoría) | +| `syft-sbom-step` | Genera y publica el SBOM de la imagen | N/A (no bloquea) | +| `cosign-sign-verify-step` | Firma por digest + smoke test de verificación | Sí (firma inválida = falla) | + +Cada `action.yml` tiene sus `inputs:` documentados inline — no hay +READMEs individuales por snippet, son chicos y el propio archivo ya +explica cada input. + +## `cosign-sign-verify-step`: un composite action no puede leer `secrets` + +Restricción real de Gitea/GitHub Actions, no de este repo: un composite +action **no tiene acceso directo** al contexto `secrets` del caller. El +workflow que lo invoca tiene que pasarlos explícitos como `with:`: + +```yaml +- uses: https://gitea.cruzcloud.net/devops/platform-templates/devsecops-snippets/cosign-sign-verify-step@main + with: + image_name: ${{ env.IMAGE_NAME }} + digest: ${{ steps.push.outputs.digest }} + public_key_path: ${{ env.APP_DIR }}/cosign.pub + cosign_private_key: ${{ secrets.COSIGN_PRIVATE_KEY }} + cosign_password: ${{ secrets.COSIGN_PASSWORD }} +``` + +Los 3 arquetipos de `pipelines/` ya lo hacen así — es solo relevante si +armás tu propio pipeline a mano usando estos snippets sueltos. diff --git a/devsecops-snippets/trivy-image-step/action.yml b/devsecops-snippets/trivy-image-step/action.yml index 1efd299..f15bae5 100644 --- a/devsecops-snippets/trivy-image-step/action.yml +++ b/devsecops-snippets/trivy-image-step/action.yml @@ -18,8 +18,7 @@ inputs: description: >- Ruta a un .trivyignore con excepciones documentadas (opcional). Cada excepción debe tener justificación y fecha en el propio - archivo — no es un mecanismo para silenciar sin revisar. Ver - devsecops-snippets/trivy-image-step/README.md. + archivo — no es un mecanismo para silenciar sin revisar. required: false default: "" diff --git a/pipelines/archetype-nextjs-frontend/README.md b/pipelines/archetype-nextjs-frontend/README.md new file mode 100644 index 0000000..465867a --- /dev/null +++ b/pipelines/archetype-nextjs-frontend/README.md @@ -0,0 +1,94 @@ +# Arquetipo: Next.js frontend (standalone) + +Cuándo usar este arquetipo: tu app es un frontend Next.js que compila a +`output: 'standalone'` y sirve HTML renderizado en servidor (SSR/RSC), +no un sitio 100% estático. Si es 100% estático, usá +`archetype-static-site` en cambio — es más liviano. + +Qué asume de tu repo: + +- Un `Dockerfile` en `app_dir` basado en (o compatible con) + `dockerfiles/nextjs.Dockerfile` — específicamente, que + `next.config.*` tiene `output: 'standalone'`. +- Un endpoint `/api/health` que devuelva 200 — lo usan tanto el + `HEALTHCHECK` del Dockerfile como las probes de + `k8s-manifests/base/deployment.yaml`. +- `cosign.pub` commiteado en `app_dir` (par de la llave privada que + firma en el pipeline). +- Un manifiesto K8s (`manifest_file`) con una línea `image: :vX.Y.Z` + que el pipeline actualiza automáticamente tras cada build exitoso + (promoción GitOps). + +## Dónde vive el archivo real + +**Importante, no es donde dice el nombre de esta carpeta:** el reusable +workflow en sí (`workflow_call`) tiene que vivir bajo `.gitea/workflows/` +por una restricción real de Gitea Actions — no se puede invocar un +`workflow_call` desde una ruta arbitraria como `pipelines/algo/x.yaml` +(confirmado en vivo, error real del servidor: `"uses:" path "..." must +be under a configured workflow directory`). El archivo real es: + +``` +.gitea/workflows/archetype-nextjs-frontend.yaml +``` + +Esta carpeta (`pipelines/archetype-nextjs-frontend/`) es solo +documentación — el README que estás leyendo. + +## Cómo usarlo desde tu repo + +En `.gitea/workflows/build.yaml` de tu propio repo: + +```yaml +name: Build and Push + +on: + push: + branches: [main] + paths: + - 'app/**' + - '.gitea/workflows/build.yaml' + +jobs: + build: + uses: https://gitea.cruzcloud.net/devops/platform-templates/.gitea/workflows/archetype-nextjs-frontend.yaml@main + with: + app_dir: app + image_name: gitea.cruzcloud.net/devops/mi-app + manifest_file: workloads/mi-app/deployment.yaml + # semgrep_configs y trivy_ignorefile son opcionales, tienen default + secrets: + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} + COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} +``` + +**OJO con el `uses:`** — tiene que ser la URL completa +`https://gitea.cruzcloud.net/...`, no la forma corta +`devops/platform-templates/...` (esa se resuelve contra github.com por +default en esta instancia, no contra Gitea local — confirmado en vivo, +es la razón real por la que este repo usa URLs absolutas en todos los +`uses:` internos también, no solo acá). + +**`@main` en vez de un tag/commit fijo:** significa que tu pipeline +siempre usa la ÚLTIMA versión del arquetipo. Cómodo para no tener que +actualizar cada repo consumidor a mano, pero un cambio incompatible acá +te rompe el pipeline sin aviso. Si preferís control explícito, fijá un +tag (`@v1.0.0`) o un commit SHA en vez de `@main` — a costa de tener que +actualizarlo vos cuando quieras el cambio nuevo. + +## Semgrep: por qué este ruleset + +`p/typescript,p/react,p/nextjs,p/security-audit` — cubre el lenguaje, el +framework de UI, Next.js específicamente (rutas, SSR), y reglas +generales de seguridad. Si tu app usa algo más (GraphQL, un ORM +específico), agregá el ruleset correspondiente al `semgrep_configs` del +`with:`. + +## Modo auditoría por default + +Igual que los 3 pipelines reales de los que sale este arquetipo: +Semgrep y Trivy IaC corren pero NO bloquean por default (primera vuelta +para revisar hallazgos antes de decidir qué debería bloquear). Trivy +imagen (CRITICAL) sí bloquea siempre, no es opcional. diff --git a/pipelines/archetype-node-api/README.md b/pipelines/archetype-node-api/README.md new file mode 100644 index 0000000..db676dc --- /dev/null +++ b/pipelines/archetype-node-api/README.md @@ -0,0 +1,66 @@ +# Arquetipo: API Node/Express (sin SSR) + +Cuándo usar este arquetipo: una API backend en Node.js/TypeScript (Express, +Fastify, Medusa, NestJS, etc.) que expone endpoints HTTP, sin código de +UI propio. Si tu app renderiza páginas del lado del servidor, usá +`archetype-nextjs-frontend` en cambio. + +Qué asume de tu repo: + +- Un `Dockerfile` en `app_dir` basado en (o compatible con) + `dockerfiles/node-api.Dockerfile`. +- Un endpoint `/health` (o el que definas en tu overlay de + `k8s-manifests`) que devuelva 200. +- `cosign.pub` commiteado en `app_dir`. +- Un manifiesto K8s (`manifest_file`) con la imagen a actualizar tras + cada build. + +## Dónde vive el archivo real + +Igual que los otros 2 arquetipos: el `workflow_call` tiene que estar +bajo `.gitea/workflows/` (restricción real de Gitea, ver +`pipelines/archetype-nextjs-frontend/README.md` para el detalle +completo del hallazgo). Archivo real: + +``` +.gitea/workflows/archetype-node-api.yaml +``` + +## Cómo usarlo desde tu repo + +```yaml +jobs: + build: + uses: https://gitea.cruzcloud.net/devops/platform-templates/.gitea/workflows/archetype-node-api.yaml@main + with: + app_dir: api + image_name: gitea.cruzcloud.net/devops/mi-api + manifest_file: workloads/mi-api/deployment.yaml + secrets: + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} + COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} +``` + +Mismas advertencias que el arquetipo de Next.js sobre `uses:` con URL +absoluta y sobre fijar `@main` vs un tag/commit — no las repito acá, ver +ese README. + +## Semgrep: por qué este ruleset + +`p/typescript,p/security-audit,p/owasp-top-ten` — sin `p/react`/`p/nextjs` +(no hay código de UI), con OWASP Top 10 sumado porque una API expuesta +públicamente es exactamente el tipo de superficie que ese ruleset cubre +(inyección, auth rota, etc.). + +## Sobre `npm`/`npx` en runtime + +`dockerfiles/node-api.Dockerfile` borra `npm` de la imagen de runtime a +propósito (reduce superficie de CVEs — ver comentario en el propio +Dockerfile). Si tu app tiene un init container o un job que corre +`npx algo`, va a fallar contra esta imagen. Invocá el binario/CLI +directo con `node`, como hace el `CMD` del propio Dockerfile — no +agregues npm de vuelta solo para eso (esto rompió un pipeline real del +lab una vez, ver `docs-portal` → playbooks → contención de recursos y +el propio commit histórico de `commerce-backend` sobre este tema). diff --git a/pipelines/archetype-static-site/README.md b/pipelines/archetype-static-site/README.md new file mode 100644 index 0000000..8a02057 --- /dev/null +++ b/pipelines/archetype-static-site/README.md @@ -0,0 +1,65 @@ +# Arquetipo: sitio estático (nginx) + +Cuándo usar este arquetipo: un sitio generado a HTML estático (MkDocs, +Hugo, Astro, un build de Vite sin backend) servido por nginx, sin +runtime de aplicación. Si tu app necesita ejecutar código en cada +request, no es este arquetipo. + +Qué asume de tu repo: + +- Un `Dockerfile` en `app_dir` basado en (o compatible con) + `dockerfiles/static-nginx.Dockerfile`. +- `cosign.pub` commiteado en `app_dir`. +- Un manifiesto K8s (`manifest_file`, el Deployment) con la imagen a + actualizar tras cada build. + +Diferencia real con los otros 2 arquetipos: Gitleaks y Trivy IaC +escanean todo `app_dir` (no un único archivo) — un sitio estático suele +tener varios manifiestos K8s por separado (deployment/service/ingress) +en vez de uno solo con todo adentro. + +## Dónde vive el archivo real + +Misma restricción de Gitea que los otros 2 arquetipos (ver +`pipelines/archetype-nextjs-frontend/README.md` para el detalle +completo). Archivo real: + +``` +.gitea/workflows/archetype-static-site.yaml +``` + +## Cómo usarlo desde tu repo + +```yaml +jobs: + build: + uses: https://gitea.cruzcloud.net/devops/platform-templates/.gitea/workflows/archetype-static-site.yaml@main + with: + app_dir: sitio + image_name: gitea.cruzcloud.net/devops/mi-sitio + manifest_file: workloads/mi-sitio/deployment.yaml + secrets: + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} + COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} +``` + +Mismas advertencias sobre `uses:` con URL absoluta y `@main` vs +tag/commit — ver el README del arquetipo de Next.js. + +## Semgrep: por qué este ruleset + +`p/python,p/security-audit` — asume MkDocs (Python). Si tu generador es +Hugo/Astro (Go/JS), este ruleset no aplica realmente — ajustalo al +lenguaje real de tu build (o quitá el step si tu sitio no tiene ningún +código ejecutable propio, solo contenido). + +## docs-portal real todavía no usa nginx no-root + +`dockerfiles/static-nginx.Dockerfile` corre nginx sin privilegios +(`nginxinc/nginx-unprivileged`) — mejora encontrada al armar este +template. El `docs-portal` real del lab **todavía** corre +`nginx:1.27-alpine` estándar (root), es un gap real, pendiente de +aplicar por separado (no forma parte de la migración de manifiestos de +esta tarea — cambiar la imagen es un cambio de app aparte).