diff --git a/Scripts/_burrow-secrets.sh b/Scripts/_burrow-secrets.sh index e08bf2a..6f1bc28 100644 --- a/Scripts/_burrow-secrets.sh +++ b/Scripts/_burrow-secrets.sh @@ -107,18 +107,25 @@ burrow_encrypt_secret_from_file() { local secret_path="$2" local source_path="$3" local agenix_path - local identity_path + local backup_file="" if [[ ! -s "${source_path}" ]]; then echo "secret source missing or empty: ${source_path}" >&2 return 1 fi agenix_path="$(burrow_secret_repo_path "${repo_root}" "${secret_path}")" - identity_path="$(burrow_agenix_identity_path "${repo_root}")" - - if [[ -n "${identity_path}" ]]; then - nix --extra-experimental-features "nix-command flakes" run "${repo_root}#agenix" -- -e "${agenix_path}" -i "${identity_path}" < "${source_path}" - else - nix --extra-experimental-features "nix-command flakes" run "${repo_root}#agenix" -- -e "${agenix_path}" < "${source_path}" + if [[ -f "${secret_path}" ]]; then + backup_file="$(mktemp "${TMPDIR:-/tmp}/burrow-secret-backup.XXXXXX")" + cp "${secret_path}" "${backup_file}" fi + rm -f "${secret_path}" + + if ! nix --extra-experimental-features "nix-command flakes" run "${repo_root}#agenix" -- -e "${agenix_path}" < "${source_path}"; then + if [[ -n "${backup_file}" && -f "${backup_file}" ]]; then + mv "${backup_file}" "${secret_path}" + fi + return 1 + fi + + [[ -n "${backup_file}" ]] && rm -f "${backup_file}" } diff --git a/Scripts/provision-forgejo-nsc.sh b/Scripts/provision-forgejo-nsc.sh index b8c9f12..537107e 100755 --- a/Scripts/provision-forgejo-nsc.sh +++ b/Scripts/provision-forgejo-nsc.sh @@ -146,25 +146,36 @@ dispatcher_secret="${REPO_ROOT}/secrets/forgejo/nsc-dispatcher-config.age" autoscaler_secret="${REPO_ROOT}/secrets/forgejo/nsc-autoscaler-config.age" if [[ "${REFRESH_TOKEN}" -eq 1 ]]; then - "${NSC_BIN}" auth check-login --duration 20m >/dev/null - raw_token_file="$(mktemp)" - trap 'rm -f "${raw_token_file}"; cleanup' EXIT - "${NSC_BIN}" auth generate-dev-token --output_to "${raw_token_file}" >/dev/null - RAW_NSC_TOKEN_FILE="${raw_token_file}" TOKEN_FILE="${token_file}" python3 - <<'PY' + ssh \ + -i "${SSH_KEY}" \ + -o IdentitiesOnly=yes \ + -o UserKnownHostsFile="${KNOWN_HOSTS_FILE}" \ + -o StrictHostKeyChecking=accept-new \ + "${HOST}" \ + 'sudo -u forgejo-nsc python3 - <<'"'"'PY'"'"' import json -import os from pathlib import Path -raw = Path(os.environ["RAW_NSC_TOKEN_FILE"]).read_text(encoding="utf-8").strip() -if not raw: - raise SystemExit("generated Namespace token is empty") +payload = {} -Path(os.environ["TOKEN_FILE"]).write_text( - json.dumps({"bearer_token": raw}, indent=2) + "\n", - encoding="utf-8", -) -PY - rm -f "${raw_token_file}" +token_json = Path("/var/lib/forgejo-nsc/.config/ns/token.json") +if token_json.exists(): + data = json.loads(token_json.read_text(encoding="utf-8")) + session = str(data.get("session_token", "")).strip() + if session: + payload["session_token"] = session + +token_cache = Path("/var/lib/forgejo-nsc/.config/ns/token.cache") +if token_cache.exists(): + bearer = token_cache.read_text(encoding="utf-8").strip() + if bearer: + payload["bearer_token"] = bearer + +if not payload: + raise SystemExit("forgejo-nsc host does not have a usable Namespace session") + +print(json.dumps(payload, indent=2)) +PY' > "${token_file}" chmod 600 "${token_file}" elif [[ -f "${token_secret}" ]]; then burrow_decrypt_age_secret_to_temp "${REPO_ROOT}" "${token_secret}" > "${token_file}" @@ -186,8 +197,13 @@ try: except json.JSONDecodeError: parsed = None -if isinstance(parsed, dict) and isinstance(parsed.get("bearer_token"), str) and parsed["bearer_token"].strip(): - raise SystemExit(0) +if isinstance(parsed, dict): + bearer = parsed.get("bearer_token") + session = parsed.get("session_token") + if isinstance(bearer, str) and bearer.strip(): + raise SystemExit(0) + if isinstance(session, str) and session.strip(): + raise SystemExit(0) path.write_text(json.dumps({"bearer_token": raw}, indent=2) + "\n", encoding="utf-8") PY diff --git a/Scripts/sync-forgejo-nsc-config.sh b/Scripts/sync-forgejo-nsc-config.sh index 431f832..d6ac48c 100755 --- a/Scripts/sync-forgejo-nsc-config.sh +++ b/Scripts/sync-forgejo-nsc-config.sh @@ -88,27 +88,9 @@ if [[ "${ROTATE_PAT}" -eq 1 ]]; then "${SCRIPT_DIR}/provision-forgejo-nsc.sh" --host "${HOST}" --ssh-key "${SSH_KEY}" fi -token_file="$( - burrow_resolve_secret_file \ - "${REPO_ROOT}" \ - "" \ - "" \ - "${REPO_ROOT}/secrets/forgejo/nsc-token.age" -)" -dispatcher_file="$( - burrow_resolve_secret_file \ - "${REPO_ROOT}" \ - "" \ - "" \ - "${REPO_ROOT}/secrets/forgejo/nsc-dispatcher-config.age" -)" -autoscaler_file="$( - burrow_resolve_secret_file \ - "${REPO_ROOT}" \ - "" \ - "" \ - "${REPO_ROOT}/secrets/forgejo/nsc-autoscaler-config.age" -)" +token_file="${REPO_ROOT}/secrets/forgejo/nsc-token.age" +dispatcher_file="${REPO_ROOT}/secrets/forgejo/nsc-dispatcher-config.age" +autoscaler_file="${REPO_ROOT}/secrets/forgejo/nsc-autoscaler-config.age" for path in "${token_file}" "${dispatcher_file}" "${autoscaler_file}"; do if [[ ! -s "${path}" ]]; then diff --git a/secrets/forgejo/nsc-autoscaler-config.age b/secrets/forgejo/nsc-autoscaler-config.age index 5d9aa28..460d194 100644 Binary files a/secrets/forgejo/nsc-autoscaler-config.age and b/secrets/forgejo/nsc-autoscaler-config.age differ diff --git a/secrets/forgejo/nsc-dispatcher-config.age b/secrets/forgejo/nsc-dispatcher-config.age index f3306ab..7c752b1 100644 Binary files a/secrets/forgejo/nsc-dispatcher-config.age and b/secrets/forgejo/nsc-dispatcher-config.age differ diff --git a/secrets/forgejo/nsc-token.age b/secrets/forgejo/nsc-token.age index fbabf0c..3ac1612 100644 Binary files a/secrets/forgejo/nsc-token.age and b/secrets/forgejo/nsc-token.age differ diff --git a/services/forgejo-nsc/README.md b/services/forgejo-nsc/README.md index 3b819d4..cdbb2bf 100644 --- a/services/forgejo-nsc/README.md +++ b/services/forgejo-nsc/README.md @@ -158,10 +158,11 @@ instances: For Burrow, use `Scripts/provision-forgejo-nsc.sh` to mint the Forgejo PAT, generate a Namespace token from the logged-in Namespace account, and refresh `secrets/forgejo/{nsc-token,nsc-dispatcher-config,nsc-autoscaler-config}.age`. -The token file is emitted as JSON with a `bearer_token` field so both the -Compute API path and the `nsc` CLI fallback can consume the same secret -material. The forge host consumes the encrypted secrets through agenix; avoid -keeping local plaintext `intake/` copies around. +The token file is emitted as JSON with a long-lived `session_token` plus the +current `bearer_token`. The `nsc` CLI paths use the session-backed login flow, +while the Compute API path can consume the bearer token directly. The forge +host consumes the encrypted secrets through agenix; avoid keeping local +plaintext `intake/` copies around. Long-lived runtime state is now sourced from age-encrypted files: diff --git a/services/forgejo-nsc/internal/nsc/dispatcher.go b/services/forgejo-nsc/internal/nsc/dispatcher.go index 7fa6d62..a3291a4 100644 --- a/services/forgejo-nsc/internal/nsc/dispatcher.go +++ b/services/forgejo-nsc/internal/nsc/dispatcher.go @@ -197,6 +197,10 @@ func (d *Dispatcher) LaunchRunner(ctx context.Context, req LaunchRequest) (strin args = append(args, "--", "/bin/sh", "-c", script) cmd := exec.CommandContext(ctx, d.opts.BinaryPath, args...) + // The Linux `nsc run` path uses the CLI auth flow. Keep using the service + // account's refreshed Namespace login session instead of forcing the + // short-lived NSC_TOKEN_FILE bearer token into CLI requests. + cmd.Env = nscCLIEnv() var buf bytes.Buffer cmd.Stdout = &buf cmd.Stderr = &buf