Back to feed

sledtools/pika branch #43

mini

Inject Apple CI SSH key into forge via sops-nix

Target branch: master

Merge Commit: 4b50f98439b4ca470c34891ed884c9bac55ee2a0

branch: merged tutorial: ready ci: success
Open CI Details

Continuous Integration

CI: success

Compact status on the review page, with full logs on the CI page.

Open CI Details

Latest run #54 success

3 passed

head fd8343be0b6a216e7587b346a4672359e8b929d1 · queued 2026-03-25 16:07:53 · 3 lane(s)

queued 16s · ran 5m 09s

check-pika-rust · success check-pika-followup · success check-apple-host-sanity · success

Summary

This branch adds a dedicated SSH private key for the Apple CI runner (pika-mini) to the pika-news service, managed end-to-end through sops-nix. A new encrypted secret pikaci_apple_ssh_key is added to the sops-encrypted secrets file, declared in the NixOS module so sops-nix decrypts it to a file on disk at deploy time, and injected into the service environment alongside five new configuration variables (host, user, remote root, retention count, lock timeout). The pikaci-apple-github-step script is updated to prefer reading the key from the file path (PIKACI_APPLE_SSH_KEY_FILE) when available, falling back to the previous inline environment variable (PIKACI_APPLE_SSH_KEY) for backward compatibility.

Tutorial Steps

Add the encrypted SSH key to the sops secrets file

Intent: Store the pikaci Apple CI SSH private key alongside the existing secrets in the sops-encrypted YAML file so it can be decrypted at deploy time by sops-nix.

Affected files: infra/secrets/pika-news.yaml

Evidence
@@ -1,38 +1,39 @@
+pikaci_apple_ssh_key: ENC[AES256_GCM,data:2ZubMQtUyowUarOKrAa4tTvKFKyQIF3ZRvM8OQ7RqS1S421SQB1iarwnohYNLL/q...

A new key pikaci_apple_ssh_key is appended to infra/secrets/pika-news.yaml. The value is a full SSH private key encrypted with AES-256-GCM via sops.

Because sops re-encrypts the entire file when any secret is added, all existing ciphertext blobs (pika_news_github_token, pika_news_claude_oauth_token, pika_news_webhook_secret) and the per-recipient age key blocks are rotated. The recipient list and unencrypted_suffix policy remain unchanged; only the lastmodified timestamp and mac are updated to reflect the new contents.

Declare the sops secret and environment variables in the NixOS module

Intent: Tell sops-nix to decrypt the new SSH key to a file on disk with restrictive permissions, then expose its path and related Apple CI configuration to the pika-news service via its environment template.

Affected files: infra/nix/modules/pika-news.nix

Evidence
@@ -74,6 +74,14 @@ in
+  sops.secrets."pikaci_apple_ssh_key" = {
+    format = "yaml";
+    sopsFile = ../../secrets/pika-news.yaml;
+    owner = serviceUser;
+    group = serviceGroup;
+    mode = "0400";
+  };
@@ -83,6 +91,12 @@ in
+      PIKACI_APPLE_SSH_KEY_FILE=${config.sops.secrets."pikaci_apple_ssh_key".path}
+      PIKACI_APPLE_SSH_HOST=pika-mini.tail029da2.ts.net
+      PIKACI_APPLE_SSH_USER=mini
+      PIKACI_APPLE_REMOTE_ROOT=/Volumes/pikaci-data/pikaci-apple
+      PIKACI_APPLE_KEEP_RUNS=3
+      PIKACI_APPLE_LOCK_TIMEOUT_SEC=1800

Two additions are made to infra/nix/modules/pika-news.nix:

1. Secret declaration (lines 77-82)

sops.secrets."pikaci_apple_ssh_key" = {
  format = "yaml";
  sopsFile = ../../secrets/pika-news.yaml;
  owner = serviceUser;
  group = serviceGroup;
  mode = "0400";
};

This mirrors the pattern already used for the GitHub token. The file is owned by the service user and readable only by that user (0400), which is critical since it contains a private key.

2. Environment template additions (lines 94-99)

Six new variables are injected into the pika-news-env sops template:

VariablePurpose
PIKACI_APPLE_SSH_KEY_FILEAbsolute path to the decrypted SSH key on disk (resolved via config.sops.secrets."pikaci_apple_ssh_key".path)
PIKACI_APPLE_SSH_HOSTTailscale hostname of the Apple CI runner
PIKACI_APPLE_SSH_USERSSH username on the runner
PIKACI_APPLE_REMOTE_ROOTRemote working directory for CI data
PIKACI_APPLE_KEEP_RUNSNumber of historical runs to retain
PIKACI_APPLE_LOCK_TIMEOUT_SECMaximum seconds to wait for the runner lock

Using config.sops.secrets.…path rather than hard-coding a path ensures the value stays correct even if sops-nix changes its secrets directory layout.

Update the CI script to prefer a file-based SSH key

Intent: Make `pikaci-apple-github-step` read the SSH key from the file path provided by sops-nix when available, while keeping backward compatibility with the inline environment variable.

Affected files: scripts/pikaci-apple-github-step

Evidence
@@ -164,7 +164,11 @@ remote_run() {
-  printf '%s\n' "$PIKACI_APPLE_SSH_KEY" >"$tmp_key"
+  if [[ -n "${PIKACI_APPLE_SSH_KEY_FILE:-}" ]]; then
+    cp "$PIKACI_APPLE_SSH_KEY_FILE" "$tmp_key"
+  else
+    printf '%s\n' "$PIKACI_APPLE_SSH_KEY" >"$tmp_key"
+  fi

Inside the remote_run() function at line 167 of scripts/pikaci-apple-github-step, the key-loading logic is replaced with a conditional:

if [[ -n "${PIKACI_APPLE_SSH_KEY_FILE:-}" ]]; then
  cp "$PIKACI_APPLE_SSH_KEY_FILE" "$tmp_key"
else
  printf '%s\n' "$PIKACI_APPLE_SSH_KEY" >"$tmp_key"
fi

Why this matters:

  • File-based path (new): When PIKACI_APPLE_SSH_KEY_FILE is set, the key is already on disk (decrypted by sops-nix), so it is simply copied to the ephemeral temp file that the SSH wrapper uses. The key never passes through an environment variable, which avoids exposure in /proc/*/environ or process listings.
  • Inline fallback (existing): If the file variable is unset or empty, the script falls back to writing $PIKACI_APPLE_SSH_KEY directly, preserving compatibility with environments that still inject the key as a string (e.g., local development or other CI systems).
  • The ${PIKACI_APPLE_SSH_KEY_FILE:-} default-value syntax prevents set -u (nounset) from aborting when the variable is undefined.

The existing cleanup_key trap and chmod 600 on $tmp_key (set earlier in the function) continue to apply regardless of which branch is taken.

Diff