Platform
Security model
What Arcus stores, what it refuses to store, where trust boundaries sit, and the design tradeoffs stated plainly — including the ones that are not in Arcus's favour.
What Arcus is defending against#
Arcus sits between an autonomous agent and everything it can reach. The threat model is not "an attacker breaks into your network". It is narrower and more specific:
| Threat | Control |
|---|---|
| An agent sends data somewhere it was never meant to reach | Default-deny policy engine |
| An agent leaks a credential or personal data in a payload | DLP scanning on every request |
| An agent takes a destructive action — a wipe, a mass transfer, a drop | Parameter guardrails |
| A stolen key is used from somewhere else | Identity binding + revocation |
| A looping or compromised agent floods a target | Threat quarantine |
| Someone later disputes what happened, or edits the record | Tamper-evident audit chain |
An agent inside your infrastructure with a working credential is the assumed starting position. Arcus's job is to make sure that position does not automatically grant the ability to send anything anywhere, and that whatever it does attempt is recorded in a form that cannot be quietly revised.
What Arcus stores#
Being specific about this matters more than any claim about how secure it is.
| Data | Stored? | Form |
|---|---|---|
| API key raw value | No | SHA-256 hash only, plus a 15-character display prefix |
| Key metadata | Yes | Label, owner email, bound agent id, timestamps, revocation state |
| Dispatch payload body | No | Only payloadSize in bytes |
| Payload of a held dispatch | Yes, temporarily | Retained until approved or rejected — see below |
target URL | Yes | Needed for policy matching and for logs to be useful |
from / to labels | Yes | Attribution |
| DLP matched values | Never | Only the category, and on Pro+ the field path |
| Destructive-pattern regexes | Not exposed | Ids, labels and severities are published; the patterns are not |
| Guardrail findings | Yes | Rule, path, condition, expected, observed value, severity |
| Target response bodies | No | Only the HTTP status code |
| Audit events | Yes, sealed | Type, actor, summary, metadata, seq, prevHash, hash |
| Passwords | No | Authentication is delegated to Clerk entirely |
| Payment details | No | Handled by the payment provider's hosted checkout |
Two entries deserve their exceptions spelled out.
Held payloads are retained. A dispatch held for approval keeps its full body until someone decides, because approving a request you cannot see is not review. Rejection ends the retention. This is a deliberate exchange of confidentiality for reviewability, and it applies only to held requests.
Guardrail findings record the observed value. A finding says observed: "DROP TABLE patients" — that is the point of the finding. If a field is sensitive enough that its value must not appear in a finding, do not write a guardrail on it; use DLP, which records the category and never the value.
Trust boundaries#
Two credential types that cannot substitute for each other#
An ark_live_ API key reaches exactly one endpoint: POST /v1/dispatch. It cannot list keys, read logs, change policies, disable guardrails, release a quarantine or read the audit chain. Presenting one to /v1/admin/* returns 401.
The control plane requires a Clerk session token. Sessions are short-lived, tied to a browser session, and cannot be minted by an agent.
The consequence: a fully compromised agent cannot weaken the rules that constrain it. It can attempt dispatches, all of which are evaluated and recorded. It cannot turn the gates off.
This boundary only holds if you respect it
Passing a Clerk session token to an agent process collapses the whole model. Call the admin API from your backend or from the dashboard — never from inside the agent.
Platform operators are a third axis#
Operator access is not a tenant role, not a tier, and not derivable from anything a customer controls. A tenant can set their own in-tenant role to ADMIN and upgrade to the highest plan and still receive 403 from every platform route.
Operator status comes from two Clerk-side sources only: publicMetadata.role on the Clerk user (settable only with the Clerk secret key, from the Clerk dashboard or Backend API — the real mechanism), and SUPER_ADMIN_EMAILS, a server-only allow-list that exists as the bootstrap path for the first operator. Neither is reachable from the application's own database, so neither can be granted by an SQL injection or a compromised admin session.
What an operator can do: read cross-tenant metadata and enforcement outcomes, impose or lift a quarantine, and change a tier. What they cannot do: read payload bodies, DLP matched values, key hashes, or edit an audit chain without verification detecting it. Throttle, release and tier change all append to the affected tenant's chain with a reason and an actor. See Platform endpoints.
Tenant isolation#
Every tenant-facing query is scoped by userId. Rate-limit counters are keyed (tenant, agent), so two customers using the same agent name never share a budget. Audit chains are per-tenant: your seq sequence is yours, and another tenant's traffic cannot advance or disturb it. Cross-tenant reads exist only in the platform module, and only over metadata.
Credential handling#
Keys are 24 random bytes from the OS CSPRNG, rendered as 48 hex characters behind an ark_live_ prefix — 192 bits of entropy. The raw value is returned exactly once, at creation.
Arcus stores the SHA-256 hash and looks keys up by hash, so a database dump does not yield usable credentials. It also stores the first 15 characters as a display prefix, which is how logs and audit events name a key without holding it.
Revocation is immediate: the next dispatch with that key returns 401. The row is kept with revokedAt set so historical logs still resolve to a recognisable prefix, and revoked keys do not consume a tier slot — you can always create the replacement before withdrawing the old one.
lastUsedAt is written best-effort, deliberately, so a slow write cannot add latency to the enforcement path. Treat it as "recently active", not as an access log. For a precise record, read the audit chain, where every dispatch event carries the key prefix as its actor.
The actor is the key, not the claim
Audit events for gateway traffic record the authenticated key prefix as actor, never the self-declared from field. A spoofed from cannot produce a misleading audit entry, because the identity in the record is the one Arcus verified.
Transport and network posture#
| Surface | Posture |
|---|---|
| Inbound to Arcus | HTTPS in any real deployment; terminate TLS at your platform's edge |
| Browser → API | CORS permits only the configured dashboard origin — never * |
| Arcus → your target | Outbound HTTPS to the target URL, 10 s per attempt, 3 attempts |
| Webhook receipt | The billing webhook verifies a signature over the raw request bytes |
The webhook detail is a real correctness property, not a formality: the raw body is parsed as a Buffer before any JSON middleware runs, because JSON.parse followed by re-serialisation does not round-trip byte-for-byte and would fail every signature check.
Arcus does not authenticate itself to your target
A delivery arrives as a plain JSON POST. If your endpoint needs to know the request came from Arcus, put a shared secret in data and check it, or restrict the endpoint by network. Arcus does not sign outbound deliveries today — this is a real gap, and it is better that you know it than that you assume otherwise.
Failure behaviour, stated honestly#
A security control that fails in an undocumented direction is worse than one you understand.
| Dependency | If it is down |
|---|---|
| Redis — rate limiting | Gate 1 fails open: dispatches proceed and are checked by the other four gates |
| Redis — delivery queue | Enqueue times out at 2 s; the row is recorded FAILED with Queue unavailable, nothing is silently dropped |
| PostgreSQL | Nothing can be recorded, so nothing is dispatched. The gateway fails closed |
| Clerk | The control plane is unavailable. The gateway is unaffected — dispatch never calls Clerk |
| Billing provider | Everyone is treated as being on the free tier; checkout returns a clear error rather than the process failing to boot |
The Redis fail-open is the one asymmetry worth understanding fully. checkAgent fails open — if the counter cannot be read, the request is not refused, because a Redis blip would otherwise take down every customer's agent traffic while policy, DLP and guardrails were all still perfectly capable of running. quarantineAgent does not fail open: if a quarantine cannot be written, that is surfaced rather than swallowed. Losing the rate limiter degrades one gate; the other four hold.
This is the only fail-open in the pipeline
Identity, policy, DLP and guardrails all fail closed. They depend on PostgreSQL, and if PostgreSQL is unreachable nothing is dispatched at all.
The audit chain: what it does and does not guarantee#
Each event stores its sequence number, the previous event's hash, and a SHA-256 over its own canonical serialisation including that prevHash. Appends take a per-tenant advisory lock inside the transaction, so two concurrent writes cannot fork the chain at the same seq. Verification recomputes every hash from sequence 1.
It guarantees: any edit, deletion or reordering of a past event breaks verification at a specific seq. Someone with database write access cannot quietly change history — they would have to recompute every subsequent hash, and the sequence gap or hash mismatch names exactly where they started.
It does not guarantee: that the chain cannot be replaced wholesale. Arcus computes the hashes and stores them in the same database as the events. An adversary with full write access and the hashing code could rebuild a consistent chain from scratch.
This is tamper-evident, not tamper-proof, and Arcus says so rather than implying more. The mitigation is straightforward and it is the reason exports preserve hashes: export periodically, archive the export somewhere Arcus's database credentials do not reach, and a wholesale replacement becomes detectable by comparing against an archive Arcus never had access to. See Compliance reporting.
Enforcement order and why it is what it is#
Gates run cheapest-and-most-decisive first: quarantine (a Redis read) → identity (a hash lookup) → policy (a route decision) → DLP (a payload walk) → guardrails (field evaluation).
The security-relevant part is that identity is resolved before anything reasons about identity. Policies match on the sender, guardrails scope to the sender, baselines are learned per sender. If from were trusted rather than verified against the key's binding, every one of those would be bypassable by changing one string. Gate 2 is what makes gates 3 through 5 mean anything.
Nothing is dispatched before the decision is recorded. The audit event and the transfer log row are written in the same transaction as the outcome, so there is no window in which a request has been delivered but not recorded.
Your responsibilities#
Arcus is one control in a system. These are the parts it cannot do for you.
Keep keys out of source control and out of payloads. DLP recognises ark_live_ and will refuse a dispatch carrying one — correct, but an avoidable incident. Read keys from the environment.
Bind every key to an agent id. An unbound key is a key any process can present under any identity, and it makes gate 2 a no-op.
Write policies narrowly. * → * ALLOW disables the policy engine. Exempt the narrowest possible route from DLP, never a whole host.
Verify the chain on a schedule. Verification you have never run is not a control. A weekly verify-then-archive job is a few lines — there is one in the compliance guide.
Rotate deliberately. Create the new key, deploy it, confirm traffic on the new prefix, then revoke the old one. Revoked keys do not occupy a slot, so the order never forces a gap.
Keep the operator allow-list small. It is cross-tenant access. Smallest possible set of addresses, accounts with MFA, reviewed when people change roles.
Return 2xx quickly and be idempotent. Deliveries retry three times and there are no idempotency keys — put a deduplication id in data and check it.
What Arcus deliberately does not do#
Listed so you can plan around the gaps rather than discover them.
- It does not inspect model behaviour. Arcus governs what an agent sends, not how it reasons. Prompt injection that causes an agent to attempt a harmful action is caught only if that action trips a gate — which is precisely why the destructive catalogue and guardrails exist.
- It does not sign outbound deliveries. Your target cannot cryptographically verify the sender.
- It does not implement idempotency keys. Duplicate suppression is the receiver's job.
- It does not encrypt payloads at rest beyond your database's own encryption, because it does not store payload bodies in the first place. Held payloads and guardrail findings are the exceptions.
- It does not enforce monthly request quotas. The per-tier volume figures are advisory capacity guidance; the only enforced rate limit is 60 dispatches per 60 seconds per agent, identical on every plan.
- It does not manage secrets for you. No vault, no key distribution. Use your platform's secret store.
Reporting a vulnerability#
If you are running hosted Arcus, contact the operator of your instance directly with a description, reproduction steps, and the impact you believe it has. Do not include live credentials or another tenant's data in the report — a key prefix and a transferLogId are enough to identify anything in the system.
If you self-host, the same applies to whoever operates your deployment. See Running Arcus for the configuration surface that is yours to secure.