Ask a team who can deploy to production and the answer is usually "only through the pipeline, with approval". Ask who can approve, who can change the pipeline and who can reach the production credentials without it, and the answer gets longer.
A deploy approval is a control only if it cannot be skipped and the approver is independent of the change. This checklist uses GitHub environments as the example, but the questions apply to any CI/CD system.
The principle: separation of duties
Separation of duties means no single person can take a change from idea to production alone. In a pipeline that comes down to three separations:
- WriteAn engineer authors the change and opens a pull request.
- ReviewA different engineer approves the code before it merges.
- ReleaseA required reviewer, other than the person who triggered the run, approves the production deployment.
- DeployThe pipeline, not a person, uses credentials that only exist inside the production environment.
If any one person can perform two of those steps alone, or skip one, the approval is ceremony. The checklist is about finding where that happens.
This is not only an audit concern. Separation of duties also limits what a single compromised account can do. If a phished laptop or a leaked personal token can push, merge and release on its own, the attacker inherits the whole path to production in one step.
Environments in GitHub Actions
A GitHub environment attaches protection rules and secrets to the jobs that reference it. A job that targets production pauses until the rules are met:
jobs:
deploy-prod:
runs-on: ubuntu-latest
environment:
name: production
url: https://app.example.com
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@<full-length-commit-sha>
- name: Deploy
run: ./scripts/deploy.sh production
In the environment settings you configure required reviewers, whether the person who triggered the run may approve it, which branches or tags may deploy, and the secrets or variables that only this environment can read. Using OIDC to your cloud provider, with a trust policy bound to the production environment claim, means there is no long-lived deploy key to leak.
The checklist
| # | Question | Good answer |
|---|---|---|
| 1 | Does the production environment have required reviewers? | Yes, a named team, not one individual |
| 2 | Can the person who triggered the run approve it? | No, "prevent self-review" is on |
| 3 | Which branches or tags can deploy to production? | Only the protected main branch or signed release tags |
| 4 | Are production secrets stored at environment level only? | Yes, no copies at repository or organisation level |
| 5 | Does the cloud trust policy require the production environment claim? | Yes, OIDC subject is bound to repo and environment |
| 6 | Can admins bypass environment rules or branch protection? | No, or bypass is logged and reviewed |
| 7 | Who can edit workflow files on the main branch? | Changes to .github/workflows/ need code owner review |
| 8 | Is there any other path to production credentials? | No personal cloud keys or console roles with deploy rights |
| 9 | Can one person merge and approve the deploy of their own change? | No, the approver group excludes the merge author or rules enforce it |
| 10 | Is every approval recorded somewhere you can query? | Yes, deployment history and audit log are kept and exported |
Where the gaps usually are
- The break-glass that became the door. An emergency admin bypass that is used every week is the real deploy process.
- The old workflow. A legacy deploy job that predates environments and still holds a repository secret.
- The tiny team. With two engineers, separation is hard. Make the second person approve, and log every exception rather than turning the rule off.
- The workflow edit. If anyone can change the workflow on a branch that is allowed to deploy, they can remove the
environmentline. Branch restrictions and code owner review on workflow files close this.
Run it in 30 minutes
- List every workflow that can reach production. Search for deploy scripts, cloud credentials and environment names.
- For each one, answer the ten questions with the repository settings open on screen.
- Mark any "no" or "not sure" as a ticket with an owner.
- Fix the bypasses first: stray secrets, missing
environmentlines, admin overrides. - Repeat whenever a new deploy target is added. It is the last gate on the line, so it is worth checking twice.
Where this sits on the line
In the tower (in development):