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:
| Variable | Purpose |
PIKACI_APPLE_SSH_KEY_FILE | Absolute path to the decrypted SSH key on disk (resolved via config.sops.secrets."pikaci_apple_ssh_key".path) |
PIKACI_APPLE_SSH_HOST | Tailscale hostname of the Apple CI runner |
PIKACI_APPLE_SSH_USER | SSH username on the runner |
PIKACI_APPLE_REMOTE_ROOT | Remote working directory for CI data |
PIKACI_APPLE_KEEP_RUNS | Number of historical runs to retain |
PIKACI_APPLE_LOCK_TIMEOUT_SEC | Maximum 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.