Best practices for securing SQLite

Fri Nov 21 2025

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=ON for sensitive deletes; use temp_store=MEMORY to 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 rekey during 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., /tmp or 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=ON increases write overhead; enable when data remnants matter
  • temp_store=MEMORY increases 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 INTO to 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, -shm files
  • Queries parameterized; no string‑built SQL
  • foreign_keys=ON, journal_mode=WAL, synchronous=FULL, temp_store=MEMORY
  • secure_delete=ON if 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