Security

Encryption

All encryption and decryption happens in the browser using the Web Crypto API.

Key generation uses RSA-OAEP 2048-bit key pairs. Encryption is hybrid: AES-256-GCM encrypts the data, and RSA-OAEP encrypts the AES key. The payload sent to the server is a Base64-encoded JSON object containing encryptedKey, encryptedData, and iv.

Key storage

Each pod gets its own private/public key pair.

Private keys are stored in localStorage under privipod_key_<hash>.

They persist until:

  • The pod is set to self-destruct (the key is removed after the owner decrypts the secret); or

  • You clear your browser storage manually; or

  • The key expires and you visit the dashboard for it to be pruned.

Note

For the truly paranoid, use a dedicated private browsing session and clear storage afterwards, or download the key to a secure location and delete it from the browser.

Secret key

Privipod uses a secret key to sign sessions and CSRF tokens. If you do not set one, a random key is generated on every startup - this logs a warning and means all users are logged out whenever the process restarts.

Set a persistent key via the PRIVIPOD_SECRET_KEY environment variable:

export PRIVIPOD_SECRET_KEY="your-long-random-string"

The key should be at least 50 characters long, contain a mix of letters, digits, and symbols, and be generated randomly - never use a memorable phrase or reuse a key from another project.

In the Docker deployment, add it to docker-compose.yml:

environment:
  - PRIVIPOD_SECRET_KEY=your-long-random-string-here

For systemd, set it in the [Service] section:

Environment=PRIVIPOD_SECRET_KEY=your-long-random-string-here

HTTPS requirement

Privipod must be served over HTTPS in production. The Web Crypto API requires a secure context, and without HTTPS the private key stored in localStorage is accessible to any script on the same origin. See install for configuration examples.

Running Modes

Privipod operates in two modes depending on whether --hostname is provided.

Setting

Untrusted-host (default)

Deployed (--hostname)

ALLOWED_HOSTS

["*"]

[hostname, …]

CSRF_TRUSTED_ORIGINS

(empty)

["https://hostname", …]

SECURE_PROXY_SSL_HEADER

set

set

SESSION_COOKIE_SECURE

True

True

CSRF_COOKIE_SECURE

True

True

SECURE_HSTS_SECONDS

0

3600 (1 hour)

Untrusted-host mode (default)

Suitable for local use or sharing via ngrok/Cloudflare Tunnel. The app port is not internet-reachable directly, so X-Forwarded-Proto headers from the tunnel are trusted.

Note

http://localhost is a secure context, but an ngrok or Cloudflare Tunnel URL is a different browser origin - private keys stored in localStorage are not shared between the two. Always use the same origin consistently.

Deployed mode (--hostname example.com / PRIVIPOD_HOSTNAME=example.com)

For Docker/Caddy or systemd/nginx deployments. Providing a hostname:

  • Restricts ALLOWED_HOSTS to the listed hostname(s), preventing Host-header link-poisoning.

  • Sets CSRF_TRUSTED_ORIGINS.

  • Enables HSTS (1-hour max-age by default, no subdomains or preload). Increase SECURE_HSTS_SECONDS in your deployment once everything is stable.

Important

Your reverse proxy must strip or overwrite inbound X-Forwarded-Proto and X-Forwarded-For headers before forwarding requests to Privipod. Caddy does this automatically; for nginx add:

proxy_set_header X-Forwarded-Proto $scheme;

Privipod’s app port must not be publicly reachable - only the proxy should connect to it.