Files
git-hardening/.design/git-harden.md
2026-03-30 13:39:40 +02:00

8.8 KiB

Feature: git-harden.sh

Summary

A single-file bash script that audits and hardens a developer's global git configuration with security-focused defaults. It runs an audit-first flow (color-coded report of current state), then interactively applies recommended settings covering object integrity, protocol restrictions, filesystem protection, hook control, SSH signing with FIDO2 support, SSH transport hardening, and credential security. A -y flag auto-applies all defaults, and --audit exits after the report for CI use.

Requirements

  • REQ-1: The script must audit all git global config settings listed in the Architecture section and report each as [OK] (matches recommended), [WARN] (set to non-recommended value), or [MISS] (not configured), with color-coded output to stderr.
  • REQ-2: The script must apply hardening settings via git config --global in interactive mode (prompt per setting, default Y) or auto-apply mode (-y).
  • REQ-3: The script must back up the current global git config to ~/.config/git/pre-harden-backup-<timestamp>.txt before making any changes.
  • REQ-4: The script must detect the platform (macOS/Linux) and select the appropriate credential helper (osxkeychain on macOS, libsecret or cache on Linux).
  • REQ-5: The script must provide an SSH signing setup wizard with two tiers: software SSH key (ed25519) and FIDO2 hardware key (ed25519-sk), detecting existing keys and hardware automatically.
  • REQ-6: In -y mode, signing must only be enabled (commit.gpgsign, tag.gpgsign) if a valid signing key is detected and its public key file is readable. If no key exists, only non-breaking settings (gpg.format, gpg.ssh.allowedSignersFile) are configured.
  • REQ-7: The script must audit and optionally harden ~/.ssh/config with secure defaults (StrictHostKeyChecking accept-new, HashKnownHosts yes, IdentitiesOnly yes, AddKeysToAgent yes, modern PubkeyAcceptedAlgorithms).
  • REQ-8: The script must print admin/org-level recommendations (branch protection, vigilant mode, force-push policy, token hygiene) as informational output without applying changes.
  • REQ-9: The script must be compatible with Bash 3.2 (macOS default). No associative arrays, no mapfile/readarray, no ${var,,} case conversion, no declare -A.
  • REQ-10: The script must be idempotent — re-running it on an already-hardened system changes nothing.
  • REQ-11: Exit codes must be: 0 (all OK or changes applied), 1 (error), 2 (audit found issues).
  • REQ-12: The script must pass shellcheck with no errors or warnings.

Acceptance Criteria

  • AC-1: Running git-harden.sh --audit on a fresh git config prints a report with [MISS] for all 20+ hardening settings and exits with code 2.
  • AC-2: Running git-harden.sh -y on a fresh config applies all settings; a subsequent --audit exits with code 0 (all [OK]).
  • AC-3: Running git-harden.sh -y twice produces identical git config output (idempotent).
  • AC-4: On macOS, credential.helper is set to osxkeychain. On Linux with libsecret available, it uses the detected libsecret path; otherwise cache --timeout=3600.
  • AC-5: If credential.helper is currently store, the audit reports [WARN] and interactive mode offers to replace it.
  • AC-6: The signing wizard detects existing ~/.ssh/id_ed25519 and offers to use it as signing key.
  • AC-7: If a FIDO2 device is detected (via ykman info or fido2-token -L), the wizard offers Tier 2 (generate ed25519-sk key). The ssh-keygen stderr is not suppressed.
  • AC-8: In -y mode with no SSH key present, commit.gpgsign and tag.gpgsign are NOT set. A note is printed directing the user to run interactively.
  • AC-9: ~/.config/git/hooks/ directory is created if missing. core.hooksPath is set with literal ~ (not expanded).
  • AC-10: ~/.ssh/config is created with mode 600 (and ~/.ssh/ with mode 700) if they don't exist. SSH directives are only appended if not already present.
  • AC-11: The script runs without error on Bash 3.2.57 (macOS default) and Bash 5.x (Linux).
  • AC-12: shellcheck git-harden.sh produces zero errors and zero warnings.
  • AC-13: A config backup file is created at ~/.config/git/pre-harden-backup-<timestamp>.txt before any changes are made.
  • AC-14: protocol.allow is set to never; only https, ssh, and file (as user) are whitelisted. git:// and ext:// are explicitly blocked.
  • AC-15: If pull.rebase is currently set, the audit phase reports a [WARN] about the conflict with pull.ff=only.

