Files
git-hardening/docs/specs/2026-03-31-v0.2.0-expanded-hardening.md
Flo 8037cb7908 feat: v0.2.0 expanded hardening
Add gitleaks pre-commit hook, global gitignore, plaintext credential
detection, SSH key hygiene audit, 8 new git config settings, and
safe.directory wildcard detection. Fix ssh-keygen macOS compatibility,
FIDO2 detection via ioreg, and interactive test isolation.

Implements docs/specs/2026-03-31-v0.2.0-expanded-hardening.md

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-31 14:03:29 +02:00

331 lines
13 KiB
Markdown

# git-harden.sh v0.2.0 — Expanded Hardening Features
## Motivation
Gap analysis of two independent research reports (Claude Opus 4.6 and Gemini 3.1 Pro, March 2026) against the v0.1.0 script identified six feature areas where the script falls short of current best-practice recommendations. All additions follow the existing audit+apply pattern and require no new CLI flags.
### Source Reports
- `docs/research/Claude Opus 4.6 report.md`
- `docs/research/Gemini 3.1 Pro report.md`
## Scope
All changes are additive — no existing behavior changes. The v0.2.0 script will:
1. Install a gitleaks pre-commit hook
2. Create and configure a global gitignore
3. Detect plaintext credential files
4. Audit SSH key hygiene
5. Add 8 new git config settings
6. Detect dangerous `safe.directory = *` wildcard
Version bump: `0.1.0``0.2.0`.
---
## Feature 1: Pre-commit Hook Installation (gitleaks)
### Background
Both reports rank pre-commit secret scanning as the single most impactful workstation-level defense. The v0.1.0 script sets `core.hooksPath = ~/.config/git/hooks` but installs no hooks, leaving the directory empty.
### Audit Behavior
- Check if `~/.config/git/hooks/pre-commit` exists and is executable.
- If it exists, check whether it contains a `gitleaks` invocation (grep for `gitleaks`).
- `[OK]` if pre-commit hook exists and references gitleaks.
- `[WARN]` if pre-commit hook exists but does NOT reference gitleaks (user-managed hook — don't touch).
- `[MISS]` if no pre-commit hook exists.
### Apply Behavior
- Check if `gitleaks` is on `$PATH` via `command -v gitleaks`.
- If gitleaks is found and no pre-commit hook exists:
- Create `~/.config/git/hooks/pre-commit` with the following content:
```bash
#!/usr/bin/env bash
# Installed by git-harden.sh — global pre-commit secret scanning
# To bypass for a single commit: SKIP_GITLEAKS=1 git commit
set -o errexit
set -o nounset
set -o pipefail
if [ "${SKIP_GITLEAKS:-0}" = "1" ]; then
exit 0
fi
if command -v gitleaks >/dev/null 2>&1; then
gitleaks protect --staged --redact --verbose
fi
```
- `chmod +x` the hook.
- If gitleaks is NOT found:
- `[WARN]` with install instructions:
- macOS: `brew install gitleaks`
- Linux: `brew install gitleaks` or download from GitHub releases
- Still create the hook script (it guards with `command -v` so it's safe without gitleaks installed). Prompt the user before creating.
- If a pre-commit hook already exists (any content): warn and skip. Do not overwrite user-managed hooks.
### Bypass Mechanism
The `SKIP_GITLEAKS=1` environment variable allows a single commit to bypass the hook without `--no-verify` (which skips ALL hooks). This is documented in the hook script itself.
### Acceptance Criteria
- `--audit` reports status of pre-commit hook with gitleaks.
- Apply creates a working hook that blocks commits containing secrets.
- Existing user hooks are never overwritten.
- Hook is safe to install even if gitleaks is not yet installed.
---
## Feature 2: Global Gitignore
### Background
Both reports stress maintaining comprehensive `.gitignore` patterns for secrets. No amount of scanning catches what was never tracked in the first place.
### Audit Behavior
- Check if `core.excludesFile` is set in global git config.
- If not set: `[MISS]`.
- If set: check whether the referenced file contains key security patterns (`.env`, `*.pem`, `*.key`).
- `[OK]` if file exists and contains at least one security pattern.
- `[WARN]` if file exists but lacks security patterns: "Global gitignore at <path> lacks secret patterns (.env, *.pem, *.key). Consider adding them."
### Apply Behavior
- If `core.excludesFile` is not set:
- Create `~/.config/git/ignore` with the following patterns:
```gitignore
# === Security: secrets & credentials ===
.env
.env.*
!.env.example
*.pem
*.key
*.p12
*.pfx
*.jks
credentials.json
service-account*.json
.git-credentials
.netrc
.npmrc
.pypirc
# === Security: Terraform state (contains secrets) ===
*.tfstate
*.tfstate.backup
# === OS artifacts ===
.DS_Store
Thumbs.db
Desktop.ini
# === IDE artifacts ===
.idea/
.vscode/
*.swp
*.swo
*~
```
- Set `core.excludesFile = ~/.config/git/ignore` via `apply_git_setting`.
- If `core.excludesFile` is already set to a different path:
- Print `[INFO]` noting the existing path. Do not modify or overwrite.
- If the file lacks security patterns, print `[WARN]` with the missing patterns (informational only — no auto-append).
### Acceptance Criteria
- Audit reports whether `core.excludesFile` is configured.
- Audit checks existing gitignore files for security pattern coverage and warns if missing.
- Apply creates the file and sets the config only when nothing is configured.
- Existing configurations are never modified — warnings are informational.
- The `!.env.example` negation allows committing example env files.
---
## Feature 3: Plaintext Credential File Detection
### Background
Both reports warn that `git-credential-store` writes passwords to `~/.git-credentials` in plaintext. The Gemini report additionally calls out infostealer malware targeting this file. Adjacent credential files (.netrc, .npmrc, .pypirc) pose similar risks.
### Audit Behavior (audit-only, no apply action)
Check for existence and content of these files:
| File | Detection | Severity |
|------|-----------|----------|
| `~/.git-credentials` | File exists | `[WARN]` — "Plaintext git credentials. Migrate to credential helper (osxkeychain/libsecret) and delete this file." |
| `~/.netrc` | File exists | `[WARN]` — "Plaintext network credentials found. May contain git hosting tokens." |
| `~/.npmrc` | File contains `_authToken=` followed by a non-empty value (regex: `_authToken=.+`) | `[WARN]` — "npm registry token in plaintext. Use `npm config set` with env vars instead." |
| `~/.pypirc` | File contains `password` | `[WARN]` — "PyPI credentials in plaintext. Use keyring or token-based auth instead." |
### Apply Behavior
None. The script does not delete or modify user credential files. Warnings are informational only.
### Section Placement
New section: "Credential Hygiene" — placed after the existing "Credential Storage" audit.
### Acceptance Criteria
- Each detected file produces a specific, actionable warning.
- No false positives on files that don't contain credentials (e.g., .npmrc with only registry URL, no token).
- No files are modified or deleted.
---
## Feature 4: SSH Key Hygiene Audit
### Background
Both reports recommend ed25519 exclusively. The Claude report notes GitHub blocks legacy RSA/SHA-1 signatures. The Gemini report recommends banning DSA and ECDSA.
### Audit Behavior (audit-only, no apply action)
- Scan `~/.ssh/*.pub` files.
- Additionally, parse `IdentityFile` directives from `~/.ssh/config` (the v0.1.0 script already has this parsing logic in `detect_existing_keys`) and include any referenced `.pub` files not already covered by the glob.
- For each `.pub` file, read the first field to determine key type.
- Use `ssh-keygen -l -f <file>` to extract bit length for RSA keys.
- Report:
| Key Type | Verdict |
|----------|---------|
| `ssh-ed25519` | `[OK]` |
| `sk-ssh-ed25519@openssh.com` | `[OK]` |
| `ssh-rsa` with >= 2048 bits | `[WARN]` — "RSA key (%d bits). Consider migrating to ed25519." |
| `ssh-rsa` with < 2048 bits | `[WARN]` — "Weak RSA key (%d bits). Migrate to ed25519 immediately." |
| `ssh-dss` | `[WARN]` — "DSA key (deprecated). Migrate to ed25519." |
| `ecdsa-sha2-*` | `[WARN]` — "ECDSA key. Consider migrating to ed25519." |
| `sk-ecdsa-sha2-*` | `[OK]` — Hardware-backed ECDSA is acceptable. |
### Apply Behavior
None. Key migration is too risky to automate. Warnings are informational.
### Section Placement
New section: "SSH Key Hygiene" — placed after the existing "SSH Configuration" audit.
### Acceptance Criteria
- All `.pub` files in `~/.ssh/` are scanned and classified.
- RSA bit length is correctly extracted.
- No false warnings on ed25519 or ed25519-sk keys.
- No keys are modified or deleted.
---
## Feature 5: Additional Git Config Settings
### Background
Eight new settings recommended by one or both reports that the v0.1.0 script does not audit or apply.
### Settings
| Setting | Value | Rationale | Report Source | Section |
|---------|-------|-----------|---------------|---------|
| `user.useConfigOnly` | `true` | Prevent commits without explicit identity — forces user.name/email to be set, blocking accidental commits under system defaults | Claude | New: "Identity" |
| `gc.reflogExpire` | `180.days` | Extend reflog retention for forensic readiness (default 90 days) | Claude | New: "Forensic Readiness" |
| `gc.reflogExpireUnreachable` | `90.days` | Extend unreachable reflog retention (default 30 days) | Claude | New: "Forensic Readiness" |
| `transfer.bundleURI` | `false` | Disable bundle URI fetching — reduces attack surface | Claude | Existing: "Object Integrity" |
| `protocol.version` | `2` | Wire protocol v2 — better performance, reduced attack surface from reference advertisements | Gemini | Existing: "Protocol Restrictions" |
| `init.defaultBranch` | `main` | Modern default branch name | Claude | New: "Defaults" |
| `core.symlinks` | `false` | Prevent symlink-based attacks (relevant to CVE-2024-32002). **Interactive-only**: prompt with default=yes, but skip in `-y` mode (may break symlink-dependent workflows like Node.js monorepos) | Claude | Existing: "Filesystem Protection" |
| `fetch.prune` | `true` | Auto-prune stale remote-tracking refs on fetch | Claude | Existing: "Object Integrity" |
### Audit & Apply Behavior
Seven of eight follow the existing `audit_git_setting` / `apply_git_setting` pattern. `core.symlinks` is the exception:
- **Audit**: reports current value like all other settings.
- **Interactive mode**: prompts with default=yes ("Disable symlinks to prevent symlink-based attacks (CVE-2024-32002)? Note: this may break projects that use symlinks, e.g. Node.js monorepos. [Y/n]").
- **`-y` mode**: skips `core.symlinks` entirely (does not auto-apply). This is because disabling symlinks can silently break real workflows, and `-y` mode should not cause unexpected breakage.
### Acceptance Criteria
- All 8 settings appear in audit output under their respective sections.
- All 8 settings are applied (with prompt or auto) in apply mode.
- Existing tests updated to cover new settings.
---
## Feature 6: `safe.directory` Wildcard Detection
### Background
The Claude report explicitly warns: "Never set `safe.directory = *`." This wildcard completely disables the ownership safety check introduced in Git 2.35.2 (CVE-2022-24765), allowing any user on a shared system to exploit a planted `.git` directory.
### Audit Behavior
- Run `git config --global --get-all safe.directory` and check if any value is `*`.
- `[WARN]` if `*` is found: "safe.directory = * disables ownership checks (CVE-2022-24765). Remove this setting."
- No output if `*` is not found (this is not a setting we apply — absence of `*` is the correct state).
### Apply Behavior
- If `*` is detected, prompt the user: "Remove dangerous safe.directory = * setting?"
- If accepted, run `git config --global --unset safe.directory '*'` (note: must handle the case where multiple values exist — use `--unset-all` if needed, but only for the `*` value).
### Section Placement
Added to existing "Repository Safety" section.
### Acceptance Criteria
- Wildcard detected and warned about in audit mode.
- Apply mode offers to remove it.
- Non-wildcard `safe.directory` entries are not affected.
---
## Audit Section Order (v0.2.0)
Updated ordering with new sections integrated:
1. Identity (`user.useConfigOnly`)
2. Object Integrity (existing + `transfer.bundleURI`, `fetch.prune`)
3. Protocol Restrictions (existing + `protocol.version`)
4. Filesystem Protection (existing + `core.symlinks`)
5. Hook Control (existing)
6. **Pre-commit Hook** (new — gitleaks)
7. Repository Safety (existing + `safe.directory` wildcard detection)
8. Pull/Merge Hardening (existing)
9. Transport Security (existing)
10. Credential Storage (existing)
11. **Credential Hygiene** (new — plaintext file detection)
12. **Global Gitignore** (new)
13. **Defaults** (new — `init.defaultBranch`)
14. **Forensic Readiness** (new — reflog retention)
15. Visibility (existing)
16. Signing Configuration (existing)
17. SSH Configuration (existing)
18. **SSH Key Hygiene** (new)
---
## Non-Goals
- Package manager integration (no `brew install` or `apt install`).
- Modifying or deleting user files (credential files, SSH keys).
- Repository-level hardening (branch protection, CODEOWNERS — these remain in admin recommendations).
- CI/CD pipeline configuration.
- GPG signing support (the script remains SSH-signing focused).
## Compatibility
Same as v0.1.0: Bash 3.2+, macOS and Linux. No new dependencies. Gitleaks is optional — the hook is safe without it.
## Testing
- Extend existing BATS test suite to cover all new audit checks and apply actions.
- Add container test cases for gitleaks hook installation (with and without gitleaks present).
- Test `safe.directory = *` detection and removal.
- Test credential file detection with mock files.
- Test SSH key hygiene with various key types.