GitHub Enterprise Organisation-Level Security Hardening

GitHub Enterprise Organisation-Level Security Hardening

Problem

GitHub repository-level security settings — branch protection rules, required reviewers, secret scanning — are well-documented and broadly adopted. Organisation-level settings receive far less attention despite controlling the security posture of every repository in the organisation simultaneously.

Organisation-level misconfigurations are governance failures that affect every repository, every developer, and every deployment. A single poorly configured org setting can undermine all per-repository hardening:

Unforced SSO. GitHub Enterprise allows organisations to configure SAML SSO, but SSO enforcement is a separate option that must be explicitly enabled. Without enforcement, members can authenticate with personal accounts, bypass SSO conditional access policies, and access repositories using personal tokens that have no expiry and no MFA requirement. Many organisations have SSO configured but not enforced, meaning the security control is optional.

Unconstrained Actions policies. By default, GitHub Actions permits workflows to use any action from any repository on GitHub.com. Supply chain attacks against third-party actions are a documented and recurring threat. Org-level Actions policies can restrict which actions are permitted — limiting to verified creators, pinned SHAs, or a private fork registry — across all repositories simultaneously.

No audit log streaming. GitHub’s organisation audit log captures every admin action, member addition, secrets change, and repository operation. By default, this log is available only in the GitHub UI with a 90-day retention window. Without streaming to an external SIEM, audit log data is unavailable for incident response investigations that span more than 90 days, and real-time alerting on suspicious admin actions is not possible.

Uncontrolled fork behaviour. Public forks of private repositories can leak internal code if the repository is accidentally made public or if a fork is created before access is revoked. Org-level fork policies control whether forks can be created and where they can go.

No IP allowlist. GitHub.com can be configured with an organisation-level IP allowlist that restricts which source IPs may access the organisation’s resources. Without this, stolen credentials can be used from any IP address globally.

Weak default repository permissions. The organisation’s default repository permission (None/Read/Write/Admin for members) determines what new members can do before any explicit repository access is granted. Default Read or Write on all repositories is common and excessive.

These are not abstract risks. Several high-profile source code exfiltrations in 2023–2025 were enabled by combination of unforced SSO and default member permissions that allowed departing employees or compromised credentials to access repositories they had no legitimate need to reach.

Target systems: GitHub Enterprise Cloud (GHEC) organisations; GitHub Enterprise Server (GHES) ≥3.8; any organisation with >10 members or any sensitive code in private repositories.


Threat Model

Adversary 1 — Stolen developer token bypassing SSO. A developer’s personal access token (PAT) is leaked via a dotfile in a public repo. The org has SSO configured but not enforced. The attacker uses the PAT — which predates SSO enforcement — to clone all private repositories, enumerate secrets, and push malicious code.

Adversary 2 — Supply chain attack via unconstrained Actions. A popular GitHub Action in the marketplace is compromised. All repositories in the org that reference the action by tag (not SHA) are affected on the next workflow run. The action exfiltrates secrets to an external endpoint.

Adversary 3 — Admin account takeover via no IP restriction. An attacker phishes an org admin’s credentials. The admin has MFA, but the attacker also phishes the TOTP code. With no IP allowlist, the attacker logs in from an overseas IP and uses admin access to disable security settings, add a backdoor user, and export the audit log before leaving.

Adversary 4 — Insider exfiltration via default read permissions. A developer joins the organisation. By default, they have Read access to all repositories. They are assigned to one project but can browse all internal repositories, cloning proprietary code before their accounts are restricted to their team.

Without org hardening: all four attacks succeed. With org hardening: SSO enforcement blocks the PAT; Actions policies block the compromised action; IP allowlist blocks the overseas login; minimum default permissions limit the insider’s access.


Configuration / Implementation

Step 1 — Enforce SAML SSO

# Via GitHub API — enforce SSO for the organisation
gh api orgs/YOUR_ORG \
  --method PATCH \
  --field saml_sso_enforcement=true

# Verify
gh api orgs/YOUR_ORG --jq '.saml_sso_enabled, .saml_sso_enforcement'
# Expected: true, true

# Find members who have not linked their account to SSO
# (These accounts retain access after enforcement is turned on)
gh api orgs/YOUR_ORG/members \
  --paginate \
  --jq '.[] | select(.organization_role != null) | .login' | while read -r user; do
  linked=$(gh api orgs/YOUR_ORG/credential-authorizations 2>/dev/null | \
    jq -r --arg u "$user" '.[] | select(.login == $u) | .login')
  [[ -z "$linked" ]] && echo "NOT SSO-LINKED: $user"
done

