Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
e3c9aa8
feat(kyber): add declarative SSH agent and keychain management
shunkakinoki Dec 13, 2025
d4a266f
fix(fish): handle keychain output in fish shell for _ssh_add_github
shunkakinoki Dec 13, 2025
a10df99
fix(fish): improve ssh key verification in _ssh_add_github
shunkakinoki Dec 13, 2025
1b3c7a3
fix(agenix): switch to correct GitHub SSH key (id_github)
shunkakinoki Dec 13, 2025
cd8102e
feat(gpg): sync GPG key from galactica to kyber for commit signing
shunkakinoki Dec 13, 2025
610d5c4
chore(agenix): rekey secrets to include kyber
shunkakinoki Dec 13, 2025
53efff6
fix(git): enable programs.git in kyber config
shunkakinoki Dec 13, 2025
d2424a9
fix(git): use proper extraConfig structure for GPG signing
shunkakinoki Dec 13, 2025
5f87a2f
chore: update
shunkakinoki Dec 13, 2025
2877d52
fix(git): move GPG signing config to base git configuration
shunkakinoki Dec 13, 2025
39339a5
fix(gpg): configure pinentry-curses for Linux/kyber
shunkakinoki Dec 13, 2025
d3f6500
chore: update
shunkakinoki Dec 13, 2025
1605c8e
chore: update
shunkakinoki Dec 13, 2025
aa1ac66
fix(git): disable automatic GPG signing by default
shunkakinoki Dec 13, 2025
9e55ccb
feat(git): enable automatic GPG signing
shunkakinoki Dec 13, 2025
88ad7a4
fix(git): make GPG signing host-specific (macOS only)
shunkakinoki Dec 13, 2025
1311a1b
test: verify GPG signing works on kyber
shunkakinoki Dec 13, 2025
d84694f
test: verify GPG signing on kyber
shunkakinoki Dec 13, 2025
46edf4e
feat(gpg): enable GPG commit signing on all systems
shunkakinoki Dec 13, 2025
9d97d9b
fix(gpg): set GPG_TTY in interactiveShellInit instead of loginShellInit
shunkakinoki Dec 13, 2025
f67ae81
test: verify GPG signing on kyber
shunkakinoki Dec 13, 2025
d21c035
fix(git): enable GPG signing by default for all systems
shunkakinoki Dec 13, 2025
41a1601
feat(git): update default.nix to use a more declarative structure
shunkakinoki Dec 13, 2025
45ad715
fix(gpg): replace with correct galactica GPG key (C2E97FCFF482925D)
shunkakinoki Dec 13, 2025
67f5d26
feat(config): enable remote management access in cliproxyapi configur…
shunkakinoki Dec 13, 2025
ba04595
feat(gpg): add automatic GPG key import from agenix on Linux
shunkakinoki Dec 13, 2025
a7f9718
refactor(gpg): make GPG key import work on all systems (macOS + Linux)
shunkakinoki Dec 13, 2025
0069080
test: verify galactica GPG key signing
shunkakinoki Dec 13, 2025
f09fb3c
chore: update
shunkakinoki Dec 13, 2025
08ccad0
test: verify galactica GPG key signing
shunkakinoki Dec 13, 2025
5ca4f57
chore: update
shunkakinoki Dec 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ DETECTED_HOST := $(shell \
else \
echo ""; \
fi; \
elif [ "$(OS)" = "Linux" ]; then \
hostname=$$(hostname 2>/dev/null || echo ""); \
if [ "$$hostname" = "kyber" ]; then \
echo "kyber"; \
else \
echo ""; \
fi; \
else \
echo ""; \
fi)
Expand Down Expand Up @@ -257,7 +264,11 @@ nix-build: nix-connect ## Build Nix configuration.
.PHONY: nix-flake-check
nix-flake-check: ## Check Nix flake configuration.
@echo "🔍 Checking Nix flake configuration..."
@$(NIX_ALLOW_UNFREE) $(NIX_EXEC) flake check --all-systems --impure $(NIX_FLAGS)
@if [ "$(OS)" = "Darwin" ]; then \
$(NIX_ALLOW_UNFREE) $(NIX_EXEC) flake check --all-systems --impure $(NIX_FLAGS); \
else \
$(NIX_ALLOW_UNFREE) $(NIX_EXEC) flake check --system $(NIX_SYSTEM) --impure $(NIX_FLAGS); \
fi
@echo "✅ Nix flake check completed successfully"

