Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69707b4475 | ||
|
|
ca4daa1539 |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -4,9 +4,23 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [0.4.0] - 2026-04-04
|
||||
|
||||
### Added
|
||||
- GCM (Git Credential Manager) detection — preferred cross-platform credential helper
|
||||
- `is_keychain_credential_helper()` recognizes osxkeychain, GCM, libsecret, and gnome-keyring
|
||||
- Distro-specific install hints when no keychain-backed credential helper is found (Debian/Ubuntu, Fedora/RHEL, Arch, openSUSE, Alpine)
|
||||
- Audit labels keychain-backed helpers as `(keychain-backed)` for clarity
|
||||
|
||||
### Changed
|
||||
- Harden step skips credential.helper prompt when user already has a keychain-backed helper
|
||||
- Audit messaging improved: clearer descriptions for missing, insecure, and unknown helpers
|
||||
- FIDO2 signing wizard, grouped SSH config directives, REASONING.md (prior unreleased work)
|
||||
|
||||
## [0.2.3] - 2026-03-31
|
||||
|
||||
### Fixed
|
||||
- Fix e2e.sh distro loop not splitting on spaces (#39)
|
||||
- FIDO2 key generation on macOS — detect Homebrew's openssh via `ssh-sk-helper` (no freeze), use its `ssh-keygen` binary for hardware key generation
|
||||
- Linux gitleaks install hint now shows `apt`/`dnf` instead of `brew`
|
||||
- e2e test runner distro loop broken by `IFS` setting — use bash array
|
||||
@@ -17,6 +31,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
## [0.2.0] - 2026-03-31
|
||||
|
||||
### Added
|
||||
- Add REASONING.md documenting trade-offs for each hardening default (#48)
|
||||
- Gitleaks pre-commit hook installation — creates `~/.config/git/hooks/pre-commit` with `SKIP_GITLEAKS` bypass
|
||||
- Global gitignore creation (`~/.config/git/ignore`) with security patterns (`.env`, `*.pem`, `*.key`, credentials, Terraform state)
|
||||
- Audit of existing global gitignore for missing security patterns
|
||||
|
||||
41
README.md
41
README.md
@@ -139,6 +139,47 @@ The script prints (but does not apply) server/org-level recommendations:
|
||||
- Clone untrusted repos with `--no-recurse-submodules`
|
||||
- Use separate signing keys per org to prevent cross-platform identity correlation (OSINT)
|
||||
|
||||
## Signing with FIDO2 hardware keys
|
||||
|
||||
The script includes an interactive wizard that:
|
||||
|
||||
1. Detects existing SSH keys (including custom-named keys from `~/.ssh/config`)
|
||||
2. Detects FIDO2 hardware (YubiKey, etc.)
|
||||
3. Offers two tiers:
|
||||
- **Software SSH key** — use existing `ed25519` or generate one
|
||||
- **FIDO2 hardware key** — generate `ed25519-sk` with touch-to-sign (if hardware detected)
|
||||
4. Configures `user.signingkey`, `commit.gpgsign`, `tag.gpgsign`
|
||||
5. Sets up `~/.config/git/allowed_signers` for local signature verification
|
||||
|
||||
These combinations of hardware and OS have been tested:
|
||||
|
||||
| Hardware | Firmware | OS | works? |
|
||||
|----------|----------|----|--------|
|
||||
| [Yubico Security Key USB C NFC](https://support.yubico.com/s/article/Security-Key-C-NFC) | 5.4.3 | macOS Tahoe | Yes |
|
||||
| [Yubico Security Key USB C NFC](https://support.yubico.com/s/article/Security-Key-C-NFC) | 5.4.3 | Debian 13 Trixie | |
|
||||
| [Yubico Security Key USB C NFC](https://support.yubico.com/s/article/Security-Key-C-NFC) | 5.4.3 | Fedora 42 | Yes |
|
||||
| [Yubico Security Key USB A NFC](https://support.yubico.com/s/article/Security-Key-NFC) | 5.4.3 | macOS Tahoe | Yes |
|
||||
| [Yubico Security Key USB A NFC](https://support.yubico.com/s/article/Security-Key-NFC) | 5.4.3 | Debian 13 Trixie | |
|
||||
| [Yubico Security Key USB A NFC](https://support.yubico.com/s/article/Security-Key-NFC) | 5.4.3 | Fedora 42 | Yes |
|
||||
| [Yubico Security Key USB A NFC](https://www.yubico.com/products/security-key-by-yubico/usb-a-nfc/) | 5.0.2 | macOS Tahoe | Yes |
|
||||
| [Yubico Security Key USB A NFC](https://www.yubico.com/products/security-key-by-yubico/usb-a-nfc/) | 5.0.2 | Debian 13 Trixie | |
|
||||
| [Yubico Security Key USB A NFC](https://www.yubico.com/products/security-key-by-yubico/usb-a-nfc/) | 5.0.2 | Fedora 42 | Yes |
|
||||
| [Yubico YubiKey 5C nano](https://support.yubico.com/s/article/YubiKey-5C-Nano) | 5.4.3 | macOS Tahoe | Yes |
|
||||
| [Yubico YubiKey 5C nano](https://support.yubico.com/s/article/YubiKey-5C-Nano) | 5.4.3 | Debian 13 Trixie | |
|
||||
| [Yubico YubiKey 5C nano](https://support.yubico.com/s/article/YubiKey-5C-Nano) | 5.4.3 | Fedora 42 | Yes |
|
||||
| [Yubico YubiKey 5 NFC](https://support.yubico.com/s/article/YubiKey-5-NFC) | 5.1.2 | macOS Tahoe | Yes* |
|
||||
| [Yubico YubiKey 5 NFC](https://support.yubico.com/s/article/YubiKey-5-NFC) | 5.1.2 | Debian 13 Trixie| |
|
||||
| [Yubico YubiKey 5 NFC](https://support.yubico.com/s/article/YubiKey-5-NFC) | 5.1.2 | Fedora 42| Yes* |
|
||||
| [SoloKeys Solo 1 Tap USB-A](https://solokeys.com/collections/all/products/solo-tap-usb-a-preorder) | | Ubuntu 24.04 | Yes |
|
||||
| [SoloKeys Solo 1 Tap USB-A](https://solokeys.com/collections/all/products/solo-tap-usb-a-preorder) | | Debian 13 Trixie | Yes |
|
||||
| [SoloKeys Solo 1 Tap USB-A](https://solokeys.com/collections/all/products/solo-tap-usb-a-preorder) | | Fedora 42 | Yes |
|
||||
| [SoloKeys Solo 1 Tap USB-A](https://solokeys.com/collections/all/products/solo-tap-usb-a-preorder) | | macOS Tahoe | Yes |
|
||||
| [HYPERSECU HyperFIDO mini](https://033c2a7e-e1da-473d-a255-6132a1d3aa6e.filesusr.com/ugd/5aae8d_f4e8a196a99f45b1859e201a7cb40962.pdf) | | macOS Tahoe | Yes |
|
||||
| [HYPERSECU HyperFIDO mini](https://033c2a7e-e1da-473d-a255-6132a1d3aa6e.filesusr.com/ugd/5aae8d_f4e8a196a99f45b1859e201a7cb40962.pdf) | | Ubuntu 24.04 | Yes |
|
||||
| [HYPERSECU HyperFIDO mini](https://033c2a7e-e1da-473d-a255-6132a1d3aa6e.filesusr.com/ugd/5aae8d_f4e8a196a99f45b1859e201a7cb40962.pdf) | | Debian 13 Trixie | |
|
||||
| [HYPERSECU HyperFIDO mini](https://033c2a7e-e1da-473d-a255-6132a1d3aa6e.filesusr.com/ugd/5aae8d_f4e8a196a99f45b1859e201a7cb40962.pdf) | | Fedora 42 | |
|
||||
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
|
||||
252
git-harden.sh
252
git-harden.sh
@@ -10,7 +10,7 @@ IFS=$'\n\t'
|
||||
# ------------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ------------------------------------------------------------------------------
|
||||
readonly VERSION="0.3.0"
|
||||
readonly VERSION="0.4.0"
|
||||
readonly BACKUP_DIR="${HOME}/.config/git"
|
||||
readonly HOOKS_DIR="${HOME}/.config/git/hooks"
|
||||
readonly ALLOWED_SIGNERS_FILE="${HOME}/.config/git/allowed_signers"
|
||||
@@ -266,13 +266,31 @@ check_dependencies() {
|
||||
detect_credential_helper
|
||||
}
|
||||
|
||||
# Check if a credential.helper value corresponds to a keychain-backed store.
|
||||
# Returns 0 (true) if the helper stores credentials in the OS keychain.
|
||||
is_keychain_credential_helper() {
|
||||
local helper="$1"
|
||||
case "$helper" in
|
||||
osxkeychain|manager|manager-core) return 0 ;;
|
||||
*git-credential-libsecret*) return 0 ;;
|
||||
*git-credential-gnome-keyring*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
detect_credential_helper() {
|
||||
# Git Credential Manager (GCM) — cross-platform, preferred when available
|
||||
if command -v git-credential-manager >/dev/null 2>&1; then
|
||||
DETECTED_CRED_HELPER="manager"
|
||||
return
|
||||
fi
|
||||
|
||||
case "$PLATFORM" in
|
||||
macos)
|
||||
DETECTED_CRED_HELPER="osxkeychain"
|
||||
;;
|
||||
linux)
|
||||
# Try to find libsecret credential helper
|
||||
# Try libsecret (GNOME Keyring / KDE Wallet / any Secret Service provider)
|
||||
local libsecret_path=""
|
||||
for path in \
|
||||
/usr/lib/git-core/git-credential-libsecret \
|
||||
@@ -286,10 +304,49 @@ detect_credential_helper() {
|
||||
|
||||
if [ -n "$libsecret_path" ]; then
|
||||
DETECTED_CRED_HELPER="$libsecret_path"
|
||||
else
|
||||
DETECTED_CRED_HELPER="cache --timeout=3600"
|
||||
print_info "libsecret not found; falling back to in-memory credential cache (1h TTL, not persistent)"
|
||||
return
|
||||
fi
|
||||
|
||||
# Fallback: in-memory cache (not persistent across reboots)
|
||||
DETECTED_CRED_HELPER="cache --timeout=3600"
|
||||
print_info "No keychain-backed credential helper found; falling back to in-memory cache (1h TTL)"
|
||||
credential_install_hint
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Print distro-specific install hints for keychain credential storage.
|
||||
credential_install_hint() {
|
||||
local distro_id=""
|
||||
if [ -f /etc/os-release ]; then
|
||||
# shellcheck disable=SC1091 # os-release is a system file, not part of this project
|
||||
distro_id="$(. /etc/os-release && printf '%s' "${ID:-}")"
|
||||
fi
|
||||
|
||||
printf ' %bTo store credentials in the OS keychain, install one of:%b\n' "$YELLOW" "$RESET" >&2
|
||||
case "$distro_id" in
|
||||
ubuntu|debian|pop|linuxmint)
|
||||
printf ' • libsecret: sudo apt install libsecret-1-dev git make && cd /usr/share/doc/git/contrib/credential/libsecret && sudo make\n' >&2
|
||||
printf ' • GCM: https://github.com/git-ecosystem/git-credential-manager/releases\n' >&2
|
||||
;;
|
||||
fedora|rhel|centos|rocky|alma)
|
||||
printf ' • libsecret: sudo dnf install git-credential-libsecret\n' >&2
|
||||
printf ' • GCM: https://github.com/git-ecosystem/git-credential-manager/releases\n' >&2
|
||||
;;
|
||||
arch|manjaro|endeavouros)
|
||||
printf ' • libsecret: sudo pacman -S libsecret\n' >&2
|
||||
printf ' • GCM: https://github.com/git-ecosystem/git-credential-manager/releases\n' >&2
|
||||
;;
|
||||
opensuse*|suse*)
|
||||
printf ' • libsecret: sudo zypper install git-credential-libsecret\n' >&2
|
||||
printf ' • GCM: https://github.com/git-ecosystem/git-credential-manager/releases\n' >&2
|
||||
;;
|
||||
alpine)
|
||||
printf ' • GCM: https://github.com/git-ecosystem/git-credential-manager/releases\n' >&2
|
||||
;;
|
||||
*)
|
||||
printf ' • libsecret: install git-credential-libsecret via your package manager\n' >&2
|
||||
printf ' • GCM: https://github.com/git-ecosystem/git-credential-manager/releases\n' >&2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -387,14 +444,15 @@ audit_git_config() {
|
||||
local cred_current
|
||||
cred_current="$(git config --global --get credential.helper 2>/dev/null || true)"
|
||||
if [ -z "$cred_current" ]; then
|
||||
print_miss "credential.helper (expected: $DETECTED_CRED_HELPER)"
|
||||
print_miss "credential.helper not set (credentials won't be cached)"
|
||||
elif [ "$cred_current" = "store" ]; then
|
||||
print_warn "credential.helper = store (INSECURE: stores passwords in plaintext; expected: $DETECTED_CRED_HELPER)"
|
||||
print_warn "credential.helper = store (INSECURE: stores passwords in plaintext ~/${cred_current})"
|
||||
elif is_keychain_credential_helper "$cred_current"; then
|
||||
print_ok "credential.helper = $cred_current (keychain-backed)"
|
||||
elif [ "$cred_current" = "$DETECTED_CRED_HELPER" ]; then
|
||||
print_ok "credential.helper = $cred_current"
|
||||
else
|
||||
# Non-store, non-recommended — could be user's custom helper
|
||||
print_warn "credential.helper = $cred_current (expected: $DETECTED_CRED_HELPER)"
|
||||
print_warn "credential.helper = $cred_current (not a known keychain-backed helper)"
|
||||
fi
|
||||
|
||||
print_header "Defaults"
|
||||
@@ -703,22 +761,22 @@ apply_setting_group() {
|
||||
shift 2
|
||||
|
||||
# Collect pending changes (settings that need updating)
|
||||
local pending_keys=""
|
||||
local pending_vals=""
|
||||
local pending_explanations=""
|
||||
local count=0
|
||||
local pending_keys=()
|
||||
local pending_vals=()
|
||||
local pending_explanations=()
|
||||
|
||||
while [ $# -ge 3 ]; do
|
||||
local key="$1" value="$2" explanation="$3"
|
||||
shift 3
|
||||
if setting_needs_change "$key" "$value"; then
|
||||
pending_keys="${pending_keys}${key}"$'\n'
|
||||
pending_vals="${pending_vals}${value}"$'\n'
|
||||
pending_explanations="${pending_explanations}${explanation}"$'\n'
|
||||
count=$((count + 1))
|
||||
pending_keys+=("$key")
|
||||
pending_vals+=("$value")
|
||||
pending_explanations+=("$explanation")
|
||||
fi
|
||||
done
|
||||
|
||||
local count="${#pending_keys[@]}"
|
||||
|
||||
# Nothing to do
|
||||
if [ "$count" -eq 0 ]; then
|
||||
return 0
|
||||
@@ -728,25 +786,15 @@ apply_setting_group() {
|
||||
printf ' %s\n\n' "$description" >&2
|
||||
|
||||
# Show what will change
|
||||
local i=0
|
||||
while [ "$i" -lt "$count" ]; do
|
||||
local key val expl
|
||||
key="$(printf '%s' "$pending_keys" | sed -n "$((i + 1))p")"
|
||||
val="$(printf '%s' "$pending_vals" | sed -n "$((i + 1))p")"
|
||||
expl="$(printf '%s' "$pending_explanations" | sed -n "$((i + 1))p")"
|
||||
printf ' %-40s %s\n' "${key} = ${val}" "# ${expl}" >&2
|
||||
i=$((i + 1))
|
||||
local i
|
||||
for ((i = 0; i < count; i++)); do
|
||||
printf ' %-40s %s\n' "${pending_keys[$i]} = ${pending_vals[$i]}" "# ${pending_explanations[$i]}" >&2
|
||||
done
|
||||
printf '\n' >&2
|
||||
|
||||
if prompt_yn "Apply these ${count} settings?"; then
|
||||
i=0
|
||||
while [ "$i" -lt "$count" ]; do
|
||||
local key val
|
||||
key="$(printf '%s' "$pending_keys" | sed -n "$((i + 1))p")"
|
||||
val="$(printf '%s' "$pending_vals" | sed -n "$((i + 1))p")"
|
||||
git config --global "$key" "$val"
|
||||
i=$((i + 1))
|
||||
for ((i = 0; i < count; i++)); do
|
||||
git config --global "${pending_keys[$i]}" "${pending_vals[$i]}"
|
||||
done
|
||||
print_info "Applied ${count} settings"
|
||||
fi
|
||||
@@ -841,8 +889,10 @@ apply_git_config() {
|
||||
"init.defaultBranch" "main" "Default branch name for new repos" \
|
||||
"log.showSignature" "true" "Show signature status in git log"
|
||||
|
||||
# Credential helper needs special logic (warn about 'store')
|
||||
if [ "$cred_current" != "$DETECTED_CRED_HELPER" ]; then
|
||||
# Credential helper needs special logic — accept any keychain-backed helper
|
||||
if is_keychain_credential_helper "$cred_current" 2>/dev/null; then
|
||||
: # Already using a keychain-backed helper — leave it alone
|
||||
elif [ "$cred_current" != "$DETECTED_CRED_HELPER" ]; then
|
||||
local cred_prompt="Set credential.helper = $DETECTED_CRED_HELPER?"
|
||||
if [ "$cred_current" = "store" ]; then
|
||||
cred_prompt="Replace INSECURE credential.helper=store with $DETECTED_CRED_HELPER?"
|
||||
@@ -1021,8 +1071,6 @@ detect_existing_keys() {
|
||||
if [ -f "$expanded_key" ]; then
|
||||
SIGNING_KEY_FOUND=true
|
||||
SIGNING_PUB_PATH="$expanded_key"
|
||||
# Derive private key path (remove .pub suffix if present)
|
||||
|
||||
return
|
||||
fi
|
||||
fi
|
||||
@@ -1552,46 +1600,87 @@ setup_allowed_signers() {
|
||||
# SSH config hardening
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
apply_ssh_directive() {
|
||||
# Read the current value of an SSH config directive (empty if absent).
|
||||
get_ssh_directive_value() {
|
||||
local directive="$1"
|
||||
local raw
|
||||
raw="$(grep -i "^[[:space:]]*${directive}[[:space:]=]" "$SSH_CONFIG" 2>/dev/null | head -1 | sed 's/^[[:space:]]*[^[:space:]=]*[[:space:]=]*//' || true)"
|
||||
strip_ssh_value "$raw"
|
||||
}
|
||||
|
||||
ssh_directive_needs_change() {
|
||||
local directive="$1"
|
||||
local value="$2"
|
||||
[ "$(get_ssh_directive_value "$directive")" != "$value" ]
|
||||
}
|
||||
|
||||
apply_single_ssh_directive() {
|
||||
local directive="$1"
|
||||
local value="$2"
|
||||
|
||||
# Check if directive already exists with correct value (case-insensitive directive match)
|
||||
local current
|
||||
current="$(grep -i "^[[:space:]]*${directive}[[:space:]=]" "$SSH_CONFIG" 2>/dev/null | head -1 | sed 's/^[[:space:]]*[^[:space:]=]*[[:space:]=]*//' || true)"
|
||||
current="$(strip_ssh_value "$current")"
|
||||
|
||||
if [ "$current" = "$value" ]; then
|
||||
return
|
||||
fi
|
||||
current="$(get_ssh_directive_value "$directive")"
|
||||
|
||||
if [ -n "$current" ]; then
|
||||
# Directive exists but with wrong value
|
||||
if prompt_yn "Update SSH directive: $directive $current -> $value?"; then
|
||||
# Use temp file to avoid sed -i portability issues
|
||||
local tmpfile
|
||||
tmpfile="$(mktemp "${SSH_CONFIG}.XXXXXX")"
|
||||
trap 'rm -f "$tmpfile"' EXIT
|
||||
# Replace first occurrence of the directive (case-insensitive)
|
||||
local replaced=false
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
if [ "$replaced" = false ] && printf '%s' "$line" | grep -qi "^[[:space:]]*${directive}[[:space:]=]"; then
|
||||
printf '%s %s\n' "$directive" "$value"
|
||||
replaced=true
|
||||
else
|
||||
printf '%s\n' "$line"
|
||||
fi
|
||||
done < "$SSH_CONFIG" > "$tmpfile"
|
||||
mv "$tmpfile" "$SSH_CONFIG"
|
||||
chmod 600 "$SSH_CONFIG"
|
||||
print_info "Updated $directive = $value in $SSH_CONFIG"
|
||||
fi
|
||||
# Replace existing directive
|
||||
local tmpfile
|
||||
tmpfile="$(mktemp "${SSH_CONFIG}.XXXXXX")"
|
||||
local replaced=false
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
if [ "$replaced" = false ] && printf '%s' "$line" | grep -qi "^[[:space:]]*${directive}[[:space:]=]"; then
|
||||
printf '%s %s\n' "$directive" "$value"
|
||||
replaced=true
|
||||
else
|
||||
printf '%s\n' "$line"
|
||||
fi
|
||||
done < "$SSH_CONFIG" > "$tmpfile"
|
||||
mv "$tmpfile" "$SSH_CONFIG"
|
||||
chmod 600 "$SSH_CONFIG"
|
||||
else
|
||||
# Directive missing entirely
|
||||
if prompt_yn "Add SSH directive: $directive $value?"; then
|
||||
printf '%s %s\n' "$directive" "$value" >> "$SSH_CONFIG"
|
||||
print_info "Added $directive $value to $SSH_CONFIG"
|
||||
printf '%s %s\n' "$directive" "$value" >> "$SSH_CONFIG"
|
||||
fi
|
||||
}
|
||||
|
||||
apply_ssh_directive_group() {
|
||||
local group_name="$1"
|
||||
local description="$2"
|
||||
shift 2
|
||||
|
||||
# Collect pending changes (directives that need updating)
|
||||
local pending_keys=()
|
||||
local pending_vals=()
|
||||
local pending_explanations=()
|
||||
|
||||
while [ $# -ge 3 ]; do
|
||||
local key="$1" value="$2" explanation="$3"
|
||||
shift 3
|
||||
if ssh_directive_needs_change "$key" "$value"; then
|
||||
pending_keys+=("$key")
|
||||
pending_vals+=("$value")
|
||||
pending_explanations+=("$explanation")
|
||||
fi
|
||||
done
|
||||
|
||||
local count="${#pending_keys[@]}"
|
||||
|
||||
if [ "$count" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '\n %b%s%b\n' "$BOLD" "$group_name" "$RESET" >&2
|
||||
printf ' %s\n\n' "$description" >&2
|
||||
|
||||
local i
|
||||
for ((i = 0; i < count; i++)); do
|
||||
printf ' %-45s %s\n' "${pending_keys[$i]} ${pending_vals[$i]}" "# ${pending_explanations[$i]}" >&2
|
||||
done
|
||||
printf '\n' >&2
|
||||
|
||||
if prompt_yn "Apply these ${count} directives?"; then
|
||||
for ((i = 0; i < count; i++)); do
|
||||
apply_single_ssh_directive "${pending_keys[$i]}" "${pending_vals[$i]}"
|
||||
done
|
||||
print_info "Applied ${count} SSH directives"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -1612,11 +1701,28 @@ apply_ssh_config() {
|
||||
print_info "Created $SSH_CONFIG with mode 600"
|
||||
fi
|
||||
|
||||
apply_ssh_directive "StrictHostKeyChecking" "accept-new"
|
||||
apply_ssh_directive "HashKnownHosts" "yes"
|
||||
apply_ssh_directive "IdentitiesOnly" "yes"
|
||||
apply_ssh_directive "AddKeysToAgent" "yes"
|
||||
apply_ssh_directive "PubkeyAcceptedAlgorithms" "ssh-ed25519,sk-ssh-ed25519@openssh.com,ecdsa-sha2-nistp256,sk-ecdsa-sha2-nistp256@openssh.com"
|
||||
apply_ssh_directive_group "Host Verification" \
|
||||
"Trust-on-first-use (TOFU): accept new host keys automatically, but reject
|
||||
changed keys (the actual MITM scenario). The default 'ask' just trains users
|
||||
to blindly type 'yes'. Hashing known_hosts prevents hostname enumeration if
|
||||
the file is exfiltrated." \
|
||||
"StrictHostKeyChecking" "accept-new" "Auto-accept new hosts, reject changed keys" \
|
||||
"HashKnownHosts" "yes" "Hash hostnames in known_hosts (privacy)"
|
||||
|
||||
apply_ssh_directive_group "Key & Agent Management" \
|
||||
"Without IdentitiesOnly, ssh-agent offers ALL loaded keys to every server —
|
||||
a malicious server can enumerate which services you have access to.
|
||||
AddKeysToAgent reduces passphrase fatigue so developers actually use them." \
|
||||
"IdentitiesOnly" "yes" "Only offer keys explicitly configured (prevents key leakage)" \
|
||||
"AddKeysToAgent" "yes" "Auto-add keys to ssh-agent after first use"
|
||||
|
||||
apply_ssh_directive_group "Algorithm Restrictions" \
|
||||
"Disables RSA and DSA negotiation entirely. This prevents downgrade attacks
|
||||
to weaker algorithms. May break connections to legacy servers that only
|
||||
support RSA — those servers should be upgraded (RSA-SHA1 deprecated since
|
||||
OpenSSH 8.7)." \
|
||||
"PubkeyAcceptedAlgorithms" "ssh-ed25519,sk-ssh-ed25519@openssh.com,ecdsa-sha2-nistp256,sk-ecdsa-sha2-nistp256@openssh.com" \
|
||||
"Ed25519 + ECDSA (software and hardware-backed)"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
@@ -497,7 +497,7 @@ SSHEOF
|
||||
source_functions
|
||||
AUTO_YES=true
|
||||
|
||||
apply_ssh_directive "StrictHostKeyChecking" "accept-new"
|
||||
apply_single_ssh_directive "StrictHostKeyChecking" "accept-new"
|
||||
|
||||
# Should still have exactly one occurrence
|
||||
local count
|
||||
@@ -515,7 +515,7 @@ SSHEOF
|
||||
source_functions
|
||||
AUTO_YES=true
|
||||
|
||||
apply_ssh_directive "StrictHostKeyChecking" "accept-new"
|
||||
apply_single_ssh_directive "StrictHostKeyChecking" "accept-new"
|
||||
|
||||
# Verify updated
|
||||
grep -q "StrictHostKeyChecking accept-new" "${TEST_HOME}/.ssh/config"
|
||||
@@ -548,7 +548,7 @@ SSHEOF
|
||||
source_functions
|
||||
AUTO_YES=true
|
||||
|
||||
apply_ssh_directive "StrictHostKeyChecking" "accept-new"
|
||||
apply_single_ssh_directive "StrictHostKeyChecking" "accept-new"
|
||||
|
||||
# Should still have exactly one occurrence
|
||||
local count
|
||||
@@ -1156,7 +1156,7 @@ EOF
|
||||
# v0.2.0: Version bump
|
||||
# ===========================================================================
|
||||
|
||||
@test "--version reports 0.2.3" {
|
||||
@test "--version reports 0.4.0" {
|
||||
run bash "$SCRIPT" --version
|
||||
assert_output --partial "0.2.3"
|
||||
assert_output --partial "0.4.0"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user