# Security administration

> The controls owners and admins set for a whole workspace (single sign-on, two-factor, session limits, IP allowlists, sign-in methods, domain capture, retention) and the records an administrator or auditor reads (sessions, the audit log, the roles matrix, trust data).

Source: https://immiscible.fly.dev/docs/guides/security-admin

Everything on this page is enforced by the server on every request, not by the console. Owners, admins and security admins (the security capability) change the security policy, single sign-on, members' sessions and retention, and choose where the audit log is streamed; owners keep billing, ownership and requests to join. See [The security admin role](#the-security-admin-role). Each change is an audit entry, and audit entries are chained into the workspace's evidence ledger. For identifiers, access profiles and entitlements, see [Identity and access](https://immiscible.fly.dev/docs/guides/identity-and-access.md).

## The security policy

[`GET /api/w/:wid/security`](https://immiscible.fly.dev/docs/api/get-api-w-wid-security.md) returns the policy, and [`PUT /api/w/:wid/security`](https://immiscible.fly.dev/docs/api/put-api-w-wid-security.md) changes any part of it. Send only the fields you are changing.

```json
{
  "requireSso": true,
  "requireMfa": true,
  "sessionIdleMinutes": 30,
  "sessionMaxHours": 12,
  "ipAllowlist": ["203.0.113.0/24", "2001:db8::/32"],
  "allowedMethods": ["sso", "passkey"],
  "domainCapture": true
}
```

The answer is the whole policy again, with `changes` (each field's `from` and `to`) and `sessionsEnded` (people signed out by turning on two-factor). A change that would lock you out is refused with `409 would_lock_you_out`, and nothing is saved.

### Require single sign-on

`requireSso` makes members whose email is on one of the workspace's verified domains sign in through its OIDC single sign-on (see [Workspace single sign-on](https://immiscible.fly.dev/docs/guides/workspace-sso.md)). It needs single sign-on configured and at least one verified domain. Password, email-code and personal-provider sign-ins for those addresses are refused at sign-in, and a session made any other way is refused on its next request with `403 sso_required` and an `ssoUrl`. People on other domains (contractors, say) are not affected.

**Break-glass.** An owner whose session was made with a passkey or an authenticator code is never refused by this rule, so a broken identity provider cannot lock the workspace out. Turning the rule on is refused if you are on a verified domain and did not sign in with single sign-on, unless you are an owner with a passkey or an authenticator app.

### Require two-factor

`requireMfa` requires every member to have an authenticator app or a passkey. Members without one are signed out at once and asked to enrol at their next sign-in; a session that somehow lacks one is refused with `403 mfa_required`. People who reach the workspace through its single sign-on are left to the identity provider. You must have a second factor yourself before you can require it.

### Session limits

`sessionIdleMinutes` (5 or more) and `sessionMaxHours` (1 or more) set shorter limits than the deployment's (`IMMISCIBLE_SESSION_IDLE_MINUTES`, `IMMISCIBLE_SESSION_MAX_DAYS`); they can never be looser. A person's session follows the strictest limit of every workspace they belong to, and is checked on each request: past the idle or absolute limit it ends and the next request answers `401`. `null` returns to the deployment's limits.

### IP allowlist

`ipAllowlist` is up to 50 IPv4 or IPv6 addresses or CIDR ranges. When it is not empty, the console, the phone app, and every credential bound to the workspace (gateway and agent keys, service tokens, OAuth tokens for MCP clients) are refused from any other address with `403 ip_not_allowed`. SCIM and inbound webhooks are not covered, because they come from your identity provider and vendors.

Saving a list that does not contain the address you are saving from is refused, and the error names that address. Behind a proxy, set `TRUST_PROXY` so the address is the client's, not the proxy's. An operator can suspend every allowlist with `IMMISCIBLE_IP_ALLOWLISTS=off` (see the [deployment reference](https://immiscible.fly.dev/docs/guides/deploy.md)).

### Allowed sign-in methods

`allowedMethods` lists which of `password`, `email` (a link or code), `google`, `microsoft`, `okta`, `passkey` and `sso` reach the workspace; `null` allows all. A session is read by its first factor, so a password confirmed with an authenticator code is `password`. Other sessions are refused with `403 sign_in_method_not_allowed`. The method you are signed in with must stay in the list, and `sso` needs single sign-on set up. The owner break-glass above applies here too.

### Domain capture

With `domainCapture` on (the default), someone signing up with an address on a verified domain does not get a team workspace of their own: their account is made and they ask to join yours. A personal workspace is still theirs to make. See [Requests to join](#requests-to-join).

To try domain capture on a laptop without a DNS record, start the server with `IMMISCIBLE_DEV_VERIFY_DOMAINS=true`: verifying a domain then succeeds without the TXT record. The server refuses to start with it in production.

## Sessions and devices

Each person sees their own sessions at [`GET /api/me/sessions`](https://immiscible.fly.dev/docs/api/get-api-me-sessions.md) and ends one with [`DELETE /api/me/sessions/:sid`](https://immiscible.fly.dev/docs/api/delete-api-me-sessions-sid.md). Each session lists `device`, `os` and `browser` (read from the browser's User-Agent), `ip`, `location`, `signInMethod`, `createdAt` and `lastSeenAt`. `location` is only "This computer" or "Private network": there is no geolocation service, so for any other address the console shows the address itself.

Owners, admins and security admins list a member's sessions and phones with [`GET /api/w/:wid/members/:uid/sessions`](https://immiscible.fly.dev/docs/api/get-api-w-wid-members-uid-sessions.md) and end them with [`DELETE /api/w/:wid/members/:uid/sessions`](https://immiscible.fly.dev/docs/api/delete-api-w-wid-members-uid-sessions.md). Where the workspace owns the person's identity (their domain is verified here, the deployment is self-hosted, or this is their only workspace) every session ends and their phones are signed out (`scope: "everywhere"`). Otherwise their sessions stop reaching this workspace only (`scope: "workspace"`) and their other workspaces are untouched. Only an owner ends an owner's sessions.

## Alert emails

Each person chooses which alert emails they get at [`GET /api/me/email-preferences`](https://immiscible.fly.dev/docs/api/get-api-me-email-preferences.md) and [`PUT /api/me/email-preferences`](https://immiscible.fly.dev/docs/api/put-api-me-email-preferences.md) (`{ "muted": { "spend_alerts": true } }`). Three can be turned off: `orphaned_agents`, `shadow_keys` and `spend_alerts`. Each of those emails carries a one-click unsubscribe (RFC 8058: `List-Unsubscribe` with `List-Unsubscribe-Post`) signed for that address and that alert alone, so a mail client can turn it off without anyone signing in. Opening the link shows a page that asks first, and offers to turn it back on.

Approval requests, sign-in and step-up codes, and the two incidents that stop an agent (a payment charged more than was approved, records moved beyond what was declared) always arrive and cannot be turned off.

## The audit log

[`GET /api/w/:wid/audit`](https://immiscible.fly.dev/docs/api/get-api-w-wid-audit.md) lists what people did, newest first: sign-ins, role changes, rules, approvals, stops, settings, keys and integrations. Each entry has the actor, a plain title, the subject, the IP address, the browser and the time. Agents' and machines' actions are in the activity feed and the evidence ledger instead.

| Query | Meaning |
| --- | --- |
| `actor` | A user id or email. |
| `kind` | `sign-in`, `roles`, `members`, `approvals`, `stops`, `rules`, `keys`, `settings`, `integrations`, `agents`, `evidence` or `other`. |
| `since`, `until` | A date (`2026-10-01`) or a time. |
| `cursor` | The `nextCursor` of the previous page. |
| `limit` | Up to 500 a page; 50 by default. |
| `format` | `csv` or `json` downloads every matching entry. |

Owners, admins, auditors and security admins read it. It reads the same audit records the evidence ledger chains, so nothing is stored twice, and it shows as far back as the plan's history window (`retentionDays`, and `readable` in words): 7 days on Free, a year on Team, ten years on Business and Enterprise. Entries older than the window are not deleted; they stay in the ledger. Every export is itself an entry.

## The audit stream

Every audit entry is chained into the evidence ledger as an `audit` record, and every ledger record can be pushed as it is written, so the audit log reaches your SIEM without anyone downloading it:

- [Splunk](https://immiscible.fly.dev/docs/guides/splunk.md) (HTTP Event Collector) and [Datadog](https://immiscible.fly.dev/docs/guides/datadog.md) Logs, as OCSF, batched, with up to six attempts and backoff.
- [Signed webhooks](https://immiscible.fly.dev/docs/guides/signed-webhooks.md) to your own endpoint, HMAC-SHA256, retried up to five times.

[`GET /api/w/:wid/audit/stream`](https://immiscible.fly.dev/docs/api/get-api-w-wid-audit-stream.md) says which destinations carry the audit log, whether each takes every record (`complete`) or only `audit` records, and when each last delivered (`lastDeliveredAt`), with failures and the last error. The console shows it under **Settings**, **Audit stream**.

One honest edge: when one request both changed something and wrote its own ledger record, that record stands for the audit entry rather than a second `audit` record. A destination that takes every record misses nothing; one that takes only `audit` records misses those, so choose every record for a complete stream.

## Data retention

[`GET /api/w/:wid/retention`](https://immiscible.fly.dev/docs/api/get-api-w-wid-retention.md) and [`PUT /api/w/:wid/retention`](https://immiscible.fly.dev/docs/api/put-api-w-wid-retention.md) set how long two kinds of data are kept, in whole days, up to the plan's limit (`limits.maxDays`):

- `callLogDays`: model calls (model, tokens, cost, latency, routing), tasks left with no calls, and MCP proxy calls.
- `promptMetadataDays`: the prompt digests kept with each call, and the digests of untrusted content seen in agent sessions.

A daily job removes what is older and writes a `retention_purge` record to the ledger saying what it removed. Nothing is purged until an owner or admin sets a value.

> **Warning**
> **Signed evidence records are never purged by retention; export access windows depend on plan.** The hash-chained ledger, receipts and checkpoints are never removed by these settings: removing one record would break the chain every later record depends on. How far back you can export them is your plan's window (`evidence.exportWindowDays`). If a task class retains prompt content as evidence, that content is kept with the evidence.

## Requests to join

[`GET /api/w/:wid/join-requests`](https://immiscible.fly.dev/docs/api/get-api-w-wid-join-requests.md) lists pending requests (`?status=approved`, `denied` or `all` for others). An owner decides with [`POST /api/w/:wid/join-requests`](https://immiscible.fly.dev/docs/api/post-api-w-wid-join-requests.md):

```json
{ "id": "jrq_9c1e...", "decision": "approve", "role": "member" }
```

Owners are emailed when a request arrives ("Maya asked to join Amethyst", with a Review button). The person is emailed the answer: "You're in: Amethyst on Immiscible" with their role in plain words and a Sign in button, or a short note that the request was not approved.

The role is any but owner, and defaults to the single sign-on default role. A request from an address nobody has confirmed yet (`emailVerified: false`) cannot be approved. The person sees their own requests at [`GET /api/me/join-requests`](https://immiscible.fly.dev/docs/api/get-api-me-join-requests.md).

## The roles matrix

Seven roles: owner, admin, security admin (`security`), approver, member, analyst and auditor. [`GET /api/w/:wid/roles`](https://immiscible.fly.dev/docs/api/get-api-w-wid-roles.md) returns what each capability allows and every workspace route with the roles that reach it. The table lives in the code, and the test suite calls every workspace route as every role and fails if any answer differs from it.

## The security admin role

`security` is for the security team, so it no longer needs full admin. It changes the security policy, single sign-on and retention, lists and ends members' sessions (never an owner's), reads the audit log and evidence, and manages where the audit log is streamed (webhooks, Splunk, Datadog). It cannot touch billing, members' roles or invitations, agents, rules, keys or workspace settings. Making someone a security admin needs the same recent proof of identity as making them an admin, and a SCIM `roles` value or an owner approving a request to join can assign it. It is not offered as single sign-on's default role for first sign-ins.

## Trust data

[`/trust.json`](https://immiscible.fly.dev/trust.json) lists the controls this deployment enforces, generated from its configuration, for security questionnaires: session limits, two-factor, single sign-on, IP allowlists, retention, encryption of secrets at rest, and what is not supported. SAML 2.0 single sign-on is supported per workspace (SP-initiated, signed assertions required), alongside OIDC with Okta, Entra and Google Workspace.
