Skip to main content

Backup & Restore

This page covers backup strategies and restore procedures for self-hosted Infracast deployments. SaaS customers have automated daily backups managed by Infracast — no action required.

SaaS Customers

If you're using app.infracast.io, your data is backed up automatically with 35-day retention. Contact support to request a data export.

What to Back Up

DataLocationCriticality
Asset graph, findings, users, tenantsPostgreSQL🔴 Critical
Evidence artifactsS3 or local filesystem🟡 High
Configuration (env vars, secrets)Your secrets manager / .env🔴 Critical
Plugin binariesContainer image / filesystem🟢 Low (re-downloadable)

PostgreSQL Backups

Option A: pg_dump (Docker / Self-Hosted)

Run a manual dump:

# Docker Compose
docker compose exec postgres pg_dump \
-U vulcan \
-d vulcan \
-F c \
-f /tmp/vulcan-$(date +%Y%m%d-%H%M%S).dump

# Copy dump from container to host
docker compose cp postgres:/tmp/vulcan-*.dump ./backups/

Automate with cron:

/etc/cron.d/vulcan-backup
# Daily backup at 02:00 UTC, retain 30 days
0 2 * * * root /usr/local/bin/vulcan-backup.sh
/usr/local/bin/vulcan-backup.sh
#!/bin/bash
set -e

BACKUP_DIR="/opt/vulcan/backups"
DATE=$(date +%Y%m%d-%H%M%S)
DUMP_FILE="$BACKUP_DIR/vulcan-$DATE.dump"

mkdir -p "$BACKUP_DIR"

docker compose -f /opt/vulcan/docker-compose.yml exec -T postgres \
pg_dump -U vulcan -d vulcan -F c > "$DUMP_FILE"

# Compress
gzip "$DUMP_FILE"

# Remove backups older than 30 days
find "$BACKUP_DIR" -name "*.dump.gz" -mtime +30 -delete

echo "Backup complete: ${DUMP_FILE}.gz"

Upload to S3 for off-site storage:

aws s3 cp "$DUMP_FILE.gz" "s3://your-backup-bucket/vulcan/$(basename $DUMP_FILE.gz)"

Option B: RDS Automated Backups (Terraform / AWS)

If using the Terraform module with RDS, automated backups are enabled by default:

# In terraform.tfvars
db_backup_retention_days = 35 # 35 days for prod, 7 for dev
db_backup_window = "02:00-03:00" # UTC
db_maintenance_window = "sun:03:00-sun:04:00"

RDS automated backups:

  • Daily snapshot taken during backup window
  • Transaction logs backed up continuously (point-in-time recovery to any second)
  • Stored in S3 in the same region

Option C: RDS Manual Snapshots

Before upgrades or major changes:

# Create a manual snapshot
aws rds create-db-snapshot \
--db-instance-identifier vulcan-prod \
--db-snapshot-identifier vulcan-pre-upgrade-$(date +%Y%m%d)

# List snapshots
aws rds describe-db-snapshots \
--db-instance-identifier vulcan-prod \
--query "DBSnapshots[*].{ID:DBSnapshotIdentifier,Time:SnapshotCreateTime,Status:Status}" \
--output table
tip

Create a manual RDS snapshot before every upgrade. Unlike automated backups, manual snapshots are retained indefinitely until you delete them.

Evidence Artifact Backups

Evidence artifacts are stored in S3 (Terraform/SaaS) or locally (Docker).

S3 (Terraform/SaaS)

Enable S3 versioning and cross-region replication on the evidence bucket:

resource "aws_s3_bucket_versioning" "evidence" {
bucket = aws_s3_bucket.evidence.id
versioning_configuration {
status = "Enabled"
}
}

Local (Docker)

# Backup local evidence directory
tar -czf vulcan-evidence-$(date +%Y%m%d).tar.gz /opt/vulcan/data/evidence/

