diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index b09b5f3..53eff4e 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -93,12 +93,59 @@ jobs: if: always() run: $COMPOSE down -v || true + scan: + name: Secret scan and release gate + needs: validate + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + TRIVY_IMAGE: ${{ vars.TRIVY_IMAGE }} + ENFORCE_PRODUCTION_PREFLIGHT: ${{ vars.ENFORCE_PRODUCTION_PREFLIGHT }} + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + # Blocking. A credential committed by accident must never reach the + # registry or the deployed stack, and the repository is clean today, so + # this gate costs nothing until it is actually needed. + - name: Secret scan + run: | + image="${TRIVY_IMAGE:-aquasec/trivy:0.58.1}" + docker run --rm -v "$PWD:/src:ro" "$image" \ + fs --scanners secret --exit-code 1 --severity HIGH,CRITICAL \ + --no-progress /src + + # PORTAINER.md described this as blocking publication. It never ran at + # all, and turning it on unconditionally would block every deploy: the + # source preflight refuses a release while the payment and messaging + # adapters are fake, which is the deliberate state the stack runs in + # today. So its verdict is always printed, and enforcement is opt-in. + # Set the repository variable ENFORCE_PRODUCTION_PREFLIGHT to "true" once + # real adapters land, and this becomes the gate the documentation claims. + - name: Production source preflight + run: | + set +e + python3 deploy/production_preflight.py --source-only + verdict=$? + set -e + if [ "$verdict" -eq 0 ]; then + echo "Source preflight passes." + exit 0 + fi + if [ "${ENFORCE_PRODUCTION_PREFLIGHT:-false}" = "true" ]; then + echo "::error::Source preflight blocked the release." + exit "$verdict" + fi + echo "::warning::Source preflight reports blockers (advisory; set ENFORCE_PRODUCTION_PREFLIGHT=true to gate)." + publish-and-deploy: name: Publish images and notify Portainer - needs: [validate, integration] + needs: [validate, integration, scan] if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 45 + env: + TRIVY_IMAGE: ${{ vars.TRIVY_IMAGE }} steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 @@ -127,6 +174,23 @@ jobs: --tag "$image:latest" --tag "$image:${{ gitea.sha }}" . docker push "$image:latest" docker push "$image:${{ gitea.sha }}" + # Reported, not blocking. The current bases carry HIGH/CRITICAL findings + # with no fix available upstream, so gating on them would stop every + # deploy without making anything safer. Read the counts each release, and + # see ROADMAP 2.6 for pinning digests and triaging what is fixable. + - name: Image vulnerability report + run: | + image="${TRIVY_IMAGE:-aquasec/trivy:0.58.1}" + for target in \ + "gitea.blyzer.com.br/blyzer/dtf-api:${{ gitea.sha }}" \ + "gitea.blyzer.com.br/blyzer/dtf-web:${{ gitea.sha }}"; do + echo "--- $target" + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock "$image" \ + image --scanners vuln --severity HIGH,CRITICAL --no-progress \ + --format table --exit-code 0 "$target" || \ + echo "::warning::Could not scan $target" + done + - name: Trigger Portainer redeployment env: PORTAINER_WEBHOOK: ${{ secrets.PORTAINER_WEBHOOK }} diff --git a/PORTAINER.md b/PORTAINER.md index 69cc051..4bb8a7e 100644 --- a/PORTAINER.md +++ b/PORTAINER.md @@ -10,10 +10,32 @@ Cloudflare R2, so MinIO is not part of this stack. ## 1. Gitea configuration -The single workflow is `.gitea/workflows/deploy.yml`. Pull requests run static -validation. A push to `main` runs the full isolated test suite, builds and scans -the production images, publishes both `latest` and the full commit SHA, then -calls Portainer. +The single workflow is `.gitea/workflows/deploy.yml`. Every push and pull request +runs static validation, the integration suite against a real stack, and a secret +scan. A push to `main` then builds the production images, publishes both `latest` +and the full commit SHA, reports their vulnerabilities, and calls Portainer. + +What actually gates a deployment: + +| Check | Gates? | +|---|---| +| `py_compile` and the unit tests | yes | +| Integration suite on a live stack (smoke, workflow, security, scanning, retention, runtime) | yes | +| Browser suites | only when the runner has Chrome; otherwise warns and continues | +| Trivy secret scan (HIGH/CRITICAL) | yes | +| Source preflight (`deploy/production_preflight.py --source-only`) | only when `ENFORCE_PRODUCTION_PREFLIGHT` is `true` | +| Trivy image vulnerabilities | no — reported after the build | + +The source preflight is advisory by default because it refuses a release while +the payment and messaging adapters are fake, which is the deliberate state the +stack runs in today. Enforcing it now would block every deployment. Set the +repository variable `ENFORCE_PRODUCTION_PREFLIGHT` to `true` once real adapters +land, and it becomes a hard gate. + +The image scan is reported rather than enforced because the current bases carry +HIGH/CRITICAL findings with no upstream fix, so failing on them would stop +releases without making anything safer. Triage what is fixable and pin base +digests first; see `ROADMAP.md` 2.6. Repository variables: @@ -27,9 +49,10 @@ Repository secrets: - `REGISTRY_USERNAME` and `REGISTRY_TOKEN` — package write credentials. - `PORTAINER_WEBHOOK` — webhook generated by the `dtf-cloud` Portainer stack. -The webhook is called only after tests and HIGH/CRITICAL secret, -misconfiguration, and image gates pass. The current local-only application -fails the source preflight intentionally, so it cannot publish yet. +The webhook is called only after the gating checks in the table above pass. +`ENFORCE_PRODUCTION_PREFLIGHT` and `TRIVY_IMAGE` are optional repository +variables; without them the preflight is advisory and a pinned default scanner +image is used. ## 2. One-time Portainer resources diff --git a/SECURITY_REPORT.md b/SECURITY_REPORT.md index 564c147..f480d62 100644 --- a/SECURITY_REPORT.md +++ b/SECURITY_REPORT.md @@ -129,9 +129,16 @@ change when the advisory database or selected base digest changes. ## Production delivery security boundary The files in `deploy/` and `.gitea/workflows/` are a guarded delivery mechanism, -not an approval to operate the current application on the public internet. The -single Gitea workflow requires a protected Docker runner, exact commit checkout, -digest-pinned base/scanner images, regressions, and HIGH/CRITICAL Trivy gates. +not an approval to operate the current application on the public internet. + +Corrected 2026-09-21: an earlier version of this section described gates the +workflow did not contain. The workflow now runs static validation, the +integration suite against a live stack, and a blocking Trivy secret scan before +publishing. The source preflight is advisory unless +`ENFORCE_PRODUCTION_PREFLIGHT` is set, and image vulnerabilities are reported +rather than enforced, because the current bases carry HIGH/CRITICAL findings +with no upstream fix. Base images are still mutable tags, not digests. +`PORTAINER.md` holds the authoritative table of what gates and what does not. It publishes both `latest` and the full commit SHA, then calls the Portainer webhook. Application/provider secrets are created directly as versioned external Swarm secrets and never cross the workflow. Rollback selects the prior commit SHA