.PHONY: nix-flake-update
Expand Down Expand Up @@ -323,6 +334,9 @@ nix-switch: ## Activate Nix configuration.
if [ -n "$(HOST)" ]; then \
echo "Switching named home config: $(HOST)"; \
USER=$(NIX_USERNAME) $(NIX_ALLOW_UNFREE) $(NIX_EXEC) run $(NIX_FLAGS) --impure .#homeConfigurations.$(HOST).activationPackage; \
elif [ -n "$(DETECTED_HOST)" ]; then \
echo "Auto-detected host: $(DETECTED_HOST)"; \
USER=$(NIX_USERNAME) $(NIX_ALLOW_UNFREE) $(NIX_EXEC) run $(NIX_FLAGS) --impure .#homeConfigurations.$(DETECTED_HOST).activationPackage; \
else \
USER=$(NIX_USERNAME) $(NIX_ALLOW_UNFREE) $(NIX_EXEC) run $(NIX_FLAGS) --impure .#$(NIX_CONFIG_TYPE)."$(NIX_USERNAME)@$(NIX_SYSTEM)".activationPackage; \
fi; \
Expand Down
2 changes: 1 addition & 1 deletion config/cliproxyapi/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ port: 8317
remote-management:
# Whether to allow remote (non-localhost) management access.
# When false, only localhost can access management endpoints (a key is still required).
allow-remote: false
allow-remote: true
# Management key. If a plaintext value is provided here, it will be hashed on startup.
# All management requests (even from localhost) require this key.
# Leave empty to disable the Management API entirely (404 for all /v0/management routes).
Expand Down
2 changes: 1 addition & 1 deletion config/k3s/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

# Activation script to sync config to /etc/rancher/k3s/
home.activation.k3s-config = lib.mkIf pkgs.stdenv.isLinux (
lib.hm.dag.entryAfter [ "writeBoundary" ] ''
config.lib.dag.entryAfter [ "writeBoundary" ] ''
if [ -f "$HOME/.config/k3s/config.yaml" ]; then
$DRY_RUN_CMD /usr/bin/sudo mkdir -p /etc/rancher/k3s
$DRY_RUN_CMD /usr/bin/sudo cp "$HOME/.config/k3s/config.yaml" /etc/rancher/k3s/config.yaml
Expand Down
2 changes: 1 addition & 1 deletion home-manager/modules/npm-globals/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
}:
{
# Install npm global packages from package.json using home-manager activation
home.activation.installNpmGlobals = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
home.activation.installNpmGlobals = config.lib.dag.entryAfter [ "writeBoundary" ] ''
export PATH=${pkgs.bun}/bin:$PATH
export BUN_INSTALL="$HOME/.bun"

Expand Down
4 changes: 2 additions & 2 deletions home-manager/modules/tailscale/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ in
Install.WantedBy = [ "default.target" ];
};

home.activation.createTailscaleDirs = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
home.activation.createTailscaleDirs = config.lib.dag.entryAfter [ "writeBoundary" ] ''
TAILSCALE_STATE_DIR="${config.xdg.dataHome}/tailscale"
TAILSCALE_RUN_DIR="$(dirname "${cfg.tailscaled.socketPath}")"

Expand All @@ -215,7 +215,7 @@ in
# Install system-level tailscaled service (requires sudo)
# Uses nix-generated service file for full declarative config
home.activation.installTailscaleService = mkIf cfg.installSystemService (
lib.hm.dag.entryAfter [ "writeBoundary" ] ''
config.lib.dag.entryAfter [ "writeBoundary" ] ''
SERVICE_FILE="/etc/systemd/system/tailscaled.service"
NIX_SERVICE="${tailscaledServiceFile}"
SUDOERS_FILE="/etc/sudoers.d/nix-tailscale"
Expand Down
2 changes: 1 addition & 1 deletion home-manager/modules/yek/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ in
];

