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.
This commit is contained in:
devops committed 2026-08-17 00:23:24 -05:00
1 parent 018d424a1c
commit a3c022c024
11 files changed
+514 -49

No files matched your search

-12
View File
@@ -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"
-17
View File
@@ -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 }}'"
@@ -49,7 +49,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
fetch-depth: 1 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: with:
source: ${{ inputs.app_dir }} 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_USER}" || { echo "ERROR: REGISTRY_USER no está configurado."; exit 1; }
test -n "${REGISTRY_PASSWORD}" || { echo "ERROR: REGISTRY_PASSWORD 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: with:
target: ${{ env.MANIFEST_FILE }} 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: with:
target: ${{ env.APP_DIR }} target: ${{ env.APP_DIR }}
configs: ${{ inputs.semgrep_configs }} configs: ${{ inputs.semgrep_configs }}
@@ -118,12 +118,12 @@ jobs:
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
${{ env.IMAGE_NAME }}:latest ${{ 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: with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
ignorefile: ${{ inputs.trivy_ignorefile }} 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: with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
artifact_suffix: ${{ 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; } test -n "${DIGEST}" || { echo "ERROR: no se pudo extraer el digest del push."; exit 1; }
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" 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: with:
image_name: ${{ env.IMAGE_NAME }} image_name: ${{ env.IMAGE_NAME }}
digest: ${{ steps.push.outputs.digest }} digest: ${{ steps.push.outputs.digest }}
@@ -47,7 +47,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
fetch-depth: 1 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: with:
source: ${{ inputs.app_dir }} 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_USER}" || { echo "ERROR: REGISTRY_USER no está configurado."; exit 1; }
test -n "${REGISTRY_PASSWORD}" || { echo "ERROR: REGISTRY_PASSWORD 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: with:
target: ${{ env.MANIFEST_FILE }} 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: with:
target: ${{ env.APP_DIR }} target: ${{ env.APP_DIR }}
configs: ${{ inputs.semgrep_configs }} configs: ${{ inputs.semgrep_configs }}
@@ -118,12 +118,12 @@ jobs:
# commerce-backend (Medusa v2) es exactamente el caso real que # commerce-backend (Medusa v2) es exactamente el caso real que
# justificó ese default (node_modules completo del framework + # justificó ese default (node_modules completo del framework +
# admin-sdk + cli, mucho más que escanear que una API chica). # 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: with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
ignorefile: ${{ inputs.trivy_ignorefile }} 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: with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
artifact_suffix: ${{ 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; } test -n "${DIGEST}" || { echo "ERROR: no se pudo extraer el digest del push."; exit 1; }
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" 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: with:
image_name: ${{ env.IMAGE_NAME }} image_name: ${{ env.IMAGE_NAME }}
digest: ${{ steps.push.outputs.digest }} digest: ${{ steps.push.outputs.digest }}
@@ -53,7 +53,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
fetch-depth: 1 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: with:
source: ${{ inputs.app_dir }} 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_USER}" || { echo "ERROR: REGISTRY_USER no está configurado."; exit 1; }
test -n "${REGISTRY_PASSWORD}" || { echo "ERROR: REGISTRY_PASSWORD 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: with:
target: ${{ env.APP_DIR }} 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: with:
target: ${{ env.APP_DIR }} target: ${{ env.APP_DIR }}
configs: ${{ inputs.semgrep_configs }} configs: ${{ inputs.semgrep_configs }}
@@ -120,12 +120,12 @@ jobs:
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
${{ env.IMAGE_NAME }}:latest ${{ 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: with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
ignorefile: ${{ inputs.trivy_ignorefile }} 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: with:
image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }} image_ref: ${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
artifact_suffix: ${{ 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; } test -n "${DIGEST}" || { echo "ERROR: no se pudo extraer el digest del push."; exit 1; }
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" 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: with:
image_name: ${{ env.IMAGE_NAME }} image_name: ${{ env.IMAGE_NAME }}
digest: ${{ steps.push.outputs.digest }} digest: ${{ steps.push.outputs.digest }}
+204
View File
@@ -1 +1,205 @@
# platform-templates # 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 <nombre>
-n <ns> -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`.
+66
View File
@@ -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 `<nombre>/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.
@@ -18,8 +18,7 @@ inputs:
description: >- description: >-
Ruta a un .trivyignore con excepciones documentadas (opcional). Ruta a un .trivyignore con excepciones documentadas (opcional).
Cada excepción debe tener justificación y fecha en el propio Cada excepción debe tener justificación y fecha en el propio
archivo — no es un mecanismo para silenciar sin revisar. Ver archivo — no es un mecanismo para silenciar sin revisar.
devsecops-snippets/trivy-image-step/README.md.
required: false required: false
default: "" default: ""
@@ -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: <tu-imagen>: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.
+66
View File
@@ -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).
+65
View File
@@ -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).