- Rust 91.5%
- Nix 8.5%
| crates | ||
| todo | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| flake.lock | ||
| flake.nix | ||
| module.nix | ||
| README.md | ||
| SPECS.md | ||
Lunitex
Lunitex creates local post-quantum encrypted backup bundles and stores them on backup servers that cannot decrypt them.
Generate Keys
lunitex generate-keys --client-name laptop
This prints two raw base64 JSON identity bundles:
lunitely-private:...for the clientidentityFile.lunitely-public:...forservices.lunitex.server.clients.<name>.identityFile.
The private bundle contains the auth token, ML-KEM public/private keys, ML-DSA public/private keys, and Ed25519 public/private keys. The public bundle contains only the auth token and public keys needed by the server.
NixOS Client Example
{
services.lunitex = {
enable = true;
client = {
enable = true;
# The backup service runs as root:root by default so it can read protected paths.
# Use a less privileged user only if all backup sources are readable by that user.
user = "root";
group = "root";
timer = "daily";
directories = [
/var/lib/myapp
/etc/myapp
];
localStoreDirectory = "/var/lib/lunitex/bundles";
# Private identity bundle generated by `lunitex generate-keys`.
# Keep this secret; it contains auth, signing, and restore private keys.
# If allowedDecryptIdentities is empty, backups are decryptable by this identity.
identityFile = "/run/secrets/laptop.private-identity";
# Optional: add extra public identities that may decrypt backups.
# Recommendation: add an offline recovery identity for important data.
allowedDecryptIdentities = [
{
name = "offline-recovery";
identityFile = "/run/secrets/offline-recovery.public-identity";
}
];
servers = [
{
# Logical server name used in logs.
name = "primary";
# QUIC backup server address.
host = "backup.example.com";
port = 4433;
}
];
retention = {
enable = true;
keepBundles = 7;
};
};
};
}
NixOS Server Example
{
services.lunitex = {
enable = true;
server = {
enable = true;
# The server service runs as lunitex:lunitex by default.
user = "lunitex";
group = "lunitex";
listenAddress = "0.0.0.0";
port = 4433;
openFirewall = true;
clients.laptop = {
# Where this client's ciphertext-only bundles are stored.
backupDir = "/srv/lunitex/laptop";
# Public identity bundle generated alongside the client's private identity.
# The server uses it for upload authorization and signature verification.
identityFile = "/run/secrets/laptop.public-identity";
# Recommendation: keep enough remote backups to survive unnoticed corruption or compromise.
keepBundles = 30;
};
};
};
}
Restore
lunitex restore ./backup.lunitex \
--output ./restore \
--identity-file /run/secrets/laptop.private-identity
The server cannot restore backups because it never receives ML-KEM private keys.
Restore refuses to overwrite existing files unless --overwrite is passed.
Upload Transport
lunitex backup --upload and lunitex upload use the QUIC listener from lunitex-server serve. The client verifies the local .lunitex bundle before upload, retries transient transport failures, and preserves the local bundle if any server upload fails.
The QUIC protocol sends already encrypted bundle files only. Upload auth uses a nonce-bound BLAKE3 proof of the identity auth token, so the raw token is not sent over the transport.
Metadata Limitations
Lunitex uses Rust tar support and preserves standard file metadata such as modes where practical. ACLs and xattrs are not preserved in this version.
Recommendations
- Store
lunitely-private:...identity files with a secret manager such as sops-nix or agenix. - Store
lunitely-public:...identity files as configuration or lower-sensitivity secrets; they include the upload auth token, so do not publish them. - Do not rely on token hashing alone for MITM protection; replay-safe upload auth belongs in the authenticated QUIC transport or a nonce-bound challenge-response protocol.
- Leaving
allowedDecryptIdentitiesempty encrypts backups to the clientidentityFileby default. - Configure
allowedDecryptIdentitieswhen you want identities other than the client identity to decrypt backups, such as an offline emergency recovery identity. - Keep the client service as
root:rootunless every configured backup source is readable by a less privileged user. - Keep the offline recovery private identity away from the backup client and backup server.
- Keep local and server retention enabled so accidental deletion or corruption is recoverable.
- Keep
openFirewall = falseunless this host is actually serving remote clients. - Review
ml-kem,ml-dsa, QUIC, and Rustls dependency advisories before production use; the post-quantum crates are young implementations.