7c20d5d6e4
P0 from v1.1 multi-persona code review (Pass 1, correctness sweep).
The three v1.1-introduced shell helpers (backup.sh, restore.sh,
rotate_secrets.sh) hardcoded the old cto-inboxos owner in the clone URL,
push URL, and default REPO_OWNER. With the org migration to jc-capital
landed at a068c8a, those hardcodes would fail at first invocation:
- scripts/backup.sh:151 clone URL
- scripts/restore.sh:145 push --mirror URL
- scripts/rotate_secrets.sh:32-33 default REPO_OWNER
- scripts/rotate_secrets.sh:101,131 string-literal fallback
The runbook §6.2 service-account line (562) also referenced the old
owner; the runbook lines that are CPO-managed were left alone.
---ci---
project: inboxos
phase: 1
milestone: v1.1
status: review
---/ci---
147 lines
5.0 KiB
Bash
Executable File
147 lines
5.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# scripts/rotate_secrets.sh — rotate a Gitea repo secret in place (REQ-INB-21).
|
|
#
|
|
# Usage:
|
|
# scripts/rotate_secrets.sh [--apply] <secret_name>
|
|
#
|
|
# In --dry-run mode (default), the helper prints a redacted diff of
|
|
# the existing vs. proposed value and exits 0 without making the PUT.
|
|
# In --apply mode the helper PUTs the new value to the Gitea
|
|
# actions-secrets endpoint via pipelines.common.gitea_request. The
|
|
# Gitea actions-secrets PUT is in-place (it overwrites the existing
|
|
# secret at the same name); the runbook §6.4 ordering is preserved
|
|
# by verifying the new value is readable after the PUT.
|
|
#
|
|
# The new value is read from STDIN (never from a CLI argument) so it
|
|
# is not visible in `ps` / process listings.
|
|
#
|
|
# Environment:
|
|
# GITEA_INSTANCE_URL e.g. http://gitea:3000 or https://git.cloudinit.dev
|
|
# GITEA_USER HTTP Basic Auth user (the cto service account)
|
|
# GITEA_PASS HTTP Basic Auth password
|
|
# REPO_OWNER jc-capital (default)
|
|
# REPO_NAME inboxos (default)
|
|
#
|
|
# Exit codes:
|
|
# 0 success (or dry-run ok)
|
|
# 2 missing env / missing secret name / missing new value
|
|
# 3 Gitea API call failed
|
|
# 4 helper internal error (e.g. requests module import failure)
|
|
set -euo pipefail
|
|
|
|
REPO_OWNER="${REPO_OWNER:-jc-capital}"
|
|
REPO_NAME="${REPO_NAME:-inboxos}"
|
|
|
|
MODE="dry-run"
|
|
SECRET_NAME=""
|
|
if [[ "${1:-}" == "--apply" ]]; then
|
|
MODE="apply"
|
|
SECRET_NAME="${2:-}"
|
|
shift 2
|
|
else
|
|
SECRET_NAME="${1:-}"
|
|
shift 1
|
|
fi
|
|
|
|
if [[ -z "${SECRET_NAME}" ]]; then
|
|
echo "Usage: $0 [--apply] <secret_name>" >&2
|
|
echo " The new value is read from STDIN." >&2
|
|
exit 2
|
|
fi
|
|
|
|
if [[ -z "${GITEA_INSTANCE_URL:-}" || -z "${GITEA_USER:-}" || -z "${GITEA_PASS:-}" ]]; then
|
|
echo "GITEA_INSTANCE_URL, GITEA_USER, and GITEA_PASS are required." >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Read the new value from STDIN.
|
|
NEW_VALUE="$(cat)"
|
|
if [[ -z "${NEW_VALUE}" ]]; then
|
|
echo "No new value provided on STDIN." >&2
|
|
exit 2
|
|
fi
|
|
|
|
# Re-redact: show a sha256:8hex digest instead of the literal value.
|
|
_redact() {
|
|
local v="$1"
|
|
python3 -c 'import hashlib,sys; print("sha256:"+hashlib.sha256(sys.stdin.buffer.read()).hexdigest()[:8])' <<<"$v"
|
|
}
|
|
|
|
NEW_DIGEST="$(_redact "${NEW_VALUE}")"
|
|
|
|
# A Gitea secret endpoint does NOT return the secret plaintext via
|
|
# the API (Gitea never reveals stored secrets, per the GitHub-compatible
|
|
# API contract). The "old value (redacted)" in the diff is therefore
|
|
# reported as the unknown-gitea-no-plaintext marker.
|
|
OLD_DIGEST='unknown (Gitea does not return secret plaintexts via the API)'
|
|
|
|
echo "[rotate_secrets] secret_name=${SECRET_NAME} mode=${MODE}"
|
|
echo "[rotate_secrets] old value (redacted): ${OLD_DIGEST}"
|
|
echo "[rotate_secrets] new value (redacted): ${NEW_DIGEST}"
|
|
|
|
if [[ "${MODE}" == "dry-run" ]]; then
|
|
echo "[rotate_secrets] dry-run ok; no PUT issued. Re-run with --apply to commit."
|
|
exit 0
|
|
fi
|
|
|
|
# Real run: PUT the new value via the Python helper, then verify.
|
|
export GITEA_INSTANCE_URL GITEA_USER GITEA_PASS
|
|
PUT_OUT="$(
|
|
python3 - "$SECRET_NAME" "$NEW_VALUE" <<'PY'
|
|
import json, os, sys
|
|
try:
|
|
from common import gitea_request, GiteaConfig
|
|
except Exception as exc:
|
|
print(f"helper-import-failed: {exc}", file=sys.stderr)
|
|
sys.exit(4)
|
|
cfg = GiteaConfig.from_env()
|
|
secret_name, new_value = sys.argv[1], sys.argv[2]
|
|
body = {"name": secret_name, "data": new_value, "kind": "secret"}
|
|
resp = gitea_request(cfg, "PUT",
|
|
f"/api/v1/repos/{os.environ.get('REPO_OWNER', 'jc-capital')}/"
|
|
f"{os.environ.get('REPO_NAME', 'inboxos')}/actions/secrets/"
|
|
f"{secret_name}",
|
|
json_body=body)
|
|
if not resp.ok:
|
|
print(f"helper-put-failed: status={resp.status_code} body={resp.text[:200]!r}",
|
|
file=sys.stderr)
|
|
sys.exit(3)
|
|
print(json.dumps(resp.json() if resp.content else {"ok": True}))
|
|
PY
|
|
)" || {
|
|
echo "[rotate_secrets] PUT failed: ${PUT_OUT}" >&2
|
|
exit 3
|
|
}
|
|
|
|
# Verify-after-PUT: GET the secret metadata. Gitea returns the secret
|
|
# name + the (last-update) timestamp but never the plaintext. A
|
|
# successful GET with the new last-update timestamp confirms the
|
|
# rotation landed.
|
|
VERIFY_OUT="$(
|
|
python3 - "$SECRET_NAME" <<'PY'
|
|
import json, os, sys
|
|
try:
|
|
from common import gitea_request, GiteaConfig
|
|
except Exception as exc:
|
|
print(f"helper-import-failed: {exc}", file=sys.stderr)
|
|
sys.exit(4)
|
|
cfg = GiteaConfig.from_env()
|
|
secret_name = sys.argv[1]
|
|
resp = gitea_request(cfg, "GET",
|
|
f"/api/v1/repos/{os.environ.get('REPO_OWNER', 'jc-capital')}/"
|
|
f"{os.environ.get('REPO_NAME', 'inboxos')}/actions/secrets/"
|
|
f"{secret_name}")
|
|
if not resp.ok:
|
|
print(f"helper-verify-failed: status={resp.status_code} body={resp.text[:200]!r}",
|
|
file=sys.stderr)
|
|
sys.exit(3)
|
|
print(json.dumps(resp.json() if resp.content else {"ok": True}))
|
|
PY
|
|
)" || {
|
|
echo "[rotate_secrets] verify-after-PUT failed: ${VERIFY_OUT}" >&2
|
|
exit 3
|
|
}
|
|
|
|
echo "[rotate_secrets] ok secret_name=${SECRET_NAME} put=${PUT_OUT} verify=${VERIFY_OUT}"
|
|
exit 0
|