Encryption & Security
ZeroFS encrypts all data with XChaCha20-Poly1305. There is no unencrypted mode. This page covers the key architecture, what is and is not visible on the object store, and password handling.
How Encryption Works
Key Architecture
- Data Encryption Key (DEK): A 256-bit key that encrypts your data
- Key Encryption Key (KEK): Derived from your password using Argon2id
- Encrypted DEK Storage: The KEK-wrapped DEK is a standalone object,
zerofs.key, at the database path on the object store — not a record inside the database
The zerofs.key object is bincode-serialized and holds the Argon2id salt, a 24-byte nonce, a plaintext version field, and the wrapped DEK. zerofs change-password rewrites it in place: the DEK is re-wrapped, the data itself is not re-encrypted.
Encryption Algorithm
- Algorithm: XChaCha20-Poly1305
- Compression: Zstd (default) or LZ4, applied before encryption
- Key Derivation: Argon2id
Configuration
# ZeroFS generates a new DEK on first run
[storage]
url = "s3://bucket/path"
encryption_password = "strong-password"
Password Change
# Reads the new password from stdin (one line, no prompt)
printf '%s\n' "$NEW_PASSWORD" |
zerofs change-password --config zerofs.toml
# ZeroFS re-wraps only the DEK, not all data
# Update config file afterwards:
# encryption_password = "new-strong-password"
What's Encrypted
Encryption applies at the SST block level: ZeroFS hands each encoded block (containing keys, values, and the block's internal index) to its block transformer, which compresses then encrypts the whole block. Decryption happens once per block on read; comparisons inside a block run on plaintext keys in memory, so there's no per-key encryption overhead.
Encrypted at Rest
- Name
File Contents- Type
- encrypted
- Description
All file data is encrypted in 32 KiB chunks using unique nonces
- Name
File Metadata- Type
- encrypted
- Description
Permissions, timestamps, ownership, directory-entry payloads, symlink targets, and other inode data
- Name
Keys Inside Blocks- Type
- encrypted
- Description
Inode IDs, directory entry names, chunk identifiers. Each block is encrypted as a unit, so the keys it indexes are protected.
- Name
SST Index & Filter Blocks- Type
- encrypted
- Description
Per-block first-key markers and bloom filter contents
Visible in Plaintext on the Object Store
- Name
Per-SST First/Last Key- Type
- plaintext
- Description
The SST footer's
SsTableInfoflatbuffer stores each SST's first and last key in plaintext. For a directory-entry SST this leaks the lexicographically-first and -last(dir_id, filename)it contains.
- Name
Manifest- Type
- plaintext
- Description
SST IDs, segment prefixes (
"meta","chunk"), object sizes, checkpoint pointers, format version
- Name
Object Metadata- Type
- plaintext
- Description
SST blob names, sizes, and counts (anything visible to an S3 LIST).
Local Cache Directory
- Name
Cache Contents- Type
- plaintext
- Description
The on-disk cache (default
~/.cache/zerofs) stores decrypted, decompressed SST blocks so subsequent reads don't pay the decrypt cost.
What S3 Sees
# Encrypted block payloads in SST data files:
s3://bucket/path/
├── compacted
│ ├── 01K1JW549K0H0MV3FH28CKBWTY.sst
│ ├── 01K1JW54FCCA109H1RJEHZ5NYK.sst
│ ├── 01K1JW54KE1XMV5DQG9KQ5R9B5.sst
├── manifest
│ ├── 00000000000000000001.manifest
│ └── ...
├── wal
│ ├── 00000000000000000001.sst
│ └── ...
├── zerofs.key # wrapped DEK
├── .zerofs_bucket_id # cache-namespace UUID
└── .zerofs_compatibility_test_<uuid>
# transient startup probe
# Inside each SST:
# - Data blocks: encrypted (keys + values opaque on disk)
# - Index blocks: encrypted (per-block first-key markers)
# - Filter blocks: encrypted (bloom filter bytes)
# - SST footer (SsTableInfo): plaintext, includes
# the SST's first and last key
# The manifest is plaintext.
Sidecar Objects
ZeroFS writes three plaintext objects to the database path alongside the SST, manifest, and WAL files.
- Name
zerofs.key- Type
- plaintext
- Description
The wrapped DEK (see Key Architecture above). Written on first read-write startup;
zerofs change-passwordrewrites it in place. The salt, nonce, and version are readable; the DEK itself stays encrypted under the password-derived KEK.
- Name
.zerofs_bucket_id- Type
- plaintext
- Description
A UUID written on first startup. ZeroFS namespaces the local cache with it: the cache lives in a per-bucket directory named after the UUID's first 8 characters. Deleting the object produces a new UUID and a fresh cache directory on the next startup; no filesystem data is lost.
- Name
.zerofs_compatibility_test_<uuid>- Type
- plaintext
- Description
A transient probe created and deleted at every read-write startup to verify that the store supports conditional puts (
PutMode::Create), which ZeroFS requires for fencing. Deletion is best-effort; leftovers from a crashed startup are cleaned at the next startup. Read-only mode skips the probe. On versioned buckets the put/delete cycle leaves noncurrent versions and delete markers.
Backups, replication rules, and lifecycle policies must include zerofs.key. Without it the data is unrecoverable even with the correct password. Prefix- or suffix-filtered rules (for example *.sst only) silently miss it.
Password Management
The KEK is derived from the encryption password; nothing else can unwrap the DEK.
Password Requirements
ZeroFS validates the password before touching the store: at zerofs run startup (the server refuses to start), in zerofs change-password, and in the standalone compactor and debug commands.
- Non-empty: empty passwords are rejected
- Minimum length: 8 characters, enforced with the error
Password must be at least 8 characters long. Use 25+ characters in practice. - Not the placeholder: the literal string
CHANGEMEis rejected withPlease choose a secure password, not 'CHANGEME'
Changing the Password
zerofs change-password reads exactly one line from stdin and trims surrounding whitespace. There is no interactive prompt, and terminal input is echoed — pipe the password instead of typing it:
printf '%s\n' "$NEW_PASSWORD" | zerofs change-password --config zerofs.toml
This form suits feeding from a secret manager. The command re-wraps the DEK inside zerofs.key; data is not re-encrypted. If the configured current password is still CHANGEME, the command fails with Current password is still the default. Please update your config file first — set a real password in the config before changing it. After a successful change, update the config file or environment variable yourself; ZeroFS does not modify the config.
Secure Password Practices
# Generate a secure password
openssl rand -base64 32
# Store in a secret manager (AWS example)
aws secretsmanager create-secret \
--name zerofs-prod-password \
--secret-string "$(openssl rand -base64 32)"
# Use in production with environment variable substitution
# In zerofs.toml:
# [storage]
# encryption_password = "${ZEROFS_PASSWORD}"
export ZEROFS_PASSWORD=$(
aws secretsmanager get-secret-value \
--secret-id zerofs-prod-password \
--query SecretString --output text
)
zerofs run --config zerofs.toml
Lost Password Recovery
If the password is lost, the data is permanently inaccessible; ZeroFS has no key material recovery mechanism. Keep a backup of the password.
Security Best Practices
Network Security
# Bind to localhost only (default) in zerofs.toml
[servers.nfs]
addresses = ["127.0.0.1:2049"]
[servers.nbd]
addresses = ["127.0.0.1:10809"]
# For remote access, use VPN or SSH tunneling
# ssh -L 2049:localhost:2049 user@zerofs-server