Back to feed

sledtools/pika branch #139

apple-mini-ssh-contract

ci: harden apple mini ssh contract

Target branch: master

Merge Commit: 4f85ffb06521fc1dc45349b3b1d877c70db50386

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 #175 success

3 passed

head 680efbf9aacc05e0e983921bdd3fab56e5295714 · queued 2026-03-27 01:10:25 · 3 lane(s)

queued 8s · ran 11m 05s

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

Summary

This branch hardens the SSH authentication contract used by the Apple mini CI wrapper (scripts/pikaci-apple-remote.sh). Previously the wrapper accepted raw key material via the PIKACI_APPLE_SSH_KEY environment variable, wrote it to a temporary file at runtime, and had conditional logic around whether the SSH binary was explicitly set. The new contract requires the caller to supply a pre-materialized key file path via PIKACI_APPLE_SSH_KEY_FILE, removes the in-process secret-to-file materialization, enforces IdentitiesOnly=yes with IdentityAgent=none unconditionally, and fails fast if the key file is missing. The companion documentation is updated to explain the at-rest vs. runtime secret distinction, the single-path SSH auth model, and the separation between the CI transport wrapper and manual operator SSH access.

Tutorial Steps

Document the separation between CI wrapper and manual SSH access

Intent: Clarify for operators that the checked-in wrapper script is the forge/CI transport contract, not the path for manual login. Direct SSH to the mini remains available for human operators.

Affected files: docs/pikaci-apple-remote-access.md

Evidence
@@ -64,10 +64,19 @@
+For manual operator access, keep using plain SSH directly to the mini:
+
+```bash
+ssh mini@pika-mini.tail029da2.ts.net
+```
+
+The checked-in wrapper is the forge/CI transport contract, not the general manual login path.

A new documentation section explicitly separates human operator access from CI transport.

Operators who need interactive access to the Apple mini should continue to SSH directly:

ssh mini@pika-mini.tail029da2.ts.net

The pikaci-apple-remote.sh wrapper is scoped exclusively to forge/CI jobs. This prevents confusion about which authentication path a human should use versus what the automation expects.

Document the single-path SSH auth contract

Intent: Make the runtime SSH authentication model explicit in the docs: the wrapper requires `PIKACI_APPLE_SSH_KEY_FILE`, forces `IdentitiesOnly=yes` and `IdentityAgent=none`, and never falls back to ambient agent keys.

Affected files: docs/pikaci-apple-remote-access.md

Evidence
@@ -92,6 +101,8 @@
+- Runtime SSH auth is single-path for the wrapper: `./scripts/pikaci-apple-remote.sh` must receive an explicit identity file via `PIKACI_APPLE_SSH_KEY_FILE`, and it always forces `IdentitiesOnly=yes` with `IdentityAgent=none`. It does not fall back to ambient `ssh-agent` keys.
@@ -92,6 +101,8 @@
+At rest, the secret is stored as inline key material (`PIKACI_APPLE_SSH_KEY`). At runtime, the forge host materializes that secret to a file path and exports `PIKACI_APPLE_SSH_KEY_FILE` to the wrapper. The wrapper's single-path contract is the runtime file path, not the encrypted-at-rest variable name.

Two new documentation paragraphs describe the layered secret model:

  1. At rest — the secret lives as inline key material in PIKACI_APPLE_SSH_KEY inside the SOPS-encrypted secrets file.
  2. At runtime — the forge host (e.g., the GitHub Actions runner) materializes that secret to a temporary file and exports the path as PIKACI_APPLE_SSH_KEY_FILE.

The wrapper's contract is the runtime file path, not the encrypted-at-rest variable. This means the wrapper never handles raw key material directly, reducing the blast radius if the wrapper's environment is logged or leaked.

Remove in-process key materialization from the wrapper

Intent: Eliminate the code path that accepted raw key material via the `PIKACI_APPLE_SSH_KEY` environment variable and wrote it to a temp file. Replace it with a strict requirement for a pre-existing key file path.

Affected files: scripts/pikaci-apple-remote.sh