# Install yek on activation
home.activation.installYek = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
home.activation.installYek = config.lib.dag.entryAfter [ "writeBoundary" ] ''
if [ ! -f "$HOME/.local/bin/yek" ]; then
$DRY_RUN_CMD ${installScript}/bin/install-yek || echo "⚠️ Failed to install yek. You can install it later by running: install-yek"
fi
Expand Down
1 change: 1 addition & 0 deletions home-manager/packages/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ with pkgs;
docker
docker-compose
gemini-cli
keychain
opencode
powertop
tailscale
Expand Down
2 changes: 2 additions & 0 deletions home-manager/programs/fish/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
g = "git";
lzd = "lazydocker";
lzg = "lazygit";
sag = "_ssh_add_github";
ta = "tmux new -A -s default";
v = "nvim";

Expand Down Expand Up @@ -135,6 +136,7 @@
"_hm_load_env_file"
"_kyber_function"
"_kybers_function"
"_ssh_add_github"
"fish_user_key_bindings"
]
);
Expand Down
40 changes: 40 additions & 0 deletions home-manager/programs/fish/functions/_ssh_add_github.fish
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
function _ssh_add_github --description "Add GitHub SSH key to ssh-agent"
# Check if key exists
if not test -f ~/.ssh/id_ed25519_github
echo "❌ GitHub SSH key not found at ~/.ssh/id_ed25519_github"
return 1
end

# Check if keychain is available
if not command -v keychain >/dev/null
echo "❌ keychain not found. Please install keychain."
return 1
end

# Initialize keychain and add the GitHub key
echo "🔑 Adding GitHub SSH key to keychain..."
# Use bash to evaluate keychain output, then use ssh-add
bash -c 'eval $(keychain --eval --quiet --confirm ~/.ssh/id_ed25519_github 2>/dev/null); ssh-add -l' >/dev/null 2>&1
Comment on lines +16 to +17

Copilot AI Dec 13, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script uses bash to evaluate keychain output but then immediately checks if ssh-add works, which may not reflect the environment set in the bash subprocess. The keychain evaluation in bash won't affect the Fish shell's environment. Consider using Fish's native evaluation of keychain output instead.

Suggested change
# Use bash to evaluate keychain output, then use ssh-add
bash -c 'eval $(keychain --eval --quiet --confirm ~/.ssh/id_ed25519_github 2>/dev/null); ssh-add -l' >/dev/null 2>&1
# Use Fish to evaluate keychain output so environment variables are set in this shell
eval (keychain --eval --quiet --confirm ~/.ssh/id_ed25519_github | psub)

Copilot uses AI. Check for mistakes.

# Alternative: directly use ssh-add if keychain already initialized ssh-agent
if not ssh-add -l >/dev/null 2>&1
# Start ssh-agent if not running
eval (ssh-agent -c)

Copilot AI Dec 13, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function starts ssh-agent with the -c flag for csh-style output, but Fish shell requires fish-style output using the -c flag is incorrect. Fish can handle the default output format, but using ssh-agent -c | source would be more appropriate for Fish shell.

Suggested change
eval (ssh-agent -c)
eval (ssh-agent | psub)

Copilot uses AI. Check for mistakes.
end

# Add the key directly
ssh-add ~/.ssh/id_ed25519_github
Comment on lines +14 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The logic for adding the SSH key is overly complex and contains a critical error. The command on line 17, bash -c 'eval $(keychain ...)', is incorrect for a fish shell. The environment variables set by eval will be scoped to the bash subshell and won't be available in the parent fish shell, making the command ineffective.

The subsequent logic (lines 20-26) correctly checks for and starts ssh-agent if needed, and then adds the key with ssh-add. This is a robust approach.

