Secret Scanning Integration¶
Prevention is good. Detection is essential. Assume secrets will leak. Build systems to catch them before damage spreads.
The Risk
Secrets leak through commits, workflow logs, artifacts, pull requests, and third-party integrations. Without automated scanning, credentials remain exposed for days or months before detection. By then, your infrastructure is already compromised.
What is Secret Scanning?¶
GitHub secret scanning automatically detects known secret formats in repositories, workflow logs, and commit history.
How It Works:
- GitHub scans commits, issues, pull requests, and workflow logs
- Pattern matching identifies known credential formats (API keys, tokens, certificates)
- Alerts sent to repository administrators and security team
- Optional push protection blocks commits containing secrets
- Partner notification for compromised service provider credentials
Coverage:
- Repository scanning: All commits, branches, and history
- Push protection: Block secret commits before they land
- Workflow logs: Scan job output for leaked credentials
- Pull requests: Scan fork contributions for secret exposure
- Partner patterns: 200+ service providers receive breach notifications
Enabling Secret Scanning¶
Secret scanning availability depends on repository visibility and GitHub plan.
Repository Settings¶
GitHub Advanced Security (GHAS) Required:
- Private/internal repositories: GHAS license required
- Public repositories: Free, enabled by default
Enable via Settings:
- Navigate to
Settings → Code security and analysis - Enable Secret scanning
- Enable Push protection (recommended)
- Enable Non-provider patterns for generic secrets
# .github/workflows/verify-security.yml
# Workflow to enforce security features are enabled
name: Verify Security Configuration
on:
schedule:
- cron: '0 8 * * 1' # Weekly Monday 8 AM
workflow_dispatch:
permissions:
contents: read
jobs:
check-scanning:
runs-on: ubuntu-latest
steps:
- name: Check secret scanning enabled
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const { data: repo } = await github.rest.repos.get({
owner: context.repo.owner,
repo: context.repo.repo
});
const required = [
{ setting: 'security_and_analysis.secret_scanning.status', name: 'Secret Scanning' },
{ setting: 'security_and_analysis.secret_scanning_push_protection.status', name: 'Push Protection' }
];
for (const check of required) {
const value = check.setting.split('.').reduce((o, k) => o?.[k], repo);
if (value !== 'enabled') {
core.setFailed(`${check.name} is not enabled (status: ${value})`);
} else {
core.info(`✓ ${check.name} enabled`);
}
}
Organization-Level Enablement¶
Enable secret scanning across all repositories in organization.
Organization Settings:
- Navigate to
Organization Settings → Code security and analysis - Enable Secret scanning for all repositories
- Enable Push protection organization-wide
- Configure Custom patterns for org-specific secrets
Enforcement via API:
#!/bin/bash
# enable-secret-scanning.sh
# Enable secret scanning and push protection for all org repos
ORG="your-org"
TOKEN="${GITHUB_TOKEN}"
# Get all repositories in organization
repos=$(gh api \
--paginate \
"/orgs/${ORG}/repos" \
--jq '.[].name')
for repo in $repos; do
echo "Enabling secret scanning for ${ORG}/${repo}..."
# Enable secret scanning
gh api \
--method PATCH \
"/repos/${ORG}/${repo}" \
-f security_and_analysis[secret_scanning][status]=enabled \
-f security_and_analysis[secret_scanning_push_protection][status]=enabled
echo "✓ ${repo} configured"
done
Best Practices:
- Enable organization-wide by default
- Require for all new repositories
- Audit compliance weekly
- Block repository creation without security features
Push Protection¶
Push protection blocks commits containing secrets before they reach GitHub.
How Push Protection Works¶
sequenceDiagram
%% Ghostty Hardcore Theme
participant Dev as Developer
participant Git as Git Client
participant GH as GitHub
participant Scan as Secret Scanner
Dev->>Git: git push origin main
Git->>GH: Push commit
GH->>Scan: Scan commit contents
alt Secret Detected
Scan-->>GH: Secret found (API key pattern)
GH-->>Git: ❌ Push rejected
Git-->>Dev: Error: secret detected<br/>Remove secret and retry
else No Secret
Scan-->>GH: No secrets found
GH-->>Git: ✓ Push accepted
Git-->>Dev: Push successful
end
Bypassing Push Protection¶
Developers can bypass push protection for false positives. Track bypasses for security review.
Bypass Process:
- Developer attempts push with secret
- GitHub blocks push with bypass option
- Developer provides justification (false positive, test credential, will revoke)
- Push allowed with bypass event logged
- Security team reviews bypass audit trail
Bypass Workflow:
# Developer pushes commit with secret
git push origin main
# > Error: secret detected in commit abc123
# > To bypass, visit: https://github.com/org/repo/security/bypass/abc123
# Developer bypasses with justification
# GitHub logs bypass event
# Security team reviews bypasses
gh api /repos/org/repo/secret-scanning/push-protection-bypasses
Monitor Bypasses:
# .github/workflows/monitor-bypasses.yml
# Alert security team when push protection bypassed
name: Monitor Push Protection Bypasses
on:
schedule:
- cron: '0 */4 * * *' # Every 4 hours
workflow_dispatch:
permissions:
contents: read
jobs:
check-bypasses:
runs-on: ubuntu-latest
steps:
- name: Get recent bypasses
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const bypasses = await github.paginate(
github.rest.secretScanning.listPushProtectionBypasses,
{
owner: context.repo.owner,
repo: context.repo.repo
}
);
const recent = bypasses.filter(b => {
const created = new Date(b.created_at);
const fourHoursAgo = new Date(Date.now() - 4 * 60 * 60 * 1000);
return created > fourHoursAgo;
});
if (recent.length > 0) {
core.warning(`${recent.length} push protection bypasses in last 4 hours`);
for (const bypass of recent) {
core.warning(`Bypass by ${bypass.pusher.login}: ${bypass.token_type}`);
}
// Trigger alert to security team (Slack, PagerDuty, etc.)
}
Custom Pattern Definitions¶
Define organization-specific secret patterns for internal credentials.
Creating Custom Patterns¶
Custom patterns use regular expressions to detect organization-specific secrets.
Pattern Format:
# Pattern components
(?i) # Case insensitive
\b # Word boundary
(internal_api_key|secret_key) # Secret identifier
[\s:=]+ # Separator
([a-f0-9]{64}) # Secret value pattern
\b # Word boundary
Organization-Level Pattern:
- Navigate to
Organization Settings → Code security → Secret scanning - Click New pattern
- Define pattern name and regular expression
- Test against sample secrets
- Enable for all or selected repositories
Example Custom Patterns:
| Secret Type | Pattern | Example Match |
|---|---|---|
| Internal API Key | (?i)\b(internal_api_key\s*[:=]\s*)([a-f0-9]{64})\b |
INTERNAL_API_KEY=a1b2c3d4... |
| Service Token | (?i)\bSVC_TOKEN_([A-Z0-9]{32})\b |
SVC_TOKEN_AB12CD34EF56GH78... |
| Database URL | (?i)postgresql://[^:]+:[^@]+@[^/]+/\w+ |
postgresql://user:pass@host/db |
| SSH Private Key | -----BEGIN (RSA\|OPENSSH) PRIVATE KEY----- |
-----BEGIN RSA PRIVATE KEY----- |
Pattern Best Practices¶
Effective Patterns:
- Use word boundaries
\bto avoid substring false positives - Include secret identifier context (e.g.,
API_KEY=) - Match actual secret format (length, character class)
- Test against real examples before deployment
- Document pattern purpose and maintenance owner
Avoid Common Mistakes:
- Too broad:
[a-z0-9]+(matches everything) - Too narrow:
PROD_KEY=abc123(only matches one value) - Missing boundaries:
password.*(matches variable names) - No context:
[a-f0-9]{64}(many false positives)
Test Pattern:
# Test custom pattern against sample file
echo "INTERNAL_API_KEY=a1b2c3d4e5f6..." > test-secret.txt
# GitHub CLI test (pattern must be created first)
gh secret-scanning list --repo org/repo
# Local regex test
grep -P '(?i)\b(internal_api_key\s*[:=]\s*)([a-f0-9]{64})\b' test-secret.txt
Secret Scanning Alerts¶
Alerts notify repository administrators when secrets are detected.
Alert Triage Workflow¶
flowchart TD
Alert["Secret Detected"] --> Verify["1. Verify Alert<br/>Real or False Positive?"]
Verify -->|Real Secret| Classify["2. Classify Severity<br/>Production or Test?"]
Verify -->|False Positive| Dismiss["Dismiss as<br/>False Positive"]
Classify -->|Production Credential| Critical["🔴 CRITICAL<br/>Immediate Response"]
Classify -->|Test/Dev Credential| Medium["🟡 MEDIUM<br/>Standard Response"]
Critical --> Revoke1["3a. Revoke credential<br/>(within 15 minutes)"]
Medium --> Revoke2["3b. Revoke credential<br/>(within 24 hours)"]
Revoke1 --> Rotate1["4a. Rotate credential<br/>Update GitHub secret"]
Revoke2 --> Rotate2["4b. Rotate credential<br/>Update GitHub secret"]
Rotate1 --> Clean1["5a. Remove from history<br/>BFG Repo-Cleaner"]
Rotate2 --> Clean2["5b. Remove from history<br/>Git filter-branch"]
Clean1 --> Document1["6. Document incident"]
Clean2 --> Document2["6. Document incident"]
%% Ghostty Hardcore Theme
style Alert fill:#66d9ef,color:#1b1d1e
style Critical fill:#f92572,color:#1b1d1e
style Medium fill:#e6db74,color:#1b1d1e
style Dismiss fill:#75715e,color:#f8f8f2