Security
This page summarizes the security mechanisms built into Celestian, as input for evaluation and configuration.
User authentication
Section titled “User authentication”| 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. |
Device credentials
Section titled “Device credentials”- 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_idpolicy 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.
Authorization
Section titled “Authorization”- 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.
Data protection
Section titled “Data protection”| 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. |
Execution of user code
Section titled “Execution of user code”| 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. |
Network and MQTT
Section titled “Network and MQTT”- Plain MQTT is not encrypted. Use MQTTS or a private network for traffic crossing the internet (MQTTS and NTP).
- Restrict
MQTTS_BINDand set up a firewall with a per-IP connection rate limit. - An open NTP server can be abused for amplification: restrict chrony’s
allowto the gateway. - The reverse proxy must forward
HostandX-Forwarded-*correctly because Server Actions matchOriginagainstHost.
Recommended practices
Section titled “Recommended practices”- Turn off self-registration after the first account is created; invite members through Invitations.
- Back up
SECRETS_KEYoff the server. - Use roles with the narrowest rights possible and API tokens with the minimum scope.
- Disable Go (
RULES_GO_WORKERS=0) if it is not used. - Restrict access to
/adminat the reverse proxy. - Back up the databases regularly and test the restore.