GitHub's environments allow adding required reviewers as a deployment protection rule. When applied to a release environment, this allows for 2-factor release workflows where a second team member must approve the workflow before it can access the release secrets.
Unfortunately GitHub applies the deployment protection to every job that runs in the workflow. If a release process has multiple steps, then each step needs to be approved as it starts.
To work around this issue, we make use of the deployment_protection_rule webhook which a GitHub
App can subscribe to. The GitHub App can then be used to approve or deny deployments to an
environment. The human approval in a 2-factor release workflow is retained by having two
environments.
release-gate: This requires approval by a humanrelease: This requires approval from the GitHub App
The GitHub App has a simple purpose: approve the release deployment if the release-gate
deployment was approved.
A minimal workflow would look like this:
name: Release
on:
workflow_dispatch:
inputs:
version:
required: true
type: string
permissions: {}
jobs:
release-gate:
name: release-gate
runs-on: ubuntu-latest
environment: release-gate
steps:
- run: echo "Release approved"
release:
name: Publish release
runs-on: ubuntu-latest
needs: [release-gate]
environment: release
permissions:
contents: write
steps:
- run: echo "Use a secret from release!"The minimal manifest for the GitHub App is:
{
"name": "ost-environment-gate",
"url": "https://github.com/open-security-tools/ost-environment-gate/",
"public": false,
"hook_attributes": {
"url": "https://example.execute-api.us-east-2.amazonaws.com/github/webhook",
"active": true
},
"default_permissions": {
"actions": "read",
"deployments": "write"
},
"default_events": [
"deployment_protection_rule"
]
}The GitHub App requires the minimum permissions to perform this action.
The webhook API is implemented in Rust and deployed as a Lambda via AWS SAM.
The GitHub App ID and webhook secret are stored in AWS SSM Parameter Store. The private key is stored in AWS Secrets Manager.
The webhook lifecycle is roughly:
- Receive an event from GitHub
- Validate the event is authentic using the webhook secret
- Discard non-
deployment_protection_ruleevents - Use the private key to mint a JWT
- Exchange the JWT for a GitHub access token (
POST /app/installations/{id}/access_tokens) - Extract the workflow run id from the event
- Validate that the workflow run comes from the expected workflow file (
GET /repos/{owner}/{repo}/actions/runs/{run_id}) - Read the workflow jobs for the run (
GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs) - Find the release gate job and check that it succeeded
- Approve or deny the deployment (
POST /repos/{owner}/{repo}/actions/runs/{run_id}/deployment_protection_rule) - Return an HTTP 204