Loading deploy/bootstrap.sh +23 −12 Changes for deploy/bootstrap.sh: 23 added lines, 12 removed lines. Original line number Diff line number Diff line Loading @@ -705,6 +705,24 @@ secret_key_to_file() { mv -f -- "$tmp" "$path" } # write_file_atomic path mode < content: path ends up holding all of content, or is # left as it was. A crash or a full disk halfway through a plain `cat >` leaves a # truncated file that the next run takes as the truth: a secrets.env cut short mints # passwords the database does not know, an offsite.env cut short a key that cannot # read the bucket. The temp file sits beside path, so mv is a rename on one # filesystem; mktemp makes it 0600 before anything is in it, it is on the disk # before the rename, and the directory is synced after it, so the new name survives # a power cut too. write_file_atomic() { local path="$1" mode="$2" tmp tmp="$(mktemp "${path}.XXXXXX")" || die "could not create a temporary file beside ${path}" remember_temp "$tmp" { cat > "$tmp" && chmod "$mode" "$tmp" && sync -- "$tmp"; } \ || die "could not write ${path}; it is left as it was" mv -f -- "$tmp" "$path" || die "could not replace ${path}; it is left as it was" sync -- "$(dirname -- "$path")" 2>/dev/null || true } # apply_setup_credential_secrets applies the Secrets behind `felis setup`'s email and # uploads-bucket screens from their host copies: felis-smtp in the control namespace and # in the workload one (the reaper's warning mails read that copy), felis-uploads-s3 in the Loading Loading @@ -4155,9 +4173,7 @@ load_or_make_secrets() { REGISTRY_PLATFORM_TOKEN="${REGISTRY_PLATFORM_TOKEN:-$(openssl rand -hex 32)}" REGISTRY_BUILD_TOKEN="${REGISTRY_BUILD_TOKEN:-$(openssl rand -hex 32)}" REGISTRY_PRUNE_TOKEN="${REGISTRY_PRUNE_TOKEN:-$(openssl rand -hex 32)}" ( umask 077 cat > "$SECRETS_ENV" <<EOF write_file_atomic "$SECRETS_ENV" 0600 <<EOF DB_PASSWORD=${DB_PASSWORD} SERVICE_TOKEN=${SERVICE_TOKEN} LIMBO_TOKEN=${LIMBO_TOKEN} Loading @@ -4169,8 +4185,6 @@ REGISTRY_PLATFORM_TOKEN=${REGISTRY_PLATFORM_TOKEN} REGISTRY_BUILD_TOKEN=${REGISTRY_BUILD_TOKEN} REGISTRY_PRUNE_TOKEN=${REGISTRY_PRUNE_TOKEN} EOF ) chmod 0600 "$SECRETS_ENV" } ensure_panel_tls_cert() { Loading Loading @@ -4432,7 +4446,8 @@ offsite_enabled() { # write_felis_toml target host:port [namespace/deployment]: the deployment is the # database's pod, where `felis db` runs pg_dump, pg_restore and psql (the host has no # PostgreSQL client); only the host copy names it. # PostgreSQL client); only the host copy names it. The file is 0600: its url holds the # database password. write_felis_toml() { local target="$1" db_addr="$2" deployment="${3:-}" deployment_line="" smtp_block auth_body auth_source_blocks registry_block archive_block offsite_section if [ -n "$deployment" ]; then Loading Loading @@ -4467,7 +4482,7 @@ deployment = \"${deployment}\"" if [ -n "$offsite_section" ]; then offsite_section="${offsite_section}"$'\n\n' # keep a blank line before the next section fi cat > "$target" <<EOF write_file_atomic "$target" 0600 <<EOF # Generated by deploy/bootstrap.sh; rerun the installer to regenerate. Hand edits are # overwritten, except [auth], [smtp], [[auth_source]], [offsite], and the operator-owned # [registry] / [archive] overrides, which carry forward. Move the install to another Loading Loading @@ -4993,9 +5008,7 @@ configure_offsite() { FELIS_OFFSITE_KEY="$(openssl rand -base64 32)" OFFSITE_KEY_NEW=1 fi ( umask 077 cat > "$OFFSITE_ENV" <<EOF write_file_atomic "$OFFSITE_ENV" 0600 <<EOF # The off-site copy's secrets (felis offsite, docs/troubleshooting.md §16). Keep a copy of # FELIS_OFFSITE_KEY somewhere other than this machine: without it the copies in the # bucket cannot be read, and this file goes with the machine. Loading @@ -5003,8 +5016,6 @@ FELIS_OFFSITE_ACCESS_KEY='${FELIS_OFFSITE_ACCESS_KEY}' FELIS_OFFSITE_SECRET_KEY='${FELIS_OFFSITE_SECRET_KEY}' FELIS_OFFSITE_KEY='${FELIS_OFFSITE_KEY}' EOF ) chmod 0600 "$OFFSITE_ENV" ok "off-site copy: secrets in ${OFFSITE_ENV}" } Loading deploy/bootstrap_test.sh +81 −2 Changes for deploy/bootstrap_test.sh: 81 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -1478,6 +1478,63 @@ else fi rm -f "$kubcalls" # --- write_file_atomic leaves the old file whole when a write fails ---------------------- # secrets.env, offsite.env and felis.host.toml were written with a plain `cat >`: a full # disk or a crash halfway left a truncated file that the next run took as the truth. wablock="$(awk '/^write_file_atomic\(\) \{/,/^}/' "$BS")" [ -n "$wablock" ] || { echo "FAIL: no write_file_atomic found in $BS"; exit 1; } [ "$(printf '%s\n' "$wablock" | wc -l)" -lt 20 ] \ || { echo "FAIL: the extracted block is not write_file_atomic -- did its closing brace move?"; exit 1; } wafile="$(mktemp)" printf '%s\n' "$wablock" > "$wafile" wadir="$(mktemp -d)" run_atomic() { # script; under the installer's shell options, its temp files removed on exit as cleanup does FNFILE="$wafile" bash -c ' set -Eeuo pipefail die() { printf "DIE: %s\n" "$*"; exit 1; } TEMP_PATHS=() remember_temp() { TEMP_PATHS+=("$1"); } trap '\''for p in "${TEMP_PATHS[@]-}"; do [ -z "$p" ] || rm -f -- "$p"; done'\'' EXIT . "$FNFILE" '"$1" 2>&1 } mode_of() { ls -l "$1" | cut -c1-10; } printf 'A=1\nB=2\n' > "$wadir/in.new" printf 'DB_PASSWORD=new\nSESSION_SECRET=new\n' > "$wadir/in.replace" out="$(run_atomic "umask 000; write_file_atomic '$wadir/new.env' 0600 < '$wadir/in.new'; echo done")" expect "an atomic write finishes" "done" "$out" expect "an atomic write holds all of its input" "$(printf 'A=1\nB=2')" "$(cat "$wadir/new.env")" expect "an atomic write is private under a permissive umask" "-rw-------" "$(mode_of "$wadir/new.env")" run_atomic "write_file_atomic '$wadir/new.env' 0640 < '$wadir/in.new'" >/dev/null expect "an atomic write sets the mode it is given" "-rw-r-----" "$(mode_of "$wadir/new.env")" printf 'DB_PASSWORD=old-and-whole\nSESSION_SECRET=kept\n' > "$wadir/old.env" out="$(run_atomic "cat() { head -c 7; return 1; }; write_file_atomic '$wadir/old.env' 0600 < '$wadir/in.replace'; echo survived")" expect "a write that fails halfway dies" "DIE: could not write $wadir/old.env; it is left as it was" "$out" expect "a write that fails halfway leaves the old file whole" \ "$(printf 'DB_PASSWORD=old-and-whole\nSESSION_SECRET=kept')" "$(cat "$wadir/old.env")" out="$(run_atomic "sync() { return 1; }; write_file_atomic '$wadir/old.env' 0600 < '$wadir/in.replace'")" expect "content that did not reach the disk does not replace the old file" "DIE: could not write $wadir/old.env" "$out" expect "the old file survives a failed sync" "DB_PASSWORD=old-and-whole" "$(cat "$wadir/old.env")" left="$(cd "$wadir" && ls -a | grep '^old\.env\.' || true)" if [ -z "$left" ]; then echo "PASS a failed write leaves no temp file beside the old one" else echo "FAIL a failed write left $left beside old.env"; fails=$((fails + 1)) fi rm -rf "$wadir" "$wafile" # The installer never runs load_or_make_secrets alone (it would mint real secrets), so # its writer is checked by what it calls. lsblock="$(awk '/^load_or_make_secrets\(\) \{/,/^}/' "$BS")" expect "secrets.env is written whole or not at all" 'write_file_atomic "$SECRETS_ENV" 0600 <<EOF' "$lsblock" case "$lsblock" in *'> "$SECRETS_ENV"'*) echo "FAIL load_or_make_secrets still writes secrets.env in place"; fails=$((fails + 1)) ;; *) echo "PASS load_or_make_secrets writes secrets.env nowhere else" ;; esac # --- installer re-runs keep the operator's [registry] overrides -------------------------- # §15's upgrade path is re-running the installer, but the build-lane mirrors and the # uploads backend live in [registry] as hand-written keys (docs/troubleshooting.md §8e or Loading @@ -1498,7 +1555,7 @@ alblock="$(awk '/^auth_lines\(\) \{/,/^}/' "$BS")" # The blocks quote themselves (the awk program uses single quotes), so they are # sourced from a file instead of being spliced into a single-quoted bash -c. fnfile="$(mktemp)" printf '%s\n' "$prblock" "$pablock" "$poblock" "$oblock" "$palblock" "$ahblock" "$alblock" "$wrblock" > "$fnfile" printf '%s\n' "$prblock" "$pablock" "$poblock" "$oblock" "$palblock" "$ahblock" "$alblock" "$wablock" "$wrblock" > "$fnfile" rdir="$(mktemp -d)" cat > "$rdir/felis.host.toml" <<'TOML' Loading Loading @@ -1539,6 +1596,9 @@ run_write() { # out-file [state-dir] [database-deployment]; under the installer' set -Eeuo pipefail trap '\''echo "ERR near line $LINENO (exit $?)" >&2'\'' ERR log() { :; } die() { printf "DIE: %s\n" "$*" >&2; exit 1; } remember_temp() { :; } [ -z "${CAT_FAILS:-}" ] || cat() { head -c 40; return 1; } persisted_smtp_block() { :; } persisted_auth_source_blocks() { :; } . "$FNFILE" Loading Loading @@ -1603,6 +1663,16 @@ else diff "$rdir/out.toml" "$rdir/out2.toml" | head fails=$((fails + 1)) fi expect "the config is private: its url holds the database password" "-rw-------" "$(ls -l "$rdir/out.toml" | cut -c1-10)" # A re-run that cannot finish writing (a full disk) keeps the config it carries from. out="$(CAT_FAILS=1 run_write "$rdir/felis.host.toml" 2>&1 || true)" expect "a config write that fails halfway dies" "DIE: could not write $rdir/felis.host.toml; it is left as it was" "$out" if cmp -s "$rdir/out.toml" "$rdir/felis.host.toml"; then echo "PASS a config write that fails halfway leaves the old config whole" else echo "FAIL a failed write cut felis.host.toml short:"; head -3 "$rdir/felis.host.toml"; fails=$((fails + 1)) fi rm -f "$rdir"/felis.host.toml.* # --- installer re-runs keep [auth]; a different root domain is refused ------------------ # The Cloudflare edge setup writes access_jwt_aud and client_ip_header into [auth] (the Loading Loading @@ -2110,7 +2180,7 @@ esac # must say so loudly. ofile="$(mktemp)" for fn in validate_offsite_settings persisted_offsite_block offsite_block offsite_enabled configure_offsite install_offsite_timer summary_offsite; do for fn in write_file_atomic validate_offsite_settings persisted_offsite_block offsite_block offsite_enabled configure_offsite install_offsite_timer summary_offsite; do blk="$(awk "/^${fn}\\(\\) \\{/,/^}/" "$BS")" [ -n "$blk" ] || { echo "FAIL: no ${fn} found in $BS"; exit 1; } [ "$(printf '%s\n' "$blk" | wc -l)" -lt 120 ] \ Loading @@ -2126,6 +2196,7 @@ run_offsite() { # script; runs with the off-site functions sourced set -Eeuo pipefail die() { printf "DIE: %s\n" "$*"; exit 1; } log() { printf "LOG: %s\n" "$*"; }; ok() { printf "OK: %s\n" "$*"; }; warn() { printf "WARN: %s\n" "$*"; } remember_temp() { :; } systemctl() { printf "SYSTEMCTL: %s\n" "$*" >&2; } fakefelis() { printf "RUN: %s\n" "$*" >&2; [ -z "${CHECK_FAILS:-}" ] || { echo "bucket: access denied" >&2; return 1; } [ -z "${KEY_MISMATCH:-}" ] || { echo "the bucket records key id 1111111111111111, this key is 2222222222222222" >&2; return 3; } Loading Loading @@ -2225,6 +2296,14 @@ out="$(FELIS_OFFSITE_ENDPOINT=https://s3.example FELIS_OFFSITE_BUCKET=felis \ FELIS_OFFSITE_KEY=c29tZXRoaW5nIGVsc2UgZW50aXJlbHkgZGlmZmVyZW50IQ== run_offsite configure_offsite)" expect "a different key is refused" "DIE: FELIS_OFFSITE_KEY differs from the key in" "$out" expect "the refusal leaves the key alone" "FELIS_OFFSITE_KEY='${key}'" "$(cat "$odir/offsite.env")" # A re-run that cannot finish writing offsite.env (a full disk) keeps the key that sealed # the bucket: a file cut short before that line would have a new key minted next run. out="$(FELIS_OFFSITE_ENDPOINT=https://s3.example FELIS_OFFSITE_BUCKET=felis \ FELIS_OFFSITE_ACCESS_KEY=AK3 run_offsite 'cat() { head -c 30; return 1; }; configure_offsite')" expect "a write of offsite.env that fails halfway dies" "DIE: could not write $odir/offsite.env; it is left as it was" "$out" expect "a failed write keeps the key that sealed the bucket" "FELIS_OFFSITE_KEY='${key}'" "$(cat "$odir/offsite.env")" expect "a failed write keeps the credentials that worked" "FELIS_OFFSITE_ACCESS_KEY='AK2'" "$(cat "$odir/offsite.env")" rm -f "$odir"/offsite.env.* rm -f "$odir/offsite.env" out="$(FELIS_OFFSITE_ENDPOINT=https://s3.example FELIS_OFFSITE_BUCKET=felis run_offsite configure_offsite)" Loading Loading
deploy/bootstrap.sh +23 −12 Changes for deploy/bootstrap.sh: 23 added lines, 12 removed lines. Original line number Diff line number Diff line Loading @@ -705,6 +705,24 @@ secret_key_to_file() { mv -f -- "$tmp" "$path" } # write_file_atomic path mode < content: path ends up holding all of content, or is # left as it was. A crash or a full disk halfway through a plain `cat >` leaves a # truncated file that the next run takes as the truth: a secrets.env cut short mints # passwords the database does not know, an offsite.env cut short a key that cannot # read the bucket. The temp file sits beside path, so mv is a rename on one # filesystem; mktemp makes it 0600 before anything is in it, it is on the disk # before the rename, and the directory is synced after it, so the new name survives # a power cut too. write_file_atomic() { local path="$1" mode="$2" tmp tmp="$(mktemp "${path}.XXXXXX")" || die "could not create a temporary file beside ${path}" remember_temp "$tmp" { cat > "$tmp" && chmod "$mode" "$tmp" && sync -- "$tmp"; } \ || die "could not write ${path}; it is left as it was" mv -f -- "$tmp" "$path" || die "could not replace ${path}; it is left as it was" sync -- "$(dirname -- "$path")" 2>/dev/null || true } # apply_setup_credential_secrets applies the Secrets behind `felis setup`'s email and # uploads-bucket screens from their host copies: felis-smtp in the control namespace and # in the workload one (the reaper's warning mails read that copy), felis-uploads-s3 in the Loading Loading @@ -4155,9 +4173,7 @@ load_or_make_secrets() { REGISTRY_PLATFORM_TOKEN="${REGISTRY_PLATFORM_TOKEN:-$(openssl rand -hex 32)}" REGISTRY_BUILD_TOKEN="${REGISTRY_BUILD_TOKEN:-$(openssl rand -hex 32)}" REGISTRY_PRUNE_TOKEN="${REGISTRY_PRUNE_TOKEN:-$(openssl rand -hex 32)}" ( umask 077 cat > "$SECRETS_ENV" <<EOF write_file_atomic "$SECRETS_ENV" 0600 <<EOF DB_PASSWORD=${DB_PASSWORD} SERVICE_TOKEN=${SERVICE_TOKEN} LIMBO_TOKEN=${LIMBO_TOKEN} Loading @@ -4169,8 +4185,6 @@ REGISTRY_PLATFORM_TOKEN=${REGISTRY_PLATFORM_TOKEN} REGISTRY_BUILD_TOKEN=${REGISTRY_BUILD_TOKEN} REGISTRY_PRUNE_TOKEN=${REGISTRY_PRUNE_TOKEN} EOF ) chmod 0600 "$SECRETS_ENV" } ensure_panel_tls_cert() { Loading Loading @@ -4432,7 +4446,8 @@ offsite_enabled() { # write_felis_toml target host:port [namespace/deployment]: the deployment is the # database's pod, where `felis db` runs pg_dump, pg_restore and psql (the host has no # PostgreSQL client); only the host copy names it. # PostgreSQL client); only the host copy names it. The file is 0600: its url holds the # database password. write_felis_toml() { local target="$1" db_addr="$2" deployment="${3:-}" deployment_line="" smtp_block auth_body auth_source_blocks registry_block archive_block offsite_section if [ -n "$deployment" ]; then Loading Loading @@ -4467,7 +4482,7 @@ deployment = \"${deployment}\"" if [ -n "$offsite_section" ]; then offsite_section="${offsite_section}"$'\n\n' # keep a blank line before the next section fi cat > "$target" <<EOF write_file_atomic "$target" 0600 <<EOF # Generated by deploy/bootstrap.sh; rerun the installer to regenerate. Hand edits are # overwritten, except [auth], [smtp], [[auth_source]], [offsite], and the operator-owned # [registry] / [archive] overrides, which carry forward. Move the install to another Loading Loading @@ -4993,9 +5008,7 @@ configure_offsite() { FELIS_OFFSITE_KEY="$(openssl rand -base64 32)" OFFSITE_KEY_NEW=1 fi ( umask 077 cat > "$OFFSITE_ENV" <<EOF write_file_atomic "$OFFSITE_ENV" 0600 <<EOF # The off-site copy's secrets (felis offsite, docs/troubleshooting.md §16). Keep a copy of # FELIS_OFFSITE_KEY somewhere other than this machine: without it the copies in the # bucket cannot be read, and this file goes with the machine. Loading @@ -5003,8 +5016,6 @@ FELIS_OFFSITE_ACCESS_KEY='${FELIS_OFFSITE_ACCESS_KEY}' FELIS_OFFSITE_SECRET_KEY='${FELIS_OFFSITE_SECRET_KEY}' FELIS_OFFSITE_KEY='${FELIS_OFFSITE_KEY}' EOF ) chmod 0600 "$OFFSITE_ENV" ok "off-site copy: secrets in ${OFFSITE_ENV}" } Loading
deploy/bootstrap_test.sh +81 −2 Changes for deploy/bootstrap_test.sh: 81 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -1478,6 +1478,63 @@ else fi rm -f "$kubcalls" # --- write_file_atomic leaves the old file whole when a write fails ---------------------- # secrets.env, offsite.env and felis.host.toml were written with a plain `cat >`: a full # disk or a crash halfway left a truncated file that the next run took as the truth. wablock="$(awk '/^write_file_atomic\(\) \{/,/^}/' "$BS")" [ -n "$wablock" ] || { echo "FAIL: no write_file_atomic found in $BS"; exit 1; } [ "$(printf '%s\n' "$wablock" | wc -l)" -lt 20 ] \ || { echo "FAIL: the extracted block is not write_file_atomic -- did its closing brace move?"; exit 1; } wafile="$(mktemp)" printf '%s\n' "$wablock" > "$wafile" wadir="$(mktemp -d)" run_atomic() { # script; under the installer's shell options, its temp files removed on exit as cleanup does FNFILE="$wafile" bash -c ' set -Eeuo pipefail die() { printf "DIE: %s\n" "$*"; exit 1; } TEMP_PATHS=() remember_temp() { TEMP_PATHS+=("$1"); } trap '\''for p in "${TEMP_PATHS[@]-}"; do [ -z "$p" ] || rm -f -- "$p"; done'\'' EXIT . "$FNFILE" '"$1" 2>&1 } mode_of() { ls -l "$1" | cut -c1-10; } printf 'A=1\nB=2\n' > "$wadir/in.new" printf 'DB_PASSWORD=new\nSESSION_SECRET=new\n' > "$wadir/in.replace" out="$(run_atomic "umask 000; write_file_atomic '$wadir/new.env' 0600 < '$wadir/in.new'; echo done")" expect "an atomic write finishes" "done" "$out" expect "an atomic write holds all of its input" "$(printf 'A=1\nB=2')" "$(cat "$wadir/new.env")" expect "an atomic write is private under a permissive umask" "-rw-------" "$(mode_of "$wadir/new.env")" run_atomic "write_file_atomic '$wadir/new.env' 0640 < '$wadir/in.new'" >/dev/null expect "an atomic write sets the mode it is given" "-rw-r-----" "$(mode_of "$wadir/new.env")" printf 'DB_PASSWORD=old-and-whole\nSESSION_SECRET=kept\n' > "$wadir/old.env" out="$(run_atomic "cat() { head -c 7; return 1; }; write_file_atomic '$wadir/old.env' 0600 < '$wadir/in.replace'; echo survived")" expect "a write that fails halfway dies" "DIE: could not write $wadir/old.env; it is left as it was" "$out" expect "a write that fails halfway leaves the old file whole" \ "$(printf 'DB_PASSWORD=old-and-whole\nSESSION_SECRET=kept')" "$(cat "$wadir/old.env")" out="$(run_atomic "sync() { return 1; }; write_file_atomic '$wadir/old.env' 0600 < '$wadir/in.replace'")" expect "content that did not reach the disk does not replace the old file" "DIE: could not write $wadir/old.env" "$out" expect "the old file survives a failed sync" "DB_PASSWORD=old-and-whole" "$(cat "$wadir/old.env")" left="$(cd "$wadir" && ls -a | grep '^old\.env\.' || true)" if [ -z "$left" ]; then echo "PASS a failed write leaves no temp file beside the old one" else echo "FAIL a failed write left $left beside old.env"; fails=$((fails + 1)) fi rm -rf "$wadir" "$wafile" # The installer never runs load_or_make_secrets alone (it would mint real secrets), so # its writer is checked by what it calls. lsblock="$(awk '/^load_or_make_secrets\(\) \{/,/^}/' "$BS")" expect "secrets.env is written whole or not at all" 'write_file_atomic "$SECRETS_ENV" 0600 <<EOF' "$lsblock" case "$lsblock" in *'> "$SECRETS_ENV"'*) echo "FAIL load_or_make_secrets still writes secrets.env in place"; fails=$((fails + 1)) ;; *) echo "PASS load_or_make_secrets writes secrets.env nowhere else" ;; esac # --- installer re-runs keep the operator's [registry] overrides -------------------------- # §15's upgrade path is re-running the installer, but the build-lane mirrors and the # uploads backend live in [registry] as hand-written keys (docs/troubleshooting.md §8e or Loading @@ -1498,7 +1555,7 @@ alblock="$(awk '/^auth_lines\(\) \{/,/^}/' "$BS")" # The blocks quote themselves (the awk program uses single quotes), so they are # sourced from a file instead of being spliced into a single-quoted bash -c. fnfile="$(mktemp)" printf '%s\n' "$prblock" "$pablock" "$poblock" "$oblock" "$palblock" "$ahblock" "$alblock" "$wrblock" > "$fnfile" printf '%s\n' "$prblock" "$pablock" "$poblock" "$oblock" "$palblock" "$ahblock" "$alblock" "$wablock" "$wrblock" > "$fnfile" rdir="$(mktemp -d)" cat > "$rdir/felis.host.toml" <<'TOML' Loading Loading @@ -1539,6 +1596,9 @@ run_write() { # out-file [state-dir] [database-deployment]; under the installer' set -Eeuo pipefail trap '\''echo "ERR near line $LINENO (exit $?)" >&2'\'' ERR log() { :; } die() { printf "DIE: %s\n" "$*" >&2; exit 1; } remember_temp() { :; } [ -z "${CAT_FAILS:-}" ] || cat() { head -c 40; return 1; } persisted_smtp_block() { :; } persisted_auth_source_blocks() { :; } . "$FNFILE" Loading Loading @@ -1603,6 +1663,16 @@ else diff "$rdir/out.toml" "$rdir/out2.toml" | head fails=$((fails + 1)) fi expect "the config is private: its url holds the database password" "-rw-------" "$(ls -l "$rdir/out.toml" | cut -c1-10)" # A re-run that cannot finish writing (a full disk) keeps the config it carries from. out="$(CAT_FAILS=1 run_write "$rdir/felis.host.toml" 2>&1 || true)" expect "a config write that fails halfway dies" "DIE: could not write $rdir/felis.host.toml; it is left as it was" "$out" if cmp -s "$rdir/out.toml" "$rdir/felis.host.toml"; then echo "PASS a config write that fails halfway leaves the old config whole" else echo "FAIL a failed write cut felis.host.toml short:"; head -3 "$rdir/felis.host.toml"; fails=$((fails + 1)) fi rm -f "$rdir"/felis.host.toml.* # --- installer re-runs keep [auth]; a different root domain is refused ------------------ # The Cloudflare edge setup writes access_jwt_aud and client_ip_header into [auth] (the Loading Loading @@ -2110,7 +2180,7 @@ esac # must say so loudly. ofile="$(mktemp)" for fn in validate_offsite_settings persisted_offsite_block offsite_block offsite_enabled configure_offsite install_offsite_timer summary_offsite; do for fn in write_file_atomic validate_offsite_settings persisted_offsite_block offsite_block offsite_enabled configure_offsite install_offsite_timer summary_offsite; do blk="$(awk "/^${fn}\\(\\) \\{/,/^}/" "$BS")" [ -n "$blk" ] || { echo "FAIL: no ${fn} found in $BS"; exit 1; } [ "$(printf '%s\n' "$blk" | wc -l)" -lt 120 ] \ Loading @@ -2126,6 +2196,7 @@ run_offsite() { # script; runs with the off-site functions sourced set -Eeuo pipefail die() { printf "DIE: %s\n" "$*"; exit 1; } log() { printf "LOG: %s\n" "$*"; }; ok() { printf "OK: %s\n" "$*"; }; warn() { printf "WARN: %s\n" "$*"; } remember_temp() { :; } systemctl() { printf "SYSTEMCTL: %s\n" "$*" >&2; } fakefelis() { printf "RUN: %s\n" "$*" >&2; [ -z "${CHECK_FAILS:-}" ] || { echo "bucket: access denied" >&2; return 1; } [ -z "${KEY_MISMATCH:-}" ] || { echo "the bucket records key id 1111111111111111, this key is 2222222222222222" >&2; return 3; } Loading Loading @@ -2225,6 +2296,14 @@ out="$(FELIS_OFFSITE_ENDPOINT=https://s3.example FELIS_OFFSITE_BUCKET=felis \ FELIS_OFFSITE_KEY=c29tZXRoaW5nIGVsc2UgZW50aXJlbHkgZGlmZmVyZW50IQ== run_offsite configure_offsite)" expect "a different key is refused" "DIE: FELIS_OFFSITE_KEY differs from the key in" "$out" expect "the refusal leaves the key alone" "FELIS_OFFSITE_KEY='${key}'" "$(cat "$odir/offsite.env")" # A re-run that cannot finish writing offsite.env (a full disk) keeps the key that sealed # the bucket: a file cut short before that line would have a new key minted next run. out="$(FELIS_OFFSITE_ENDPOINT=https://s3.example FELIS_OFFSITE_BUCKET=felis \ FELIS_OFFSITE_ACCESS_KEY=AK3 run_offsite 'cat() { head -c 30; return 1; }; configure_offsite')" expect "a write of offsite.env that fails halfway dies" "DIE: could not write $odir/offsite.env; it is left as it was" "$out" expect "a failed write keeps the key that sealed the bucket" "FELIS_OFFSITE_KEY='${key}'" "$(cat "$odir/offsite.env")" expect "a failed write keeps the credentials that worked" "FELIS_OFFSITE_ACCESS_KEY='AK2'" "$(cat "$odir/offsite.env")" rm -f "$odir"/offsite.env.* rm -f "$odir/offsite.env" out="$(FELIS_OFFSITE_ENDPOINT=https://s3.example FELIS_OFFSITE_BUCKET=felis run_offsite configure_offsite)" Loading