All Articles
Security

The Encryption Checklist for a Small Django Production Stack

Shahid MalikBy Shahid MalikSeptember 5, 20267 min read

Thirteen encryption checks, framed as 'encryption at multiple layers, not just HTTPS.' Most of the layers already existed — TLS to Postgres, client-side-encrypted backups — one item doesn't apply to this architecture at all, and two needed an AWS console check I couldn't run myself.

Part three of the Tabeer.ai production-readiness audit, this time an encryption-specific section: thirteen checks, framed as "you should have encryption at multiple layers, not just HTTPS." Correct framing — HTTPS alone leaves the database connection, the backups, and the storage layer unaddressed. Here's each layer, checked against what Tabeer.ai actually runs, not assumed.

Already True, Verified Directly

HTTPS/TLS for every production request. Cloudflare and nginx both enforce it in front of the app, and — after part one of this audit — Django itself now redirects and sends its own HSTS header too, so the guarantee holds even if a request ever reached the app server directly.

TLS between the application and PostgreSQL. Already configured, not something this pass added:

DATABASE_URL=postgres://user:pass@tabeer.xxx.rds.amazonaws.com:5432/tabeer?sslmode=verify-full&sslrootcert=/opt/tabeerdotai/backend/rds-global-bundle.pem

sslmode=verify-full is the strict end of Postgres's SSL modes — it doesn't just encrypt the connection, it verifies the server certificate against the CA bundle and confirms the hostname matches, closing the door on a man-in-the-middle presenting a valid-but-wrong certificate.

Backups encrypted at rest, and in transit. The Restic backup pipeline encrypts client-side before anything leaves the server — confirmed earlier by inspecting the actual S3 objects: opaque content-addressed chunks, not a readable dump. The S3 bucket itself also has SSE-S3 encryption enabled independently (defense in depth — even if Restic's encryption were somehow bypassed, the bucket's own encryption still applies), and every byte travels to S3 over HTTPS by default — there's no way to configure the s3: backend to use plaintext.

Secrets outside git, and never committed. Verified two different ways: current .gitignore excludes .env, and a full git log --all --full-history -- "**/.env" search turned up zero hits across the entire repository history — not just "currently ignored," genuinely never committed at any point.

Password reset tokens. Django's default_token_generator — HMAC-based, and it folds the user's current password hash into the token, which is what makes it automatically single-use. Reusing a reset link after a successful reset (or requesting a second one) fails as an invalid token, with no separate "mark this token used" bookkeeping needed.

Sensitive data not written to logs. Grepped for logging calls anywhere near passwords, tokens, or raw request bodies — the only hits were logger.warning calls logging a bare user ID when a Celery task's target user no longer exists, nothing resembling PII. Sentry's send_default_pii is already False on both the frontend and backend, so even an unhandled exception's context doesn't leak personal data into error reports.

Encryption keys kept separate from what they encrypt. The Restic repository password lives in a dedicated backup.env on the server, entirely separate from the S3 bucket holding the encrypted data. For RDS and S3's own encryption, AWS's key management (KMS) is architecturally separate from the data by design — the key store and the object store are different services.

Doesn't Apply to This Architecture

TLS between the application and Redis. Redis runs on the same box as the Django app, reachable only via loopback (127.0.0.1) — it's never been reachable over the network, and never will be under this deployment. TLS protects data crossing a network segment an attacker could intercept; a loopback connection to your own process never leaves the machine at all. Encrypting it would be pure overhead with no attacker in the threat model it defends against — the same reasoning applied earlier to why Trivy's dependency scanner wasn't worth duplicating: match the control to the actual gap, not to everything a checklist can list.

Object/file storage encrypted. Doesn't currently apply either, for a more mundane reason — Tabeer.ai's media uploads (profile pictures) live on the EC2 instance's own disk (MEDIA_ROOT), not in S3 or any other object storage service. There's no separate object-storage layer yet for this item to describe. (The disk itself encrypting at rest is a real, related question — see below.)

Needed a Console Check I Couldn't Run

Two items — database encrypted at rest and, by extension, the EC2 instance's disk (EBS volume) — needed rds:DescribeDBInstances and ec2:DescribeVolumes, and the IAM credentials available for this work are deliberately scoped to S3 only (set up that way for the backup pipeline, not broadened since). Rather than either guess or ask for broader AWS access just to answer one checklist line, this stayed a direct question back to the person who already has console access — a two-minute check in the RDS and EC2 consoles beats widening an IAM policy for a single read.

This is worth calling out on its own: "I don't have the access to verify this" is a legitimate answer, and a more honest one than assuming a default. RDS storage encryption defaults to enabled on instances created through the modern console, but "usually on by default" and "confirmed on for this instance" are different claims — the whole point of this three-part audit was refusing to substitute the first for the second anywhere else, so the same standard applies here too.

The Pattern Across All Three Parts

Deploy-check settings, auth hardening, and this encryption pass all followed the identical shape: take the claim, go find the actual answer in the running system, and only change what's actually wrong. Across all three sections combined — 43 checklist items — the real gap count was under a third of what got flagged. The other two-thirds were either already correct or didn't apply to this specific architecture at all. A checklist is a useful prompt for what to check; it's not a substitute for actually checking it.

If you've got a security checklist or audit report you want verified against reality before acting on it, get in touch.

Related Articles

Security

Rate Limiting, Token Blacklisting, and Admin MFA — Auth Hardening for Tabeer.ai

Part two of the production-readiness checklist: authentication and account security. Five of fifteen items were genuinely missing — rate limiting, real logout, admin MFA, consent records, and self-service data export/deletion — and here's exactly how each got fixed without a new dependency for most of them.

10 min read
Shahid Malik - AI-First Odoo Consultant

Shahid Malik

AI-First Odoo ERP Specialist

Shahid Malik is an AI-first Odoo consultant helping businesses solve complex ERP and business process challenges. His work combines Odoo consulting, process optimization, automation, integrations, migrations, and practical AI solutions to build scalable and reliable business systems.

Book a consultation for your Odoo project
Discuss Your Odoo Project