#!/usr/bin/env bash # # age-keygen.sh — generate the SOPS age keypair for the platform. # # Phase 1, T-1.4 (ADR-0007 SOPS+age, R-SEC-01). AUTHORED, NOT RUN. # ============================================================================= # *** THE OPERATOR RUNS THIS, ONCE, AT BOOTSTRAP. AGENTS MUST NOT RUN IT. *** # ============================================================================= # # What it does: # - Generates an age keypair to a path OUTSIDE this git repo (default: # ~/.config/sops/age/keys.txt — SOPS's conventional location, honoured by the # SOPS_AGE_KEY_FILE default lookup). # - Prints ONLY the PUBLIC recipient (age1...) to stdout. The public recipient # is safe to paste into bootstrap/sops/.sops.yaml. The PRIVATE key stays in # the file and is NEVER printed, NEVER committed (see .gitignore: age.key, # *.key). Spec docs/07 + AGENTS.md §5: no secret material in logs/PR/output. # # Hard guarantees: # - REFUSES to write anywhere inside this repository (private key must never # land where git could track it). # - REFUSES to overwrite an existing key file (rotation is deliberate — see # age-key-secret.md "Rotation"). # # Next step after running this: install the private key into the cluster as the # `sops-age` Secret — see age-key-secret.md. Then paste the printed recipient # into .sops.yaml. # set -euo pipefail # --- resolve target path (override with AGE_KEY_FILE) ------------------------- KEY_FILE="${AGE_KEY_FILE:-${HOME}/.config/sops/age/keys.txt}" # --- refuse to write inside the repo ----------------------------------------- # Repo root = git toplevel if available, else two levels up from this script # (bootstrap/sops/ -> repo root). script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if repo_root="$(git -C "${script_dir}" rev-parse --show-toplevel 2>/dev/null)"; then : else repo_root="$(cd "${script_dir}/../.." && pwd)" fi abspath() { # portable realpath for a possibly-not-yet-existing path local p="$1" local d b d="$(dirname "$p")" b="$(basename "$p")" mkdir -p "$d" printf '%s/%s\n' "$(cd "$d" && pwd)" "$b" } key_abs="$(abspath "${KEY_FILE}")" case "${key_abs}/" in "${repo_root}/"*) echo "ERROR: refusing to write the age private key inside the repo:" >&2 echo " ${key_abs}" >&2 echo " repo root: ${repo_root}" >&2 echo " Set AGE_KEY_FILE to a path OUTSIDE the repo (default is fine)." >&2 exit 1 ;; esac # --- refuse to clobber an existing key --------------------------------------- if [[ -e "${key_abs}" ]]; then echo "ERROR: ${key_abs} already exists — refusing to overwrite." >&2 echo " Rotating? Move the old key aside deliberately (see age-key-secret.md)." >&2 exit 1 fi command -v age-keygen >/dev/null 2>&1 || { echo "ERROR: age-keygen not found. Use the pinned dev container" >&2 echo " (infra/devcontainer, AGE_VERSION in its versions.env)." >&2 exit 1 } # --- generate ---------------------------------------------------------------- umask 077 # 0600 key file age-keygen -o "${key_abs}" >/dev/null echo "age private key written (0600) to: ${key_abs}" echo "This file is OUTSIDE the repo and must stay that way — do NOT commit it." echo echo "Public recipient (safe to share — paste into bootstrap/sops/.sops.yaml):" age-keygen -y "${key_abs}" # prints ONLY the public recipient (age1...)