In unregulated environments, CI/CD is about velocity. Merge to main, tests pass, deploy to production. No human in the loop. Maximum throughput.
In regulated environments — government systems, defense platforms, FedRAMP-authorized applications — that velocity still matters, but every production deployment needs an auditable approval chain. Someone authorized needs to explicitly approve the change. That approval needs a timestamp, an identity, and a record that persists for years.
The naive approach kills velocity: route every deployment through a Change Advisory Board that meets Tuesdays. The engineered approach embeds approval gates directly in the pipeline — fast enough to deploy multiple times per day, rigorous enough to satisfy any auditor.
We build regulated pipelines for government systems. Here’s the architecture and the code.
Why Approval Gates Exist in Regulated Systems
Approval gates in CI/CD aren’t bureaucratic overhead — they implement specific compliance controls:
- NIST 800-53 CM-3 (Configuration Change Control) — requires approval of changes before implementation, with documentation of associated security impact
- NIST 800-171 3.4.3 — track, review, approve or disapprove, and log changes to organizational systems
- CMMC Level 2 CM.L2-3.4.3 — same control, CMMC framing
- FedRAMP Continuous Monitoring — requires documented change control processes with evidence of approval for system modifications
The auditor doesn’t care whether approval happens in a meeting room or a GitHub pull request. They care that:
- A qualified person reviewed the change
- That person explicitly approved it
- The approval is recorded with identity and timestamp
- The deployment didn’t proceed without approval
CI/CD approval gates satisfy all four requirements programmatically.
Architecture: Multi-Stage Approval Pipeline
The approval gate architecture separates the pipeline into stages, each with its own promotion criteria:
Commit → Build → Test → Security Scan → Dev Deploy (auto) →
→ Staging Deploy (auto) → Integration Test →
→ Production Approval Gate (manual) → Production Deploy →
→ Post-Deploy Verification → Audit Log The key insight: automation handles everything that doesn’t require human judgment. Security scanning, testing, and non-production deployments proceed automatically. The human gate exists only at the boundary where risk is highest — production deployment.
This design preserves velocity (commits flow through build/test/scan/dev within minutes) while satisfying compliance (production changes require documented approval).
GitHub Actions Implementation
GitHub Actions provides native environment protection rules that implement approval gates without custom tooling.
Environment Configuration
First, define protected environments in the repository settings. For a regulated pipeline, you need at minimum:
development— auto-deploy on merge to develop branchstaging— auto-deploy on merge to main branchproduction— requires manual approval from designated reviewers
Workflow with Approval Gates
name: Regulated Deployment Pipeline
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
build:
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.build.outputs.tag }}
steps:
- uses: actions/checkout@v4
- name: Build Application
id: build
run: |
TAG="${GITHUB_SHA::8}-$(date +%Y%m%d%H%M%S)"
docker build -t app:${TAG} .
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- name: Upload Build Artifact
uses: actions/upload-artifact@v4
with:
name: build-${{ steps.build.outputs.tag }}
path: ./dist
security-scan:
needs: build
runs-on: ubuntu-latest
steps:
- name: Container Security Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: app:${{ needs.build.outputs.image_tag }}
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH
exit-code: 1
- name: Upload Scan Results
if: always()
uses: actions/upload-artifact@v4
with:
name: security-scan-${{ needs.build.outputs.image_tag }}
path: trivy-results.sarif
deploy-staging:
needs: [build, security-scan]
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: us-west-2
- name: Deploy to Staging
run: |
aws ecs update-service \
--cluster staging \
--service app \
--force-new-deployment
integration-test:
needs: deploy-staging
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Integration Tests
run: npm run test:integration
env:
API_URL: ${{ vars.STAGING_API_URL }}
deploy-production:
needs: [integration-test]
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_PROD_ROLE_ARN }}
aws-region: us-west-2
- name: Deploy to Production
id: deploy
run: |
DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
aws ecs update-service \
--cluster production \
--service app \
--force-new-deployment
echo "deploy_time=${DEPLOY_TIME}" >> "$GITHUB_OUTPUT"
- name: Record Deployment Audit Entry
run: |
aws dynamodb put-item \
--table-name deployment-audit-log \
--item '{
"PK": {"S": "DEPLOY#production"},
"SK": {"S": "'"${{ steps.deploy.outputs.deploy_time }}"'"},
"actor": {"S": "${{ github.actor }}"},
"commit": {"S": "${{ github.sha }}"},
"image_tag": {"S": "${{ needs.build.outputs.image_tag }}"},
"pipeline_run": {"S": "${{ github.run_id }}"},
"approval_environment": {"S": "production"},
"status": {"S": "deployed"}
}' The environment: production directive is the approval gate. GitHub Actions pauses the workflow at this job and waits for a designated reviewer to approve in the GitHub UI. The approval is recorded with the reviewer’s identity, timestamp, and optional review comment.
Environment Protection Rules
Configure the production environment with protection rules:
Repository Settings → Environments → production:
✓ Required reviewers: [security-lead, engineering-lead]
✓ Wait timer: 0 minutes (optional delay for change window compliance)
✓ Deployment branches: main only
✓ Custom deployment protection rules: (optional webhook for external approval systems) The required reviewers setting enforces separation of duties — the developer who wrote the code cannot self-approve the production deployment. This satisfies NIST 800-53 CM-5 (Access Restrictions for Change) without a separate approval system.
GitLab CI Implementation
GitLab CI implements approval gates through manual jobs and protected environments.
stages:
- build
- security
- deploy-staging
- test
- approval
- deploy-production
- audit
build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
security-scan:
stage: security
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy image --exit-code 1 --severity CRITICAL,HIGH
$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
artifacts:
reports:
container_scanning: trivy-report.json
deploy-staging:
stage: deploy-staging
environment:
name: staging
script:
- aws ecs update-service --cluster staging --service app
--force-new-deployment
integration-tests:
stage: test
script:
- npm run test:integration
production-approval:
stage: approval
environment:
name: production
script:
- echo "Production deployment approved by ${GITLAB_USER_LOGIN}
at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
when: manual
allow_failure: false
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy-production:
stage: deploy-production
environment:
name: production
needs:
- production-approval
script:
- aws ecs update-service --cluster production --service app
--force-new-deployment
rules:
- if: $CI_COMMIT_BRANCH == "main"
audit-record:
stage: audit
needs:
- deploy-production
script:
- |
aws dynamodb put-item \
--table-name deployment-audit-log \
--item "{
\"PK\": {\"S\": \"DEPLOY#production\"},
\"SK\": {\"S\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"},
\"actor\": {\"S\": \"${GITLAB_USER_LOGIN}\"},
\"commit\": {\"S\": \"${CI_COMMIT_SHA}\"},
\"pipeline_id\": {\"S\": \"${CI_PIPELINE_ID}\"},
\"status\": {\"S\": \"deployed\"}
}" The when: manual directive creates the approval gate. The job appears in the GitLab pipeline UI with a play button that only authorized users can click. GitLab’s protected environment feature restricts who can trigger manual jobs in specific environments.
Audit Trail Architecture
Approval gates are only useful if the audit trail is durable and queryable. We structure the audit trail with three layers:
Layer 1: Pipeline Platform Records
GitHub Actions and GitLab CI both maintain records of who approved deployments, when, and with what comments. These records are accessible through the platform’s API and UI. However, they’re tied to the platform — if you migrate CI/CD providers, the history doesn’t follow.
Layer 2: Deployment Audit Table
A DynamoDB table (or equivalent) stores a permanent record of every production deployment:
{
"PK": "DEPLOY#production",
"SK": "2026-03-07T14:30:00Z",
"actor": "engineer-1",
"approver": "security-lead",
"commit_sha": "a1b2c3d4",
"image_tag": "a1b2c3d4-20260307143000",
"pipeline_run_id": "12345678",
"security_scan_status": "passed",
"integration_test_status": "passed",
"change_description": "Update API rate limiting configuration",
"rollback_commit": "e5f6g7h8",
"ttl": 1868745600
} The TTL aligns with the data retention requirement (typically 3-7 years for government systems). This table is the authoritative deployment record that auditors query.
Layer 3: CloudTrail Integration
Every AWS API call made during deployment is recorded in CloudTrail. The session name from the OIDC credential assumption ties each API call back to the specific pipeline run and approver. This creates an end-to-end chain: code commit → approval → deployment → specific AWS resource changes.
Our observability patterns for regulated systems detail how these three audit layers integrate into a unified compliance evidence pipeline.
Advanced Patterns: Policy-as-Code Gates
Beyond manual approval, regulated pipelines benefit from automated policy gates that enforce organizational rules without human intervention.
Open Policy Agent (OPA) for Terraform
Before Terraform applies infrastructure changes, an OPA policy gate validates the plan against organizational policies:
# policy/terraform_gates.rego
package terraform.production
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_security_group_rule"
resource.change.after.cidr_blocks[_] == "0.0.0.0/0"
resource.change.after.type == "ingress"
msg := sprintf("Security group rule %s opens ingress to 0.0.0.0/0", [resource.address])
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
not resource.change.after.server_side_encryption_configuration
msg := sprintf("S3 bucket %s missing encryption configuration", [resource.address])
}
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_db_instance"
resource.change.after.publicly_accessible == true
msg := sprintf("RDS instance %s is publicly accessible", [resource.address])
} # Pipeline step
terraform-policy-check:
needs: [terraform-plan]
steps:
- name: Evaluate OPA Policies
run: |
terraform show -json tfplan > tfplan.json
opa eval --data policy/ --input tfplan.json \
"data.terraform.production.deny" --format pretty
VIOLATIONS=$(opa eval --data policy/ --input tfplan.json \
"data.terraform.production.deny" --format json | jq '.result[0].expressions[0].value | length')
if [ "$VIOLATIONS" -gt 0 ]; then
echo "Policy violations detected. Deployment blocked."
exit 1
fi Policy-as-code gates catch configuration drift and security misconfigurations before they reach the manual approval stage. The manual approver reviews a deployment that has already passed automated policy validation — reducing cognitive load and approval time.
SBOM Generation Gate
For software supply chain security requirements, a gate that generates and validates a Software Bill of Materials ensures every production deployment has a current, complete dependency inventory:
sbom-generation:
needs: [build]
steps:
- name: Generate SBOM
run: |
syft packages app:${{ needs.build.outputs.image_tag }} \
-o spdx-json > sbom.spdx.json
- name: Validate SBOM Against Policy
run: |
# Check for known banned licenses
BANNED=$(jq '[.packages[].licenseConcluded
| select(. == "GPL-3.0" or . == "AGPL-3.0")] | length' sbom.spdx.json)
if [ "$BANNED" -gt 0 ]; then
echo "Banned license detected in dependencies"
exit 1
fi
- name: Archive SBOM
uses: actions/upload-artifact@v4
with:
name: sbom-${{ needs.build.outputs.image_tag }}
path: sbom.spdx.json Balancing Velocity and Compliance
The goal isn’t to add gates — it’s to add the minimum gates that satisfy compliance while preserving deployment velocity. Over-gated pipelines create the same bottlenecks as the Change Advisory Boards they replaced.
Gate sparingly. One manual approval for production deployment is sufficient for most compliance frameworks. Additional manual gates (staging approval, architecture review approval) should only exist if a specific compliance control requires them.
Automate everything else. Security scanning, policy validation, integration testing, SBOM generation — these all run automatically. If a check can be codified, it should be a pipeline step, not a meeting agenda item.
Measure gate latency. Track the time between “pipeline ready for approval” and “approval granted.” If this consistently exceeds an hour during business hours, the approval process needs adjustment — either the reviewer pool is too small or the review burden is too high.
Provide context in the approval request. The approval notification should include the change summary, security scan results, test results, and diff link. An approver who has to hunt for context will delay approval.
Our approach to DevOps pipelines for government systems builds on these principles — compliance as a pipeline property, not a process overhead.
Frequently Asked Questions
Do CI/CD approval gates satisfy CMMC change management requirements?
Yes. CMMC Level 2 practice CM.L2-3.4.3 requires tracking, reviewing, approving/disapproving, and logging changes to organizational systems. CI/CD approval gates with documented reviewers, timestamped approvals, and persistent audit records satisfy this control. The key is ensuring the approval record is durable (not just in pipeline logs) and the reviewer has appropriate authorization.
How many approvers should be required for production deployments?
Most compliance frameworks require at least one authorized approver. We recommend two reviewers for production environments — one technical reviewer who validates the change content, and one security/compliance reviewer who validates the security scan results and compliance impact. GitHub Actions and GitLab both support multi-reviewer requirements on protected environments.
Can automated gates replace manual approval entirely?
For most government compliance frameworks, at least one human approval is required for production changes. Automated gates (security scans, policy validation, testing) reduce the risk and burden on the human approver but don’t eliminate the requirement. Some frameworks allow risk-based thresholds — low-risk changes may proceed with automated gates only, while high-risk changes require manual review.
How do we handle emergency deployments that can’t wait for normal approval?
Define an emergency change process in your change management plan: a designated subset of approvers (security lead, on-call engineering lead) can approve emergency deployments outside normal hours. The pipeline still enforces the approval gate — the emergency process is about who approves and how quickly, not about bypassing the gate. All emergency changes should be reviewed retrospectively within 24 hours.
What audit retention period do government systems require?
NIST 800-53 AU-11 requires audit records to be retained for an organization-defined period. In practice, most government systems require 3-7 years of deployment audit records. DynamoDB TTL or S3 lifecycle policies automate retention and expiration. Ensure the retention period is documented in your System Security Plan and aligned with the authorizing official’s requirements.
Discuss your project with Rutagon
Contact Us →