Architecture

The script is a single file git-harden.sh at the repo root, using #!/usr/bin/env bash with strict mode (set -o errexit, set -o nounset, set -o pipefail, IFS=$'\n\t').

Internal structure (functions)

All variables inside functions use local. Global constants use readonly UPPER_CASE. The script follows the conventions in AGENTS.md (Shell Script Development Standards v2.0).

Entry point and argument parsing:

  • main() — orchestrates the full flow: preflight, audit, apply, recommendations
  • parse_args() — handles -y, --audit, --help flags
  • die() — fatal error handler (prints to stderr, exits 1)

Platform detection:

  • detect_platform() — sets PLATFORM to macos or linux via uname -s
  • check_dependencies() — verifies git >= 2.34.0, ssh-keygen present; detects optional tools (ykman, fido2-token, credential helpers)

Audit functions (read-only, no side effects):

  • audit_git_config() — checks each hardening setting against git config --global --get
  • audit_ssh_config() — checks ~/.ssh/config for recommended directives
  • audit_signing() — checks signing config and key availability
  • print_audit_report() — renders the color-coded report to stderr

Apply functions (write operations):

  • apply_git_config() — sets each non-OK setting via git config --global
  • apply_ssh_config() — appends missing directives to ~/.ssh/config
  • signing_wizard() — interactive signing setup (tier selection, key generation/detection)
  • detect_existing_keys() — scans ~/.ssh/ and ~/.ssh/config IdentityFile directives
  • detect_fido2_hardware() — checks ykman info and fido2-token -L
  • generate_ssh_key() — wraps ssh-keygen -t ed25519
  • generate_fido2_key() — wraps ssh-keygen -t ed25519-sk with touch prompt
  • setup_allowed_signers() — creates/updates ~/.config/git/allowed_signers
  • print_admin_recommendations() — prints org-level advice to stderr

Helpers:

  • prompt_yn() — prompt with configurable default, respects -y mode
  • print_ok(), print_warn(), print_miss() — color-coded status output to stderr

Git config settings applied

Object integrity: transfer.fsckObjects=true, fetch.fsckObjects=true, receive.fsckObjects=true

Protocol restrictions (default deny): protocol.allow=never, protocol.https.allow=always, protocol.ssh.allow=always, protocol.file.allow=user, protocol.git.allow=never, protocol.ext.allow=never

Filesystem protection: core.protectNTFS=true, core.protectHFS=true, core.fsmonitor=false

Hook control: core.hooksPath=~/.config/git/hooks

Repository safety: safe.bareRepository=explicit, submodule.recurse=false

Pull/merge hardening: pull.ff=only, merge.ff=only

Transport security: url."https://".insteadOf=http://, http.sslVerify=true

Credential storage: credential.helper (platform-detected)

Signing: gpg.format=ssh, user.signingkey (detected), commit.gpgsign=true, tag.gpgsign=true, tag.forceSignAnnotated=true, gpg.ssh.allowedSignersFile=~/.config/git/allowed_signers

Visibility: log.showSignature=true

Optional (interactive only): core.symlinks=false, merge.verifySignatures=true

SSH config directives appended to ~/.ssh/config

StrictHostKeyChecking accept-new, HashKnownHosts yes, IdentitiesOnly yes, AddKeysToAgent yes, PubkeyAcceptedAlgorithms ssh-ed25519,sk-ssh-ed25519@openssh.com,ecdsa-sha2-nistp256,sk-ecdsa-sha2-nistp256@openssh.com

Dependencies

Required: git >= 2.34.0, ssh-keygen Optional: ykman or fido2-token (FIDO2 detection), OS keychain (osxkeychain on macOS, libsecret on Linux)

Open Questions

No unresolved questions remain. All design decisions have been validated through brainstorming, research, spec review, and external review.

Out of Scope

  • GPG signing support — SSH signing covers the same use cases with far less complexity
  • Server-side changes — the script only modifies the developer's local config
  • Undo/restore command — the script is idempotent; devs can manually unset any setting with git config --global --unset
  • Windows/WSL support
  • Per-repo config modification — global config only
  • jj support
  • Hook dispatcher scripts for projects using husky/lefthook/pre-commit — mentioned in admin recommendations but not implemented