Back to Blog
3 min read

GitHub Secret Scanning: Protecting Your Credentials

Accidentally committing secrets to your repository is one of the most common security mistakes. GitHub Secret Scanning helps detect and prevent exposed credentials before they become a problem.

How Secret Scanning Works

GitHub partners with service providers to detect over 100 types of secrets. When a secret is detected, both you and the provider are notified, allowing for immediate revocation.

Enabling Secret Scanning

# Enable via repository settings or API
name: Enable Secret Scanning
on:
  workflow_dispatch:

jobs:
  enable:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@v6
        with:
          github-token: ${{ secrets.ADMIN_TOKEN }}
          script: |
            await github.rest.repos.update({
              owner: context.repo.owner,
              repo: context.repo.repo,
              security_and_analysis: {
                secret_scanning: { status: 'enabled' },
                secret_scanning_push_protection: { status: 'enabled' }
              }
            });

Push Protection

Push protection blocks commits containing secrets before they reach the repository:

# When you try to push a commit with a secret:
$ git push origin main

remote: error: GH013: Repository rule violations found for refs/heads/main.
remote:
remote: - GITHUB PUSH PROTECTION
remote:   ——————————————————————————————————————————
remote:   Resolve the following violations before pushing again
remote:
remote:   - Push cannot contain secrets
remote:
remote:
remote:     —— Azure Storage Account Access Key ——————————
remote:     locations:
remote:       - commit: abc123
remote:         path: config/settings.json:15
remote:
remote:     To push, remove secret from commit(s) or follow this URL to allow the secret.
remote:     https://github.com/org/repo/security/secret-scanning/unblock-secret/...

Handling Secret Alerts

import requests
from datetime import datetime

class SecretScanningManager:
    def __init__(self, token, owner, repo):
        self.token = token
        self.owner = owner
        self.repo = repo
        self.base_url = f"https://api.github.com/repos/{owner}/{repo}"
        self.headers = {
            "Authorization": f"Bearer {token}",
            "Accept": "application/vnd.github+json"
        }

    def get_alerts(self, state="open"):
        url = f"{self.base_url}/secret-scanning/alerts"
        response = requests.get(url, headers=self.headers, params={"state": state})
        return response.json()

    def resolve_alert(self, alert_number, resolution, comment=None):
        """
        Resolutions: false_positive, wont_fix, revoked, used_in_tests
        """
        url = f"{self.base_url}/secret-scanning/alerts/{alert_number}"
        data = {
            "state": "resolved",
            "resolution": resolution
        }
        if comment:
            data["resolution_comment"] = comment

        response = requests.patch(url, headers=self.headers, json=data)
        return response.json()

    def get_alert_locations(self, alert_number):
        url = f"{self.base_url}/secret-scanning/alerts/{alert_number}/locations"
        response = requests.get(url, headers=self.headers)
        return response.json()

    def generate_report(self):
        alerts = self.get_alerts()
        report = {
            "generated_at": datetime.now().isoformat(),
            "total_alerts": len(alerts),
            "by_type": {},
            "details": []
        }

        for alert in alerts:
            secret_type = alert["secret_type"]
            report["by_type"][secret_type] = report["by_type"].get(secret_type, 0) + 1

            locations = self.get_alert_locations(alert["number"])
            report["details"].append({
                "number": alert["number"],
                "type": secret_type,
                "created_at": alert["created_at"],
                "locations": [loc["details"]["path"] for loc in locations]
            })

        return report

# Usage
manager = SecretScanningManager(token, "myorg", "myrepo")
alerts = manager.get_alerts()
for alert in alerts:
    print(f"Alert #{alert['number']}: {alert['secret_type']}")

Custom Secret Patterns

Define organization-wide custom patterns:

# Organization-level custom pattern (via UI or API)
name: Internal API Key
pattern: 'MYORG_[A-Za-z0-9]{32}'
description: Internal service API keys
severity: high

Pre-commit Hook for Local Detection

#!/bin/bash
# .git/hooks/pre-commit

# Common secret patterns
patterns=(
    'AKIA[0-9A-Z]{16}'                          # AWS Access Key
    '[a-zA-Z0-9_-]*:[a-zA-Z0-9_-]*@.*\.blob\.core\.windows\.net'  # Azure Storage
    'ghp_[a-zA-Z0-9]{36}'                       # GitHub PAT
    'sk-[a-zA-Z0-9]{48}'                        # OpenAI API Key
)

for pattern in "${patterns[@]}"; do
    if git diff --cached | grep -qE "$pattern"; then
        echo "ERROR: Potential secret detected matching pattern: $pattern"
        echo "Please remove the secret before committing."
        exit 1
    fi
done

exit 0

Automation for Secret Rotation

import subprocess
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient

def rotate_exposed_secret(secret_name, vault_url):
    """Automatically rotate a secret that was exposed."""

    credential = DefaultAzureCredential()
    client = SecretClient(vault_url=vault_url, credential=credential)

    # Generate new secret
    new_value = subprocess.run(
        ["openssl", "rand", "-base64", "32"],
        capture_output=True, text=True
    ).stdout.strip()

    # Update in Key Vault
    client.set_secret(secret_name, new_value)

    # Log rotation
    print(f"Rotated secret: {secret_name}")

    return new_value

Secret scanning is a critical layer in your defense-in-depth security strategy.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.