Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a13eab-1e6a-7b33-9c37-372e90ada3a2
61 lines
2.8 KiB
Bash
Executable file
61 lines
2.8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Encrypt a private service-file snapshot without a plaintext tar intermediate.
|
|
# Usage: pack-bundle.sh SECRETS_DIR PUBLIC_AGE_RECIPIENT [NEW_OUTPUT_FILE]
|
|
# Use the independently registered recipient; private age keys never belong in
|
|
# the payload. Run under owner-exclusive custody maintenance (no same-UID writers).
|
|
set +x
|
|
set -euo pipefail
|
|
umask 077
|
|
fail() { printf '%s\n' 'ERROR: bundle refused or failed; no new recovery artifact published.' >&2; exit 1; }
|
|
[[ $# -ge 2 && $# -le 3 ]] || fail
|
|
SECRETS_DIR="${1%/}"
|
|
RECIPIENT="$2"
|
|
OUTPUT="${3:-ops-bundle-$(date +%Y%m%dT%H%M%S)-$RANDOM.tar.age}"
|
|
[[ "$RECIPIENT" == age1* ]] || fail
|
|
printf '' | age -r "$RECIPIENT" >/dev/null 2>&1 || fail
|
|
[[ ! -e "$OUTPUT" && ! -L "$OUTPUT" ]] || fail
|
|
PARENT="$(dirname -- "$OUTPUT")"
|
|
[[ -d "$PARENT" ]] || fail
|
|
STAGING=$(mktemp -d "$PARENT/.netkingdom-bundle-XXXXXXXX") || fail
|
|
trap 'rm -rf -- "$STAGING"' EXIT
|
|
trap 'exit 129' HUP
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
# Emit only member names to a private local list. Never follow links or include
|
|
# special/hardlinked files, permissive custody inputs or age private material.
|
|
if ! python3 - "$SECRETS_DIR" "$OUTPUT" > "$STAGING/members" 2>/dev/null <<'PY'
|
|
import os, pathlib, stat, sys
|
|
root = pathlib.Path(os.path.abspath(sys.argv[1]))
|
|
output = pathlib.Path(os.path.abspath(sys.argv[2]))
|
|
for path in (root, *root.parents, output.parent, *output.parent.parents):
|
|
if path.is_symlink(): raise ValueError()
|
|
if root == output or root in output.parents: raise ValueError()
|
|
count = 0
|
|
for path in [root, *sorted(root.rglob('*'))]:
|
|
info = path.lstat()
|
|
if info.st_uid != os.getuid() or info.st_mode & 0o077: raise ValueError()
|
|
if stat.S_ISDIR(info.st_mode): continue
|
|
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_size > 32 * 1024 * 1024:
|
|
raise ValueError()
|
|
with path.open('rb') as source:
|
|
if b'AGE-SECRET-KEY-' in source.read(): raise ValueError()
|
|
name = str(path.relative_to(root.parent))
|
|
if any(ord(c) < 32 for c in name): raise ValueError()
|
|
sys.stdout.buffer.write(name.encode() + b'\0')
|
|
count += 1
|
|
if not count: raise ValueError()
|
|
PY
|
|
then fail; fi
|
|
# Source/root are data arguments; member names are NUL delimited and verbatim.
|
|
if ! tar -C "$(dirname -- "$SECRETS_DIR")" --null --verbatim-files-from \
|
|
--no-recursion -T "$STAGING/members" -cf - 2>/dev/null \
|
|
| age -r "$RECIPIENT" -o "$STAGING/ciphertext" 2>/dev/null; then fail; fi
|
|
python3 - "$STAGING/ciphertext" 2>/dev/null <<'PYSYNC' || fail
|
|
import os, sys
|
|
fd=os.open(sys.argv[1], os.O_RDONLY)
|
|
try: os.fsync(fd)
|
|
finally: os.close(fd)
|
|
PYSYNC
|
|
# Hard-link publication atomically refuses an output created concurrently.
|
|
ln -T -- "$STAGING/ciphertext" "$OUTPUT" 2>/dev/null || fail
|
|
printf '%s\n' 'Encrypted bundle published privately; retain it off-host and independently verify recovery before removing source custody.'
|