From c3f812ddaf4af0fd4d4c6e41cac2fa356d3024ce Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Sun, 12 Jul 2026 19:55:39 +0000 Subject: [PATCH] fix(coolify): validate the dump bindings, and stop printing $EDITOR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both found by the first run on a real control-plane box — neither was reachable by the argument-parsing tests. $EDITOR is unset on a freshly-bootstrapped server, which is precisely rig's target environment. The printed next-step `$EDITOR /etc/coolify-dump.env` expanded to nothing, so bash tried to EXECUTE the 0600 bindings file and said "Permission denied" — an error that reads like a filesystem problem and is not one. Print `nano`. A bare bucket name in S3_BUCKET reads to `aws` as a LOCAL path, so the upload died with "Invalid argument type" and a usage dump — after pg_dump had run and age had encrypted 14MB, with nothing in the error pointing at the actual mistake. The script now validates the bindings up front: S3_BUCKET must be an s3:// URI, S3_ENDPOINT must carry a scheme. Both fail with the value quoted and the reason stated, before a database is read. Note what still cannot be validated, and now says so in the script: age's X25519 header does not reveal its recipient, so a valid-but-WRONG key (staging's instead of prod's) yields a flawless backup nobody can open. Only decrypting an artifact proves the recipient. The printed next-steps now walk through that read-back explicitly, from a machine holding the private key — never the box. The dump script ships as an embedded heredoc, so a typo in it would first surface at 04:00 on a live control plane. test/cli.sh now extracts it and asserts it is valid bash and that both new guards fire. Co-Authored-By: Claude Opus 4.8 --- commands/coolify-backup-install.sh | 40 +++++++++++++++++++++++++++--- test/cli.sh | 14 +++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/commands/coolify-backup-install.sh b/commands/coolify-backup-install.sh index d3781d6..4f9a64b 100755 --- a/commands/coolify-backup-install.sh +++ b/commands/coolify-backup-install.sh @@ -110,10 +110,31 @@ cat > "$SCRIPT_PATH" <<'DUMP_SCRIPT' # to answer "what WAS the state" and to recover a credential otherwise lost. 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)}" : "${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)}" +# 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_USER="${PG_USER:-coolify}" PG_DB="${PG_DB:-coolify}" @@ -213,14 +234,25 @@ cat < | { head -5; cat >/dev/null; } + 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 diff --git a/test/cli.sh b/test/cli.sh index 1950c08..6261322 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -80,6 +80,20 @@ else echo "skip: runner non-root refusal (running as root)" 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); # 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. -- 2.45.2