Bite 33.3 — Off‐Host Backup Protection - RodrigoMattosoSilveira/EnterpriseRemoteSystems GitHub Wiki
Branch: 935-bite-333-off-host-backup-protection
Validated HEAD SHA for this revision: 589773522e9351ba93212878026074328ce42d6a
Execution prerequisite
This checklist is the manual acceptance contract for the S3-enabled revision of Bite 33.3. Do not execute the ERS-specific S3 steps against the earlier SSH-only implementation. Before promotion, record the exact S3-enabled HEAD SHA used for testing.
Bite 33.3 must prove that a verified Bite 33.2 backup can leave the ERS application host, be stored in Hetzner Object Storage through its S3-compatible API, be downloaded back, and pass the complete Bite 33.2 verification contract.
The Production design adopted for ERS is:
ERS Production host
│
├── live SQLite database
│
├── Bite 33.2 verified local backup
│ ├── app-<timestamp>.db
│ └── app-<timestamp>.db.manifest.json
│
└── Bite 33.3 S3 replication
↓
Hetzner Object Storage
private bucket
versioning enabled
Object Lock enabled
│
├── backup database object
├── manifest object
└── off-host receipt object
↓
exact S3 object versions downloaded again
↓
SHA-256 comparison
↓
full Bite 33.2 SQLite/schema/data verification
A successful manual verification must prove all of the following:
- the source backup and manifest are valid before replication;
- the Hetzner bucket is private;
- bucket Versioning is enabled;
- Object Lock is enabled;
- a default Governance retention policy protects newly uploaded backup objects;
- the database and manifest are uploaded together under the ERS backup prefix;
- ERS records the exact S3 VersionId for the database and manifest;
- the remote database SHA-256 exactly matches the source database SHA-256;
- the remote manifest SHA-256 exactly matches the source manifest SHA-256;
- the S3
ETagis not used as a substitute for SHA-256; - ERS downloads the recorded database and manifest versions back and verifies them;
- ERS creates an off-host receipt locally and in Object Storage;
- the receipt contains recovery evidence but not the S3 secret key;
- off-host verification succeeds even when the local backup database and manifest are temporarily unavailable;
- retrying replication of the same accepted backup does not silently create replacement database/manifest versions;
- an exact retained backup version cannot be deleted normally while Object Lock retention is active;
- the original known-good remote backup remains verifiable after all negative tests;
- Production refuses to run with off-host protection disabled or with a non-S3 transport after ERS adopts Hetzner Object Storage as its Production backup transport.
Hetzner Object Storage currently exposes location-specific S3-compatible endpoints:
fsn1 → https://fsn1.your-objectstorage.com
nbg1 → https://nbg1.your-objectstorage.com
hel1 → https://hel1.your-objectstorage.com
For this ERS Production design:
- the Bucket must be private;
- Object Lock must be enabled when the Bucket is created;
- Versioning must be enabled;
- the default Object Lock retention used by this test is:
- Mode:
GOVERNANCE - Days:
30
- Mode:
- no lifecycle rule is required by Bite 33.3;
- S3 credentials must not be committed to the ERS repository;
- the AWS CLI is used only as an independent operator verification tool.
The S3-enabled Bite 33.3 implementation should preserve:
make server-offhost-backup
make server-offhost-backup-verifyand select S3 through configuration equivalent to:
SERVER_OFFHOST_BACKUP_ENABLED=true
SERVER_OFFHOST_BACKUP_TRANSPORT=s3
SERVER_OFFHOST_BACKUP_S3_ENDPOINT=https://hel1.your-objectstorage.com
SERVER_OFFHOST_BACKUP_S3_REGION=hel1
SERVER_OFFHOST_BACKUP_S3_BUCKET=<configured-bucket>
SERVER_OFFHOST_BACKUP_S3_PREFIX=ers-backups
AWS_SHARED_CREDENTIALS_FILE=<absolute-path-outside-source-control>
AWS_PROFILE=ers-backupThe expected S3 object layout is:
<ERS-S3-PREFIX>/
└── <environment>/
└── <backup-filename>/
├── <backup-filename>
├── <backup-filename>.manifest.json
└── <backup-filename>.offhost.json
The off-host receipt must record at minimum:
transport = s3
endpoint
region
bucket
database object key
database VersionId
database SHA-256
manifest object key
manifest VersionId
manifest SHA-256
verification timestamp
verification method
These tests must not use:
- the Production database;
- the Test database;
- Production backup objects;
- a shared object prefix containing customer backups.
The source database is:
backend/data/app.db
The test uses a unique prefix:
manual-validation/<UTC-timestamp>
The bucket itself is not deleted.
Because Object Lock retention is deliberately tested, some test object versions can remain until retention expires.
Do not use --bypass-governance-retention merely to clean up test objects early.
Run from the ERS repository root:
git branch --show-current
git rev-parse HEADExpected branch:
935-bite-333-off-host-backup-protection
Record:
S3-enabled Bite 33.3 HEAD:
Confirm tools:
for command in sqlite3 aws jq shasum python3 make; do
command -v "$command" >/dev/null 2>&1 && echo "PASS: $command is available" || echo "FAIL: $command is missing"
doneCreate workspace:
rm -rf /tmp/ers-333-s3-manual
mkdir -p /tmp/ers-333-s3-manual/aws
mkdir -p /tmp/ers-333-s3-manual/local
mkdir -p /tmp/ers-333-s3-manual/downloaded
mkdir -p /tmp/ers-333-s3-manual/server-root/development
chmod 700 /tmp/ers-333-s3-manual/awsVerify:
test -d /tmp/ers-333-s3-manual/aws && test -d /tmp/ers-333-s3-manual/local && test -d /tmp/ers-333-s3-manual/downloaded && echo "PASS: Bite 33.3 S3 workspace created"Expected:
PASS: Bite 33.3 S3 workspace created
PASS: correct branch, S3-enabled SHA recorded, all tools available, workspace created.
In Hetzner Console, create or select an S3 access-key/secret-key pair authorized to use the ERS backup bucket.
Run in zsh:
read "ERS333_S3_REGION?Enter Hetzner Object Storage region (fsn1, nbg1, or hel1): "Validate:
case "$ERS333_S3_REGION" in
fsn1|nbg1|hel1)
echo "PASS: supported Hetzner Object Storage region selected"
;;
*)
echo "FAIL: region must be fsn1, nbg1, or hel1"
exit 2
;;
esacDerive endpoint:
export ERS333_S3_ENDPOINT="https://${ERS333_S3_REGION}.your-objectstorage.com"
printf 'Hetzner S3 endpoint: %s\n' "$ERS333_S3_ENDPOINT"Enter bucket and credentials:
read "ERS333_S3_BUCKET?Enter the private Hetzner Object Storage bucket name for ERS backups: "
read "ERS333_S3_ACCESS_KEY?Enter the Hetzner S3 access key: "
read -s "ERS333_S3_SECRET_KEY?Enter the Hetzner S3 secret key: "
printf '\n'Create temporary AWS credentials:
cat > /tmp/ers-333-s3-manual/aws/credentials <<EOF
[ers-backup]
aws_access_key_id=$ERS333_S3_ACCESS_KEY
aws_secret_access_key=$ERS333_S3_SECRET_KEY
EOF
chmod 600 /tmp/ers-333-s3-manual/aws/credentials
unset ERS333_S3_SECRET_KEYCreate AWS config:
cat > /tmp/ers-333-s3-manual/aws/config <<EOF
[profile ers-backup]
region=$ERS333_S3_REGION
output=json
EOF
chmod 600 /tmp/ers-333-s3-manual/aws/configSet session:
export AWS_SHARED_CREDENTIALS_FILE="/tmp/ers-333-s3-manual/aws/credentials"
export AWS_CONFIG_FILE="/tmp/ers-333-s3-manual/aws/config"
export AWS_PROFILE="ers-backup"
export AWS_REGION="$ERS333_S3_REGION"
export ERS333_S3_PREFIX="manual-validation/$(date -u +%Y%m%dT%H%M%SZ)"Persist only non-secret values:
cat > /tmp/ers-333-s3-manual/session.env <<EOF
export ERS333_S3_REGION='$ERS333_S3_REGION'
export ERS333_S3_ENDPOINT='$ERS333_S3_ENDPOINT'
export ERS333_S3_BUCKET='$ERS333_S3_BUCKET'
export ERS333_S3_PREFIX='$ERS333_S3_PREFIX'
export AWS_SHARED_CREDENTIALS_FILE='/tmp/ers-333-s3-manual/aws/credentials'
export AWS_CONFIG_FILE='/tmp/ers-333-s3-manual/aws/config'
export AWS_PROFILE='ers-backup'
export AWS_REGION='$ERS333_S3_REGION'
EOFTest access:
aws s3api head-bucket --bucket "$ERS333_S3_BUCKET" --endpoint-url "$ERS333_S3_ENDPOINT"Expected exit code:
echo $?0
If the bucket does not yet exist, create it once with Object Lock enabled:
aws s3api create-bucket --bucket "$ERS333_S3_BUCKET" --region "$ERS333_S3_REGION" --object-lock-enabled-for-bucket --endpoint-url "$ERS333_S3_ENDPOINT"Then repeat head-bucket.
PASS: credentials work, secret is outside repository and not in session.env, bucket is reachable.
Load:
source /tmp/ers-333-s3-manual/session.envEnable versioning:
aws s3api put-bucket-versioning --bucket "$ERS333_S3_BUCKET" --versioning-configuration Status=Enabled --endpoint-url "$ERS333_S3_ENDPOINT"Assert:
test "$(
aws s3api get-bucket-versioning --bucket "$ERS333_S3_BUCKET" --endpoint-url "$ERS333_S3_ENDPOINT" | jq -r '.Status'
)" = "Enabled" && echo "PASS: bucket versioning is enabled"Expected:
PASS: bucket versioning is enabled
Verify Object Lock:
aws s3api get-object-lock-configuration --bucket "$ERS333_S3_BUCKET" --endpoint-url "$ERS333_S3_ENDPOINT" | jq .If this fails because Object Lock is absent, STOP. Create a new bucket with Object Lock enabled; Hetzner cannot add Object Lock retroactively.
Configure 30-day Governance retention:
aws s3api put-object-lock-configuration --bucket "$ERS333_S3_BUCKET" --object-lock-configuration '{
"ObjectLockEnabled": "Enabled",
"Rule": {
"DefaultRetention": {
"Mode": "GOVERNANCE",
"Days": 30
}
}
}' --endpoint-url "$ERS333_S3_ENDPOINT"Assert:
aws s3api get-object-lock-configuration --bucket "$ERS333_S3_BUCKET" --endpoint-url "$ERS333_S3_ENDPOINT" | jq -e '
.ObjectLockConfiguration.ObjectLockEnabled == "Enabled"
and .ObjectLockConfiguration.Rule.DefaultRetention.Mode == "GOVERNANCE"
and .ObjectLockConfiguration.Rule.DefaultRetention.Days == 30
'Expected:
echo $?0
PASS: Versioning enabled, Object Lock enabled, Governance 30 days.
Run:
source /tmp/ers-333-s3-manual/session.env
aws s3api get-bucket-acl --bucket "$ERS333_S3_BUCKET" --endpoint-url "$ERS333_S3_ENDPOINT" > /tmp/ers-333-s3-manual/bucket-acl.jsonCheck:
if jq -e '
[.Grants[]? |
select(
.Grantee.URI == "http://acs.amazonaws.com/groups/global/AllUsers"
or
.Grantee.URI == "http://acs.amazonaws.com/groups/global/AuthenticatedUsers"
)
] | length > 0
' /tmp/ers-333-s3-manual/bucket-acl.json >/dev/null; then
echo "FAIL: bucket ACL contains a public group grant"
else
echo "PASS: bucket ACL contains no public group grant"
fiExpected:
PASS: bucket ACL contains no public group grant
Terminal 1:
unset APP_ENV
make local-backendKeep it running.
Terminal 2:
test -f backend/data/app.db && echo "PASS: normal LOCAL database exists"Expected:
PASS: normal LOCAL database exists
Load:
source /tmp/ers-333-s3-manual/session.envCreate:
export ERS333_GOOD_BACKUP="/tmp/ers-333-s3-manual/local/app-$(date -u +%Y%m%dT%H%M%SZ).db"
sqlite3 backend/data/app.db ".backup '$ERS333_GOOD_BACKUP'"Confirm:
test -s "$ERS333_GOOD_BACKUP" && echo "PASS: source backup exists and is non-empty"Create manifest:
./scripts/run-backup-go-tool.sh ers-backup create-manifest \
--backup "$ERS333_GOOD_BACKUP" \
--environment development \
--source-container manual-local-backend \
--source-database-path "$(pwd)/backend/data/app.db"Verify:
./scripts/run-backup-go-tool.sh ers-backup verify \
--backup "$ERS333_GOOD_BACKUP" \
--manifest "$ERS333_GOOD_BACKUP.manifest.json" \
--expected-environment developmentExpected:
Backup verification passed
Record SHA-256:
export ERS333_GOOD_BACKUP_SHA256="$(
shasum -a 256 "$ERS333_GOOD_BACKUP" | awk '{print $1}'
)"
export ERS333_GOOD_MANIFEST_SHA256="$(
shasum -a 256 "$ERS333_GOOD_BACKUP.manifest.json" | awk '{print $1}'
)"Persist:
{
printf "export ERS333_GOOD_BACKUP='%s'\n" "$ERS333_GOOD_BACKUP"
printf "export ERS333_GOOD_BACKUP_SHA256='%s'\n" "$ERS333_GOOD_BACKUP_SHA256"
printf "export ERS333_GOOD_MANIFEST_SHA256='%s'\n" "$ERS333_GOOD_MANIFEST_SHA256"
} >> /tmp/ers-333-s3-manual/session.envPASS: source backup/manifest verify and both SHA-256 values are recorded.
Load:
source /tmp/ers-333-s3-manual/session.envCreate:
cat > /tmp/ers-333-s3-manual/server-root/development/.env.development <<EOF
APP_ENV=development
SERVER_OFFHOST_BACKUP_ENABLED=true
SERVER_OFFHOST_BACKUP_TRANSPORT=s3
SERVER_OFFHOST_BACKUP_S3_ENDPOINT=$ERS333_S3_ENDPOINT
SERVER_OFFHOST_BACKUP_S3_REGION=$ERS333_S3_REGION
SERVER_OFFHOST_BACKUP_S3_BUCKET=$ERS333_S3_BUCKET
SERVER_OFFHOST_BACKUP_S3_PREFIX=$ERS333_S3_PREFIX
AWS_SHARED_CREDENTIALS_FILE=/tmp/ers-333-s3-manual/aws/credentials
AWS_PROFILE=ers-backup
EOFConfirm secret is not embedded:
if grep -q 'aws_secret_access_key' /tmp/ers-333-s3-manual/server-root/development/.env.development; then
echo "FAIL: secret key was embedded in ERS environment file"
else
echo "PASS: ERS environment file references credentials without embedding the secret key"
fiExpected:
PASS: ERS environment file references credentials without embedding the secret key
make server-offhost-backup is a deployed-server target. It sets ERS_BACKUP_ENV_DIR and expects the selected server environment directory to contain docker-compose.server.yml. This manual suite deliberately uses a synthetic LOCAL environment directory only for S3 configuration, so the LOCAL test must invoke the Go backup runner directly.
Run:
source /tmp/ers-333-s3-manual/session.env
./scripts/run-backup-go-tool.sh ers-offhost-backup replicate \
--environment development \
--env-file /tmp/ers-333-s3-manual/server-root/development/.env.development \
--backup "$ERS333_GOOD_BACKUP"Expected:
- exit
0; - S3 round-trip verification success;
- local receipt at
<backup>.offhost.json.
Verify:
test -s "$ERS333_GOOD_BACKUP.offhost.json" && echo "PASS: local S3 off-host receipt exists"Expected:
PASS: local S3 off-host receipt exists
Display:
jq . "$ERS333_GOOD_BACKUP.offhost.json"Assert:
jq -e '
.status == "verified"
and .transport == "s3"
and .remote.endpoint != null
and .remote.region != null
and .remote.bucket != null
and .remote.backup.key != null
and .remote.backup.version_id != null
and .remote.backup.sha256 != null
and .remote.manifest.key != null
and .remote.manifest.version_id != null
and .remote.manifest.sha256 != null
' "$ERS333_GOOD_BACKUP.offhost.json"Expected:
echo $?0
Capture:
export ERS333_DB_KEY="$(jq -r '.remote.backup.key' "$ERS333_GOOD_BACKUP.offhost.json")"
export ERS333_DB_VERSION_ID="$(jq -r '.remote.backup.version_id' "$ERS333_GOOD_BACKUP.offhost.json")"
export ERS333_MANIFEST_KEY="$(jq -r '.remote.manifest.key' "$ERS333_GOOD_BACKUP.offhost.json")"
export ERS333_MANIFEST_VERSION_ID="$(jq -r '.remote.manifest.version_id' "$ERS333_GOOD_BACKUP.offhost.json")"Persist:
{
printf "export ERS333_DB_KEY='%s'\n" "$ERS333_DB_KEY"
printf "export ERS333_DB_VERSION_ID='%s'\n" "$ERS333_DB_VERSION_ID"
printf "export ERS333_MANIFEST_KEY='%s'\n" "$ERS333_MANIFEST_KEY"
printf "export ERS333_MANIFEST_VERSION_ID='%s'\n" "$ERS333_MANIFEST_VERSION_ID"
} >> /tmp/ers-333-s3-manual/session.envSecret check:
if grep -q 'aws_secret_access_key' "$ERS333_GOOD_BACKUP.offhost.json"; then
echo "FAIL: receipt contains S3 secret-key material"
else
echo "PASS: receipt contains no S3 secret-key material"
fiExpected:
PASS: receipt contains no S3 secret-key material
Promotion blocker: receipt has object keys but no VersionIds.
The Bite 33.2 manifest intentionally records the original backup filename. Therefore the independently downloaded database and manifest must retain their original filenames. Do not rename the database to backup.db.
Load:
source /tmp/ers-333-s3-manual/session.envDerive the original backup filename from the verified local backup path:
export ERS333_BACKUP_NAME="$(basename "$ERS333_GOOD_BACKUP")"
export ERS333_DOWNLOADED_BACKUP="/tmp/ers-333-s3-manual/downloaded/$ERS333_BACKUP_NAME"
export ERS333_DOWNLOADED_MANIFEST="$ERS333_DOWNLOADED_BACKUP.manifest.json"
printf 'Downloaded DB path: %s\n' "$ERS333_DOWNLOADED_BACKUP"
printf 'Downloaded manifest path: %s\n' "$ERS333_DOWNLOADED_MANIFEST"Expected paths resemble:
/tmp/ers-333-s3-manual/downloaded/app-20261009T201450Z.db
/tmp/ers-333-s3-manual/downloaded/app-20261009T201450Z.db.manifest.json
Remove any stale copies from a previous Test 09 attempt:
rm -f \
"$ERS333_DOWNLOADED_BACKUP" \
"$ERS333_DOWNLOADED_MANIFEST"Download the exact recorded database VersionId while preserving its original filename:
aws s3api get-object \
--bucket "$ERS333_S3_BUCKET" \
--key "$ERS333_DB_KEY" \
--version-id "$ERS333_DB_VERSION_ID" \
--endpoint-url "$ERS333_S3_ENDPOINT" \
"$ERS333_DOWNLOADED_BACKUP" \
> /tmp/ers-333-s3-manual/downloaded/backup-get.jsonDownload the exact recorded manifest VersionId:
aws s3api get-object \
--bucket "$ERS333_S3_BUCKET" \
--key "$ERS333_MANIFEST_KEY" \
--version-id "$ERS333_MANIFEST_VERSION_ID" \
--endpoint-url "$ERS333_S3_ENDPOINT" \
"$ERS333_DOWNLOADED_MANIFEST" \
> /tmp/ers-333-s3-manual/downloaded/manifest-get.jsonConfirm both downloaded files exist and are non-empty:
test -s "$ERS333_DOWNLOADED_BACKUP" \
&& echo "PASS: exact S3 database version downloaded"
test -s "$ERS333_DOWNLOADED_MANIFEST" \
&& echo "PASS: exact S3 manifest version downloaded"Expected:
PASS: exact S3 database version downloaded
PASS: exact S3 manifest version downloaded
Compute independent SHA-256 values:
ERS333_DOWNLOADED_DB_SHA256="$(
shasum -a 256 "$ERS333_DOWNLOADED_BACKUP" | awk '{print $1}'
)"
ERS333_DOWNLOADED_MANIFEST_SHA256="$(
shasum -a 256 "$ERS333_DOWNLOADED_MANIFEST" | awk '{print $1}'
)"
printf 'Source DB SHA-256: %s\n' "$ERS333_GOOD_BACKUP_SHA256"
printf 'Downloaded DB SHA-256: %s\n' "$ERS333_DOWNLOADED_DB_SHA256"
printf 'Source manifest SHA-256: %s\n' "$ERS333_GOOD_MANIFEST_SHA256"
printf 'Downloaded manifest SHA: %s\n' "$ERS333_DOWNLOADED_MANIFEST_SHA256"Compare:
test "$ERS333_DOWNLOADED_DB_SHA256" = "$ERS333_GOOD_BACKUP_SHA256" \
&& echo "PASS: exact S3 database version matches source SHA-256"
test "$ERS333_DOWNLOADED_MANIFEST_SHA256" = "$ERS333_GOOD_MANIFEST_SHA256" \
&& echo "PASS: exact S3 manifest version matches source SHA-256"Expected:
PASS: exact S3 database version matches source SHA-256
PASS: exact S3 manifest version matches source SHA-256
Now run the full Bite 33.2 Go verification against the independently downloaded pair:
./scripts/run-backup-go-tool.sh ers-backup verify \
--backup "$ERS333_DOWNLOADED_BACKUP" \
--manifest "$ERS333_DOWNLOADED_MANIFEST" \
--expected-environment developmentExpected:
Backup verification passed
Immediately run:
echo $?Expected:
0
Important: do not use the S3 ETag as the database checksum. The PASS condition is based on independently computed SHA-256 values.
PASS: the exact recorded S3 database and manifest versions preserve the original filenames, reproduce the original SHA-256 values, and pass the complete Bite 33.2 verification contract.
Inspect retention:
aws s3api get-object-retention --bucket "$ERS333_S3_BUCKET" --key "$ERS333_DB_KEY" --version-id "$ERS333_DB_VERSION_ID" --endpoint-url "$ERS333_S3_ENDPOINT" | jq .Assert Governance:
test "$(
aws s3api get-object-retention --bucket "$ERS333_S3_BUCKET" --key "$ERS333_DB_KEY" --version-id "$ERS333_DB_VERSION_ID" --endpoint-url "$ERS333_S3_ENDPOINT" | jq -r '.Retention.Mode'
)" = "GOVERNANCE" && echo "PASS: DB version is protected by Governance retention"Attempt ordinary exact-version delete:
set +e
aws s3api delete-object --bucket "$ERS333_S3_BUCKET" --key "$ERS333_DB_KEY" --version-id "$ERS333_DB_VERSION_ID" --endpoint-url "$ERS333_S3_ENDPOINT" >/tmp/ers-333-s3-manual/delete-protected.out 2>&1
ERS333_DELETE_STATUS=$?
set -eAssert:
test "$ERS333_DELETE_STATUS" -ne 0 && echo "PASS: retained DB version cannot be deleted normally"Expected:
PASS: retained DB version cannot be deleted normally
Re-download exact version and compare SHA-256.
This test deliberately moves the local .db and .manifest.json away from their normal paths. The final restoration steps are mandatory before Test 12.
Load the persisted session:
source /tmp/ers-333-s3-manual/session.envDerive the original filename again. This is intentionally recomputed so the test remains correct even after restarting Terminal:
export ERS333_BACKUP_NAME="$(basename "$ERS333_GOOD_BACKUP")"
export ERS333_HIDDEN_BACKUP="/tmp/ers-333-s3-manual/local-hidden/$ERS333_BACKUP_NAME"
export ERS333_HIDDEN_MANIFEST="$ERS333_HIDDEN_BACKUP.manifest.json"Create the holding directory:
mkdir -p /tmp/ers-333-s3-manual/local-hiddenMove the local pair away from its normal location:
mv "$ERS333_GOOD_BACKUP" "$ERS333_HIDDEN_BACKUP"
mv "$ERS333_GOOD_BACKUP.manifest.json" "$ERS333_HIDDEN_MANIFEST"Confirm both normal paths are absent:
test ! -e "$ERS333_GOOD_BACKUP" \
&& test ! -e "$ERS333_GOOD_BACKUP.manifest.json" \
&& echo "PASS: local backup pair is unavailable"Expected:
PASS: local backup pair is unavailable
Verify the remote exact versions using only the retained off-host receipt:
./scripts/run-backup-go-tool.sh ers-offhost-backup verify-replica \
--environment development \
--env-file /tmp/ers-333-s3-manual/server-root/development/.env.development \
--receipt "$ERS333_GOOD_BACKUP.offhost.json"Expected output includes:
Off-host replica verification passed:
Immediately run:
echo $?Expected:
0
If Terminal has been restarted since the files were moved, reload the session and recompute the filenames first:
source /tmp/ers-333-s3-manual/session.env
export ERS333_BACKUP_NAME="$(basename "$ERS333_GOOD_BACKUP")"
export ERS333_HIDDEN_BACKUP="/tmp/ers-333-s3-manual/local-hidden/$ERS333_BACKUP_NAME"
export ERS333_HIDDEN_MANIFEST="$ERS333_HIDDEN_BACKUP.manifest.json"Show the current state before restoring:
printf 'Normal backup: %s\n' "$ERS333_GOOD_BACKUP"
printf 'Hidden backup: %s\n' "$ERS333_HIDDEN_BACKUP"
printf 'Normal manifest: %s\n' "$ERS333_GOOD_BACKUP.manifest.json"
printf 'Hidden manifest: %s\n' "$ERS333_HIDDEN_MANIFEST"
ls -l \
"$ERS333_HIDDEN_BACKUP" \
"$ERS333_HIDDEN_MANIFEST"Restore the database:
if [[ ! -e "$ERS333_GOOD_BACKUP" && -f "$ERS333_HIDDEN_BACKUP" ]]; then
mv "$ERS333_HIDDEN_BACKUP" "$ERS333_GOOD_BACKUP"
fiRestore the manifest:
if [[ ! -e "$ERS333_GOOD_BACKUP.manifest.json" && -f "$ERS333_HIDDEN_MANIFEST" ]]; then
mv "$ERS333_HIDDEN_MANIFEST" "$ERS333_GOOD_BACKUP.manifest.json"
fiAssert that both files are back in their original locations:
test -s "$ERS333_GOOD_BACKUP" \
&& echo "PASS: local backup restored"
test -s "$ERS333_GOOD_BACKUP.manifest.json" \
&& echo "PASS: local manifest restored"Expected:
PASS: local backup restored
PASS: local manifest restored
Confirm the restored bytes still match the SHA-256 values recorded before off-host replication:
test "$(
shasum -a 256 "$ERS333_GOOD_BACKUP" | awk '{print $1}'
)" = "$ERS333_GOOD_BACKUP_SHA256" \
&& echo "PASS: restored backup SHA-256 is unchanged"
test "$(
shasum -a 256 "$ERS333_GOOD_BACKUP.manifest.json" | awk '{print $1}'
)" = "$ERS333_GOOD_MANIFEST_SHA256" \
&& echo "PASS: restored manifest SHA-256 is unchanged"Expected:
PASS: restored backup SHA-256 is unchanged
PASS: restored manifest SHA-256 is unchanged
Finally, re-run Bite 33.2 verification on the restored local pair:
./scripts/run-backup-go-tool.sh ers-backup verify \
--backup "$ERS333_GOOD_BACKUP" \
--manifest "$ERS333_GOOD_BACKUP.manifest.json" \
--expected-environment developmentExpected:
Backup verification passed
Only after this succeeds should you continue to Test 12.
PASS: remote verification succeeds while the local DB/manifest are unavailable, and the original local pair is then restored byte-for-byte and re-verifies successfully.
This test requires the local database and manifest restored by the final section of Test 11.
Load persisted state:
source /tmp/ers-333-s3-manual/session.envConfirm the local pair exists before continuing:
test -s "$ERS333_GOOD_BACKUP" \
&& echo "PASS: source backup is available for retry" \
|| { echo "FAIL: source backup is missing; complete the Test 11 restoration first"; return 2 2>/dev/null || exit 2; }
test -s "$ERS333_GOOD_BACKUP.manifest.json" \
&& echo "PASS: source manifest is available for retry" \
|| { echo "FAIL: source manifest is missing; complete the Test 11 restoration first"; return 2 2>/dev/null || exit 2; }Expected:
PASS: source backup is available for retry
PASS: source manifest is available for retry
Capture the S3 version inventory before the retry:
aws s3api list-object-versions \
--bucket "$ERS333_S3_BUCKET" \
--prefix "$ERS333_S3_PREFIX" \
--endpoint-url "$ERS333_S3_ENDPOINT" \
> /tmp/ers-333-s3-manual/versions-before.jsonCount the exact database-key versions before the retry:
ERS333_DB_VERSION_COUNT_BEFORE="$(
jq --arg key "$ERS333_DB_KEY" \
'[.Versions[]? | select(.Key == $key)] | length' \
/tmp/ers-333-s3-manual/versions-before.json
)"Count the exact manifest-key versions before the retry:
ERS333_MANIFEST_VERSION_COUNT_BEFORE="$(
jq --arg key "$ERS333_MANIFEST_KEY" \
'[.Versions[]? | select(.Key == $key)] | length' \
/tmp/ers-333-s3-manual/versions-before.json
)"Print the counts:
printf 'DB versions before retry: %s\n' "$ERS333_DB_VERSION_COUNT_BEFORE"
printf 'Manifest versions before retry: %s\n' "$ERS333_MANIFEST_VERSION_COUNT_BEFORE"Repeat the exact same ERS replication request:
./scripts/run-backup-go-tool.sh ers-offhost-backup replicate \
--environment development \
--env-file /tmp/ers-333-s3-manual/server-root/development/.env.development \
--backup "$ERS333_GOOD_BACKUP"Expected output includes:
Off-host backup verification passed using existing exact S3 versions:
The command must exit 0.
Capture the S3 version inventory again:
aws s3api list-object-versions \
--bucket "$ERS333_S3_BUCKET" \
--prefix "$ERS333_S3_PREFIX" \
--endpoint-url "$ERS333_S3_ENDPOINT" \
> /tmp/ers-333-s3-manual/versions-after.jsonCount database versions after the retry:
ERS333_DB_VERSION_COUNT_AFTER="$(
jq --arg key "$ERS333_DB_KEY" \
'[.Versions[]? | select(.Key == $key)] | length' \
/tmp/ers-333-s3-manual/versions-after.json
)"Count manifest versions after the retry:
ERS333_MANIFEST_VERSION_COUNT_AFTER="$(
jq --arg key "$ERS333_MANIFEST_KEY" \
'[.Versions[]? | select(.Key == $key)] | length' \
/tmp/ers-333-s3-manual/versions-after.json
)"Compare:
test "$ERS333_DB_VERSION_COUNT_BEFORE" = "$ERS333_DB_VERSION_COUNT_AFTER" \
&& echo "PASS: retry did not create another DB version"
test "$ERS333_MANIFEST_VERSION_COUNT_BEFORE" = "$ERS333_MANIFEST_VERSION_COUNT_AFTER" \
&& echo "PASS: retry did not create another manifest version"Expected:
PASS: retry did not create another DB version
PASS: retry did not create another manifest version
PASS: ERS re-verifies the exact already-recorded S3 versions and does not create replacement database or manifest versions.
Create:
cat >/tmp/ers-333-s3-manual/production-disabled.env <<'EOF'
APP_ENV=production
SERVER_OFFHOST_BACKUP_ENABLED=false
SERVER_OFFHOST_BACKUP_TRANSPORT=s3
SERVER_OFFHOST_BACKUP_S3_ENDPOINT=https://hel1.your-objectstorage.com
SERVER_OFFHOST_BACKUP_S3_REGION=hel1
SERVER_OFFHOST_BACKUP_S3_BUCKET=invalid-negative-test
SERVER_OFFHOST_BACKUP_S3_PREFIX=ers-backups
EOFRun:
set +e
make server-environment-contract-probe ENV=production SERVER_ENV_PROBE_FILE=/tmp/ers-333-s3-manual/production-disabled.env
ERS333_PROD_DISABLED_STATUS=$?
set -eExpected non-zero.
Assert:
test "$ERS333_PROD_DISABLED_STATUS" -ne 0 && echo "PASS: Production cannot disable off-host backup protection"Create:
cat >/tmp/ers-333-s3-manual/production-ssh.env <<'EOF'
APP_ENV=production
SERVER_OFFHOST_BACKUP_ENABLED=true
SERVER_OFFHOST_BACKUP_TRANSPORT=ssh
EOFRun exactly:
set +e
make server-environment-contract-probe \
ENV=production \
SERVER_ENV_PROBE_FILE=/tmp/ers-333-s3-manual/production-ssh.env
ERS333_PROD_SSH_STATUS=$?
set -eThen inspect the exit code:
printf 'Exit status: %s\n' "$ERS333_PROD_SSH_STATUS"Expected: non-zero. Then assert the PASS condition:
test "$ERS333_PROD_SSH_STATUS" -ne 0 \
&& echo "PASS: Production rejects non-S3 off-host backup transport" \
|| echo "FAIL: Production accepted non-S3 off-host backup transport"Expected:
PASS: Production rejects non-S3 off-host backup transportAssert:
PASS: Production rejects non-S3 off-host backup transport
Run:
./scripts/run-backup-go-tool.sh ers-offhost-backup verify-replica \
--environment development \
--env-file /tmp/ers-333-s3-manual/server-root/development/.env.development \
--receipt "$ERS333_GOOD_BACKUP.offhost.json"Expected exit 0.
Independently download the exact recorded database VersionId one final time and compare:
final exact-version SHA-256
=
original source DB SHA-256
Expected:
PASS: final off-host DB version still matches original SHA-256
Run:
make off-host-backup-protection-check
make backup-creation-retention-verification-check
make production-environment-guardrails-check
make local-checkThen push the branch and require CI to pass.
Remove LOCAL temporary material:
rm -rf /tmp/ers-333-s3-manualDo not delete the Production-intended bucket.
Do not use Governance bypass merely to erase protected test versions.
The manual-validation/<timestamp> objects may remain until their retention period expires.
Promote only if all of the following are true:
- S3-enabled Bite 33.3 SHA is recorded.
- Hetzner endpoint uses HTTPS and
fsn1,nbg1, orhel1. - Bucket is private.
- Versioning is enabled.
- Object Lock is enabled.
- Default retention is Governance / 30 days.
- Credentials are outside the ERS repository.
- ERS environment does not embed the secret key.
- Bite 33.2 source backup and manifest verify before replication.
- Source DB SHA-256 is independently recorded.
- Source manifest SHA-256 is independently recorded.
- ERS S3 replication succeeds.
- ERS performs round-trip S3 download before success.
- Receipt records DB object key and exact VersionId.
- Receipt records DB SHA-256.
- Receipt records manifest object key and exact VersionId.
- Receipt records manifest SHA-256.
- Receipt contains no S3 secret-key material.
- Exact DB VersionId downloads successfully.
- Exact manifest VersionId downloads successfully.
- Downloaded DB SHA-256 equals source DB SHA-256.
- Downloaded manifest SHA-256 equals source manifest SHA-256.
- Downloaded pair passes full Bite 33.2 verification.
-
ETagis not used as DB checksum. - Exact DB version is under Object Lock retention.
- Normal exact-version deletion is refused.
- Off-host verification succeeds with local DB/manifest unavailable.
- Retry creates no additional DB/manifest versions.
- Production refuses off-host protection disabled.
- Production refuses non-S3 transport.
- Final exact recorded DB VersionId still matches original SHA-256.
- 33.1 automated gate passes.
- 33.2 automated gate passes.
- 33.3 automated gate passes.
-
make local-checkpasses. - CI passes.
Do not promote if:
- Production can run without off-host protection.
- Production can silently use SSH after S3 adoption.
- Bucket is public.
- Versioning is disabled.
- Object Lock is absent.
- Secret key is committed or embedded in ERS environment configuration.
- ERS reports upload success without round-trip verification.
- Receipt records keys but not VersionIds.
- Later verification uses latest object instead of recorded VersionId.
- Remote DB SHA-256 differs from source.
- Remote manifest SHA-256 differs from source.
-
ETagis treated as SHA-256. - Retained DB version can be normally deleted during retention.
- Off-host verification requires the original local backup pair.
- Retry silently creates replacement DB/manifest versions.
- Any 33.1/33.2/33.3/LOCAL/CI gate fails.
Branch:
S3-enabled SHA:
Hetzner Object Storage region:
Hetzner S3 endpoint:
Bucket:
Bucket Versioning status:
Bucket Object Lock status:
Default retention mode:
Default retention days:
Manual-test S3 prefix:
Source backup filename:
Source DB SHA-256:
Source manifest SHA-256:
S3 DB object key:
S3 DB VersionId:
S3 DB SHA-256:
S3 manifest object key:
S3 manifest VersionId:
S3 manifest SHA-256:
Initial S3 replication completed:
Independent exact-VersionId verification completed:
Final verification completed:
make off-host-backup-protection-check:
make backup-creation-retention-verification-check:
make production-environment-guardrails-check:
make local-check:
CI:
Do not retain:
- S3 secret key;
- application passwords;
- JWT secrets;
- ledger keys;
- API keys.