# Upload to S3
aws s3 cp vulcan-evidence-*.tar.gz s3://your-backup-bucket/evidence/

Configuration Backup

Back up environment variables and secrets regularly:

# Export current running env (Docker)
docker compose exec vulcan-api env | grep -v PASSWORD | sort > backup-env-$(date +%Y%m%d).txt

# Store secrets separately in your vault/secrets manager
# Never commit secrets to git

Terraform state should be stored in a remote backend:

backend.tf
terraform {
backend "s3" {
bucket = "your-terraform-state-bucket"
key = "vulcan/prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}

Restore Procedures

Restore PostgreSQL from pg_dump

# Stop the API to prevent writes during restore
docker compose stop vulcan-api

# Drop and recreate the database
docker compose exec postgres psql -U vulcan -c "DROP DATABASE IF EXISTS vulcan;"
docker compose exec postgres psql -U vulcan -c "CREATE DATABASE vulcan;"

# Restore from dump file
docker compose exec -T postgres pg_restore \
-U vulcan \
-d vulcan \
--no-owner \
--role=vulcan \
< ./backups/vulcan-20260101-020000.dump.gz

# Restart the API (runs migrations automatically)
docker compose start vulcan-api

# Verify
curl http://localhost:8080/healthz

Restore RDS from Automated Backup (Point-in-Time)

# Restore to a specific time
aws rds restore-db-instance-to-point-in-time \
--source-db-instance-identifier vulcan-prod \
--target-db-instance-identifier vulcan-prod-restored \
--restore-time "2026-04-01T02:00:00Z"

# Once restored, update DATABASE_URL to point to new instance
# Then run migrations

Restore RDS from Manual Snapshot

# Restore from snapshot
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier vulcan-prod-restored \
--db-snapshot-identifier vulcan-pre-upgrade-20260401 \
--db-instance-class db.r5.large \
--multi-az

# Update ECS task definition / DATABASE_URL to new endpoint

Data Retention Policies

Data TypeSaaS RetentionSelf-Hosted DefaultNotes
Asset graphIndefiniteIndefiniteNodes updated in place
Finding historyIndefiniteIndefiniteAppend-only status changes
Audit logs2 yearsIndefiniteCompliance requirement
Evidence artifacts3 yearsConfigurableFedRAMP recommends 3 years
Discovery job logs90 days90 daysPruned automatically
RDS automated backups35 daysPer configSet in Terraform

Automatic Data Pruning

Infracast automatically prunes old job logs after 90 days. This runs as a background job nightly at 03:00 UTC (server time). No configuration is required.

For compliance requirements beyond 90 days for job logs, export logs to S3 before pruning:

# Export old job logs before they're pruned (optional)
curl -H "Authorization: Bearer $TOKEN" \
"https://api.infracast.io/api/v1/tenants/$TENANT/jobs?before=2025-01-01&format=jsonl" \
-o job-logs-archive.jsonl

Backup Verification

Test your backups regularly — untested backups are not backups.

#!/bin/bash
# Weekly backup verification test
# Run on a separate test instance

TEST_DB="vulcan_restore_test"
LATEST_BACKUP=$(ls -t /opt/vulcan/backups/*.dump.gz | head -1)

echo "Testing restore of $LATEST_BACKUP..."

# Create test database
psql -U postgres -c "CREATE DATABASE $TEST_DB;"

# Restore
pg_restore -U postgres -d "$TEST_DB" < "$LATEST_BACKUP"

# Verify row counts
TENANT_COUNT=$(psql -U postgres -d "$TEST_DB" -tAc "SELECT COUNT(*) FROM tenants;")
NODE_COUNT=$(psql -U postgres -d "$TEST_DB" -tAc "SELECT COUNT(*) FROM nodes;")

echo "Tenants: $TENANT_COUNT"
echo "Nodes: $NODE_COUNT"

# Cleanup
psql -U postgres -c "DROP DATABASE $TEST_DB;"

echo "Backup verification complete."