Skip to content

Security

This page summarizes the security mechanisms built into Celestian, as input for evaluation and configuration.

Aspect Mechanism
Passwords Stored with argon2id (m = 19 MiB, t = 2, p = 1). The minimum length is configurable and checked against a list of common passwords; a password may not contain the username or part of the e-mail.
Sessions A short-lived EdDSA JWT access token (15 minutes) and an opaque refresh token that is rotated on every use (7 days). Only the refresh token hash is stored. Reusing an old token outside a 30-second window is treated as theft and revokes the whole session family.
Lockout Repeated failed sign-ins (10 in 15 minutes by default) block temporarily per identifier and per user, so switching between e-mail and username does not add attempts. A workspace admin can unlock from the UI.
E-mail verification A link with an expiring token. Verification uses a button (POST) so e-mail link scanners do not consume the token.
Password reset Revokes all sessions and clears the lockout. Forgot-password and resend-verification requests always get the same answer (202) so registered e-mails are not revealed.
Session cookie Encrypted (JWE), httpOnly, and Secure behind HTTPS. The browser never calls the backend directly.
Admin console A separate session (a different audience), a sameSite=strict cookie limited to /admin, and credentials that are an argon2id hash in the server configuration.
  • The device key is stored as an argon2id hash and is shown only once. A device connects with username = the device ID.
  • The broker calls the backend (auth callout) on every connection, publish, and subscribe. Anonymous clients are refused. Decisions are cached briefly.
  • The publisher recorded on a message is the identity authenticated by the broker, not the payload content, so it cannot be forged.
  • The client_id policy requiring the device ID prefix in production prevents one credential from being used with many identities.
  • Bootstrap: a challenge-response protocol (HMAC-SHA256) with an AES-256-GCM encrypted response, a single-use nonce, lockout per External ID, and uniform refusal replies.
  • Firmware is downloaded through HMAC-signed addresses that expire, and integrity is verified with SHA-256.
  • Deny by default and deny beats allow.
  • Anti-escalation of rights: you cannot grant a permission you do not hold; the Admin right can only be given by an Admin; a workspace always has at least one active Admin.
  • Per-object and per-tag permissions, direct policies, and inheritance through groups.
  • 404 for non-members so a workspace’s existence does not leak.
  • Data is always limited to the workspace from the access context, never from a client parameter.
  • Personal Access Tokens: a SHA-256 hash, locked to one workspace, effective rights = the intersection of the owner’s rights and the scope, with no access-management actions.
Aspect Mechanism
Stored credentials PostgreSQL passwords, Telegram tokens, webhook headers, and Bootstrap External Keys are encrypted with AES-256-GCM using the server key (SECRETS_KEY) and bound to the workspace. The API never returns their values.
Journals Every mutation is recorded in the same transaction with a before and after snapshot. Secrets never enter the journal.
CSV export Values starting with =, +, -, or @ are neutralized (anti CSV injection).
Public dashboard links A random 256-bit token, read-only, data only through the saved widget configuration, rate limited, not indexed, and without a referrer. The person granting access must themselves be allowed to read every channel used.
Deletion A workspace is frozen with a grace period before permanent deletion; messages in TimescaleDB are cleaned up gradually.
Component Safeguards
Lua A fresh interpreter per execution, only the base, table, string, and math libraries; no os, io, package, debug, load*, or require; a timeout and a limit on result size.
Go A separate child process with static validation against a package allowlist, a timeout, and a memory limit; on Linux, plus seccomp, dropped capabilities, no_new_privs, and resource limits.
Templates (e-mail, webhook, PostgreSQL, Bootstrap) A restricted text/template (no arbitrary function calls, recursion, or printf in Bootstrap); report templates are validated with an HTML allowlist (no scripts, iframes, forms, or on* attributes).
Network outputs PostgreSQL, Webhook, and Telegram refuse private, local, link-local, and CGNAT addresses (checked on the resolved IP, so DNS rebinding does not pass) unless the administrator allows it. Table and column names are validated and values are always parameters.
  • Plain MQTT is not encrypted. Use MQTTS or a private network for traffic crossing the internet (MQTTS and NTP).
  • Restrict MQTTS_BIND and set up a firewall with a per-IP connection rate limit.
  • An open NTP server can be abused for amplification: restrict chrony’s allow to the gateway.
  • The reverse proxy must forward Host and X-Forwarded-* correctly because Server Actions match Origin against Host.
  1. Turn off self-registration after the first account is created; invite members through Invitations.
  2. Back up SECRETS_KEY off the server.
  3. Use roles with the narrowest rights possible and API tokens with the minimum scope.
  4. Disable Go (RULES_GO_WORKERS=0) if it is not used.
  5. Restrict access to /admin at the reverse proxy.
  6. Back up the databases regularly and test the restore.