Encryption
Penombre can seal file bytes at rest with a key you hold, but start with
disk-level encryption on DATA_DIR: for a stolen or decommissioned drive it
covers strictly more, because it also covers the database. Application-level
encryption then adds one thing disk encryption cannot: file bytes that stay
sealed when they are copied off the server.
#What each layer protects
| Someone has | Disk encryption | ENCRYPTION_KEY |
|---|---|---|
| A stolen, powered-off disk (database on it) | files and DB ok | files ok, DB exposed |
A copy of STORAGE_PATH only (NAS, backup) | exposed | safe |
| Disk, database and the key together | exposed | exposed |
| Root on the running server | exposed | exposed |
Neither hides sizes, file counts, folder shape or modification times. Names live in the database, never in the sealed bytes: disk encryption is what protects them.
#Disk encryption first
By default everything Penombre writes lives under DATA_DIR (see
Storage): file bytes and thumbnails under STORAGE_PATH, and the
SQLite database under DATA_DIR/db. The database holds names, email addresses,
password hashes, two-factor secrets, API keys and OAuth client secrets, so it
needs the same protection as the files.
Put DATA_DIR on an encrypted block device or filesystem before Penombre ever
writes to it:
- LUKS (Linux): encrypt the block device the volume is mounted from.
- ZFS native encryption:
zfs create -o encryption=on .... - FileVault (macOS): whole-disk encryption for a Mac used as a server.
- Your cloud provider's encrypted-volume option, for a mounted cloud disk.
A Postgres database (DATABASE_URL) and mounted volumes (VOLUME_<NAME>_PATH,
see Volumes) live outside DATA_DIR: encrypt the disks they sit
on too.
#Sealing file bytes
Set ENCRYPTION_KEY to 32 random bytes in base64, on the app and on any
external worker:
openssl rand -base64 32Or keep it in a file and point ENCRYPTION_KEY_FILE at it, which is the
recommended way: a Docker secret mounted outside DATA_DIR means a backup of
/data never carries its own key.
services:
penombre:
environment:
ENCRYPTION_KEY_FILE: /run/secrets/penombre_encryption_key
secrets:
- penombre_encryption_key
secrets:
penombre_encryption_key:
file: ./encryption.keyLose the key and every sealed file is gone. There is no recovery without it.
Back it up the way you back up a password manager's master key, somewhere other
than the backups of DATA_DIR.
Every file is sealed with AES-256-GCM under its own random key, which is itself
wrapped by ENCRYPTION_KEY and stored in the file's header. Previews,
waveforms, streaming with seeking, zips, shares, public links and office editing
all keep working: the app and the worker decrypt as they read.
#Turning it on for an existing instance
- Back up
DATA_DIR, and keep the key out of that backup. - Set the key on the app and on any external worker, then restart.
- New uploads are sealed at once; older files stay readable meanwhile.
- A background sweep seals the existing files a batch at a time. It needs free space for one copy of the largest file. The log reports its progress.
- Temporary zips in
STORAGE_PATH/.tmpexpire within an hour.
Encryption does not reach back in time: old backups, snapshots and the blocks an SSD has not reclaimed yet still hold plaintext.
#What it covers
- Personal and shared drives: sealed as soon as a key is set, existing files included.
- Mounted volumes: plaintext by default.
VOLUME_<NAME>_ENCRYPT=trueseals what Penombre writes there (uploads, saves, copies, thumbnails). Files already on the volume, or dropped in by other tools, are never rewritten. It cannot be combined withVOLUME_<NAME>_READONLY. - Simple mode is not supported: its root is a library other tools read, and sealing it would turn it into ciphertext for them. The app refuses to start with both set.
Nothing binds a sealed file to its path: someone with write access to the disk can swap two sealed files between accounts. Disk encryption and ordinary filesystem permissions are the answer to that attacker.
#Once it is on
The first boot with a key records its id in the database. From then on the app
refuses to start without a key that can open the files: removing
ENCRYPTION_KEY, or replacing it with another one, stops the boot with a
message naming the key it expects.
#Rotating the key
- Move the current key to
ENCRYPTION_KEY_PREVIOUS(comma-separated if there are several) and set a newENCRYPTION_KEY, on the app and on any external worker. Restart. - The sweep rewrites each file's header under the new key; the bytes keep their own file key. It also moves what Penombre sealed on mounted volumes.
- Once the log says
ENCRYPTION_KEY_PREVIOUS can be removed, remove it.
A rotation does not re-encrypt the content: someone who holds both the old key and an old copy of a file can still read that copy.
#Recovering files without Penombre
The worker binary opens a sealed file on its own, with the same variables:
ENCRYPTION_KEY_FILE=./encryption.key penombre-worker -decrypt path/to/file > out
ENCRYPTION_KEY_FILE=./encryption.key penombre-worker -encrypt-check /data/storage-decrypt writes the plaintext to stdout (plaintext files pass through);
-encrypt-check counts plaintext, sealed and unreadable files under a
directory. Neither needs the database, so a copy of STORAGE_PATH and the key
are enough to get every file back.
#Performance
AES is hardware accelerated on any recent x86 or ARM server. A Raspberry Pi 4 has no crypto extensions and manages roughly 50 to 100 MB/s, which is felt when streaming large video.
This guide lives in the project repo: edit it there, and this page follows within a day.