I suggest simplifying the script to remove the confusing and non-functional keychain call. The remaining logic is sufficient to add the key, prompting for a passphrase if necessary. While this makes the keychain availability check on line 9 redundant for this specific function, keychain is still used for agent initialization at login, so ssh-add should work correctly here.

    # Ensure ssh-agent is running. keychain should have started it on login,
    # but we can start it here as a fallback if needed.
    if not ssh-add -l >/dev/null 2>&1
        echo "ssh-agent not running. Starting it..."
        eval (ssh-agent -c)
    end

    # Add the key directly. This will prompt for a passphrase if there is one.
    echo "🔑 Adding GitHub SSH key to ssh-agent..."
    ssh-add ~/.ssh/id_ed25519_github

Comment on lines +17 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Simplify redundant SSH agent initialization flow.

The function has confusing redundant logic:

  1. Line 17 uses bash to evaluate keychain (then ignores the result)
  2. Lines 20-23 check and potentially start ssh-agent
  3. Line 26 directly adds the key

This creates unnecessary complexity. Since keychain is already initialized in loginShellInit (from ssh-agent.nix), the function should verify the agent is running and add the key only if not already present.

Apply this diff to simplify:

-    # Initialize keychain and add the GitHub key
-    echo "🔑 Adding GitHub SSH key to keychain..."
-    # Use bash to evaluate keychain output, then use ssh-add
-    bash -c 'eval $(keychain --eval --quiet --confirm ~/.ssh/id_ed25519_github 2>/dev/null); ssh-add -l' >/dev/null 2>&1
-
-    # Alternative: directly use ssh-add if keychain already initialized ssh-agent
+    # Check if ssh-agent is running (keychain should have started it)
     if not ssh-add -l >/dev/null 2>&1
-        # Start ssh-agent if not running
-        eval (ssh-agent -c)
+        echo "❌ ssh-agent not running. Run 'eval (keychain --eval)' first."
+        return 1
     end
 
+    # Check if key is already added
+    if ssh-add -l | grep -qE "(id_ed25519_github|shunkakinoki@gmail.com)"
+        echo "✅ GitHub SSH key already loaded"
+        return 0
+    end
+
+    # Add the key
+    echo "🔑 Adding GitHub SSH key..."
     ssh-add ~/.ssh/id_ed25519_github

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In home-manager/programs/fish/functions/_ssh_add_github.fish around lines 17 to
26, remove the redundant bash keychain eval and the manual ssh-agent startup;
instead simply check whether the github key is already loaded and, if not, call
ssh-add to add it. Implement this by running ssh-add -l and testing for the
presence of ~/.ssh/id_ed25519_github (or its key fingerprint/name) and only call
ssh-add ~/.ssh/id_ed25519_github when that check fails; do not attempt to start
ssh-agent or invoke keychain here since agent initialization is handled in
loginShellInit.


# Verify the key was added (check for either the filename or email)
if ssh-add -l | grep -qE "(id_ed25519_github|shunkakinoki@gmail.com)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n home-manager/programs/fish/functions/_ssh_add_github.fish | head -40

Repository: shunkakinoki/dotfiles

Length of output: 1797


🌐 Web query:

ssh-add -l output format what fields does it show

💡 Result:

ssh-add -l prints one line per identity with these fields:

  • bits — key size (e.g. 4096)
  • fingerprint — fingerprint of the public key (default SHA256:... unless changed with -E)
  • comment — free‑form comment (usually user@host or whatever was set in the pubkey); ssh-agent does not record the source filename.
  • algorithm — key type in parentheses (e.g. (RSA), (ED25519))

Example:
4096 SHA256:abc... user@host (RSA). (mankier.com)

Citations:


The grep pattern is ineffective; verification relies entirely on the hardcoded email.

The ssh-add -l command does not output the filename id_ed25519_github; it only shows the comment field (usually user@host format). This means the first pattern in the regex will never match. The verification succeeds only if the public key's comment is exactly shunkakinoki@gmail.com. If the key was generated with a different comment (e.g., shun@laptop or user@hostname), verification will fail despite the key being correctly added.

Either document that the key must be generated with this specific comment, or use a more robust check (e.g., verify by fingerprint or check ssh-add -L for the full key).

