UNPKG

@sebastienrousseau/dotfiles

Version:

The Trusted Shell Platform — Universal dotfiles managed by Chezmoi. Features Bash & Zsh for macOS, Linux & WSL. Rust modern tooling & enterprise-grade security.

139 lines (108 loc) 5.99 kB
# Commit Signing — Policy & Setup Every commit that reaches `master` in this repository must carry a cryptographic signature that GitHub can verify. The policy is enforced in three independent layers, so a single bypass does not break the chain. This document explains the policy, walks through SSH and GPG setup, and lists the verification commands you can run locally before pushing. ## Why Closes [#853](https://github.com/sebastienrousseau/dotfiles/issues/853). Local hooks alone are bypassable — `git commit --no-verify`, `git push --no-verify`, or unsetting `DOTFILES_ALLOW_UNSKIPPED_PUSH` all let an unsigned commit reach a remote if GitHub-side enforcement is missing. The chain below removes every escape hatch. ## Enforcement layers 1. **Local commit hook** — `dot_config/git/hooks/executable_commit-msg` is deployed by chezmoi. It rejects an unsigned commit at the `commit-msg` stage on the developer machine. 2. **Local pre-push hook** — `scripts/git-hooks/pre-push` runs `git verify-commit` against every commit in the push range. A single unverified commit aborts the push. `--no-verify` skips this layer; the next two catch it. 3. **GitHub Rulesets** — `.github/rulesets/master.json` declares `required_signatures` on `refs/heads/master`. The rule is part of the repo so it's reproducible across forks. Apply with `gh ruleset import .github/rulesets/master.json`. 4. **`compliance-guard.yml` workflow** — runs on every PR targeting `master`. Walks the commit range with `git verify-commit` and marks unsigned commits in the PR summary; fails the workflow when `unsigned_count > 0`. A merge to `master` therefore requires (Ruleset accepts the push) AND (the workflow's signed-commit check passes) AND (the maintainer's push key is allowed). The protection holds even if a contributor's local hooks are missing or skipped. ## Setting up SSH signing (recommended) SSH signing is the 2026 default. It reuses your existing SSH key — no new key material, no GPG agent, no Kleopatra UI. GitHub has recognised SSH signatures since [2022](https://github.blog/changelog/2022-08-23-ssh-commit-verification-now-supported/). ```sh # 1. Tell git to sign with SSH. git config --global gpg.format ssh # 2. Point at the SSH key you want git to use. git config --global user.signingkey "$HOME/.ssh/id_ed25519.pub" # 3. Turn on auto-signing for every commit and tag. git config --global commit.gpgsign true git config --global tag.gpgsign true # 4. Tell GitHub which SSH key signs your commits. gh ssh-key add ~/.ssh/id_ed25519.pub --type signing --title "$(hostname) signing" # 5. (Optional) Populate the allowed_signers file so # `git log --show-signature` can verify locally. mkdir -p "$HOME/.config/git" echo "$(git config user.email) $(cat ~/.ssh/id_ed25519.pub)" \ > "$HOME/.config/git/allowed_signers" git config --global gpg.ssh.allowedSignersFile \ "$HOME/.config/git/allowed_signers" ``` After this, `git log --show-signature` shows `Good "git" signature for you@example.com with ED25519 key SHA256:…` on every new commit. ## Setting up GPG signing (legacy / for tag-signing mirrors that don't yet support SSH) ```sh # 1. Generate or import a key. ED25519 is the modern recommendation; # RSA-4096 is the conservative fallback. gpg --quick-generate-key "Your Name <you@example.com>" ed25519 sign 2y # 2. Find the long key ID. gpg --list-secret-keys --keyid-format=long # 3. Tell git which key. git config --global user.signingkey <LONG_KEY_ID> git config --global commit.gpgsign true # 4. Export and upload the public key to GitHub. gpg --armor --export <LONG_KEY_ID> | gh gpg-key add - ``` ## Verifying locally before pushing ```sh # Every commit in the push range. git log "$(git merge-base @{u} HEAD)..HEAD" \ --pretty='%H %G? %s' \ | awk '$2 != "G" {print "UNSIGNED:", $0}' # Or the canonical command the pre-push hook uses: for c in $(git rev-list "$(git merge-base @{u} HEAD)..HEAD"); do git verify-commit "$c" >/dev/null 2>&1 \ && echo "✓ $c" \ || echo "✗ $c — unsigned, will be rejected by master ruleset" done ``` ## Troubleshooting | Symptom | Likely cause | Fix | |---|---|---| | `error: gpg failed to sign the data` | gpg-agent isn't running, or `GPG_TTY` not exported | `export GPG_TTY=$(tty)` in your shell rc; restart agent with `gpgconf --kill gpg-agent` | | `error: Load key "/.../id_ed25519": Permission denied` | SSH key permissions too open | `chmod 600 ~/.ssh/id_ed25519` | | GitHub shows "Unverified" on a commit signed locally | Signing key not uploaded to GitHub | `gh ssh-key add … --type signing` (SSH) or `gh gpg-key add` (GPG) | | Pre-push hook rejects a merge commit you didn't author | Upstream commit lacks a signature | Either pull the rebased branch, or fast-forward instead of merging | | Ruleset import via `gh` complains "invalid JSON" | Rulesets API expects the `target` + `rules` envelope, not just the rules array | Use the file as-is — `gh ruleset import .github/rulesets/master.json` | ## Re-applying the ruleset after a manual edit If someone edits the ruleset in the GitHub UI by mistake, the file-of-truth wins. Re-apply: ```sh gh api -X POST repos/{owner}/{repo}/rulesets \ --input .github/rulesets/master.json ``` (or `-X PUT` against the existing ruleset's ID if it already exists). ## References - `.github/rulesets/master.json` — the enforced policy. - `.github/workflows/compliance-guard.yml` — the workflow that fails PRs containing unsigned commits. - `scripts/git-hooks/pre-push` — the local pre-push gate. - `dot_config/git/hooks/executable_commit-msg` — the local commit-msg gate. - [GitHub: About commit signature verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) - [GitHub: Telling Git about your SSH key](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key)