fix(coolify): validate the dump bindings, and stop printing $EDITOR #10
2 changed files with 50 additions and 4 deletions
|
|
@ -110,10 +110,31 @@ cat > "$SCRIPT_PATH" <<'DUMP_SCRIPT'
|
||||||
# to answer "what WAS the state" and to recover a credential otherwise lost.
|
# to answer "what WAS the state" and to recover a credential otherwise lost.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
die() { printf 'coolify-dump: ERROR: %s\n' "$1" >&2; exit 1; }
|
||||||
|
|
||||||
: "${AGE_RECIPIENT:?not set — fill /etc/coolify-dump.env (age PUBLIC key)}"
|
: "${AGE_RECIPIENT:?not set — fill /etc/coolify-dump.env (age PUBLIC key)}"
|
||||||
: "${S3_BUCKET:?not set — fill /etc/coolify-dump.env (e.g. s3://backups/coolify-db)}"
|
: "${S3_BUCKET:?not set — fill /etc/coolify-dump.env (e.g. s3://backups/coolify-db)}"
|
||||||
: "${S3_ENDPOINT:?not set — fill /etc/coolify-dump.env (e.g. https://hel1.your-objectstorage.com)}"
|
: "${S3_ENDPOINT:?not set — fill /etc/coolify-dump.env (e.g. https://hel1.your-objectstorage.com)}"
|
||||||
|
|
||||||
|
# Validate the bindings HERE, before spending a pg_dump on them. A bare bucket
|
||||||
|
# name reads to `aws` as a LOCAL path, so it fails deep in the upload with
|
||||||
|
# "Invalid argument type" and a usage dump — after the database has been read
|
||||||
|
# and encrypted, and with nothing pointing at the actual mistake.
|
||||||
|
case "$S3_BUCKET" in
|
||||||
|
s3://?*) ;;
|
||||||
|
*) die "S3_BUCKET must be an s3:// URI (got: '${S3_BUCKET}') — aws reads a bare bucket name as a local path" ;;
|
||||||
|
esac
|
||||||
|
case "$S3_ENDPOINT" in
|
||||||
|
http://?*|https://?*) ;;
|
||||||
|
*) die "S3_ENDPOINT needs a scheme (got: '${S3_ENDPOINT}') — e.g. https://hel1.your-objectstorage.com" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# NOTE: no check can tell you the recipient is the RIGHT key. age's X25519
|
||||||
|
# header does not reveal who it encrypts to, so a valid-but-wrong recipient
|
||||||
|
# (staging's key instead of prod's) produces a perfect backup nobody can open.
|
||||||
|
# Only decrypting an artifact proves that. Do it once, from a machine that
|
||||||
|
# holds the private key — never on this box.
|
||||||
|
|
||||||
PG_CONTAINER="${PG_CONTAINER:-coolify-db}"
|
PG_CONTAINER="${PG_CONTAINER:-coolify-db}"
|
||||||
PG_USER="${PG_USER:-coolify}"
|
PG_USER="${PG_USER:-coolify}"
|
||||||
PG_DB="${PG_DB:-coolify}"
|
PG_DB="${PG_DB:-coolify}"
|
||||||
|
|
@ -213,14 +234,25 @@ cat <<EOF
|
||||||
rig-coolify-backup: installed. The timer is live but the backup does NOT work yet —
|
rig-coolify-backup: installed. The timer is live but the backup does NOT work yet —
|
||||||
rig has no credentials and cannot verify an upload. Two steps remain, both yours:
|
rig has no credentials and cannot verify an upload. Two steps remain, both yours:
|
||||||
|
|
||||||
1. Fill in the bindings: \$EDITOR ${ENV_FILE}
|
1. Fill in the bindings: nano ${ENV_FILE}
|
||||||
(age recipient = a PUBLIC key; S3 bucket, endpoint, access key, secret, region)
|
(age recipient = a PUBLIC key; S3 bucket, endpoint, access key, secret, region)
|
||||||
|
S3_BUCKET must be an s3:// URI, not a bare bucket name.
|
||||||
|
|
||||||
2. Prove it end to end — do NOT wait for the timer to find out:
|
2. Run it once by hand — do NOT wait for the timer to find out:
|
||||||
systemctl start coolify-dump.service
|
systemctl start coolify-dump.service
|
||||||
journalctl -u coolify-dump.service -n 20 --no-pager
|
journalctl -u coolify-dump.service -n 20 --no-pager
|
||||||
then confirm the object really landed and is really age ciphertext:
|
|
||||||
aws s3 ls "\$S3_BUCKET/" --endpoint-url "\$S3_ENDPOINT"
|
3. Prove you can OPEN it. A successful upload only proves the file ARRIVED.
|
||||||
|
If the recipient is the wrong key, every run succeeds forever and produces
|
||||||
|
an artifact nobody can decrypt — and you cannot tell by looking at it.
|
||||||
|
From a machine holding the private key (NEVER this box), stream it down
|
||||||
|
and decrypt; you want PostgreSQL SQL out the other end:
|
||||||
|
|
||||||
|
ssh root@$(hostname) 'set -a; . ${ENV_FILE}; set +a; \\
|
||||||
|
line=\$(aws s3 ls "\$S3_BUCKET/" --endpoint-url "\$S3_ENDPOINT" | sort | tail -1); \\
|
||||||
|
aws s3 cp "\$S3_BUCKET/\${line##* }" - --endpoint-url "\$S3_ENDPOINT"' \\
|
||||||
|
| age -d -i <your-key-file> | { head -5; cat >/dev/null; }
|
||||||
|
|
||||||
A backup you have never read back is not yet a backup.
|
A backup you have never read back is not yet a backup.
|
||||||
|
|
||||||
Until step 1 is done the unit fails loudly on every run. That is deliberate — a
|
Until step 1 is done the unit fails loudly on every run. That is deliberate — a
|
||||||
|
|
|
||||||
14
test/cli.sh
14
test/cli.sh
|
|
@ -80,6 +80,20 @@ else
|
||||||
echo "skip: runner non-root refusal (running as root)"
|
echo "skip: runner non-root refusal (running as root)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# The dump script ships to control-plane boxes as an embedded heredoc. A syntax
|
||||||
|
# error in it would be invisible here and would first surface at 04:00 on a live
|
||||||
|
# control plane. Extract it and syntax-check what actually gets written.
|
||||||
|
DUMP_TMP="$(mktemp)"
|
||||||
|
sed -n "/<<'DUMP_SCRIPT'/,/^DUMP_SCRIPT\$/p" "$ROOT/commands/coolify-backup-install.sh" \
|
||||||
|
| sed '1d;$d' > "$DUMP_TMP"
|
||||||
|
check "embedded dump script extracted (guards the sed above)" 0 "" grep -q "pg_dump" "$DUMP_TMP"
|
||||||
|
check "embedded dump script is valid bash" 0 "" bash -n "$DUMP_TMP"
|
||||||
|
check "embedded dump script rejects a bare bucket name" 1 "must be an s3:// URI" \
|
||||||
|
env AGE_RECIPIENT=age1x S3_BUCKET=my-bucket S3_ENDPOINT=https://s3.example.com bash "$DUMP_TMP"
|
||||||
|
check "embedded dump script rejects a schemeless endpoint" 1 "needs a scheme" \
|
||||||
|
env AGE_RECIPIENT=age1x S3_BUCKET=s3://b/k S3_ENDPOINT=s3.example.com bash "$DUMP_TMP"
|
||||||
|
rm -f "$DUMP_TMP"
|
||||||
|
|
||||||
# Regression: /etc/os-release defines VERSION (e.g. "13 (trixie)" on Debian);
|
# Regression: /etc/os-release defines VERSION (e.g. "13 (trixie)" on Debian);
|
||||||
# sourcing it in the main shell clobbers a script's $VERSION and splices the
|
# sourcing it in the main shell clobbers a script's $VERSION and splices the
|
||||||
# OS string into download URLs. It must only ever be sourced in a subshell.
|
# OS string into download URLs. It must only ever be sourced in a subshell.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue