security: never bundle credential repos to R2 in plaintext
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:
@@ -734,3 +734,21 @@ def test_g23_brand_css_filename_is_versioned():
|
||||
assert m, "brand CSS must carry a version in its FILENAME"
|
||||
assert (ROOT / "deploy" / "branding" / "public" / "assets" / "css"
|
||||
/ 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"}
|
||||
|
||||
@@ -23,6 +23,23 @@ BUCKET="${R2_BUCKET_BACKUPS:-windy-git-backups}"
|
||||
KEEP_DAYS="${BACKUP_KEEP_DAYS:-30}"
|
||||
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"; }
|
||||
trap cleanup EXIT
|
||||
|
||||
@@ -44,6 +61,10 @@ count=0
|
||||
for repo in "$GIT_ROOT"/*/*.git; do
|
||||
owner="$(basename "$(dirname "$repo")")"
|
||||
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"
|
||||
|
||||
# --all captures every ref, not just the default branch. A bundle of one
|
||||
|
||||
Reference in New Issue
Block a user