Bypass Controls¶
Protection rules block bad changes. They also block emergency fixes. Bypass controls provide escape hatches without creating permanent security gaps.
Bypasses Without Controls are Permanent Gaps
Disabling protection without approval, time limits, and audit trails converts temporary exceptions into permanent vulnerabilities. Formalize the bypass process.
Ad-hoc bypasses never get restored. Formalized bypasses auto-restore on schedule.
What are Bypass Controls?¶
Bypass control: Temporary disablement of branch protection with formal approval, time restriction, and automatic restoration.
Without bypass controls: Developer disables protection for hotfix. Protection never re-enabled. Repository permanently vulnerable. No record of who disabled or why. Compliance violation discovered in audit.
With bypass controls: Developer requests bypass via GitHub Issue. Platform team approves with 2-hour time box. Workflow disables protection and logs action. Timer auto-restores protection after 2 hours. Full audit trail for compliance.
Bypass Scenarios¶
Emergency Hotfix¶
Production down. Critical fix needed. CI pipeline broken. Status checks cannot pass.
Traditional bypass: Admin force-pushes, bypasses reviews, no audit trail.
Controlled bypass: Temporary status check bypass with approval and restoration.
Planned Maintenance¶
Migration requires force-push to rewrite history. Temporary linear history bypass needed.
Traditional bypass: Disable all protection, run migration, hope someone re-enables.
Controlled bypass: Time-boxed bypass for specific protection rule with scheduled restoration.
Repository Initialization¶
New repository needs initial commits before CI configured. Status checks not yet defined.
Traditional bypass: No protection until "later". Later never comes.
Controlled bypass: Temporary bypass during setup phase with automatic tier application after 24 hours.
Approval Workflows¶
GitHub Issues Approval Pattern¶
Request bypass via GitHub Issue. Require approval from security team.
# .github/workflows/bypass-request.yml
name: Protection Bypass Request
on:
issues:
types: [labeled]
jobs:
approve-bypass:
if: contains(github.event.issue.labels.*.name, 'bypass-approved')
runs-on: ubuntu-latest
steps:
- name: Parse bypass request
id: parse
run: |
REPO=$(echo "${{ github.event.issue.body }}" | grep "Repository:" | cut -d: -f2 | xargs)
DURATION=$(echo "${{ github.event.issue.body }}" | grep "Duration:" | cut -d: -f2 | xargs)
echo "repo=${REPO}" >> $GITHUB_OUTPUT
echo "duration=${DURATION}" >> $GITHUB_OUTPUT
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.ENFORCEMENT_APP_ID }}
private-key: ${{ secrets.ENFORCEMENT_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Backup and disable protection
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
REPO="${{ steps.parse.outputs.repo }}"
DEFAULT_BRANCH=$(gh api "repos/${REPO}" --jq '.default_branch')
# Backup current configuration
gh api "repos/${REPO}/branches/${DEFAULT_BRANCH}/protection" > backup.json
# Disable protection
gh api --method DELETE "repos/${REPO}/branches/${DEFAULT_BRANCH}/protection"
- name: Log and schedule restoration
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
# Log bypass action
echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"repo\":\"${{ steps.parse.outputs.repo }}\",\"requestor\":\"${{ github.event.issue.user.login }}\",\"approver\":\"${{ github.event.sender.login }}\",\"duration\":\"${{ steps.parse.outputs.duration }}\"}" >> bypass-audit.log
# Calculate delay and schedule restoration
DURATION="${{ steps.parse.outputs.duration }}"
SECONDS=$(echo "${DURATION}" | sed 's/h/*3600/; s/m/*60/' | bc)
gh workflow run restore-protection.yml \
-f repository="${{ steps.parse.outputs.repo }}" \
-f backup_file="backup.json" \
-f delay="${SECONDS}"
gh issue comment ${{ github.event.issue.number }} --body \
"✅ Bypass approved. Protection will auto-restore in ${{ steps.parse.outputs.duration }}."
Issue template:
**Repository:** org/repo-name
**Duration:** 2h
**Reason:** Emergency hotfix for production outage
**Justification:** Why bypass is necessary
External Approval System Integration¶
Integrate with ServiceNow, PagerDuty, or Jira for approval tracking.
#!/usr/bin/env python3
import requests, sys, os
def check_servicenow_approval(ticket_id):
url = f"https://instance.service-now.com/api/now/table/change_request/{ticket_id}"
response = requests.get(url, headers={"Authorization": f"Bearer {os.environ['SERVICENOW_TOKEN']}"})
ticket = response.json()["result"]
return ticket["approval"] == "approved" and ticket["state"] == "implement"
if __name__ == "__main__":
sys.exit(0 if check_servicenow_approval(sys.argv[1]) else 1)
Workflow integration: - run: python3 check-approval.py ${{ github.event.inputs.ticket_id }}
Time-Boxing Patterns¶
Fixed Duration Bypass¶
Bypass expires after specified duration. Protection auto-restores.
# .github/workflows/restore-protection.yml
name: Restore Branch Protection
on:
workflow_dispatch:
inputs:
repository:
required: true
backup_file:
required: true
delay:
required: true
jobs:
restore:
runs-on: ubuntu-latest
steps:
- name: Wait for bypass duration
run: sleep ${{ github.event.inputs.delay }}
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.ENFORCEMENT_APP_ID }}
private-key: ${{ secrets.ENFORCEMENT_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: Restore and verify protection
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
REPO="${{ github.event.inputs.repository }}"
DEFAULT_BRANCH=$(gh api "repos/${REPO}" --jq '.default_branch')
# Restore from backup
gh api --method PUT "repos/${REPO}/branches/${DEFAULT_BRANCH}/protection" \
--input ${{ github.event.inputs.backup_file }}
# Verify restoration
if gh api "repos/${REPO}/branches/${DEFAULT_BRANCH}/protection" &>/dev/null; then
echo "✅ Protection successfully restored"
else
echo "❌ Protection restoration failed"
exit 1
fi
# Log restoration
echo "{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"repo\":\"${REPO}\",\"action\":\"restore\"}" >> bypass-audit.log
Scheduled Restoration Check¶
Periodic verification that expired bypasses were restored.
#!/bin/bash
# verify-bypass-restoration.sh
jq -c 'select(.action != "restore")' bypass-audit.log | while read bypass; do
TIMESTAMP=$(echo "${bypass}" | jq -r '.timestamp')
DURATION=$(echo "${bypass}" | jq -r '.duration')
REPO=$(echo "${bypass}" | jq -r '.repository')
BYPASS_TIME=$(date -d "${TIMESTAMP}" +%s)
EXPIRY_TIME=$((BYPASS_TIME + $(echo "${DURATION}" | sed 's/h/*3600/; s/m/*60/' | bc)))
if [[ $(date +%s) -gt ${EXPIRY_TIME} ]]; then
DEFAULT_BRANCH=$(gh api "repos/${REPO}" --jq '.default_branch')
if gh api "repos/${REPO}/branches/${DEFAULT_BRANCH}/protection" &>/dev/null; then
echo "✅ ${REPO}: Protection confirmed active"
else
echo "❌ CRITICAL: ${REPO} protection still disabled after expiry"
fi
fi
done
Audit Trail¶
Comprehensive Logging¶
Log every bypass action for compliance. Capture requestor, approver, reason, duration, backup location, timestamps, and compliance framework mapping.
{"bypass_id": "bypass-20260102-001", "timestamp": "2026-01-02T15:30:00Z",
"repository": "org/api-service", "requestor": "alice", "approver": "security-team-bob",
"reason": "Emergency hotfix for CVE-2026-1234", "duration_granted": "2h",
"disabled_at": "2026-01-02T15:30:15Z", "restored_at": "2026-01-02T17:30:22Z",
"compliance_framework": ["SOC2-CC6.1", "ISO27001-A.14.2.5"]}
Evidence Storage¶
Store bypass evidence for compliance audits with 7-year retention.
- name: Upload bypass evidence to S3
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT:role/ComplianceEvidence
- run: |
aws s3 cp "bypass-${BYPASS_ID}.json" \
"s3://compliance-evidence/bypass-controls/$(date +%Y/%m)/bypass-${BYPASS_ID}.json" \
--metadata "retention=7years"
Compliance Reporting¶
Generate bypass control report for auditors.
#!/usr/bin/env python3
# generate-bypass-report.py
from datetime import datetime
def generate_bypass_report(audit_log, start_date, end_date):
"""Generate bypass control compliance report."""
bypasses = load_bypasses(audit_log, start_date, end_date)
report = {"report_type": "Bypass Controls Audit",
"period": {"start": start_date.isoformat(), "end": end_date.isoformat()},
"summary": {"total": len(bypasses), "approved": 0, "restored": 0},
"violations": []}
for bypass in bypasses:
if not bypass.get("approver"):
report["violations"].append({"bypass_id": bypass["bypass_id"], "violation": "No approver", "severity": "critical"})
else:
report["summary"]["approved"] += 1
if not bypass.get("restored_at"):
report["violations"].append({"bypass_id": bypass["bypass_id"], "violation": "Not restored", "severity": "critical"})
else:
report["summary"]["restored"] += 1
# Check duration exceeded
disabled = datetime.fromisoformat(bypass["disabled_at"])
restored = datetime.fromisoformat(bypass["restored_at"])
granted = parse_duration(bypass.get("duration_granted", "0h"))
actual = (restored - disabled).total_seconds()
if actual > granted * 1.1:
report["violations"].append({"bypass_id": bypass["bypass_id"], "violation": f"Exceeded duration", "severity": "medium"})
total = report["summary"]["total"]
report["summary"]["compliance_percentage"] = ((total - len(report["violations"])) / total * 100) if total > 0 else 100
return report
Best Practices¶
1. Require approval for all bypasses: No self-service bypass. Security or platform team approval mandatory.
2. Default to shortest viable duration: Request 2 hours for hotfix, not 24 hours. Tightest time-box reduces risk window.
3. Backup before disabling: Store current protection configuration. Restoration fails without backup.
4. Verify restoration automatically: Scheduled job confirms expired bypasses restored. Alert on failures.
5. Log to immutable storage: S3 Object Lock. Append-only log files. Auditors verify log integrity.
6. Generate compliance reports monthly: Proactive reporting identifies violations before audits. Fix gaps early.
Troubleshooting¶
Bypass request denied: Verify approver has required team membership. Check approval label applied correctly.
Restoration failed after expiry: Backup file missing or corrupted. Manually re-apply tier configuration from Security Tiers.
Protection restored before work completed: Duration too short. Request extension via new bypass issue. Never manually disable again.
Audit log shows missing restoration: Workflow failure. Check workflow run logs. Manually verify protection status and restore if needed.
See Troubleshooting for additional issues.
Related Patterns¶
- Emergency Access: Break-glass procedures for critical incidents
- Exception Management: Permanent and temporary exception patterns
- Security Tiers: Tier configurations for restoration
- Audit Evidence: Evidence collection and storage
- Drift Detection: Automated detection of unauthorized bypasses
- Compliance Reporting: Framework-specific bypass reporting
Next Steps¶
- Create bypass request issue template in
.github/ISSUE_TEMPLATE/ - Deploy bypass approval workflow to organization repository
- Implement restoration workflow with time-boxing
- Configure audit logging to immutable storage
- Schedule monthly bypass compliance report generation
Protection was absolute until it needed to flex. Bypass requests required approval. Durations were time-boxed. Restoration was automatic. Audit logs captured every action. Compliance reviewers found perfect controls. Zero unauthorized bypasses. The system enforced security while enabling agility.