Encrypt Django Database and Media Backups

Encrypt PostgreSQL dumps and media backups for a Django app: use age or GPG for client-side encryption, restic or pgBackRest built-in encryption, server-side encryption with KMS on S3, manage and store keys separately, and test restores with decryption.

Problem statement

Your nightly pg_dump contains every user's email, password hash, address, and order history. It sits in an S3 bucket, maybe copied to a second provider and a laptop for testing. If any copy leaks, so does your entire database. Backups need encryption that protects them even if the storage provider, a bucket policy, or a credential fails, with keys stored somewhere other than next to the backups.

Encrypted backupspg_dumpstreamEncryptage, GPG, resticUploadS3 with SSE tooKeys elsewherevault, offline copyEncrypt before upload, and keep the key where the backups are not.
  1. Encrypt client-side before upload: pg_dump -Fc | age -r <public-key> | aws s3 cp - s3://backups/....
  2. Or use a tool with built-in encryption: restic for files and dumps, pgBackRest or WAL-G for PostgreSQL with point-in-time recovery.
  3. Enable server-side encryption (SSE-KMS) on the bucket as an extra layer, not the only one.
  4. Store private keys outside the backup account: a password manager, a separate cloud account's secrets manager, and an offline copy.
  5. Test restores regularly, including decryption with the keys from where they are actually stored.

Step-by-step solution

1. Choose the encryption layer

  • Client-side (age, GPG, restic, pgBackRest): data is encrypted on your server; the storage provider and anyone with bucket access see only ciphertext.
  • Server-side (S3 SSE-S3 or SSE-KMS): the provider encrypts at rest; anyone with read access to the bucket and the KMS key gets plaintext.

Use client-side encryption for protection against leaked bucket credentials or misconfigured policies; add SSE-KMS for defense in depth and compliance.

2. Encrypt dumps with age

age uses public-key encryption, so the backup server only needs the public key and cannot decrypt old backups if compromised:

age-keygen -o backup-key.txt          # do this on a secure machine; keep the private key offline
# public key looks like: age1qz...

pg_dump --format=custom --no-owner "$DATABASE_URL" \
  | age -r age1qzexample... \
  | aws s3 cp - "s3://shop-backups/pg/shop-$(date -u +%Y%m%dT%H%MZ).dump.age"

Restore:

aws s3 cp s3://shop-backups/pg/shop-20261003T0200Z.dump.age - \
  | age -d -i backup-key.txt \
  | pg_restore --no-owner --dbname "$RESTORE_DATABASE_URL"
option and who can read backupsEncryptsBucket access reveals data?S3 SSE-S3at rest, provider key✕yesS3 SSE-KMSat rest, your KMS key!if KMS allowedage or GPGclient-side filenoresticclient-side reponopgBackRest / WAL-Gclient-side, PITRnoClient-side encryption protects backups even when storage access leaksdjango-deployment.com
Combine client-side encryption with SSE-KMS for layered protection.

3. Use built-in encryption in backup tools

restic encrypts and deduplicates everything in its repository with a password:

export RESTIC_REPOSITORY=s3:s3.amazonaws.com/shop-media-backups
export RESTIC_PASSWORD_FILE=/etc/restic/password
restic backup /srv/shop/media
restic forget --keep-daily 14 --keep-weekly 8 --prune

pgBackRest supports repo1-cipher-type=aes-256-cbc with a passphrase, and WAL-G supports libsodium or OpenPGP encryption, both covering base backups and WAL archives for point-in-time recovery.

4. Manage keys separately

The private key or passphrase must survive the loss of your servers and must not live in the same account as the backups:

  • Store it in a password manager with restricted access, and in a secrets manager in a separate cloud account.
  • Keep an offline copy (printed or on hardware) for disaster scenarios.
  • Document who can access it and how to use it in the recovery runbook.

Losing the key means losing every backup encrypted with it.

5. Test decryption as part of restore tests

Schedule restore drills that fetch the key from where it is actually stored, decrypt, restore into a fresh database, and run Django checks against it. Automate a weekly restore of the latest dump into a scratch database and alert if it fails.

nightly job and recovery1pg_dump to stdoutcustom format2Encrypt with public keyage, no secret on server3Upload with SSE-KMSversioned bucket4Key stored elsewherevault + offline5Weekly decrypt + restoreproves it worksAn untested encrypted backup is a locked box with a lost keydjango-deployment.com
Object lock on the bucket also protects backups from deletion by attackers.

Explanation

Encryption at rest by the storage provider protects against stolen disks in the provider's data center, not against someone who obtains read access to the bucket. Client-side encryption makes the backup file itself unreadable without the key, so leaked credentials, public buckets, or copied files reveal nothing. Public-key schemes such as age go further: the server that creates backups cannot decrypt them, limiting damage if it is compromised.

Edge cases / notes

  • Key rotation. Rotate keys periodically; keep old private keys as long as backups encrypted with them are retained.
  • Compliance. Regulations such as GDPR treat encrypted backups more favorably in breach assessments, but retention and deletion obligations still apply.
  • Performance. Encryption adds little CPU cost compared with compression and upload time.

Next steps

FAQ

Is S3 server-side encryption enough for backups?

It protects data at rest, but anyone with bucket and key access can read it. Add client-side encryption for stronger protection.

Should backups use public-key encryption?

It is a good choice: the backup server holds only the public key and cannot decrypt existing backups.

Where should I keep the decryption key?

Outside the backup account: a password manager, a separate secrets manager account, and an offline copy.

Do I need to encrypt media backups too?

Yes, if uploads contain personal or sensitive data. restic encrypts them by default.

Keep going