From a3cc70387c677d895422bcb4a0b86de51c6769dd Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sun, 10 May 2026 16:54:08 +0800 Subject: [PATCH 1/3] fix(k3s): surface scp failures during client activation Why: scp stderr was redirected to /dev/null and wrapped in an if-then with no else branch, so any sync failure (host key mismatch, missing authorized key, network drop) made make switch look successful while silently leaving ~/.kube/config-kyber absent. Now logs success and pipes scp stderr to the activation output on failure, while still exiting 0 so a missing key on a fresh machine does not break home-manager activation. --- config/k3s/activate-client.sh | 9 ++++++++- spec/activate_k3s_client_spec.sh | 12 ++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/config/k3s/activate-client.sh b/config/k3s/activate-client.sh index 1078ac018..f549f8565 100755 --- a/config/k3s/activate-client.sh +++ b/config/k3s/activate-client.sh @@ -15,7 +15,14 @@ fi mkdir -p "$HOME/.kube" +SCP_ERR=$(mktemp) +trap 'rm -f "$SCP_ERR"' EXIT + if scp -o ConnectTimeout=5 -o BatchMode=yes \ - "${REMOTE_HOST}:${REMOTE_KUBECONFIG_PATH}" "$LOCAL_KUBECONFIG" 2>/dev/null; then + "${REMOTE_HOST}:${REMOTE_KUBECONFIG_PATH}" "$LOCAL_KUBECONFIG" 2>"$SCP_ERR"; then chmod 600 "$LOCAL_KUBECONFIG" + echo "k3s-client: kubeconfig synced from ${REMOTE_HOST}" +else + echo "k3s-client: failed to fetch kubeconfig from ${REMOTE_HOST}" >&2 + sed 's/^/k3s-client: /' "$SCP_ERR" >&2 fi diff --git a/spec/activate_k3s_client_spec.sh b/spec/activate_k3s_client_spec.sh index da66cc947..5916ccfad 100644 --- a/spec/activate_k3s_client_spec.sh +++ b/spec/activate_k3s_client_spec.sh @@ -44,4 +44,16 @@ When run bash -c "grep 'chmod 600' '$SCRIPT'" The output should include 'chmod 600' End End + +Describe 'failure visibility' +It 'does not silently discard scp stderr' +When run bash -c "grep 'scp ' '$SCRIPT'" +The output should not include '2>/dev/null' +End + +It 'logs scp failures to stderr' +When run bash -c "grep 'failed to fetch kubeconfig' '$SCRIPT'" +The output should include 'failed to fetch kubeconfig' +End +End End From 9d9fe582208dba0aa657a1356326aecf1486a194 Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sun, 10 May 2026 17:03:38 +0800 Subject: [PATCH 2/3] feat(k3s): authorize galactica on kyber for kubeconfig sync Why: scp from galactica to kyber failed with publickey denial because galactica's pubkey was not in kyber's authorized_keys, so the silent sync after switch was a no-op. The pubkey was already duplicated across three secrets.nix files, so factor it out at the same time. - Extract galactica/kyber/matic pubkeys to named-hosts/pubkeys.nix and inherit from each per-host secrets.nix. - config/k3s/activate.sh idempotently appends galactica's pubkey to ~/.ssh/authorized_keys on kyber, with a placeholder guard so the unsubstituted template is a no-op. - config/k3s/default.nix templates the pubkey through pkgs.replaceVars, sourcing it from the same shared file. --- config/k3s/activate.sh | 29 ++++++++++ config/k3s/default.nix | 8 ++- named-hosts/galactica/secrets.nix | 7 +-- named-hosts/kyber/secrets.nix | 5 +- named-hosts/matic/secrets.nix | 3 +- named-hosts/pubkeys.nix | 8 +++ spec/activate_k3s_spec.sh | 96 +++++++++++++++++++++++++++++++ 7 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 named-hosts/pubkeys.nix diff --git a/config/k3s/activate.sh b/config/k3s/activate.sh index 68eacef48..30b0eda78 100755 --- a/config/k3s/activate.sh +++ b/config/k3s/activate.sh @@ -1,6 +1,35 @@ #!/usr/bin/env bash set -euo pipefail +GALACTICA_AUTHORIZED_KEY="@galacticaAuthorizedKey@" + +ensure_authorized_key() { + local key="$1" + local ssh_dir="$HOME/.ssh" + local auth_file="$ssh_dir/authorized_keys" + + if [ -z "$key" ] || [[ $key == "@"*"@" ]]; then + return 0 + fi + + mkdir -p "$ssh_dir" + chmod 700 "$ssh_dir" + touch "$auth_file" + chmod 600 "$auth_file" + + if grep -qxF "$key" "$auth_file"; then + return 0 + fi + + if [ -s "$auth_file" ] && [ "$(tail -c1 "$auth_file" | wc -l)" -eq 0 ]; then + printf '\n' >>"$auth_file" + fi + printf '%s\n' "$key" >>"$auth_file" + echo "k3s-server: authorized galactica SSH key for kubeconfig sync" +} + +ensure_authorized_key "$GALACTICA_AUTHORIZED_KEY" + if [ ! -f "$HOME/.config/k3s/config.yaml" ]; then exit 0 fi diff --git a/config/k3s/default.nix b/config/k3s/default.nix index 405cbee26..e569f4742 100644 --- a/config/k3s/default.nix +++ b/config/k3s/default.nix @@ -8,6 +8,12 @@ let inherit (inputs.host) isKyber isGalactica; kubeconfig = "${config.home.homeDirectory}/.kube/config-kyber"; + # Authorize galactica on kyber so the client activation can scp the + # kubeconfig over Tailscale. + galacticaAuthorizedKey = (import ../../named-hosts/pubkeys.nix).galactica; + serverActivateScript = pkgs.replaceVars ./activate.sh { + galacticaAuthorizedKey = galacticaAuthorizedKey; + }; in { home.file.".config/k3s/config.yaml" = lib.mkIf isKyber { @@ -40,7 +46,7 @@ in home.activation.k3s-server = lib.mkIf isKyber ( lib.hm.dag.entryAfter [ "writeBoundary" ] '' - $DRY_RUN_CMD ${pkgs.bash}/bin/bash "${./activate.sh}" + $DRY_RUN_CMD ${pkgs.bash}/bin/bash "${serverActivateScript}" '' ); diff --git a/named-hosts/galactica/secrets.nix b/named-hosts/galactica/secrets.nix index 0f765afd5..500c7773d 100644 --- a/named-hosts/galactica/secrets.nix +++ b/named-hosts/galactica/secrets.nix @@ -1,10 +1,5 @@ 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"; - # Matic's SSH public key - matic = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEeknNbHasmT+43PgO9oy0mutoe+V2R2ZNRa5SPOLmLN skakinoki@matic"; + inherit (import ../pubkeys.nix) galactica kyber matic; # All machines that can decrypt shared secrets allMachines = [ galactica diff --git a/named-hosts/kyber/secrets.nix b/named-hosts/kyber/secrets.nix index d67240115..4e115602e 100644 --- a/named-hosts/kyber/secrets.nix +++ b/named-hosts/kyber/secrets.nix @@ -3,10 +3,7 @@ # 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"; + inherit (import ../pubkeys.nix) galactica kyber; # All machines that can decrypt shared secrets allMachines = [ galactica diff --git a/named-hosts/matic/secrets.nix b/named-hosts/matic/secrets.nix index ae583c880..49696815c 100644 --- a/named-hosts/matic/secrets.nix +++ b/named-hosts/matic/secrets.nix @@ -1,7 +1,6 @@ # Matic secrets - synced from galactica via agenix let - galactica = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEKze2jlpV7SyTKA2ezqbumpCiDn+5Sj4z5SxrqfzesX shunkakinoki@gmail.com"; - matic = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEeknNbHasmT+43PgO9oy0mutoe+V2R2ZNRa5SPOLmLN skakinoki@matic"; + inherit (import ../pubkeys.nix) galactica matic; allMachines = [ galactica matic diff --git a/named-hosts/pubkeys.nix b/named-hosts/pubkeys.nix new file mode 100644 index 000000000..5aefefe53 --- /dev/null +++ b/named-hosts/pubkeys.nix @@ -0,0 +1,8 @@ +# SSH public keys for all machines. +# Single source of truth shared by per-host secrets.nix files and any +# activation script that needs to authorize cross-host access. +{ + galactica = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEKze2jlpV7SyTKA2ezqbumpCiDn+5Sj4z5SxrqfzesX shunkakinoki@gmail.com"; + kyber = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO0IZtP3KSzY6GVSZ+R+VQYYfu3sEOVaQGDblQxAtwNM ubuntu@kyber"; + matic = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEeknNbHasmT+43PgO9oy0mutoe+V2R2ZNRa5SPOLmLN skakinoki@matic"; +} diff --git a/spec/activate_k3s_spec.sh b/spec/activate_k3s_spec.sh index 119103acf..537c849b8 100644 --- a/spec/activate_k3s_spec.sh +++ b/spec/activate_k3s_spec.sh @@ -49,4 +49,100 @@ When run bash -c "grep -A 1 '! -f' '$SCRIPT'" The output should include 'exit 0' End End + +Describe 'authorized_keys management' +It 'declares a placeholder for galactica authorized key' +When run bash -c "grep 'GALACTICA_AUTHORIZED_KEY' '$SCRIPT'" +The output should include '@galacticaAuthorizedKey@' +End + +It 'defines an idempotent ensure_authorized_key helper' +When run bash -c "grep 'ensure_authorized_key' '$SCRIPT'" +The output should include 'ensure_authorized_key' +End + +It 'guards against unsubstituted placeholder' +When run bash -c "grep -E '\\\"@\\\"\\*\\\"@\\\"' '$SCRIPT'" +The output should include '@' +End + +It 'uses fixed-string match against authorized_keys' +When run bash -c "grep 'grep -qxF' '$SCRIPT'" +The output should include 'grep -qxF' +End + +ensure_authorized_key_function() { + cat <<'BASH' +ensure_authorized_key() { +local key="$1" +local ssh_dir="$HOME/.ssh" +local auth_file="$ssh_dir/authorized_keys" +if [ -z "$key" ] || [[ "$key" == "@"*"@" ]]; then +return 0 +fi +mkdir -p "$ssh_dir" +chmod 700 "$ssh_dir" +touch "$auth_file" +chmod 600 "$auth_file" +if grep -qxF "$key" "$auth_file"; then +return 0 +fi +if [ -s "$auth_file" ] && [ "$(tail -c1 "$auth_file" | wc -l)" -eq 0 ]; then +printf '\n' >>"$auth_file" +fi +printf '%s\n' "$key" >>"$auth_file" +} +BASH +} + +It 'appends the key when authorized_keys does not exist' +When run bash -c ' +tmp=$(mktemp -d) +HOME="$tmp" +'"$(ensure_authorized_key_function)"' +ensure_authorized_key "ssh-ed25519 AAAATEST test@example" +grep -qxF "ssh-ed25519 AAAATEST test@example" "$tmp/.ssh/authorized_keys" +' +The status should be success +End + +It 'is idempotent on repeated runs' +When run bash -c ' +tmp=$(mktemp -d) +HOME="$tmp" +'"$(ensure_authorized_key_function)"' +ensure_authorized_key "ssh-ed25519 AAAATEST test@example" +ensure_authorized_key "ssh-ed25519 AAAATEST test@example" +ensure_authorized_key "ssh-ed25519 AAAATEST test@example" +count=$(grep -cxF "ssh-ed25519 AAAATEST test@example" "$tmp/.ssh/authorized_keys") +test "$count" = "1" +' +The status should be success +End + +It 'preserves existing keys when appending' +When run bash -c ' +tmp=$(mktemp -d) +HOME="$tmp" +mkdir -p "$tmp/.ssh" +printf "ssh-ed25519 AAAAEXISTING old@example\n" >"$tmp/.ssh/authorized_keys" +'"$(ensure_authorized_key_function)"' +ensure_authorized_key "ssh-ed25519 AAAANEW new@example" +grep -qxF "ssh-ed25519 AAAAEXISTING old@example" "$tmp/.ssh/authorized_keys" && +grep -qxF "ssh-ed25519 AAAANEW new@example" "$tmp/.ssh/authorized_keys" +' +The status should be success +End + +It 'no-ops when given an unsubstituted placeholder' +When run bash -c ' +tmp=$(mktemp -d) +HOME="$tmp" +'"$(ensure_authorized_key_function)"' +ensure_authorized_key "@galacticaAuthorizedKey@" +test ! -e "$tmp/.ssh/authorized_keys" +' +The status should be success +End +End End From c3b49d257ac66ddd93e2ab7dec8727bc935a47bb Mon Sep 17 00:00:00 2001 From: Shun Kakinoki Date: Sun, 10 May 2026 17:14:04 +0800 Subject: [PATCH 3/3] fix(spec): silence SC2016 in activate_k3s_spec The bash -c snippets quote with single quotes intentionally so the inner $tmp/$HOME expand at the right moment. Add SC2016 to the existing per-file disable list to keep CI green. --- spec/activate_k3s_spec.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/activate_k3s_spec.sh b/spec/activate_k3s_spec.sh index 537c849b8..a6fc149d0 100644 --- a/spec/activate_k3s_spec.sh +++ b/spec/activate_k3s_spec.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# shellcheck disable=SC2329 +# shellcheck disable=SC2329,SC2016 Describe 'config/k3s/activate.sh' SCRIPT="$PWD/config/k3s/activate.sh"