This guide is a concise, practical checklist to harden SQLite in apps and services. It focuses on real‑world steps you can apply today, with copy‑paste snippets.
TL;DR checklist
- Encrypt at rest with SQLCipher (or SEE); never store plaintext databases
- Keep keys in a secrets manager; rotate and avoid hard‑coding
- Owner‑only permissions on DB/journal/WAL files (chmod 600), private directories
- Use parameterized queries to prevent SQL injection
- Prefer
journal_mode=WAL,synchronous=FULL,foreign_keys=ON - Consider
secure_delete=ONfor sensitive deletes; usetemp_store=MEMORYto reduce disk traces - Disable extension loading; use read‑only/query‑only connections when possible
- Back up securely (encrypted), test restores, and scrub plaintext copies
- Run the app as a non‑root user; restrict filesystem access
1) Encrypt the database at rest (SQLCipher)
SQLite itself has no built‑in encryption. Use SQLCipher (open source) or SEE (commercial) to encrypt the entire database, including pages, WAL, and journals.
Install (example macOS):
brew install sqlcipher
Initialize or open an encrypted DB:
PRAGMA key = 'your strong passphrase here';
-- Optional: set KDF iteration count and cipher settings per SQLCipher docs
-- PRAGMA kdf_iter = 256000;
-- PRAGMA cipher_page_size = 4096;
Python example:
# pip install pysqlcipher3
import sqlite3
from pysqlcipher3 import dbapi2 as sqlcipher
conn = sqlcipher.connect("app.db")
cur = conn.cursor()
cur.execute("PRAGMA key = ?;", ("env_or_secret_manager_key",))
cur.execute("CREATE TABLE IF NOT EXISTS notes(id INTEGER PRIMARY KEY, body TEXT);")
conn.commit()
Key tips:
- The passphrase must come from a secrets manager or OS keystore (not in code)
- Rotate keys with SQLCipher’s
PRAGMA rekeyduring a maintenance window
2) Manage keys safely
- Store keys in a secrets manager (AWS Secrets Manager, GCP Secret Manager, Vault)
- Inject via environment variables or secure files owned by the app user
- Rotate on a schedule and on staff offboarding; monitor for key misuse
.env (development only):
SQLCIPHER_KEY="change-me"
3) Lock down file permissions and location
SQLite writes multiple files: the main .db, -wal, and -shm when WAL is enabled. All must be protected.
# Run your app as its own user; restrict ownership and permissions
chown appuser:appuser app.db*
chmod 600 app.db*
umask 077
Recommendations:
- Place DB files in a private, non‑shared directory
- Avoid world‑readable paths (e.g.,
/tmpor shared volumes) - On mobile, disable system backups for app data if the DB is sensitive
4) Prevent SQL injection with parameterized queries
Never concatenate user input into SQL. Use parameters in every query.
Python:
cur.execute("SELECT * FROM users WHERE email = ?", (email,))
Node (better-sqlite3):
db.prepare("INSERT INTO notes (body) VALUES (?)").run(body);
5) Safer PRAGMA defaults for security and durability
Set these after opening each connection (or via your ORM/driver hooks):
PRAGMA foreign_keys = ON; -- enforce referential integrity
PRAGMA journal_mode = WAL; -- better durability and concurrency
PRAGMA synchronous = FULL; -- stronger crash safety for WAL
PRAGMA temp_store = MEMORY; -- reduce sensitive temp files on disk
PRAGMA secure_delete = ON; -- overwrite deleted content (perf trade-off)
Notes:
secure_delete=ONincreases write overhead; enable when data remnants mattertemp_store=MEMORYincreases RAM usage; ensure capacity planning
Read‑only or query‑only connections reduce risk for services that only need reads:
PRAGMA query_only = ON; -- disallow writes for this connection
6) Disable extension loading
Block loading of arbitrary extensions to reduce attack surface.
Python:
conn.enable_load_extension(False)
C:
sqlite3_enable_load_extension(db, 0);
7) Backups and migrations without leaks
- Keep backups encrypted; do not dump to plaintext
- Use
VACUUM INTOto atomically export a copy (SQLCipher supports encrypted copies when a key is set)
-- Plain SQLite
VACUUM INTO 'backup.sqlite';
-- SQLCipher: set key on the new file first (see SQLCipher docs)
-- PRAGMA key = 'current_key';
-- PRAGMA cipher_export = 'encrypted.sqlite';
-- SELECT sqlcipher_export('main');
Operational tips:
- Ensure backup storage is access‑controlled and encrypted
- Test restore procedures regularly
- Scrub old plaintext copies and temporary export files
8) OS and container hardening
- Run the app as a non‑root user; apply least‑privilege filesystem access
- Use full‑disk encryption on servers/laptops; prefer encrypted volumes
- Constrain the process with AppArmor/SELinux where available
- Monitor for unexpected reads of the DB directory
9) Observability and integrity signals
- Log DB open/close events, failed auth, and migration operations (without secrets)
- Track file hashes and sizes to detect unexpected changes
- Alert on permission drift (e.g., files no longer
0600)
Final checklist
- SQLCipher (or SEE) in use; keys from secrets manager/OS keystore
- Owner‑only permissions on
.db,-wal,-shmfiles - Queries parameterized; no string‑built SQL
-
foreign_keys=ON,journal_mode=WAL,synchronous=FULL,temp_store=MEMORY -
secure_delete=ONif sensitive deletions matter - Extension loading disabled; read‑only/query‑only where applicable
- Encrypted backups; restores tested; old plaintext copies removed
- App runs as non‑root with least‑privilege filesystem access