Encryption at rest
Encryption at rest on the server is independent of E2EE. E2EE protects user record content from the server itself; encryption at rest protects sensitive material the server does have to handle (like AI provider API keys it proxies on behalf of users) from anyone who can read the disk or a database backup.
The master key
Section titled “The master key”The server holds one symmetric master key, used to derive per-user data keys via HKDF. It must be:
- Exactly 64 hex characters (32 bytes).
- Configured via the environment in production:
Encryption:MasterKeyorENCRYPTION_MASTER_KEY. - Backed up offline. If you lose it, ciphertext stored with it becomes permanently unreadable — even if the database survives.
Generate one with:
openssl rand -hex 32Once set on a running server, don’t change it casually. Rotating the master key requires re-encrypting every blob that was wrapped with it; there is no automated rotation today. Plan a maintenance window if you ever need to rotate.
What’s encrypted with it
Section titled “What’s encrypted with it”- Stored AI provider API keys in
ServerProviderrows. - Refresh tokens are stored as one-way hashes (PBKDF2), not encrypted — those don’t decrypt back, they’re verified.
E2EE-encrypted record payloads (memory, sessions, todos, etc.) are not wrapped with the master key. They’re already client-side ciphertext when the server receives them.
Wire format
Section titled “Wire format”The server uses AES-256-GCM with a 96-bit nonce and a 128-bit auth tag. Wire format is Base64 of nonce ‖ ciphertext ‖ tag. Per-user data keys are derived via HKDF-SHA256 from the master key.
| Element | Size |
|---|---|
| Nonce | 96 bits (12 bytes) |
| Ciphertext | variable |
| Auth tag | 128 bits (16 bytes) |
| Encoding | Base64 |
Unique nonce per record is enforced — never reuse a nonce with the same key.
Disk-level layers
Section titled “Disk-level layers”Beyond the application-level encryption above, you should also:
- Encrypt the host disk. On Hetzner, the OS volume is not encrypted by default — enable LUKS (or boot from an encrypted image) before storing production data.
- Encrypt Postgres backups.
pg_dumpoutput is plaintext. Pipe throughgpgorageand store the encryption key separately. - Restrict
/opt/pia/.env.prodtochmod 600. The deployment guide already does this; verify it after first boot.
What the server cannot protect against
Section titled “What the server cannot protect against”- A compromised host with root access. Root can read the master key from
/procof the running server process. - A backup with the master key included. If you back up the env file alongside the database, an attacker who steals both has everything.
- A coerced administrator. The server has no defence against an admin who legitimately holds the master key handing it to a third party.