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.
By Shahid Malik