# Security model

> Encryption, read-only guarantees, PII redaction, the shared-credential threat model, and what leaves your network.

Source: https://triagic.com/docs/admin/security

## What leaves your network [#what-leaves-your-network]

Very little, and the exact list matters.

**Leaves:** ticket text and metadata (after redaction), evidence the agent gathered,
and prompts, all sent to **your** configured LLM provider, using **your** key. Plus
configuration reads and writes between the portal and each desktop.

**Does not leave:** query results as a corpus, investigation reports, Console threads,
usage totals, or anything the agent read that did not end up in a prompt. There is no
telemetry channel that uploads investigation content to us.

**Also leaves, and only this:** usage analytics. Counts, not content. Which screens get
opened (the screen name, never a ticket ID), how often triage runs, the app version, the
operating system, and whether the install is on a trial or a licence, tagged with the
same anonymous machine identifier licensing already uses and your organization's ID. No
ticket text, no prompts, no replies, no file names, no credentials, nothing typed into
the app. A member can turn it off per machine under **Cloud connection** in the sidebar.

The agent runs on a member's own machine and connects to your infrastructure from
there. Your databases never need to be publicly reachable.

## Encryption at rest [#encryption-at-rest]

Every credential (shared integration secrets, AI provider keys, personal integration
credentials, issue-tracker tokens) is encrypted with **AES-256-GCM**.

Two keys are involved, by design:

1. Secrets in the cloud are encrypted under the platform key, delivered over TLS only
   to an authenticated desktop.
2. On arrival they are **re-encrypted under that machine's own local key**, which never
   leaves the machine.

API responses only ever contain masked (`•••`) secret values. There is no endpoint,
and no admin screen, that returns a plaintext secret.

> **Warning:** Losing a desktop's local key is unrecoverable
>
> Stored secrets on that machine can no longer be decrypted and the affected
> integrations report `cannot decrypt stored secrets`. Organization-shared integrations
> re-sync from the portal; personal ones must be re-entered.

## Read-only guarantees [#read-only-guarantees]

Write protection is enforced **at the MCP server level** (read-only flags,
non-destructive tool sets, disabled write operations), not by asking the model not to
write.

That is the strong version of the guarantee, but it still depends on the MCP server's
own code being correct. For production, do both:

* Point every integration at a **read-only database user** or a **read-scoped token**.
* Leave the read-only toggles on.

With both, the guarantee is independent of any single layer being bug-free.

A handful of integrations have no server-side write block at all, so for those the
credential is the *only* boundary. They are called out in
[Connecting data sources](/docs/integrations#use-read-only-credentials-everywhere).

## PII redaction [#pii-redaction]

On by default. Before any ticket content is assembled into a prompt, emails, phone
numbers, national ID numbers, IPv4 addresses and Luhn-valid card numbers are redacted
out of the subject, body and metadata.

Redaction applies identically on every path: background triage, the Console, ticket
follow-up chat, playbook classification, and similar-ticket search. None of them
skip it.

It also runs over the identity fields taken from CRM association data, rather than
relying on the AI extraction step to catch them. PII removal does not depend on a
model behaving.

## The shared-credential threat model [#the-shared-credential-threat-model]

Shared integrations let one credential serve the whole organization. The guarantee is:

**A member cannot recover the secret value.** The member-facing view has no credential,
environment or header fields at all (not masked ones, none), so there is nothing to
inspect in the UI or at the API level.

**A member cannot repoint a stored credential.** Members cannot edit shared integration
configurations, which closes the "change the command to `env` and read the
environment" exfiltration path. That is enforced by write access, not by masking.

> **Warning:** &#x22;Use, not view&#x22; is about the secret, not the data
>
> A member who can use a shared Postgres integration can read whatever that Postgres
> user can read, through the agent. "Cannot view" means the credential's value, never
> the data the credential unlocks. Scope the credential itself accordingly.

## Passkeys [#passkeys]

Anyone who can sign in to the portal can register passkeys under **Security**:
owners, org admins and members. A passkey signs you in to the portal and approves
desktop sign-ins through the browser flow; the desktop app itself never handles
passkey material, because its window runs on a local address that has no web origin
to bind a credential to.

What a passkey does not change:

* **Revocation stays with the password.** A passkey login mints the same session as
  a password login, bound to the current password. Changing the password signs out
  every browser session and every desktop, however they signed in. Removing a
  passkey removes the ability to use it; it does not sign anything out.
* **Disabling a member disables their passkeys.** Every passkey login re-reads the
  member row; a disabled member's passkey stops working on the next attempt.
* **Seats still apply.** A desktop approved through the browser is subject to the
  same seat check as a password sign-in.

The desktop's browser sign-in code is eight characters (the hyphen in the middle
is a display separator, not a ninth character) from an alphabet without `0`, `O`,
`1` or `I`, expires in ten minutes, and is single use. Approving it requires a
signed-in portal session; the code alone grants nothing.

See [Sign in with browser](/docs/desktop/sign-in#sign-in-with-browser-passkeys)
for what a member sees on the desktop side, and [Idle lock](/docs/desktop/lock)
for how the same passkey approves an unlock, not just a first sign-in.

## Webhook verification [#webhook-verification]

Incoming HubSpot webhooks are verified against an HMAC-SHA256 signature, computed over
method, URL, raw body and a request timestamp that must be within five minutes of now,
whenever a webhook secret is configured.

**Without that secret configured, the endpoint accepts unsigned payloads.** Set it
before pointing a real HubSpot subscription at anything but a local test.

## Rate limiting [#rate-limiting]

| Endpoint                                 | Limit                                                                                                                        |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Login                                    | 10 requests / minute                                                                                                         |
| Passkey and browser sign-in verification | 10 requests / minute per address, plus a shared budget across all addresses that throttles harder under distributed guessing |
| Webhooks                                 | 120 requests / minute                                                                                                        |
| Everything else under the API            | 600 requests / minute                                                                                                        |

## Audit [#audit]

Every mutation to authentication, organizations and teams, playbooks, integrations,
issue trackers, filed issues and ticket playbook assignment writes an append-only
audit row with actor, organization, action, target, before/after payload and
timestamp. See [Audit log](/docs/admin/audit).

## Practical hardening checklist [#practical-hardening-checklist]

* Read-only database users and read-scoped tokens for every integration.
* A [monthly spend cap](/docs/admin/spending) set before members start working.
* Least-privilege issue-tracker tokens: issues read/write and metadata, not full
  `repo`, where fine-grained tokens are available.
* Leave PII redaction on.
* Configure the webhook secret before any production webhook.
* Review the audit log's actor column periodically; disable members promptly when they
  leave.