🤖 Prompt for AI Agents
In home-manager/programs/fish/functions/_ssh_add_github.fish around line 29, the
current grep against ssh-add -l relies on a filename that never appears and on a
hardcoded email comment; replace this with a robust check using ssh-add -L
(which prints the public keys) and match either the public key blob or the key
fingerprint (or both) instead of the comment alone, e.g., call ssh-keygen -lf on
each public key line from ssh-add -L to compare the expected fingerprint, or
grep the full public key string/comment from ssh-add -L; alternatively, if you
prefer keeping the simple approach, update the function comment to require
generating the key with the specific email and document that constraint.

echo "✅ GitHub SSH key added successfully"
echo "🧪 Testing GitHub connection..."
ssh -T git@github.com
return 0
else
echo "⚠️ Could not verify key was added, but ssh-add may have succeeded"
echo "🧪 Testing GitHub connection anyway..."
ssh -T git@github.com
return $status
end
Comment on lines +34 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fallback error handling could be more robust.

The fallback path (lines 34-39) returns the status of ssh -T git@github.com, but this command always exits with status 1 for successful authentication (GitHub's design). This makes the return value misleading.

Apply this diff to handle GitHub's authentication response correctly:

     else
         echo "⚠️  Could not verify key was added, but ssh-add may have succeeded"
         echo "🧪 Testing GitHub connection anyway..."
-        ssh -T git@github.com
-        return $status
+        # GitHub returns exit code 1 for successful auth with message
+        # "Hi username! You've successfully authenticated..."
+        if ssh -T git@github.com 2>&1 | grep -q "successfully authenticated"
+            return 0
+        else
+            return 1
+        end
     end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
else
echo "⚠️ Could not verify key was added, but ssh-add may have succeeded"
echo "🧪 Testing GitHub connection anyway..."
ssh -T git@github.com
return $status
end
else
echo "⚠️ Could not verify key was added, but ssh-add may have succeeded"
echo "🧪 Testing GitHub connection anyway..."
# GitHub returns exit code 1 for successful auth with message
# "Hi username! You've successfully authenticated..."
if ssh -T git@github.com 2>&1 | grep -q "successfully authenticated"
return 0
else
return 1
end
end

end
6 changes: 5 additions & 1 deletion home-manager/programs/git/default.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{ pkgs, lib, ... }:
{ lib, ... }:
{
programs = {
git = {
Expand Down Expand Up @@ -77,6 +77,10 @@
conflictStyle = "zdiff3";
};
};
signing = {
signByDefault = true;
key = "shunkakinoki@gmail.com";
};
ignores = lib.splitString "\n" (builtins.readFile ./.gitignore.global);
};
delta = {
Expand Down
2 changes: 1 addition & 1 deletion home-manager/programs/neovim/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ in
force = true;
};

home.activation.copyNvimPackLock = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
home.activation.copyNvimPackLock = config.lib.dag.entryAfter [ "writeBoundary" ] ''
$DRY_RUN_CMD mkdir -p "$HOME/.config/nvim"
$DRY_RUN_CMD cp -f ${nvimPackLockJson} "$HOME/.config/nvim/nvim-pack-lock.json"
$DRY_RUN_CMD chmod 644 "$HOME/.config/nvim/nvim-pack-lock.json"
Expand Down
2 changes: 2 additions & 0 deletions home-manager/services/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ let
neversslKeepalive = import ./neverssl-keepalive { inherit pkgs; };
ollama = import ./ollama { inherit pkgs; };
brewUpgrader = import ./brew-upgrader { inherit pkgs; };
sshAgent = import ./ssh-agent.nix;
in
[
brewUpgrader
Expand All @@ -14,4 +15,5 @@ in
dotfilesUpdater
neversslKeepalive
ollama
sshAgent
]
40 changes: 40 additions & 0 deletions home-manager/services/ssh-agent.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (pkgs.stdenv) isLinux isDarwin;
in
{
# Configure keychain for Linux systems
# macOS uses built-in keychain via UseKeychain SSH option
programs.fish.loginShellInit = lib.mkIf isLinux (
lib.mkAfter ''
# Initialize keychain for SSH key management
# This automatically starts ssh-agent and loads SSH keys
if command -v keychain > /dev/null
# Load keys that exist
set -l keys

# Always try to load the default key (no passphrase)
if test -f ~/.ssh/id_ed25519
set -a keys ~/.ssh/id_ed25519
end

# Load GitHub key if it exists (may have passphrase)
if test -f ~/.ssh/id_ed25519_github
set -a keys ~/.ssh/id_ed25519_github
end

# Initialize keychain with found keys
if test (count $keys) -gt 0
# Use --quiet to suppress most output, --eval to set environment variables
# --confirm will skip keys that need a passphrase in non-interactive contexts
eval (keychain --eval --quiet --confirm $keys ^/dev/null; or true)
Comment on lines +34 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

keychain --confirm flag behavior non-interactive ssh passphrase

💡 Result:

Short answer: --confirm tells keychain to add keys with SSH confirmation enabled (ssh-add -c). That makes every use require an interactive confirmation via SSH_ASKPASS; in non‑interactive contexts (cron, CI, headless shells without SSH_ASKPASS) the key will either block waiting for confirmation or fail, so it is unsuitable for non‑interactive use. [1][2]

What to do

  • Remove --confirm when you need non‑interactive access (let keychain load keys normally). [1][2]
  • Or provide a working SSH_ASKPASS program and DISPLAY/X11 or an alternative GUI prompt so confirmations can succeed in your environment. [1][2]
  • Alternative: preload the key into an agent without -c (ssh-add without -c) or use OS keychain integration (macOS UseKeychain / ssh-add options) depending on platform. [2][3]

Sources

  1. keychain man page (--confirm description). [1]
  2. ssh-add man / docs describing -c (confirmation via SSH_ASKPASS) and effects on non‑interactive runs (quoted/summary). [2]
  3. Platform notes (macOS UseKeychain / ssh-add behavior) for alternatives. [3]

Remove --confirm flag or provide SSH_ASKPASS in non-interactive login contexts.

The --confirm flag makes ssh-add require interactive confirmation for every key usage via SSH_ASKPASS. In non-interactive contexts (headless servers, systemd services without a display), this will block or fail. The fallback or true won't prevent the blocking during evaluation. Either remove --confirm to allow normal key loading, or ensure SSH_ASKPASS and a display mechanism (DISPLAY/X11 or alternative GUI prompt) are available in this service environment.

🤖 Prompt for AI Agents
In home-manager/services/ssh-agent.nix around lines 34-35, remove the
problematic --confirm flag from the keychain invocation or ensure an
SSH_ASKPASS-based prompt is available in the service environment; specifically,
either drop --confirm so keys are loaded non-interactively (preventing blocking
in headless/systemd contexts), or set up and export SSH_ASKPASS and any required
DISPLAY/X11 or alternative GUI prompt before invoking keychain so confirmations
can be handled without a tty.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error suppression may hide legitimate keychain failures.

The ^/dev/null; or true pattern suppresses all errors from keychain, which could hide configuration problems or missing dependencies. While this prevents login shell interruption, it makes debugging difficult.

Consider logging suppressed errors to a file for debugging:

-        eval (keychain --eval --quiet --confirm $keys ^/dev/null; or true)
+        eval (keychain --eval --quiet --confirm $keys 2>>~/.keychain-errors.log; or true)

Or at minimum, document why errors are suppressed:

         # Use --quiet to suppress most output, --eval to set environment variables
-        # --confirm will skip keys that need a passphrase in non-interactive contexts
+        # --confirm will skip keys that need a passphrase in non-interactive contexts
+        # Errors are suppressed to prevent login shell failures if keychain has issues
         eval (keychain --eval --quiet --confirm $keys ^/dev/null; or true)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
eval (keychain --eval --quiet --confirm $keys ^/dev/null; or true)
# Use --quiet to suppress most output, --eval to set environment variables
# --confirm will skip keys that need a passphrase in non-interactive contexts
# Errors are suppressed to prevent login shell failures if keychain has issues
eval (keychain --eval --quiet --confirm $keys ^/dev/null; or true)
🤖 Prompt for AI Agents
In home-manager/services/ssh-agent.nix around line 35, the use of `^/dev/null;
or true` blindly suppresses keychain errors; replace this with a non-destructive
suppression that preserves diagnostics by redirecting keychain stderr to a log
file (e.g. append stderr to a designated log under /var/log or a user-specific
log) while allowing the shell to continue (use a safe `|| true` if necessary),
ensure the log directory/file exists and has appropriate permissions, and add a
brief comment explaining why errors are not fatal and where to find the logs for
debugging.

Copilot AI Dec 13, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling with ^/dev/null; or true combined with the semicolon inside the eval may cause issues. In Fish, the or operator should typically be outside the eval command substitution. Consider restructuring to eval (keychain --eval --quiet --confirm $keys 2>/dev/null) and handling errors separately, or ensuring the error handling logic is correct for the intended behavior.

Suggested change
eval (keychain --eval --quiet --confirm $keys ^/dev/null; or true)
eval (keychain --eval --quiet --confirm $keys ^/dev/null); or true

Copilot uses AI. Check for mistakes.
end
end
''
);
}
Binary file modified named-hosts/galactica/keys/gpg.age
Binary file not shown.
9 changes: 5 additions & 4 deletions named-hosts/galactica/secrets.nix
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ let
in
{
# Shared SSH key for GitHub authentication (accessible on all machines)
"keys/id_ed25519.age" = {
file = ./keys/id_ed25519.age;
# This is ~/.ssh/id_github on galactica, the GitHub CLI-authorized key
"keys/id_github.age" = {
file = ./keys/id_github.age;
Comment on lines +15 to +16

Copilot AI Dec 13, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code references id_github.age but the actual file in the repository is id_ed25519.age. This mismatch will cause the secret deployment to fail. Either rename the actual file to id_github.age or update the code to reference the correct filename id_ed25519.age.

Suggested change
"keys/id_github.age" = {
file = ./keys/id_github.age;
"keys/id_ed25519.age" = {
file = ./keys/id_ed25519.age;

Copilot uses AI. Check for mistakes.
publicKeys = allMachines;
};
# GPG key (galactica only)
# GPG key (shared with all machines for commit signing)
"keys/gpg.age" = {
file = ./keys/gpg.age;
publicKeys = [ galactica ];
publicKeys = allMachines;
};
}
63 changes: 57 additions & 6 deletions named-hosts/kyber/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,19 @@ Once Tailscale is set up:
kyber # Fish abbreviation that runs: ssh ubuntu@kyber
```

## Syncing SSH Keys from Galactica
## SSH Key Management

To sync the GitHub SSH key from galactica to kyber:
### Automated Setup

### On Galactica
This configuration uses:

- **agenix**: Encrypts and syncs the GitHub SSH key from galactica
- **keychain**: Manages ssh-agent and automatically loads SSH keys
- **Declarative deployment**: SSH keys are deployed during `make switch`

### Syncing SSH Keys from Galactica

#### On Galactica (one-time setup)

```bash
cd ~/dotfiles
Expand All @@ -64,15 +72,58 @@ git commit -m "chore(agenix): rekey secrets for kyber access"
git push
```

### On Kyber
#### On Kyber

```bash
cd ~/dotfiles
git pull
make switch
```

The GitHub SSH key will be automatically:

1. Decrypted from `named-hosts/galactica/keys/id_ed25519.age`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The secret file for the GitHub SSH key was renamed from id_ed25519.age to id_github.age in the Nix configurations, but this documentation still refers to the old name. Please update it to maintain consistency and avoid confusion.

Suggested change
1. Decrypted from `named-hosts/galactica/keys/id_ed25519.age`
1. Decrypted from `named-hosts/galactica/keys/id_github.age`

Copilot AI Dec 13, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation references id_ed25519.age but the code has been updated to use id_github.age. This inconsistency will confuse users trying to follow the manual deployment instructions. Update this line to reference id_github.age to match the code changes.

Suggested change
1. Decrypted from `named-hosts/galactica/keys/id_ed25519.age`
1. Decrypted from `named-hosts/galactica/keys/id_github.age`

Copilot uses AI. Check for mistakes.
2. Deployed to `~/.ssh/id_ed25519_github`
3. Loaded into ssh-agent via keychain (if no passphrase)

Comment on lines +83 to +88

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Documentation references outdated filename.

Line 85 references id_ed25519.age but the secrets configuration uses id_github.age. Update to match the actual filename.

 The GitHub SSH key will be automatically:

-1. Decrypted from `named-hosts/galactica/keys/id_ed25519.age`
+1. Decrypted from `named-hosts/galactica/keys/id_github.age`
 2. Deployed to `~/.ssh/id_ed25519_github`
 3. Loaded into ssh-agent via keychain (if no passphrase)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The GitHub SSH key will be automatically:
1. Decrypted from `named-hosts/galactica/keys/id_ed25519.age`
2. Deployed to `~/.ssh/id_ed25519_github`
3. Loaded into ssh-agent via keychain (if no passphrase)
The GitHub SSH key will be automatically:
1. Decrypted from `named-hosts/galactica/keys/id_github.age`
2. Deployed to `~/.ssh/id_ed25519_github`
3. Loaded into ssh-agent via keychain (if no passphrase)
🤖 Prompt for AI Agents
In named-hosts/kyber/README.md around lines 83 to 88, the documentation
references the outdated filename id_ed25519.age; update that line to the actual
secret filename id_github.age so the steps reflect the secrets configuration
(i.e., decrypt from named-hosts/galactica/keys/id_github.age, then deploy/load
as described).

### Adding Passphrase-Protected Keys

If the GitHub SSH key has a passphrase, add it manually:

```bash
sag # Abbreviation for _ssh_add_github function

# Or manually
keychain --eval --quiet --confirm ~/.ssh/id_ed25519_github
```

# Test GitHub access
### Verify GitHub Access

```bash
ssh -T git@github.com
# Expected: Hi username! You've successfully authenticated...
```

### Troubleshooting

**Key not deployed after `make switch`:**

```bash
# Check if key exists
ls -la ~/.ssh/id_ed25519_github

# Manually deploy if needed
age -d -i ~/.ssh/id_ed25519 -o ~/.ssh/id_ed25519_github \
named-hosts/galactica/keys/id_ed25519.age

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The secret file for the GitHub SSH key was renamed from id_ed25519.age to id_github.age in the Nix configurations, but this documentation still refers to the old name. Please update it to maintain consistency and avoid confusion.

Suggested change
named-hosts/galactica/keys/id_ed25519.age
named-hosts/galactica/keys/id_github.age

Copilot AI Dec 13, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation references id_ed25519.age but the code has been updated to use id_github.age. This inconsistency will confuse users trying to follow the troubleshooting instructions. Update this line to reference id_github.age to match the code changes.

Suggested change
named-hosts/galactica/keys/id_ed25519.age
named-hosts/galactica/keys/id_github.age

Copilot uses AI. Check for mistakes.
chmod 0600 ~/.ssh/id_ed25519_github
Comment on lines +115 to +118

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Troubleshooting command uses wrong filename.

The manual decrypt command references id_ed25519.age instead of id_github.age.

 # Manually deploy if needed
 age -d -i ~/.ssh/id_ed25519 -o ~/.ssh/id_ed25519_github \
-  named-hosts/galactica/keys/id_ed25519.age
+  named-hosts/galactica/keys/id_github.age
 chmod 0600 ~/.ssh/id_ed25519_github
🤖 Prompt for AI Agents
In named-hosts/kyber/README.md around lines 115 to 118, the manual decrypt
command references the wrong filename (id_ed25519.age) — it should reference
id_github.age; update the age command to use the correct encrypted file path
(named-hosts/galactica/keys/id_github.age) so the correct key is decrypted, and
keep the subsequent chmod step unchanged.

```

The SSH key will be automatically decrypted and deployed to `~/.ssh/id_ed25519_github`.
**GitHub authentication fails:**

```bash
# Check if key is in ssh-agent
ssh-add -l | grep github

# Add the key
sag
```
Loading
Loading