Security

Operating mesgd requires protecting several kinds of authority: account credentials establish identity, configured administrator names authorize account management, and routing grants restrict a program's message access. Client encryption adds a separate boundary around payload contents. Understanding which boundary an operation relies on is necessary when configuring the service or building an application.

Transport security

Accounts and messages use HTTPS on the port in mesgd.config. Clients must trust the server certificate and verify its hostname. The current listener uses verify_none and does not require a client certificate; configuring cacert alone does not enable mutual TLS. Send JSON with Content-Length. mesgd rejects a body over 16 KiB, a Transfer-Encoding header, or a duplicate Content-Length on /auth. Responses send Cache-Control: no-store. Do not put a token or a password in a URL.

During OAuth, mesgd sets a short-lived Secure, HttpOnly, SameSite=Lax cookie so the browser that started the flow is the browser that finishes it. The session you get at the end is still a bearer token. Callbacks do not put tokens in redirect URLs. Calls to Google and GitHub check the certificate and hostname and do not follow redirects.

Login sessions

A person or a program proves identity with a login session. mesgd returns it from login, from signup confirmation, or from the end of an OAuth flow. Send it as Authorization: Bearer.

The session is a compact HS256 JWT. iss and aud are mesgd. It lasts 24 hours unless {session_ttl, Seconds} sets another lifetime, an integer from 60 to 2592000. mesgd stores a hash of the token and the session metadata, checks the signature, and checks that the session row is still live. A restart does not drop live sessions. One account can hold eight. A session already issued keeps the expiry it was given. Before that bearer expires, POST /auth/refresh exchanges it for a new one and retires the one that was sent, so the same session continues under the new bearer.

The session records how the caller signed in: password and authenticator, a provider and authenticator, or authenticator recovery. An OAuth login also records the provider. Signing in does not make the caller an administrator, and the session carries no path list.

mesgd accepts that session on a WebSocket. Delivery for that connection scans every listener. When you need the connection to stay inside assigned paths, issue a grant token instead.

A missing credential is 401 on a gated route, including mounts such as /mesgd/pki. {disable_auth, true} or an entry in public_paths is how you open an exception. Use disable_auth only for a test without a client SDK. Only the atom true turns the check off.

GET /user/<name>/keys is public so another program can encrypt to an active account. Unknown, pending, and inactive accounts are 404. Sending a bearer token does not change that body. Other /user routes stay gated. The old /user PUT route is disabled.

Passwords and authenticators

Passwords are 12 to 1024 bytes. mesgd stores a salted PBKDF2-HMAC-SHA256 hash with 600,000 iterations. Usernames are case-sensitive, up to 128 bytes, and may contain letters, digits, _, @, ., and -.

The second factor is TOTP: SHA-1, six digits, 30-second steps, and mesgd accepts one step on either side of the current step. Send the code as a string so a leading zero stays a zero. mesgd remembers every accepted counter. A code used for confirmation, login, reset, a key update, a session revocation, or an administrator action cannot be used again until the authenticator moves to the next step.

mesgd seals the authenticator secret with AES-256-GCM under keys/auth.key, or the path in auth_keyfile. The key is 32 bytes and readable only by its owner. Back it up apart from the database. See Configuration.

Five failed attempts lock that account for five minutes. The count and the lock survive a restart. Each node also allows 120 account calls and 240 OAuth calls per minute. That limit is per node, not per IP address. Put your own limiter in front of a public deployment.

A saved recovery code plus a fresh authenticator code replaces the password, replaces the recovery codes, and ends existing sessions. It does not replace a lost authenticator. That handoff is in Recovery. mesgd does not send email or SMS, and it does not prove who the person is. You do that before you issue a handoff.

Providers

Google and GitHub can identify the person. mesgd still requires the authenticator before it issues a session. Provider access tokens and ID tokens are not stored as mesgd credentials and are not returned as sessions. mesgd does not merge two accounts because they share an email. One identity per provider is stored as provider plus subject, and that row is written when enrollment is confirmed.

OAuth flow state lives in the mesgd_oauth process. A restart drops a flow that has not finished. In a cluster, send one browser's OAuth requests to one node until the flow finishes.

Public keys

An account may store one public signing key and one public encryption key. Other programs use them to verify and encrypt. Holding a public key does not sign anyone in. mesgd rejects a JWK that contains private fields. The shapes are in Account API.

admin.json is a PKI identity for server operations. It does not authorize /auth/admin.

Administrative authority

Administrator names come from {admin, Name} and auth_admins. They are mesgd usernames. Configuration is the only way to grant or remove that role. A change takes effect on the next call.

An administrator action needs that person's live session, a fresh authenticator code, and a reason of 3 to 512 bytes. mesgd refuses an action aimed at the caller, so an administrator cannot disable or remove their own account with the API.

The node shell is a separate trust boundary. Anyone who can connect with the distribution cookie can run mesgd user, including hard-reset and recover. Protect the cookie and the shell the way you protect the database. bootstrap_admin and bootstrap_recovery are calls on the node. They are not HTTP routes.

mesgd user remove leaves the username taken so public signup cannot claim it. hard-reset, from the node shell only, drops that reservation and the provider links, then enrolls a new password and authenticator.

Routing grants and payload encryption

A grant token lists path patterns for one hour. On each use, mesgd keeps a pattern only when it is both in the token and still assigned. Removing access takes effect on the next check. Adding access needs a new token. A grant token cannot call /auth. See Routing.

Applications can sign and encrypt payloads using the SDK. A payload encrypted for a recipient remains unreadable to mesgd when it does not hold that recipient's private key. Routing requires visible fields outside that ciphertext. The current JSON listener does not implement the SDK samples' encrypted registration handshake or automatic decryption and re-encryption for distribution lists; publishing public keys does not enable either operation.

History and audit records

The account history keeps the latest 100 events: login, credential failure, logout, session refresh, link and unlink, password reset, and key or session changes. It does not store passwords, authenticator codes, recovery codes, raw tokens, or IP addresses.

Administrator actions are stored with actor, target, action, time, and reason. The audit omits secrets and handoff codes. mesgd keeps 1000 of those events.

Crash reports from the account worker redact worker state and request bodies. Do not log enrollment tokens, passwords, or recovery codes in anything you put in front of mesgd.