Security model
What the server guarantees, so you can build on it instead of re-implementing it.
Last updated
- Isolation. Every tenant table has row-level security keyed on the workspace; tenant transactions run as a database role without bypass. A query without a bound workspace returns nothing, and the search indexes were made to work under that policy rather than around it.
- Credentials. Argon2id passwords; two-factor with recovery codes; single sign-on with OpenID Connect; server-side sessions with absolute and idle lifetimes, revoked on privilege change; API, app and bot tokens stored as SHA-256 hashes with visible prefixes (
kmk_,kap_,kbt_). Only an active workspace lets any credential through, checked on every request, so a suspended or closed workspace stops at once. - Secrets. Channel credentials, webhook, bot and app secrets are envelope-encrypted with additional data binding them to their row; nothing echoes them back after creation. A saved secret is used only with the server it was entered for: changing a mailbox's host, a single sign-on issuer, a connector's address or the AI provider's address asks for the secret again.
- API keys see everything. API keys, app tokens and bots read the workspace unmasked, so only someone whose role sees every conversation and contact with no field masked may create one that reads conversations or contacts, and every request checks that its maker still qualifies.
- Email. Each message is parsed and stored on its own, so a hostile message is quarantined instead of stopping the mailbox; downloads and message ids are bounded; outbound Message-IDs cannot be guessed, and a reply joins a conversation by its references only when it comes from someone already in it. Remote images in email load only after "Show images". Every inbound email is also kept as it arrived, for 90 days by default (a workspace setting); a copy is downloaded only by someone who manages inboxes and could read the whole message in clear, and every download is audited (email inboxes).
- Exports. Every CSV Kommio produces neutralises cells that a spreadsheet would run as formulas.
- Virus scanning. A self-hosted installation can have every file scanned by ClamAV before it is stored; a flagged file is refused or replaced by a placeholder and never served. It is off by default, and Kommio Cloud does not scan files yet (files and storage).
- Personal data by role. Every personal field carries a tag (email, phone, address, ID document, payment, IP address) and each role sees it visible, masked or hidden, everywhere a value leaves the server: lists and details, search, the change history, audit snapshots, report labels, satisfaction comments and the stored bodies in the webhook, bot and automation delivery ledgers. Custom attributes and files on messages can carry the same tags (roles and personal data).
- Identity documents. A file marked as an ID document or payment detail, by an agent from the thread or by the
flag_attachmentsrule and macro action, follows the role's grant for that kind: a masked file shows its kind and size without name, link or preview and opens only through an audited reveal; a hidden one is a placeholder. A marked file is never named to nor sent to an AI provider, is listed in an export only for someone who sees its kind in clear, and goes with the contact's erasure whoever sent it. Settings › Privacy can delete files marked as ID documents a set number of days after upload. - Erasure. Erasing a contact removes what the customer wrote, their files and headers, the copies of their emails as they arrived, every file marked as personal data in their conversations, and replaces their details with
[erased]in the team's records, audit and events; the workspace's finished exports are withdrawn and the contact's chat tokens and links stop working (details). - Website chat. An anonymous visitor is known only by a random session token in their browser; browser fingerprints are not collected or used, while the IP-based location and browser details are still shown to agents. A signed-in visitor nobody knows is a session, not a contact, until they write or an agent starts a chat. A follow-up email's link back to the chat works once.
- Egress. One SSRF-safe client for every user-supplied URL: DNS pinned, private ranges refused unless allowlisted, redirects re-validated, size and time capped. Everything outbound is signed with HMAC-SHA256 over a timestamp and the body.
- Ingress. HTML bodies sanitised on write and again on render; uploads checked by magic bytes and size, served as downloads unless images; rate limits per key, workspace and IP.
- Browser. The agent app ships a strict Content Security Policy (same-origin scripts and connections, panels only in https frames, never embeddable). Panels run sandboxed with a short-lived signed context.
- Audit. Every change records actor, before and after, and the request it came from; app hook deliveries are logged with their outcome.
- Least privilege. Nobody can grant a role, invitation, key or app more than they hold themselves (the subset rule).
- Supply chain. Every change runs the suites, the Playwright flows, an upgrade rehearsal, the load gate,
govulncheckand a container scan; the Go toolchain is pinned to a patched release.
Two-factor authentication and sessions
Everyone can turn on two-factor authentication under Settings, Account security, with any TOTP authenticator app. The same flow is in the API for the signed-in person:
| Call | What it does |
|---|---|
GET /auth/ | Whether two-factor is on, and how many recovery codes are left. |
POST /auth/ | Starts enrolment: returns the secret and an otpauth_url for the QR code. Refused while two-factor is already on, so nobody can swap the authenticator without the old factor. |
POST /auth/ | Confirms with a code from the app; returns ten single-use recovery_codes, shown once. The confirming session stays signed in and every other session is signed out. |
POST /auth/ | Replaces every recovery code; needs a current code. |
POST /auth/ | Turns two-factor off; needs the password and a code. |
POST /auth/ | Completes a sign-in that answered mfa_required, with a code or a recovery code. |
A code cannot be used twice, and recovery codes are consumed atomically. Sessions are listed with GET /auth/sessions and can be signed out one by one (DELETE /auth/sessions/{id}) or all but the current one (POST /auth/sessions/revoke-others). GET /auth/me includes mfa_enabled.
Workspace single sign-on
Administrators add OpenID Connect providers under Settings, Single sign-on (settings:manage, and the subset rule, since a provider can sign in any member): issuer, client id, a write-only client secret, scopes (default openid email profile), a button label and optional allowed email domains. Register the redirect URI the page shows at your identity provider; it has the form https://app.kommio.io/. A test sign-in reports the outcome without signing anyone in, and every change is audited without the secret.
- People sign in from
https://app.kommio.io/login?workspace=<slug>, which shows the workspace's buttons. - A workspace provider signs in a member only after they linked it once by signing in with their password (and second factor), within the allowed domains; it never signs in the workspace owner, and a session it opens stays in that workspace.
- It never creates users: people join by invitation, then sign in with the provider.
Starting two-factor enrolment needs the current password, and the platform console needs a session that passed a second factor.
The same providers are in the API under /account/sso-providers.
Audited reveal of masked fields
A role can see a sensitive field masked rather than in clear. A person holding sensitive:reveal can ask for one field of one record in clear, and every reveal writes a sensitive.revealed row to the audit log with who, what and the reason given. Hidden fields stay hidden, and the record must be visible to the caller. In the agent app this is the eye button next to a masked value.
curl -X POST "$KOMMIO_URL/sensitive/reveal" -H "Authorization: Bearer $KOMMIO_KEY" -H "Content-Type: application/json" \
-d '{"object": "contact", "id": 77, "field": "emails", "reason": "Customer asked to confirm the address on file"}'Response 200 OK
{ "field": "emails", "values": ["[email protected]"] }Fields that can be revealed: a contact's emails, display_email and phones, a web chat conversation's visitor.ip, a custom attribute marked as personal data as attributes.<key> on a contact or conversation, and a marked file (object: "attachment", field: "file"), which answers the file's name and a link that opens it for ten minutes for that person only. A field that is already visible to the caller answers 422; a hidden one answers 403.
Who revealed what
Settings › Security policy shows the last 30 days of reveals per person, with the newest ones, and links to the audit log filtered to them. The same list is GET /sensitive/reveals?days=30 (audit:read).
Tenant-held keys (phase 1, experimental)
Tenant-held keys give a workspace its own encryption key that Kommio never holds in usable form: a random 256-bit workspace key, stored only wrapped by a root derived (Argon2id) from a 24-word recovery phrase that only the workspace keeps. Database copies, dumps, backups and disks then hold the wrapped key and nothing that opens it. While the service runs, the unlocked key is in the server's memory, because the server has to decrypt to serve; someone with live root on the running server could read it.
- Turning it on. Where the operator has enabled it, Settings › Security policy shows a Tenant-held keys (experimental) card to people holding
security:keys(the Administrator role only; a signed-in person, never an API key or app). Create shows the 24 words once; write them down, tick that you did, and type back the four words the card asks for. Five wrong confirmations, or 24 hours without one, discard the key and you start again. The key then starts locked, and the app unlocks it with the phrase it just showed. - Unlock and lock. Unlocking takes the whole phrase (case and spacing do not matter; a mistyped word is caught by the checksum before anything else and does not count as an attempt). Five wrong phrases within 15 minutes block further attempts for the workspace. Lock clears the key from that server's memory.
- Recovery. The phrase is the only way to open the workspace key: if it is lost, nobody can open it, not the workspace, the operator or Kommio. In phase 1 nothing is encrypted with it, so losing it costs nothing yet; from the next phase it would mean losing the encrypted content. Keep it on paper, offline.
- What is never stored or logged: the phrase or any word of it, the root and the unwrapped key. Enabling, confirming, unlocking and locking are audited without a word. The platform console shows only whether a workspace has tenant-held keys and whether they are unlocked on the answering server.
- Not done yet: encrypting any content, handing the unlocked key between deploy slots, your own KMS as the root, key rotation, turning it off once enabled, an idle auto-lock and encrypting existing workspaces. These are the later phases on the roadmap.
Report a vulnerability to [email protected] (/.well-known/security.txt).
Every claim on these pages is covered by a test in the repository, and the API reference is generated from the same contract every server serves at /api/openapi. Corrections to [email protected].