From 96ec0f89101ea414e764565e022b9bc83a96071c Mon Sep 17 00:00:00 2001 From: Arpit Chakladar Date: Thu, 10 Sep 2026 04:32:33 +0530 Subject: fix(security/ssh): fixed the loading ssh key from gopass Now on every home-manager switch we run a script that loads the key form gopass to gpg-agent, then the gpg-agent is responsible for storing it. Anytime gopass changes the ssh key we must run home-manager switch to load the ssh key into gpg agent. --- modules/security/gopass/default.nix | 37 ------------- modules/security/gopass/gopass-ssh-load.sh | 71 ------------------------ modules/security/ssh/default.nix | 75 +++++++++++++++++-------- modules/security/ssh/gpg-ssh-key-load.sh | 88 ++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+), 131 deletions(-) delete mode 100644 modules/security/gopass/gopass-ssh-load.sh create mode 100644 modules/security/ssh/gpg-ssh-key-load.sh (limited to 'modules/security') diff --git a/modules/security/gopass/default.nix b/modules/security/gopass/default.nix index 4cf9182..e78e595 100644 --- a/modules/security/gopass/default.nix +++ b/modules/security/gopass/default.nix @@ -5,30 +5,6 @@ pkgs, ... }: -let - gopassKeys = config.security.ssh.gopassKeys; - - gopassSshLoadScript = pkgs.writeShellApplication { - name = "gopass-ssh-load"; - runtimeInputs = [ - config.security.gopass.package - config.security.gpg.package - config.security.ssh.package - config.terminal.bash.package - ]; - text = - builtins.replaceStrings - [ - "@@GOPASS_SSH_KEYS@@" - "@@GNUPGHOME@@" - ] - [ - (lib.concatStringsSep " " gopassKeys) - config.home.sessionVariables.GNUPGHOME - ] - (builtins.readFile ./gopass-ssh-load.sh); - }; -in { options.security.gopass = { enable = lib.mkEnableOption "Enables gopass."; @@ -38,15 +14,6 @@ in default = config.programs.password-store.package; description = "The gopass package to use."; }; - ssh-agent = { - enable = lib.mkEnableOption "gopass-backed SSH keys for git"; - package = lib.mkOption { - type = lib.types.package; - readOnly = true; - default = gopassSshLoadScript; - description = "The gopass-ssh-load script package."; - }; - }; sync = { enable = lib.mkEnableOption "Enables git-backed syncing of the gopass data directory."; remote = lib.mkOption { @@ -127,10 +94,6 @@ in }; }) - (lib.mkIf config.security.gopass.ssh-agent.enable { - home.packages = [ config.security.gopass.ssh-agent.package ]; - }) - (lib.mkIf config.security.gopass.sync.enable { home.activation.gopassSyncInit = let diff --git a/modules/security/gopass/gopass-ssh-load.sh b/modules/security/gopass/gopass-ssh-load.sh deleted file mode 100644 index 8c1b11f..0000000 --- a/modules/security/gopass/gopass-ssh-load.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env bash - -# gopass-ssh-load -# -# Load SSH keys into the SSH agent from the gopass password store. -# -# This script reads private keys and (optionally) their passphrases from gopass -# entries under the `ssh/` directory and adds them to the SSH agent served by -# gpg-agent. It is meant to be run manually whenever a key is imported into or -# rotated within the gopass store, so the SSH agent picks up the change. -# -# Behaviour: -# * It first verifies that a usable SSH agent socket exists and bails out if -# not. -# * It exits early (without doing anything) when the agent already has at -# least one Ed25519/RSA/ECDSA identity loaded, to avoid useless work and -# unnecessary gpg passphrase prompts. -# * For each key listed in GOPASS_SSH_KEYS it writes the corresponding -# `ssh/` entry to a temporary file, strips the passphrase using the -# `ssh//passphrase` entry, and registers the key with `ssh-add`. -# -# Temporary key files are written with mode 600 and removed afterwards. - -set -o errexit -set -o nounset -set -o pipefail - -export GNUPGHOME="@@GNUPGHOME@@" -export GOPASS_SSH_KEYS="@@GOPASS_SSH_KEYS@@" - -info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } -warn() { printf '\033[1;33m==> warning:\033[0m %s\n' "$*" >&2; } -error() { printf '\033[1;31m==> error:\033[0m %s\n' "$*" >&2; } -die() { error "$*"; exit 1; } - -SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" -export SSH_AUTH_SOCK -if [ -z "$SSH_AUTH_SOCK" ] || [ ! -S "$SSH_AUTH_SOCK" ]; then - die "SSH_AUTH_SOCK is not set or valid." -fi - -if ssh-add -l 2>/dev/null | grep -qE "(ED25519|RSA|ECDSA)"; then - exit 0 -fi - -# GOPASS_SSH_KEYS holds a space-separated list of gopass entry names under ssh -if [ -z "${GOPASS_SSH_KEYS:-}" ]; then - die "GOPASS_SSH_KEYS is not set. Example: GOPASS_SSH_KEYS=\"github gitlab\"" -fi - -# shellcheck disable=SC2086 -read -r -a keys <<< "$GOPASS_SSH_KEYS" - -for key in "${keys[@]}"; do - if gopass cat "ssh/$key" > /dev/null 2>&1; then - tmpdir=$(mktemp -d) - keyfile="$tmpdir/key" - gopass cat "ssh/$key" > "$keyfile" 2>/dev/null - chmod 600 "$keyfile" - - passphrase=$(gopass cat "ssh/$key/passphrase" 2>/dev/null || true) - if [ -n "$passphrase" ]; then - ssh-keygen -p -P "$passphrase" -N "" -f "$keyfile" 2>/dev/null - fi - - ssh-add "$keyfile" 2>/dev/null - rm -rf "$tmpdir" - else - warn "no gopass entry ssh/$key" - fi -done diff --git a/modules/security/ssh/default.nix b/modules/security/ssh/default.nix index 8d1f881..e18b933 100644 --- a/modules/security/ssh/default.nix +++ b/modules/security/ssh/default.nix @@ -5,40 +5,69 @@ pkgs, ... }: +let + gpgSshKeyLoad = pkgs.writeShellApplication { + name = "gpg-ssh-key-load"; + runtimeInputs = [ + config.terminal.bash.package + config.security.gopass.package + config.security.gpg.package + config.security.ssh.package + pkgs.coreutils + ]; + text = + builtins.replaceStrings + [ + "@@GOPASS_SSH_KEY@@" + "@@GNUPGHOME@@" + ] + [ + config.security.ssh.sshKeyGopassPath + config.home.sessionVariables.GNUPGHOME + ] + (builtins.readFile ./gpg-ssh-key-load.sh); + }; +in { options.security.ssh = { - enable = lib.mkEnableOption "Enables ssh."; + enable = lib.mkEnableOption "Enables ssh via the gpg-agent."; package = lib.mkOption { type = lib.types.package; readOnly = true; - default = config.programs.ssh.package; + default = pkgs.openssh; description = "The ssh package to use."; }; - gopassKeys = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - description = "SSH keys to load from the gopass store (entries under ssh/)."; + sshKeyGopassPath = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + gopass entry holding the private SSH key. The key is loaded into the + gpg-agent during home-manager switch so ssh works without a ~/.ssh + directory. + ''; }; }; - config = lib.mkIf config.security.ssh.enable { - programs.ssh = { - enable = true; - - package = pkgs.openssh; + config = lib.mkMerge [ + (lib.mkIf config.security.ssh.enable { + home.packages = [ config.security.ssh.package ]; - enableDefaultConfig = false; - extraOptionOverrides = { - AddKeysToAgent = "yes"; - ForwardAgent = "yes"; - ServerAliveInterval = "60"; - ServerAliveCountMax = "3"; - VisualHostKey = "yes"; - HashKnownHosts = "yes"; - }; - }; + assertions = [ + { + assertion = config.security.gpg.enable; + message = '' + Enabling `security.ssh` requires `security.gpg` so that + gpg-agent can be used as the ssh-agent. + ''; + } + ]; + }) - services.ssh-agent.enable = lib.mkIf config.security.gpg.enable false; - }; + (lib.mkIf (config.security.ssh.enable && config.security.ssh.sshKeyGopassPath != null) { + home.activation.gpgSshKeyLoad = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + run ${lib.getExe gpgSshKeyLoad} || true + ''; + }) + ]; } diff --git a/modules/security/ssh/gpg-ssh-key-load.sh b/modules/security/ssh/gpg-ssh-key-load.sh new file mode 100644 index 0000000..d662dbe --- /dev/null +++ b/modules/security/ssh/gpg-ssh-key-load.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +# gpg-ssh-key-load +# +# Load the SSH key stored in gopass into the gpg-agent. +# +# SSH keys are served by gpg-agent (enable-ssh-support): the private key at +# @@GOPASS_SSH_KEY@@ in the gopass store is added to the agent, so ssh works +# without ever needing a ~/.ssh directory. It runs automatically during every +# home-manager switch and exits early when the agent already holds an identity, +# so the gpg passphrase prompt only appears when it is actually needed. + +set -o errexit +set -o nounset +set -o pipefail + +export GNUPGHOME="@@GNUPGHOME@@" + +GOPASS_SSH_KEY="@@GOPASS_SSH_KEY@@" + +info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m==> warning:\033[0m %s\n' "$*" >&2; } +error() { printf '\033[1;31m==> error:\033[0m %s\n' "$*" >&2; } +die() { error "$*"; exit 1; } + +usage() { + cat </dev/null | grep -qE "(ED25519|RSA|ECDSA)"; then + exit 0 +fi + +if ! gopass show -o "$GOPASS_SSH_KEY" > /dev/null 2>&1; then + die "no gopass entry $GOPASS_SSH_KEY" +fi + +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT +keyfile="$tmpdir/key" +gopass show -o "$GOPASS_SSH_KEY" > "$keyfile" 2>/dev/null +chmod 600 "$keyfile" + +passphrase=$(gopass show -o "$GOPASS_SSH_KEY/passphrase" 2>/dev/null || true) +if [ -n "$passphrase" ]; then + ssh-keygen -p -P "$passphrase" -N "" -f "$keyfile" 2>/dev/null +fi + +info "Loading SSH key ($GOPASS_SSH_KEY) into gpg-agent..." +if ! timeout 60 ssh-add "$keyfile"; then + error "Failed to load SSH key into gpg-agent." + error "Run \"ssh-add <(gopass show -o $GOPASS_SSH_KEY)\" manually." + exit 1 +fi + +info "SSH key loaded into gpg-agent." \ No newline at end of file -- cgit v1.2.3