diff --git a/docs/src/content/docs/deployment/production-checklist.mdx b/docs/src/content/docs/deployment/production-checklist.mdx index 2884adff..35715aae 100644 --- a/docs/src/content/docs/deployment/production-checklist.mdx +++ b/docs/src/content/docs/deployment/production-checklist.mdx @@ -186,6 +186,9 @@ Use this checklist before deploying Rack Gateway to production. Each item addres - CircleCI token configured - Approval job names match - GitHub integration tested + - `service_image_patterns` set for each app, pinning the image repository (approval-bound builds are refused without it) + - `approved_deploy_commands` lists the exact commands CI runs (an empty list allows none) + - `vcs_repo` set to `owner/repo` for CircleCI auto-approval - [ ] **CI/CD tokens created** - API tokens with `cicd` role diff --git a/docs/src/content/docs/integrations/deploy-approvals/circleci.mdx b/docs/src/content/docs/integrations/deploy-approvals/circleci.mdx index d836cb59..99d467db 100644 --- a/docs/src/content/docs/integrations/deploy-approvals/circleci.mdx +++ b/docs/src/content/docs/integrations/deploy-approvals/circleci.mdx @@ -141,7 +141,7 @@ jobs: --app myapp \ --git-commit "$CIRCLE_SHA1" \ --branch "$CIRCLE_BRANCH" \ - --ci-metadata "{\"workflow_id\":\"$CIRCLE_WORKFLOW_ID\",\"pipeline_number\":<< pipeline.number >>}" \ + --ci-metadata "{\"workflow_id\":\"$CIRCLE_WORKFLOW_ID\",\"pipeline_number\":\"<< pipeline.number >>\"}" \ --message "Deploy $CIRCLE_BRANCH@${CIRCLE_SHA1:0:7} to production" environment: RACK_GATEWAY_API_TOKEN: $RACK_GATEWAY_API_TOKEN @@ -241,6 +241,12 @@ sequenceDiagram participant Gateway as Gateway participant CircleCI as CircleCI API + Gateway->>Gateway: Deploy approval still approved and unexpired? + Gateway->>CircleCI: GET /workflow/{workflow_id} + CircleCI-->>Gateway: pipeline_id + Gateway->>CircleCI: GET /pipeline/{pipeline_id} + CircleCI-->>Gateway: vcs.revision, repository URLs + Gateway->>Gateway: Revision = approved commit? Repository = vcs_repo? Not a fork? Gateway->>CircleCI: GET /workflow/{workflow_id}/job CircleCI-->>Gateway: List of jobs Gateway->>Gateway: Find job matching approval_job_name @@ -248,12 +254,17 @@ sequenceDiagram CircleCI-->>Gateway: 202 Accepted ``` +The `ci_metadata` comes from the CI job that requested the approval, so the gateway doesn't trust it on its +own: the hold job is only approved when the workflow's pipeline is building the approved commit of the app's +repository, and the job is re-checked against the deploy approval on every attempt (retries can run later, +after a rejection or expiry). + ### What Gateway Needs | Source | Data | |--------|------| -| CI metadata | `workflow_id` | -| App settings | `circleci_approval_job_name` | +| CI metadata | `workflow_id` (a CircleCI workflow UUID), `pipeline_number` (string) | +| App settings | `circleci_approval_job_name`, `vcs_repo` (`owner/repo`) | | Gateway config | `CIRCLECI_TOKEN` | ## Pipeline URL Display @@ -345,6 +356,8 @@ curl -H "Circle-Token: YOUR_TOKEN" https://circleci.com/api/v2/me - [ ] `ci_metadata` includes `workflow_id` - [ ] `CIRCLECI_TOKEN` has correct permissions - [ ] App `ci_provider` is set to `circleci` +- [ ] App `vcs_repo` is set to the repository CircleCI builds (`owner/repo`) +- [ ] The workflow was building the approved commit (not a later re-run of another commit) **Check gateway logs:** ```bash @@ -358,6 +371,9 @@ convox logs --app rack-gateway | grep -i circleci | `approval job 'xxx' not found in workflow` | Job name mismatch | | `403 Forbidden` | Token lacks permissions or expired | | `workflow not found` | Invalid workflow_id in metadata | +| `invalid CircleCI workflow_id` | `workflow_id` is not a UUID (the job is cancelled) | +| `CircleCI pipeline does not match the deploy approval` | The pipeline builds another commit or repository, is a fork, or its repository can't be determined (the job is cancelled; approve the hold manually after checking) | +| `deploy approval is no longer active` | The approval was rejected, expired or deployed before the job ran (the job is cancelled) | ### API Token Permissions diff --git a/docs/src/content/docs/integrations/deploy-approvals/index.mdx b/docs/src/content/docs/integrations/deploy-approvals/index.mdx index 94220c8d..4ab56099 100644 --- a/docs/src/content/docs/integrations/deploy-approvals/index.mdx +++ b/docs/src/content/docs/integrations/deploy-approvals/index.mdx @@ -94,11 +94,14 @@ graph TB ### Git Commit Verification -Every approval is tied to a specific git commit hash: +Every approval is tied to a specific full (40-character) git commit SHA. An API token deploying under an approval must: -- Approval cannot be reused for different code -- Manifest validation ensures deployed images match approved commit -- Prevents deploying arbitrary code even with compromised CI/CD token +- Build the archive it uploaded under that approval (one upload and one build per approval) +- Send no `git-sha`, or one equal to the approved commit (the gateway always uses the approved commit, never the client's) +- Reference only pre-built images, each tagged with the approved commit (`` or `-`, e.g. `-amd64`) and matching the app's `service_image_patterns` +- Run only commands listed in `approved_deploy_commands` (an empty list allows none) + +`service_image_patterns` is required for approval-bound builds: the commit tag alone does not pin the image repository. ### MFA Step-Up @@ -183,7 +186,9 @@ Configure via UI or environment variables: | `vcs_repo` | Repository in org/repo format | | `ci_provider` | CI system (circleci) | | `circleci_approval_job_name` | CircleCI approval job name | -| `circleci_auto_approve_on_approval` | Enable auto-approval | +| `circleci_auto_approve_on_approval` | Enable auto-approval (the gateway first checks the workflow's pipeline builds the approved commit of `vcs_repo`) | +| `service_image_patterns` | **Required.** Map of service name (or `*`) to an anchored regex; `{{GIT_COMMIT}}` is replaced with the approved commit, e.g. `{"*": "docker\\.io/org/app:{{GIT_COMMIT}}-amd64"}` | +| `approved_deploy_commands` | Exact commands a token may run under an approval (e.g. migrations); empty allows none |