Skip to content

Commit d3bcc83

Browse files
fix(security): enforce SSH-only git remotes estate-wide (standards#69) (#147)
## Summary - Wires token-in-URL detection into `governance-reusable.yml` (`security-policy` job) as a new **SSH-remote policy** step: scans every `.git/config` present in the checkout tree for `x-access-token:`, `gho_`, `ghp_`, `ghs_`, and `github_pat_` credentials embedded in HTTPS remote URLs; fails hard on any hit. All downstream repos that call the governance bundle inherit this check automatically via the single `uses:` delegation. - Updates `REMOTE-URL-POLICY.adoc` (→ v1.1.0): records the 2026-05-19 recursive audit (1 offender found and remediated locally), upgrades the detection one-liner to recurse all `.git` trees rather than just `~/dev/*/`, and expands the Enforcement section with the CI gate reference, a recommended pre-push hook, and provisioning guidance. ## Audit result (2026-05-19) Full recursive sweep of ~130 `.git/config` files under `/home/hyperpolymath/dev` (repos, scratch, tools, worktrees, audit clones): | Repo | Remote | Masked URL | Action | |---|---|---|---| | `dev/repos/file-soup` | `origin` | `https://x-access-token:gho_****@github.com/hyperpolymath/file-soup` | Switched to `git@github.com:hyperpolymath/file-soup.git` (local .git/config only) | All other repos were already on SSH or plain HTTPS (no embedded credential). ## Outstanding owner action (EXTERNAL — not resolved by this PR) **Token rotation required.** The token `gho_****` (prefix `gho_1q9dB2…`) was found in `dev/repos/file-soup/.git/config`. A token that has appeared in a URL is considered compromised regardless of whether it was pushed. The owner must revoke/rotate it at: https://github.com/settings/tokens This is a manual browser action; it cannot be automated here. ## Test plan - [ ] CI passes on this PR (governance-reusable SSH-remote policy step shows ✅) - [ ] Manually verify `git remote -v` in `dev/repos/file-soup` shows SSH form - [ ] Owner rotates the exposed token at github.com/settings/tokens - [ ] Human review + merge (no `--admin`; Refs not Closes) Refs #69 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4044ece commit d3bcc83

2 files changed

Lines changed: 85 additions & 14 deletions

File tree

.github/workflows/governance-reusable.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,29 @@ jobs:
298298
exit 1
299299
fi
300300
echo "✅ Security policy check passed"
301+
- name: SSH-remote policy (token-in-URL detection)
302+
# Estate Remote-URL policy (standards#69 / REMOTE-URL-POLICY.adoc).
303+
# Scans every .git/config present in the checkout tree for token-in-URL
304+
# remotes. Fails hard so a compromised credential cannot silently reach
305+
# a PR or main-branch push.
306+
run: |
307+
found=0
308+
while IFS= read -r cfg; do
309+
if grep -qE "url[[:space:]]*=[[:space:]]*https://[^[:space:]]*(x-access-token:|:gho_|:ghp_|:ghs_|:github_pat_)" "$cfg" 2>/dev/null; then
310+
echo "❌ token-in-URL remote detected in: $cfg"
311+
grep -E "url[[:space:]]*=" "$cfg" \
312+
| sed 's/\(x-access-token:\|gho_\|ghp_\|ghs_\|github_pat_\)[^@]*/\1****/g'
313+
found=$((found + 1))
314+
fi
315+
done < <(find . -name "config" -path "*/.git/config" 2>/dev/null)
316+
if [ "$found" -gt 0 ]; then
317+
echo ""
318+
echo "Remediation: git remote set-url <name> git@github.com:<org>/<repo>.git"
319+
echo "Policy: REMOTE-URL-POLICY.adoc — SSH-only remotes; no PAT/token in URL ever."
320+
exit 1
321+
fi
322+
echo "✅ SSH-remote policy: no token-in-URL remotes detected"
323+
301324
- name: Tooling version integrity
302325
# Estate Tooling Version Integrity policy (root cause: burble#39).
303326
# Inline + dependency-free so it runs in any caller repo.

REMOTE-URL-POLICY.adoc

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
33
= Hyperpolymath Git Remote URL Policy (SSH-only)
44
Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
5-
:revnumber: 1.0.0
6-
:revdate: 2026-05-17
5+
:revnumber: 1.1.0
6+
:revdate: 2026-05-19
77
:toc:
88
:toc-placement: preamble
99

@@ -55,24 +55,43 @@ written into a remote URL, a tracked file, or a commit.
5555
[discrete]
5656
=== 3. Audit
5757

58-
Run estate-wide before any sync:
58+
Run estate-wide before any sync (recurses all `.git` trees):
5959

6060
[source,bash]
6161
----
62-
for d in ~/dev/*/; do
63-
[ -d "$d/.git" ] || continue
64-
u=$(git -C "$d" remote get-url origin 2>/dev/null)
65-
case "$u" in
66-
*x-access-token*|*ghp_*|*gho_*|*ghs_*|*github_pat_*|https://*:*@*)
67-
echo "TOKEN-IN-URL: $d" ;;
68-
esac
69-
done
62+
find ~/dev -name "config" -path "*/.git/config" \
63+
| xargs grep -l \
64+
"x-access-token\|:gho_\|:ghp_\|:ghs_\|:github_pat_\|://[^@]*:[^@]*@" \
65+
2>/dev/null \
66+
| while IFS= read -r cfg; do
67+
dir="${cfg%/.git/config}"
68+
echo "TOKEN-IN-URL: $dir"
69+
grep -E "url\s*=" "$cfg" | sed 's/\(x-access-token:\|gho_\|ghp_\|ghs_\|github_pat_\)[^@]*/\1****/g'
70+
done
7071
----
7172

7273
Audit of `2026-05-17`: 20 local `~/dev` clones scanned, *0*
7374
token-in-URL remotes (the exposed `repos/ci` clone was scrubbed to
7475
SSH in the originating session).
7576

77+
Audit of `2026-05-19` (standards#69): full recursive sweep of ~130
78+
`.git/config` files across `/home/hyperpolymath/dev` (repos, scratch,
79+
tools, worktrees, audit clones). *1* offender found and remediated:
80+
81+
[cols="1,1,1,1"]
82+
|===
83+
|Repo path |Remote |Offending URL (masked) |Remediated to
84+
85+
|`dev/repos/file-soup`
86+
|`origin`
87+
|`https://x-access-token:gho_****@github.com/hyperpolymath/file-soup`
88+
|`git@github.com:hyperpolymath/file-soup.git`
89+
|===
90+
91+
The token `gho_****` (prefix `gho_1q9dB2…`) that appeared in
92+
`file-soup/.git/config` is considered compromised and MUST be rotated
93+
— see <<_remediation_of_an_exposed_token>>.
94+
7695
[discrete]
7796
=== 4. Remediation of an exposed token
7897

@@ -85,6 +104,35 @@ one remaining open item under standards#69.
85104

86105
== Enforcement
87106

88-
`gitbot` rejects any push whose `.git/config` (when present in tree) or
89-
remediation script reintroduces a token-in-URL remote. New clones in
90-
provisioning scripts use the SSH form by default.
107+
=== CI gate (estate-wide, automated)
108+
109+
The `security-policy` job in
110+
`.github/workflows/governance-reusable.yml` now includes a
111+
`SSH-remote policy` step. It scans every `.git/config` that is
112+
checked-in or present in the working tree for the token-in-URL
113+
patterns and fails the PR gate if any are found. Downstream repos
114+
inherit this check automatically via the single `uses:` delegation.
115+
116+
=== Local pre-push hook (recommended)
117+
118+
Add the following to `.git/hooks/pre-push` (or the estate hook
119+
installer) to catch tokens before they reach CI:
120+
121+
[source,bash]
122+
----
123+
#!/usr/bin/env bash
124+
# hypatia:ignore cicd_rules/banned_language_file
125+
# pre-push hook: reject token-in-URL remotes
126+
if git -C . config --get-all remote.origin.url \
127+
| grep -qE "x-access-token:|:gho_|:ghp_|:ghs_|:github_pat_|://[^@]+:[^@]+@"; then
128+
echo "ERROR: token-in-URL remote detected — see REMOTE-URL-POLICY.adoc"
129+
exit 1
130+
fi
131+
----
132+
133+
=== Provisioning
134+
135+
New clones created by provisioning scripts MUST use the SSH form. The
136+
`gitbot` fleet creates remotes with `git@github.com:hyperpolymath/…`
137+
by default; HTTPS-with-credentials is explicitly blocked in
138+
`gitbot-fleet` clone templates.

0 commit comments

Comments
 (0)