Evidence
@@ -65,10 +65,6 @@
-ssh_binary_defaulted=1
-if [[ -n "${PIKACI_APPLE_SSH_BINARY:-}" ]]; then
-  ssh_binary_defaulted=0
-fi
@@ -76,7 +72,7 @@
-ssh_key_override="${PIKACI_APPLE_SSH_KEY:-}"
+ssh_key_path="${PIKACI_APPLE_SSH_KEY_FILE:-}"

The variable initialization block is simplified:

  • The ssh_binary_defaulted flag and its conditional check are removed entirely. The wrapper no longer needs to know whether the SSH binary was explicitly overridden to decide whether to set up key-based auth.
  • ssh_key_override (which held raw key material from PIKACI_APPLE_SSH_KEY) is replaced by ssh_key_path (which holds a file path from PIKACI_APPLE_SSH_KEY_FILE).

This shifts the responsibility for writing key material to disk from the wrapper to the calling forge host, which is the correct layer to handle secret materialization.

Add fail-fast validation in prepare_ssh_binary

Intent: Ensure the wrapper exits immediately with a clear error if the required SSH key file is not set or does not exist, rather than silently falling through or producing a confusing SSH error downstream.

Affected files: scripts/pikaci-apple-remote.sh

Evidence
@@ -201,23 +197,27 @@
+  if [[ -z "$ssh_key_path" ]]; then
+    echo "error: set PIKACI_APPLE_SSH_KEY_FILE" >&2
+    exit 2
+  fi
+
+  if [[ ! -f "$ssh_key_path" ]]; then
+    echo "error: missing SSH private key file: $ssh_key_path" >&2
+    exit 2
+  fi
+  ssh_key_file="$ssh_key_path"

The prepare_ssh_binary function is rewritten with two guard clauses:

  1. If PIKACI_APPLE_SSH_KEY_FILE is unset or empty, the wrapper prints error: set PIKACI_APPLE_SSH_KEY_FILE to stderr and exits with code 2.
  2. If the path is set but the file does not exist, the wrapper prints the missing path and exits with code 2.

Previously, the function would silently return if no key override was provided or if the SSH binary was explicitly set, which could lead to confusing authentication failures later in the pipeline. The new behavior makes misconfiguration immediately obvious.

Stop writing temp key files and fix the SSH wrapper generation

Intent: Remove the temp-file key materialization logic and fix the SSH wrapper to use the configured ssh_binary correctly via printf quoting.

Affected files: scripts/pikaci-apple-remote.sh

Evidence
@@ -201,23 +197,27 @@
-  ssh_key_file="$(mktemp "${TMPDIR:-/tmp}/pikaci-apple-ssh-key.XXXXXX")"
   ssh_wrapper="$(mktemp "${TMPDIR:-/tmp}/pikaci-apple-ssh-wrapper.XXXXXX")"
-  chmod 600 "$ssh_key_file"
-  printf '%s\n' "$ssh_key_override" >"$ssh_key_file"
   cat >"$ssh_wrapper" <<EOF
 #!/usr/bin/env bash
-exec ssh \\
+exec $(printf '%q' "$ssh_binary") \\
@@ -201,23 +197,27 @@
-  rm -f "$ssh_wrapper" "$ssh_key_file"
+  rm -f "$ssh_wrapper"

Three related changes tighten the wrapper's security posture:

  1. No more temp key file — the mktemp + chmod 600 + printf sequence that wrote raw key material to a temp file is deleted. The wrapper now points ssh_key_file directly at the caller-supplied path.
  2. Cleanup simplified — the cleanup trap no longer removes $ssh_key_file since the wrapper did not create it. Only the SSH wrapper script (which is still generated) is cleaned up.
  3. SSH binary correctly quoted — the generated wrapper script now uses $(printf '%q' "$ssh_binary") instead of a hardcoded ssh, ensuring that a custom SSH binary path with spaces or special characters is handled correctly. This also means the IdentitiesOnly=yes and IdentityAgent=none options are always applied regardless of whether a custom binary is configured, closing a previous gap where these options were only set conditionally.

Diff