From b8b7f4724f7c0a1bb50d9737fd4e9ed280a7b8ac Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 03:55:37 +0000 Subject: [PATCH 1/4] feat(agenix): configure SSH key sync from galactica to kyber MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add kyber's public key to galactica secrets for shared key access - Configure kyber to decrypt and deploy GitHub SSH key to ~/.ssh/id_ed25519_github - Update SSH config to use synced GitHub key for github.com - Enable post-quantum SSH algorithms (sntrup761x25519-sha512@openssh.com) - Modernize host key and public key algorithm preferences 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- home-manager/programs/ssh/default.nix | 9 ++++++++ named-hosts/galactica/secrets.nix | 18 ++++++++++------ named-hosts/kyber/default.nix | 15 ++++++++++++- named-hosts/kyber/secrets.nix | 31 +++++++++++++++------------ 4 files changed, 52 insertions(+), 21 deletions(-) diff --git a/home-manager/programs/ssh/default.nix b/home-manager/programs/ssh/default.nix index 3b5ad7f82..912eec47b 100644 --- a/home-manager/programs/ssh/default.nix +++ b/home-manager/programs/ssh/default.nix @@ -20,6 +20,14 @@ extraOptions = { IgnoreUnknown = "UseKeychain"; UseKeyChain = "yes"; + # Enable post-quantum key exchange algorithms + # sntrup761x25519 is a hybrid post-quantum algorithm combining + # Streamlined NTRU Prime (sntrup761) with X25519 + KexAlgorithms = "sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256"; + # Prefer modern host key algorithms + HostKeyAlgorithms = "ssh-ed25519-cert-v01@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256"; + # Prefer modern public key algorithms + PubkeyAcceptedAlgorithms = "ssh-ed25519-cert-v01@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256"; }; }; "localhost" = { @@ -30,6 +38,7 @@ }; "github.com" = { serverAliveInterval = 0; + identityFile = "~/.ssh/id_ed25519_github"; extraOptions = { ControlMaster = "auto"; ControlPath = "~/.ssh/github.sock"; diff --git a/named-hosts/galactica/secrets.nix b/named-hosts/galactica/secrets.nix index 29e25935a..e7bd4c0ff 100644 --- a/named-hosts/galactica/secrets.nix +++ b/named-hosts/galactica/secrets.nix @@ -1,14 +1,20 @@ +let + # Galactica's SSH public key + galactica = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEKze2jlpV7SyTKA2ezqbumpCiDn+5Sj4z5SxrqfzesX shunkakinoki@gmail.com"; + # Kyber's SSH public key + kyber = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO0IZtP3KSzY6GVSZ+R+VQYYfu3sEOVaQGDblQxAtwNM ubuntu@kyber"; + # All machines that can decrypt shared secrets + allMachines = [ galactica kyber ]; +in { + # Shared SSH key for GitHub authentication (accessible on all machines) "keys/id_ed25519.age" = { file = ./keys/id_ed25519.age; - publicKeys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEKze2jlpV7SyTKA2ezqbumpCiDn+5Sj4z5SxrqfzesX shunkakinoki@gmail.com" - ]; + publicKeys = allMachines; }; + # GPG key (galactica only) "keys/gpg.age" = { file = ./keys/gpg.age; - publicKeys = [ - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEKze2jlpV7SyTKA2ezqbumpCiDn+5Sj4z5SxrqfzesX shunkakinoki@gmail.com" - ]; + publicKeys = [ galactica ]; }; } diff --git a/named-hosts/kyber/default.nix b/named-hosts/kyber/default.nix index 7d719f76d..a0cb1d494 100644 --- a/named-hosts/kyber/default.nix +++ b/named-hosts/kyber/default.nix @@ -45,7 +45,20 @@ home-manager.lib.homeManagerConfiguration { # Agenix configuration age.identityPaths = [ "/home/${username}/.ssh/id_ed25519" ]; - age.secrets = builtins.mapAttrs (name: value: { file = value.file; }) (import ./secrets.nix); + age.secrets = builtins.mapAttrs ( + name: value: { + file = value.file; + # Deploy GitHub SSH key to ~/.ssh/ with correct permissions + } // ( + if name == "keys/id_ed25519.age" then + { + path = "/home/${username}/.ssh/id_ed25519_github"; + mode = "0600"; + } + else + { } + ) + ) (import ./secrets.nix); programs.home-manager.enable = true; diff --git a/named-hosts/kyber/secrets.nix b/named-hosts/kyber/secrets.nix index 980660cf5..b9913df11 100644 --- a/named-hosts/kyber/secrets.nix +++ b/named-hosts/kyber/secrets.nix @@ -2,21 +2,24 @@ # To add a new secret: # 1. Add the secret definition here # 2. Run: make encrypt-key-kyber KEY_FILE=/path/to/secret +let + # Galactica's SSH public key + galactica = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEKze2jlpV7SyTKA2ezqbumpCiDn+5Sj4z5SxrqfzesX shunkakinoki@gmail.com"; + # Kyber's SSH public key + kyber = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO0IZtP3KSzY6GVSZ+R+VQYYfu3sEOVaQGDblQxAtwNM ubuntu@kyber"; + # All machines that can decrypt shared secrets + allMachines = [ galactica kyber ]; +in { # Tailscale auth key - generate from https://login.tailscale.com/admin/settings/keys - # "keys/tailscale-auth.age" = { - # file = ./keys/tailscale-auth.age; - # publicKeys = [ - # # Kyber's SSH public key (run: ssh ubuntu@kyber "cat ~/.ssh/id_ed25519.pub") - # "ssh-ed25519 AAAA..." - # ]; - # }; + "keys/tailscale-auth.age" = { + file = ./keys/tailscale-auth.age; + publicKeys = [ kyber ]; + }; - # SSH key sync (your local key encrypted for kyber) - # "keys/id_ed25519.age" = { - # file = ./keys/id_ed25519.age; - # publicKeys = [ - # "ssh-ed25519 AAAA..." - # ]; - # }; + # Shared SSH key for GitHub authentication (synced from galactica) + "keys/id_ed25519.age" = { + file = ../galactica/keys/id_ed25519.age; + publicKeys = allMachines; + }; } From a9b8bb38c1811c9cee9a177b5c2c8319ab9e278a Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 04:00:39 +0000 Subject: [PATCH 2/4] docs(kyber): add helper script and instructions for SSH key sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add rekey-galactica.sh script to automate remote rekeying - Document SSH key sync process in kyber README - Provide clear instructions for both galactica and kyber sides 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- named-hosts/kyber/README.md | 28 ++++++++++++++++++++ named-hosts/kyber/rekey-galactica.sh | 38 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100755 named-hosts/kyber/rekey-galactica.sh diff --git a/named-hosts/kyber/README.md b/named-hosts/kyber/README.md index 7e31cd332..34f0a3755 100644 --- a/named-hosts/kyber/README.md +++ b/named-hosts/kyber/README.md @@ -48,3 +48,31 @@ Once Tailscale is set up: ```bash kyber # Fish abbreviation that runs: ssh ubuntu@kyber ``` + +## Syncing SSH Keys from Galactica + +To sync the GitHub SSH key from galactica to kyber: + +### On Galactica + +```bash +cd ~/dotfiles +git pull +make rekey-galactica +git add named-hosts/galactica/keys/ +git commit -m "chore(agenix): rekey secrets for kyber access" +git push +``` + +### On Kyber + +```bash +cd ~/dotfiles +git pull +make switch + +# Test GitHub access +ssh -T git@github.com +``` + +The SSH key will be automatically decrypted and deployed to `~/.ssh/id_ed25519_github`. diff --git a/named-hosts/kyber/rekey-galactica.sh b/named-hosts/kyber/rekey-galactica.sh new file mode 100755 index 000000000..d3e2ea825 --- /dev/null +++ b/named-hosts/kyber/rekey-galactica.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Remotely rekey galactica secrets to include kyber's public key +# This script should be run from kyber to trigger the rekey on galactica + +set -e + +echo "🔑 Rekeying galactica secrets to include kyber..." +echo "" +echo "This will:" +echo "1. Connect to galactica via Tailscale SSH" +echo "2. Run the rekey command to re-encrypt secrets with both keys" +echo "3. Commit and push the changes" +echo "" + +# Try Tailscale SSH first +if tailscale ssh shunkakinoki@galactica "cd ~/dotfiles && make rekey-galactica" 2>/dev/null; then + echo "✅ Rekey completed via Tailscale SSH" +else + echo "❌ Tailscale SSH failed. Please run this manually on galactica:" + echo "" + echo " cd ~/dotfiles" + echo " git pull" + echo " make rekey-galactica" + echo " git add named-hosts/galactica/keys/" + echo " git commit -m 'chore(agenix): rekey secrets for kyber access'" + echo " git push" + echo "" + exit 1 +fi + +# Pull the changes +echo "" +echo "📥 Pulling re-encrypted secrets..." +cd ~/dotfiles +git pull + +echo "" +echo "✅ Done! Now run: make switch" From 4b70b449a896b329ee5457db9826e92c22d6c1b1 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 04:26:58 +0000 Subject: [PATCH 3/4] fix(dotfiles-updater): add missing PATH dependencies for Linux systemd service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dotfiles-updater.service was failing with exit code 127 because install.sh requires coreutils (uname, id, dirname), gnumake, curl, and nix commands that weren't available in the systemd service PATH. This aligns the Linux service dependencies with the macOS launchd version which already included coreutils. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- home-manager/services/dotfiles-updater/default.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/home-manager/services/dotfiles-updater/default.nix b/home-manager/services/dotfiles-updater/default.nix index d299e5588..dee1a48f1 100644 --- a/home-manager/services/dotfiles-updater/default.nix +++ b/home-manager/services/dotfiles-updater/default.nix @@ -40,6 +40,10 @@ in lib.makeBinPath [ pkgs.git pkgs.bash + pkgs.coreutils + pkgs.gnumake + pkgs.curl + pkgs.nix ] }"; ExecStart = "${./update.sh}"; From 0e3fe34acd160ad299c3dd0e17dddf5b2adcc4e1 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sat, 13 Dec 2025 04:48:25 +0000 Subject: [PATCH 4/4] style: apply nixfmt formatting to secrets and config files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes treefmt check failures by applying consistent formatting to: - Multi-line array formatting for allMachines - Proper line breaks for merge operators (//) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- named-hosts/galactica/secrets.nix | 5 ++++- named-hosts/kyber/default.nix | 6 ++++-- named-hosts/kyber/secrets.nix | 5 ++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/named-hosts/galactica/secrets.nix b/named-hosts/galactica/secrets.nix index e7bd4c0ff..4503371ca 100644 --- a/named-hosts/galactica/secrets.nix +++ b/named-hosts/galactica/secrets.nix @@ -4,7 +4,10 @@ let # Kyber's SSH public key kyber = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO0IZtP3KSzY6GVSZ+R+VQYYfu3sEOVaQGDblQxAtwNM ubuntu@kyber"; # All machines that can decrypt shared secrets - allMachines = [ galactica kyber ]; + allMachines = [ + galactica + kyber + ]; in { # Shared SSH key for GitHub authentication (accessible on all machines) diff --git a/named-hosts/kyber/default.nix b/named-hosts/kyber/default.nix index a0cb1d494..8c9584974 100644 --- a/named-hosts/kyber/default.nix +++ b/named-hosts/kyber/default.nix @@ -46,10 +46,12 @@ home-manager.lib.homeManagerConfiguration { # Agenix configuration age.identityPaths = [ "/home/${username}/.ssh/id_ed25519" ]; age.secrets = builtins.mapAttrs ( - name: value: { + name: value: + { file = value.file; # Deploy GitHub SSH key to ~/.ssh/ with correct permissions - } // ( + } + // ( if name == "keys/id_ed25519.age" then { path = "/home/${username}/.ssh/id_ed25519_github"; diff --git a/named-hosts/kyber/secrets.nix b/named-hosts/kyber/secrets.nix index b9913df11..6f04e77e6 100644 --- a/named-hosts/kyber/secrets.nix +++ b/named-hosts/kyber/secrets.nix @@ -8,7 +8,10 @@ let # Kyber's SSH public key kyber = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO0IZtP3KSzY6GVSZ+R+VQYYfu3sEOVaQGDblQxAtwNM ubuntu@kyber"; # All machines that can decrypt shared secrets - allMachines = [ galactica kyber ]; + allMachines = [ + galactica + kyber + ]; in { # Tailscale auth key - generate from https://login.tailscale.com/admin/settings/keys