ci: add the release gates, and document the ones that do not gate
PORTAINER.md and SECURITY_REPORT.md described a pipeline that required regressions, HIGH/CRITICAL secret, misconfiguration and image gates, and stated that the source preflight stopped this application from publishing. None of it ran: the workflow built and called the webhook unconditionally. Add a blocking Trivy secret scan. Verified both ways: a planted AWS key pair, GitHub token and private key block the job, and the repository passes clean. Note that Trivy allowlists documented example credentials, so this gate is a backstop, not permission to commit secrets. The source preflight now runs on every push and always prints its verdict, but enforces only when ENFORCE_PRODUCTION_PREFLIGHT is true. Enforcing it today would block every deployment, because it refuses a release while the payment and messaging adapters are fake, which is the deliberate state the stack runs in. Set the variable when real adapters land. Image vulnerabilities are reported after each build rather than enforced. The current bases carry 56 HIGH and 3 CRITICAL findings, only 15 of them with an upstream fix, so failing on them would stop releases without making anything safer. Pinning digests and triaging the fixable ones is ROADMAP 2.6. Both documents now carry a table of what gates and what does not, instead of describing checks that did not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -93,12 +93,59 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: $COMPOSE down -v || true
|
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:
|
publish-and-deploy:
|
||||||
name: Publish images and notify Portainer
|
name: Publish images and notify Portainer
|
||||||
needs: [validate, integration]
|
needs: [validate, integration, scan]
|
||||||
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'
|
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
|
env:
|
||||||
|
TRIVY_IMAGE: ${{ vars.TRIVY_IMAGE }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
@@ -127,6 +174,23 @@ jobs:
|
|||||||
--tag "$image:latest" --tag "$image:${{ gitea.sha }}" .
|
--tag "$image:latest" --tag "$image:${{ gitea.sha }}" .
|
||||||
docker push "$image:latest"
|
docker push "$image:latest"
|
||||||
docker push "$image:${{ gitea.sha }}"
|
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
|
- name: Trigger Portainer redeployment
|
||||||
env:
|
env:
|
||||||
PORTAINER_WEBHOOK: ${{ secrets.PORTAINER_WEBHOOK }}
|
PORTAINER_WEBHOOK: ${{ secrets.PORTAINER_WEBHOOK }}
|
||||||
|
|||||||
37
PORTAINER.md
37
PORTAINER.md
@@ -10,10 +10,32 @@ Cloudflare R2, so MinIO is not part of this stack.
|
|||||||
|
|
||||||
## 1. Gitea configuration
|
## 1. Gitea configuration
|
||||||
|
|
||||||
The single workflow is `.gitea/workflows/deploy.yml`. Pull requests run static
|
The single workflow is `.gitea/workflows/deploy.yml`. Every push and pull request
|
||||||
validation. A push to `main` runs the full isolated test suite, builds and scans
|
runs static validation, the integration suite against a real stack, and a secret
|
||||||
the production images, publishes both `latest` and the full commit SHA, then
|
scan. A push to `main` then builds the production images, publishes both `latest`
|
||||||
calls Portainer.
|
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:
|
Repository variables:
|
||||||
|
|
||||||
@@ -27,9 +49,10 @@ Repository secrets:
|
|||||||
- `REGISTRY_USERNAME` and `REGISTRY_TOKEN` — package write credentials.
|
- `REGISTRY_USERNAME` and `REGISTRY_TOKEN` — package write credentials.
|
||||||
- `PORTAINER_WEBHOOK` — webhook generated by the `dtf-cloud` Portainer stack.
|
- `PORTAINER_WEBHOOK` — webhook generated by the `dtf-cloud` Portainer stack.
|
||||||
|
|
||||||
The webhook is called only after tests and HIGH/CRITICAL secret,
|
The webhook is called only after the gating checks in the table above pass.
|
||||||
misconfiguration, and image gates pass. The current local-only application
|
`ENFORCE_PRODUCTION_PREFLIGHT` and `TRIVY_IMAGE` are optional repository
|
||||||
fails the source preflight intentionally, so it cannot publish yet.
|
variables; without them the preflight is advisory and a pinned default scanner
|
||||||
|
image is used.
|
||||||
|
|
||||||
## 2. One-time Portainer resources
|
## 2. One-time Portainer resources
|
||||||
|
|
||||||
|
|||||||
@@ -129,9 +129,16 @@ change when the advisory database or selected base digest changes.
|
|||||||
## Production delivery security boundary
|
## Production delivery security boundary
|
||||||
|
|
||||||
The files in `deploy/` and `.gitea/workflows/` are a guarded delivery mechanism,
|
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
|
not an approval to operate the current application on the public internet.
|
||||||
single Gitea workflow requires a protected Docker runner, exact commit checkout,
|
|
||||||
digest-pinned base/scanner images, regressions, and HIGH/CRITICAL Trivy gates.
|
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
|
It publishes both `latest` and the full commit SHA, then calls the Portainer
|
||||||
webhook. Application/provider secrets are created directly as versioned external
|
webhook. Application/provider secrets are created directly as versioned external
|
||||||
Swarm secrets and never cross the workflow. Rollback selects the prior commit SHA
|
Swarm secrets and never cross the workflow. Rollback selects the prior commit SHA
|
||||||
|
|||||||
Reference in New Issue
Block a user