No description
  • Rust 91.5%
  • Nix 8.5%
Find a file
2026-06-25 12:02:59 +02:00
crates (refactor): remove key ids 2026-06-25 12:02:59 +02:00
todo (feat): implement remaining backup todos 2026-06-25 11:34:28 +02:00
.gitignore (feat): add root gitignore 2026-06-25 10:19:10 +02:00
AGENTS.md (feat): default decrypt identity to client 2026-06-24 18:19:56 +02:00
Cargo.lock (feat): implement remaining backup todos 2026-06-25 11:34:28 +02:00
Cargo.toml (feat): implement remaining backup todos 2026-06-25 11:34:28 +02:00
flake.lock (fix): expose module.nix 2026-05-22 23:40:53 +02:00
flake.nix (remake): big remake 2026-06-24 13:26:58 +02:00
module.nix (feat): implement remaining backup todos 2026-06-25 11:34:28 +02:00
README.md (refactor): remove key ids 2026-06-25 12:02:59 +02:00
SPECS.md (refactor): remove key ids 2026-06-25 12:02:59 +02:00

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 client identityFile.
  • lunitely-public:... for services.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 allowedDecryptIdentities empty encrypts backups to the client identityFile by default.
  • Configure allowedDecryptIdentities when you want identities other than the client identity to decrypt backups, such as an offline emergency recovery identity.
  • Keep the client service as root:root unless 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 = false unless 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.