From 31424c58fa9deb5a4cfc49fd48778ae13cfd0a81 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Fri, 9 Oct 2026 17:00:56 +1300 Subject: [PATCH 1/3] Bind deploy approvals to the approved commit and artifact An approval used to authorize "a build" for the token and app, so a compromised CI job could ship any tarball or image under it: git-sha was optional (DocSpring CI never sends one), the build's archive wasn't compared with the upload, and image patterns were optional and unanchored. Token builds under an approval now: - must use the archive uploaded under that same approval, chosen deterministically (no "most recent approved" lookup); - take the commit from the approval record, never from the client. A git-sha, if sent, must equal it, and approvals need a full 40-hex SHA; - may only reference pre-built images tagged or -. Services built from source are rejected; - require service_image_patterns, so the image repository is pinned too. Patterns are anchored and the substituted commit is regex-escaped. Also: - approved_deploy_commands fails closed: an empty list allows no commands under an approval. The CLI E2E seed stored it as {"commands": [...]}, which was silently read as empty and so allowed anything; it now uses the plain array production uses. - GitHub verification: "branch" mode requires the commit to be on the feature branch (compare status behind/identical, not any 200), "latest" mode compares full SHAs, and URL segments are path-escaped. - CircleCI auto-approve checks the workflow's pipeline revision and repository against the approval (and rejects fork pipelines) before approving the hold job. workflow_id must be a UUID. - CircleCI and GitHub API tokens are no longer stored in River job args; workers read them from config. The Jobs API redacts token-, secret-, password- and key-like arg keys. --- .../integrations/deploy-approvals/index.mdx | 15 +- internal/gateway/app/setup.go | 2 + internal/gateway/circleci/client.go | 19 +- .../gateway/circleci/pipeline_verification.go | 117 +++++++ .../circleci/pipeline_verification_test.go | 109 ++++++ internal/gateway/db/commit_sha.go | 22 ++ internal/gateway/db/database_test.go | 43 ++- .../db/deploy_approval_requests_mutations.go | 6 +- .../db/deploy_approval_requests_queries.go | 18 +- internal/gateway/github/client.go | 128 +++++--- internal/gateway/github/verify_commit_test.go | 94 ++++++ internal/gateway/handlers/admin_jobs.go | 11 +- .../gateway/handlers/deploy_approval_admin.go | 18 +- .../handlers/deploy_approval_notification.go | 9 +- .../deploy_approval_requests_mfa_test.go | 4 +- .../handlers/deploy_approval_requests_test.go | 62 ++-- .../handlers/deploy_approval_validation.go | 7 +- .../gateway/handlers/job_args_redaction.go | 60 ++++ .../handlers/job_args_redaction_test.go | 42 +++ internal/gateway/jobs/circleci/approve.go | 34 +- .../gateway/jobs/circleci/approve_test.go | 23 +- internal/gateway/jobs/client.go | 8 +- internal/gateway/jobs/github/pr_comment.go | 21 +- .../gateway/jobs/github/pr_comment_test.go | 11 +- internal/gateway/proxy/approval_resolvers.go | 309 ++++++++++++++++++ .../proxy/deploy_approval_binding_test.go | 242 ++++++++++++++ .../proxy/deploy_approval_tracking_test.go | 68 ++-- .../proxy/deploy_approval_validation.go | 186 ++++------- internal/gateway/proxy/env_filters.go | 6 +- internal/gateway/proxy/forward.go | 29 +- internal/gateway/proxy/manifest_policy.go | 152 +++++++++ internal/gateway/proxy/manifest_validation.go | 74 +---- internal/gateway/proxy/token_permissions.go | 271 --------------- scripts/lib/cli-e2e/db.sh | 2 +- web/e2e/db.ts | 2 +- 35 files changed, 1556 insertions(+), 668 deletions(-) create mode 100644 internal/gateway/circleci/pipeline_verification.go create mode 100644 internal/gateway/circleci/pipeline_verification_test.go create mode 100644 internal/gateway/db/commit_sha.go create mode 100644 internal/gateway/github/verify_commit_test.go create mode 100644 internal/gateway/handlers/job_args_redaction.go create mode 100644 internal/gateway/handlers/job_args_redaction_test.go create mode 100644 internal/gateway/proxy/approval_resolvers.go create mode 100644 internal/gateway/proxy/deploy_approval_binding_test.go create mode 100644 internal/gateway/proxy/manifest_policy.go diff --git a/docs/src/content/docs/integrations/deploy-approvals/index.mdx b/docs/src/content/docs/integrations/deploy-approvals/index.mdx index 94220c8d..b5f8ac0d 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 |