security: never bundle credential repos to R2 in plaintext
All checks were successful
check / gate (push) Successful in 23s
canary / probe (push) Successful in 7s

kit-army-config (the lockbox) and every *-soul / anima repo carry
credentials; the nightly R2 bundles are unencrypted, so the R2 key was a
key to every secret. Excluded by name (BACKUP_EXCLUDE); they are backed up
encrypted by the Windy Drops lane (restic) and stay mirrored on Veron.
Behavioural test runs the script's own exclusion function.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 10:48:42 -04:00
parent 7a63f90da3
commit 50c1464043
2 changed files with 39 additions and 0 deletions

View File

@@ -734,3 +734,21 @@ def test_g23_brand_css_filename_is_versioned():
assert m, "brand CSS must carry a version in its FILENAME" assert m, "brand CSS must carry a version in its FILENAME"
assert (ROOT / "deploy" / "branding" / "public" / "assets" / "css" assert (ROOT / "deploy" / "branding" / "public" / "assets" / "css"
/ f"theme-windy.v{m.group(1)}.css").exists() / f"theme-windy.v{m.group(1)}.css").exists()
def test_backup_never_bundles_credential_repos_to_r2():
"""kit-army-config (the lockbox) and the *-soul / anima repos carry
credentials; the R2 bundles are plaintext. Behavioural: run the script's
own exclusion function against the names."""
import subprocess
script = (ROOT / "scripts" / "backup.sh").read_text()
fn = script[script.index('EXCLUDE="'):script.index("cleanup()")]
# A file named like a pattern in cwd must not break the match (glob expansion).
probe = "cd \"$(mktemp -d)\" && touch x-soul && " + fn + (
'for n in kit-army-config anima windy-0-soul kit-0c5-soul herm-0-soul '
'soulsafe windy-chat eternitas; do excluded "$n" && echo "X $n" || echo "- $n"; done'
)
out = subprocess.run(["bash", "-c", probe], capture_output=True, text=True, check=True).stdout
skipped = {ln[2:] for ln in out.splitlines() if ln.startswith("X ")}
assert skipped == {"kit-army-config", "anima", "windy-0-soul", "kit-0c5-soul", "herm-0-soul"}

View File

@@ -23,6 +23,23 @@ BUCKET="${R2_BUCKET_BACKUPS:-windy-git-backups}"
KEEP_DAYS="${BACKUP_KEEP_DAYS:-30}" KEEP_DAYS="${BACKUP_KEEP_DAYS:-30}"
FAILED=0 FAILED=0
# NEVER bundle these to R2 (orchestrator decision 2026-09-23). They carry
# credentials in plaintext — kit-army-config IS the lockbox, and the soul repos
# hold agent memory with keys in it — and these bundles are unencrypted, so
# anyone holding the R2 key could read every secret in the fleet. They are
# backed up ENCRYPTED elsewhere (Windy Drops lane, restic, restore-tested) and
# stay mirrored on Veron's own disk in Gitea. Extended globs, matched on name.
EXCLUDE="${BACKUP_EXCLUDE:-kit-army-config anima *-soul}"
excluded() {
local n=$1 pat pats
read -ra pats <<< "$EXCLUDE" # read never glob-expands; `for p in $EXCLUDE` would
for pat in "${pats[@]}"; do
# shellcheck disable=SC2053 # unquoted RHS: glob match is the point
[[ "$n" == $pat ]] && return 0
done
return 1
}
cleanup() { rm -rf "$WORK"; } cleanup() { rm -rf "$WORK"; }
trap cleanup EXIT trap cleanup EXIT
@@ -44,6 +61,10 @@ count=0
for repo in "$GIT_ROOT"/*/*.git; do for repo in "$GIT_ROOT"/*/*.git; do
owner="$(basename "$(dirname "$repo")")" owner="$(basename "$(dirname "$repo")")"
name="$(basename "$repo" .git)" name="$(basename "$repo" .git)"
if excluded "$name"; then
log "skip ${owner}/${name} (credential-bearing: never bundled to R2 in plaintext)"
continue
fi
out="$WORK/${owner}__${name}.bundle" out="$WORK/${owner}__${name}.bundle"
# --all captures every ref, not just the default branch. A bundle of one # --all captures every ref, not just the default branch. A bundle of one