# S3 Backup Bucket Audit Procedure A repeatable methodology for auditing all Wasabi S3 backup buckets — versioning status, file counts, total sizes, staleness checks, and fresh-object verification. ## Why audit - Catch backup pipeline failures early (the MikroTik router backup pipeline stopped producing configs after Jul 5, 2026, and went unnoticed for 3 days) - Verify the live-sync checkpoint is actually producing objects from today - Confirm versioning wasn't accidentally disabled during a bucket config change - Know your total storage consumption for cost tracking ## Prerequisites ```bash # AWS CLI with Wasabi endpoint configured /opt/awscli-venv/bin/aws configure # ~/.aws/credentials must exist ENDPOINT="https://s3.us-east-1.wasabisys.com" ``` ## Quick audit script Run this on all existing buckets at once: ```bash ENDPOINT="https://s3.us-east-1.wasabisys.com" for BUCKET in hermes-vps-backups itpropartner-backups mikrotik-ccr-backups itpropartner-system-configs itpropartner-docker-volumes; do echo "=== $BUCKET ===" # Versioning status echo "--- Versioning ---" /opt/awscli-venv/bin/aws s3api get-bucket-versioning --bucket "$BUCKET" --endpoint-url "$ENDPOINT" 2>&1 # Top-level layout and latest object echo "--- Top-level listing (last 5 modified) ---" /opt/awscli-venv/bin/aws s3 ls s3://"$BUCKET"/ --endpoint-url "$ENDPOINT" --human-readable --recursive 2>&1 | sort -k1,2r | head -5 # Summary (count + size) /opt/awscli-venv/bin/aws s3 ls s3://"$BUCKET"/ --endpoint-url "$ENDPOINT" --human-readable --recursive --summarize 2>&1 | tail -2 # Staleness: count objects from today TODAY=$(date +%F) TODAY_COUNT=$(/opt/awscli-venv/bin/aws s3 ls s3://"$BUCKET"/ --endpoint-url "$ENDPOINT" --recursive 2>&1 | grep -c "$TODAY" || true) echo "Objects from today ($TODAY): $TODAY_COUNT" echo "" done ``` ## Detailed per-bucket checks ### 1. Versioning ```bash aws s3api get-bucket-versioning --bucket --endpoint-url https://s3.us-east-1.wasabisys.com ``` Expected: `{"Status": "Enabled", "MFADelete": "Disabled"}` If `Status` is absent or `Suspended`, the bucket is no longer protected against accidental overwrites/deletes. ### 2. File count and total size ```bash aws s3 ls s3:/// --endpoint-url --recursive --human-readable --summarize ``` The summary lines at the bottom give you: - `Total Objects: N` - `Total Size: X.X GiB/MiB` ### 3. Staleness check (per top-level prefix) Check whether any objects exist from today: ```bash aws s3 ls s3:/// --endpoint-url --recursive | grep "$(date +%F)" | sort -r | head -5 ``` If zero objects from today, check yesterday: ```bash aws s3 ls s3:/// --endpoint-url --recursive | grep "$(date -d yesterday +%F)" | sort -r | head -5 ``` **Staleness thresholds:** | Type | Max acceptable gap | |---|---| | Live Hermes sync | < 30 minutes (expected every 15 min) | | Full Hermes backup | < 48 hours (daily at 1 AM UTC) | | Router config backup | < 48 hours (daily at 6 AM UTC) | ### 4. Live sync freshness (deep check for `hermes-vps-backups`) The most recent object timestamp tells you when the 15-min live sync last ran: ```bash aws s3 ls s3://hermes-vps-backups/live/ --endpoint-url https://s3.us-east-1.wasabisys.com --recursive 2>&1 | sort -k1,2r | head -5 ``` Also count today's objects in the live prefix specifically: ```bash aws s3 ls s3://hermes-vps-backups/live/ --endpoint-url https://s3.us-east-1.wasabisys.com --recursive 2>&1 | grep -c "$(date +%F)" ``` This should be a large number (hundreds to thousands) indicating the sync is actively pushing state changes. ### 5. Full backup archive check ```bash aws s3 ls s3://hermes-vps-backups/hermes-full-backup/ --endpoint-url https://s3.us-east-1.wasabisys.com --human-readable ``` Check that a `.tar.gz` file exists from within the last 48 hours. Also check the manifest file is present beside each archive. ### 6. Caddyfile copy check The live sync pushes `/etc/caddy/Caddyfile` to `s3://hermes-vps-backups/live/caddy/Caddyfile` every 15 min. Verify it's current: ```bash aws s3 ls s3://hermes-vps-backups/live/caddy/ --endpoint-url https://s3.us-east-1.wasabisys.com ``` ### 7. Empty purpose-specific bucket check The `itpropartner-system-configs` and `itpropartner-docker-volumes` buckets exist on Wasabi but may contain 0 objects if their sync scripts are not running. Check both: ```bash for BUCKET in itpropartner-system-configs itpropartner-docker-volumes; do echo "=== $BUCKET ===" /opt/awscli-venv/bin/aws s3 ls s3://"$BUCKET"/ --endpoint-url "$ENDPOINT" --recursive --summarize 2>&1 | tail -2 done ``` Expected: `Total Objects: > 0`. If 0 objects, the corresponding sync script (`hermes-system-config-sync.sh` or `hermes-docker-sync.sh`) is not running or failing silently. ## Known bucket inventory (as of Jul 11, 2026) | Bucket | Purpose | Status | |---|---|---| | `hermes-vps-backups` | Hermes full backup + live sync + standby | ✅ Versioning ON, active objects | | `itpropartner-backups` | Portal/ITP backups | ✅ Versioning ON, minimal objects | | `mikrotik-ccr-backups` | Router config exports | ✅ Versioning ON, stale | | `itpropartner-system-configs` | System configs | 🟡 **Bucket exists but EMPTY** — sync script not running or failing | | `itpropartner-docker-volumes` | Docker volume data | 🟡 **Bucket exists but EMPTY** — sync script not running or failing | ## Common failure patterns | Symptom | Likely cause | Fix | |---|---|---| | Zero objects from today on `live/` | Live sync cron job stopped or credentials rotated | Check `hermes cron list` for `hermes-live-sync` status; verify `~/.aws/credentials` | | Bucket listing returns objects but versioning says `Suspended` | Someone ran `put-bucket-versioning` with `Status=Suspended` | Re-enable: `aws s3api put-bucket-versioning --bucket --versioning-configuration Status=Enabled --endpoint-url ` | | Full backup archive is >48h old | Daily cron job stopped running; live sync masks the gap | Check system crontab; run `bash /root/.hermes/scripts/hermes-backup.sh` manually; verify `BACKUP_DIR` not on `/tmp` | | Router config backups stopped | VPN tunnel down, SSH key rotated, or pipeline script error | SSH to gateway CCR and test manually; check the tower config YAML | | `AccessDenied` on `ListBuckets` | Expected — IAM policy scopes to specific bucket ARNs | Test by bucket name directly, not `aws s3 ls` (no arg) | | `InvalidAccessKeyId` | Stale credentials in `~/.aws/credentials` | Get current key from password manager and update the file | | Bucket exists but has 0 objects | Sync script not running or failing silently | Check system crontab for the sync job; run the script manually and check output; verify `aws` is in PATH (venv activation) | | `s3api get-bucket-versioning` returns empty | Versioning not yet enabled on bucket or `s3:GetBucketVersioning` missing from IAM policy | Enable with `put-bucket-versioning`; check IAM policy includes `s3:GetBucketVersioning` on the bucket ARN | | AWS CLI `command not found` in cron | Backup/sync script missing venv activation | Add `source /opt/awscli-venv/bin/activate` near the top of the script (after `set -euo pipefail`, before first `aws` call) | | Staleness >48h but watchdog didn't alert | Watchdog uses 36h threshold, so it reports OK for ~1.5 days after last successful backup | The watchdog is working as designed (grace period prevents false alarms). The real fix is preventing the backup from failing in the first place | ## Periodic audit cadence - **Weekly**: Run the quick audit script on existing buckets - **Monthly**: Review full backup vs live sync size trends for cost tracking - **Post-migration**: Run the full audit procedure immediately after any server migration or credential rotation