Steps after enabling enforcement:

  1. Notify all members with 2-week lead time
  2. Identify members without linked SSO credentials (command above)
  3. Remove unlinked members or grant a temporary exemption period
  4. After enforcement: audit that all github_token OAuth app authorisations have the SSO scope

Step 2 — Configure organisation-wide Actions policies

# Set Actions policy: allow only verified creators and your own organisation
gh api orgs/YOUR_ORG/actions/permissions \
  --method PUT \
  --field 'enabled_repositories=all' \
  --field 'allowed_actions=selected'

# Configure which actions are allowed
gh api orgs/YOUR_ORG/actions/permissions/selected-actions \
  --method PUT \
  --field 'github_owned_actions=true' \
  --field 'verified_allowed_actions=true' \
  --field 'patterns_allowed=["your-org/*"]'

# Verify
gh api orgs/YOUR_ORG/actions/permissions/selected-actions

Via the GitHub UI (Settings → Actions → General):

  • Allow GitHub-owned actions: enabled
  • Allow actions created by Marketplace verified creators: enabled
  • Allow specified actions: add your org’s own actions (your-org/*)
  • Patterns: pin specific third-party actions by SHA digest

Enforce SHA pinning at the organisation level via a Kyverno-equivalent for GitHub (using GitHub’s custom Actions policy via rulesets):

# Create a repository ruleset that requires SHA pinning in workflow files
gh api orgs/YOUR_ORG/rulesets \
  --method POST \
  --field name="require-sha-pinned-actions" \
  --field target="repository" \
  --field enforcement="active" \
  --field conditions='{"ref_name":{"include":["refs/heads/main"],"exclude":[]}}' \
  --field rules='[{
    "type": "workflows",
    "parameters": {
      "workflows": [{"path": ".github/workflows/*.yml", "sha": "required"}]
    }
  }]'

Step 3 — Enable and stream audit logs

# Stream audit log to an S3 bucket (GHEC only)
gh api orgs/YOUR_ORG/audit-log-streaming \
  --method PUT \
  --field 'enabled=true' \
  --field 'vendor=s3' \
  --field 'url=arn:aws:s3:::your-github-audit-logs' \
  --field 'key_id=YOUR_KMS_KEY_ID'

# Or stream to Splunk
gh api orgs/YOUR_ORG/audit-log-streaming \
  --method PUT \
  --field 'enabled=true' \
  --field 'vendor=splunk' \
  --field 'url=https://splunk.internal:8088/services/collector' \
  --field 'token=YOUR_HEC_TOKEN'

Configure audit log alerting in your SIEM for critical organisation events:

# Key events to alert on immediately:
# - org.add_member (new member added — verify expected)
# - org.remove_member (member removed — verify expected)
# - org.update_member (role changed — verify expected)
# - repo.create (new repository — track sensitive code creation)
# - repo.destroy (repository deleted — potential data loss)
# - protected_branch.update_admin_enforced (branch protection weakened)
# - org.update_default_repository_permission (org-wide permission change)
# - secrets.* (secret creation/deletion/update)
# - audit_log_streaming.* (streaming config changes)
# - saml_sso.* (SSO config changes)

Step 4 — Configure IP allowlist

# Add your organisation's egress IPs to the GitHub IP allowlist
# This restricts API and web access to known corporate IPs

# Add a CIDR block
gh api orgs/YOUR_ORG/ip-allow-list-entries \
  --method POST \
  --field 'allow_list_value=203.0.113.0/24' \
  --field 'name="Corporate VPN egress"' \
  --field 'is_active=true'

# Add GitHub Actions runners' IPs (required if using GitHub-hosted runners)
# GitHub Actions uses GitHub's IP ranges — get the current list:
curl -s https://api.github.com/meta | jq -r '.actions[]' | while read -r cidr; do
  gh api orgs/YOUR_ORG/ip-allow-list-entries \
    --method POST \
    --field "allow_list_value=$cidr" \
    --field "name=GitHub Actions" \
    --field 'is_active=true'
done

# Enable the allowlist (warning: this will block all access from unlisted IPs)
gh api orgs/YOUR_ORG \
  --method PATCH \
  --field 'ip_allow_list_enabled_setting=enabled'

Before enabling: verify your entire developer population accesses GitHub from corporate IPs or via VPN. Enable in audit-only mode first:

# Audit mode — logs violations without blocking
gh api orgs/YOUR_ORG \
  --method PATCH \
  --field 'ip_allow_list_enabled_setting=audit_log_only'
# Review audit log for 1 week for unexpected source IPs
# Then switch to "enabled"

Step 5 — Set minimum default repository permissions

# Set default member permission to None (no implicit access to any repository)
gh api orgs/YOUR_ORG \
  --method PATCH \
  --field 'default_repository_permission=none'

# Verify
gh api orgs/YOUR_ORG --jq '.default_repository_permission'
# Expected: "none"

# Set members_can_create_repositories to false
# (repository creation should go through platform team)
gh api orgs/YOUR_ORG \
  --method PATCH \
  --field 'members_can_create_repositories=false' \
  --field 'members_can_create_private_repositories=false' \
  --field 'members_can_create_public_repositories=false'

Step 6 — Control fork behaviour

# Prevent members from forking private repositories to personal accounts
gh api orgs/YOUR_ORG \
  --method PATCH \
  --field 'members_can_fork_private_repositories=false'

# Use GitHub's fork policy controls for private repo network forks
# This prevents forks escaping outside the organisation

Step 7 — Audit org configuration weekly

#!/bin/bash
# audit-org-config.sh — weekly check of critical org settings

ORG=your-org

echo "=== GitHub Org Security Audit: $ORG ==="

# SSO enforcement
sso=$(gh api orgs/$ORG --jq '.saml_sso_enforcement')
echo "SSO enforcement: $sso $([ "$sso" = "true" ] && echo "✓" || echo "⚠ NOT ENFORCED")"

# Default repo permission
perm=$(gh api orgs/$ORG --jq '.default_repository_permission')
echo "Default repo permission: $perm $([ "$perm" = "none" ] && echo "✓" || echo "⚠ SHOULD BE NONE")"

# IP allowlist
ip_list=$(gh api orgs/$ORG --jq '.ip_allow_list_enabled_setting')
echo "IP allowlist: $ip_list"

# Actions policy
actions=$(gh api orgs/$ORG/actions/permissions --jq '.allowed_actions')
echo "Actions policy: $actions $([ "$actions" = "selected" ] && echo "✓" || echo "⚠ NOT RESTRICTED")"

# Audit log streaming
streaming=$(gh api orgs/$ORG/audit-log-streaming 2>/dev/null --jq '.enabled')
echo "Audit log streaming: $streaming $([ "$streaming" = "true" ] && echo "✓" || echo "⚠ NOT STREAMING")"

# Fork policy
forks=$(gh api orgs/$ORG --jq '.members_can_fork_private_repositories')
echo "Members can fork private repos: $forks $([ "$forks" = "false" ] && echo "✓" || echo "⚠")"

Expected Behaviour

Setting Before hardening After hardening
saml_sso_enforcement false (optional SSO) true
default_repository_permission read none
allowed_actions all selected (verified + org-owned)
ip_allow_list_enabled_setting disabled enabled
Audit log streaming Not configured Streaming to SIEM; 90+ day retention
members_can_fork_private_repositories true false

Verification:

gh api orgs/YOUR_ORG --jq '{
  sso_enforced: .saml_sso_enforcement,
  default_permission: .default_repository_permission,
  can_fork_private: .members_can_fork_private_repositories
}'
# Expected:
# {
#   "sso_enforced": true,
#   "default_permission": "none",
#   "can_fork_private": false
# }

Trade-offs

Aspect Benefit Cost Mitigation
SSO enforcement Blocks pre-SSO tokens; ties all access to corporate IdP Breaks automation using old PATs; requires migration of all bots to GitHub Apps or fine-grained PATs Audit all existing PATs before enforcement; migrate automated processes first; provide 2-week notice
default_repository_permission: none Least privilege by default Breaks existing workflows where members assumed implicit read access Use Teams to explicitly grant access; audit-first: enable and monitor for breakage before enforcing
IP allowlist Blocks stolen credential use from external IPs Breaks developers working from unexpected locations; blocks 3rd party integrations Enable in audit-only mode first; enumerate all integration IPs; require VPN for off-network access
Actions policy: selected only Blocks supply chain attacks via marketplace Requires maintaining an approved action list Use Renovate to manage action updates; pin to SHA; team submits PR to update approved list

Failure Modes

Failure Symptom Detection Recovery
SSO enforcement blocks automated service account CI/CD pipeline fails with 403; bot cannot authenticate Actions workflow fails; alert fires Migrate bot to GitHub App with fine-grained PAT; regenerate PAT with SSO authorisation
IP allowlist blocks new remote developer Developer cannot access GitHub from home; reports 403 Helpdesk ticket; IP allowlist audit log shows blocked IP Add developer’s VPN exit IP or require VPN; audit log shows which IP was blocked
Actions policy blocks a needed third-party action Workflow fails with “action not allowed” Workflow failure log shows policy block Add the action’s repo to the approved patterns; or fork to org namespace and use your-org/action-name@sha
Default permission change breaks team sharing Team members lose access to repos they previously had implicit read on Access denied reports; dev team escalation Map all repositories to explicit Team assignments before changing default permission