seekrit
Docs/Agent access policy

Agent access policy

seekrit-proxy answers "may this secret go to this host?" from a file on its own disk. That file is a good default and stays supported — but the rules are the part that churns, and editing a file on every machine that runs an agent is the wrong shape for it. This page is the other option: write the rules here, sign them in your browser, and let each proxy pick them up.

  admin ──▶ dashboard: edit rules ──▶ sign in your browser (your own key)
                                          │  opaque signed bundle
                                          ▼
                                     seekrit API  (stores, serves, cannot forge)
                                          │
     agent ──{{seekrit:NAME}}──▶  seekrit-proxy  ◀── GET /v1/agents/:agent/policy
                                          │  verify signature ∩ pinned signers
                                          ▼
                                  allowlisted upstream

Why signing, and not just an API call

Serving policy from an API naively would break the claim the proxy's allowlist makes. If seekrit could add attacker.example.com to your rules, a compromise here would end with your proxy faithfully decrypting a real credential and sending it there. No plaintext would ever pass through our servers — the letter of the zero-knowledge model would survive — but we would have gained authority over where plaintext goes, which is the same class of harm.

So policy is signed client-side with the key you already have (the one your passphrase unlocks — no second keypair to manage), and every proxy verifies that signature against thumbprints pinned in its own local file. That has three concrete consequences:

  • seekrit can withhold policy. A proxy that cannot fetch keeps what it has until that bundle expires, then refuses everything. Fail-closed, always.
  • seekrit cannot widen policy, and neither can anyone who compromises the API. A bundle not signed by a pinned key is refused outright; there is no fallback to an unsigned one.
  • An agent cannot widen its own policy, because publishing needs a human's signing key. That is a guarantee, not an implementation detail — see below.

Create an agent identity

Dashboard → Agents → New agent, or from a terminal:

seekrit agents create "Nova" --slug nova --app storefront --env production

An identity is a policy subject and nothing else: it holds no key, and no key can ever be wrapped to it, so creating one grants nothing. What it gives you is a name — nova, scribe — that a proxy config and a policy can both refer to.

Bind it to an environment when you can. That lets the editor offer the secret names that environment resolves (the dashboard already knows them without decrypting anything), and stops a service token bound elsewhere from fetching this agent's policy.

Write the rules

Rules are checked top to bottom, first match wins, so a narrow rule belongs above a broad one. Each rule names:

FieldMeaningEmpty means
HostBare hostname — no scheme, port, or wildcard— (required)
MethodsHTTP methods this rule coversany method
PathsGlob patterns (* within a segment, ** across)any path
Injectable secretsNames substitutable toward this hostno secret

That asymmetry is deliberate: operation constraints are opt-in, while injection has always been default-deny. A rule with no secrets is useful on its own — it is how you let an agent read an API while only one operation may carry the key.

Every rule collapses to one line — methods host paths → secrets, the same shape the publish diff shows — so a policy of thirty rules stays something you can scroll. Click a rule to open it for editing and click its header again to shrink it; Expand all and Collapse all do the whole list. A short policy opens expanded; past a few rules it opens collapsed instead. Two things stay visible on a collapsed rule, because that is the state a long policy sits in: a host nobody filled in, and a warning that the rule names a secret no layer provides.

Which secrets you can pick

Every name the bound environment resolves, not just the ones stored in it. A proxy resolves its environment the same way any other client does: the groups composed into it, then the environment's own secrets, then a branch overlay if it is bound to a branch — merged into one flat set of names before anything is substituted. So a rule may name a group's secret, and the picker lists it under the group it comes from.

The picker is grouped by layer, highest precedence first, and a name defined in two layers is listed once, under the layer that wins — which is the value a proxy would actually substitute. A name in a rule that no layer provides is flagged: the rule stays valid and the request is still permitted, but there is nothing to put in the placeholder.

note

Nothing you type takes effect until it is signed and published. The editor holds a draft; the live policy is whatever version your proxies last fetched.

Try it before an agent does

The Dry run panel answers the question that actually matters: would POST https://api.openai.com/v1/chat/completions carrying OPENAI_API_KEY be permitted for this agent? It reports allow or deny, which rule decided, and which constraint refused.

Use it. Allowlist mistakes are otherwise silent until an agent breaks in production, and the panel is a real prediction rather than an approximation: it runs the same evaluator the proxy runs, and the two implementations are pinned to shared test vectors so they cannot drift apart.

Publish

Publishing shows a diff against the live version, asks for the lifetime, and then asks you to unlock your keyring — because that unlock is the authorization step. The bundle is signed in your tab and the API stores an opaque blob.

Expiry is required (7 days by default, 1 hour to 90 days). It bounds how long a revoked policy can keep working in a proxy partitioned from us, and it forces republication to act as a liveness signal. A proxy warns as expiry approaches, and the dashboard flags it too.

caution

Publishing requires a human's signing key, so an admin service token cannot publish policy. This closes self-widening: a prompt-injected agent that can create apps and mint tokens still cannot broaden its own reach. The cost is that fully headless publishing needs a held signing key and its passphrase, which is also one reason file-only policy stays supported.

Or keep the rules in your repo

A rule set is a change to who may reach what, which makes it exactly the kind of thing that wants a diff and a reviewer. The CLI does the whole loop without giving up the signing property:

seekrit agents policy pull nova -o nova.policy.json

Edit that file, commit it, review it like any other change, then:

seekrit agents policy publish nova -f nova.policy.json --ttl 7d

publish shows the same diff the dashboard shows, asks before it signs, and signs on your machine with your own key — the API still receives an opaque bundle it cannot forge. seekrit agents simulate runs the same evaluator as the Dry run panel and exits non-zero on a denial, so a policy test belongs in CI next to your other tests.

What this does not change is who may publish. There is no service-token path: the CLI refuses one before it does any work. Publishing from CI therefore means that machine holds a signing key and its passphrase — a real choice with a real cost, not a workaround. On a developer machine, leave SEEKRIT_PASSPHRASE unset and let the prompt do its job, especially where an agent has shell access.

See the CLI reference for every command and flag.

Point a proxy at it

The Trust anchor panel prints exactly what to paste into seekrit-proxy.toml:

[policy]
source = "server"
agent = "nova"
signers = ["kNc8…thumbprint"]

Copy it into the file and commit it. Pin a second admin's thumbprint too — with only one pinned signer, a lost passphrase means nobody can publish. Retiring a signer is an edit to that file, not a click here; the list has to live where we cannot reach it, or the argument above collapses.

Or a Worker

A Cloudflare Worker cannot run the proxy binary, so the Cloudflare Computer gateway verifies the same bundle itself. The pinned signers move from a TOML file to your wrangler.jsonc, which plays the same role — config you deployed, that the API cannot reach:

seekritEgress({
  token: env.SEEKRIT_TOKEN,
  policy: {
    signers: env.POLICY_SIGNERS.split(','),
    agent: 'nova',
    refreshSeconds: 300,
  },
});

The verification is identical — the same envelope, the same thumbprint check, the same golden vectors. What differs is refresh: a Worker has no long-lived process to hold a timer, so see the refresh section for the three layers available and which to pick.

Change something while an agent is running

This is the flow the short refresh interval exists for:

  1. The agent hits a tool it has no credential for and gets a 403 naming the constraint that refused it.
  2. You add the secret to the environment and a rule for it here, then publish.
  3. Within refresh_interval (10s by default) the running proxy has both the new rule and the new credential — they arrive together, because server policy mode re-resolves secrets on the same interval. No restart.

Dispatch: one run at a time

Policy is a ceiling — it says what any run of this agent may do. Often you want less than that for a particular run: this one is triaging an issue and needs the GitHub token but has no business touching Stripe. That is a task.

TASK=$(seekrit agents dispatch nova --scope GITHUB_TOKEN --ttl 15m --label "triage 412")

The orchestrator hands $TASK to the run, which presents it in x-seekrit-ticket. Three things are true of that token, and they are what make it safe to give to the thing being constrained:

  • It carries no key and identifies nobody. Decryption still happens with the enforcement point's own credential. The task only says how much of that already-held ability this run may spend.
  • It can only narrow. --scope must name secrets the published policy already permits; anything else is refused by name at dispatch rather than dropped silently. A task can never restore something a policy later removed.
  • It expires on its own — fifteen minutes by default, twelve hours at most. seekrit agents revoke <taskId> ends it sooner, and an orchestrator can do that with the credential it already has, so "the run finished, drop its authority" is one command.

seekrit agents tasks nova lists what is authorized right now, and the audit trail records every dispatch with its scopes and the policy version the run was cut from — the per-run counterpart to the publish record.

To have a proxy honour dispatched runs, add a [tasks] block (or generate one with seekrit proxy init --tasks) — see runs dispatched through seekrit. The same page covers the trust ratchet, which narrows a run further as it touches protected things.

caution

Dispatching needs an admin service token, an M2M client, or an org admin. A runtime (member) token is refused, so an agent cannot dispatch its own authority — but whoever can dispatch can dispatch for any identity their credential reaches. Server-side dispatch takes over the job the proxy's local control token used to do, and inherits its rule: the dispatching credential belongs to the orchestrator, and must not be readable by the agents it starts.

Review the grant against what happened

A policy written once drifts. Rules nobody exercises stay forever because deleting them feels risky; rules the agent actually needs never get added, because a denial at 3am is easier to work around than to fix. Both directions are visible if the enforcement point reports what it decided:

# in seekrit-proxy.toml, or `seekrit proxy init --activity`
[activity]
flush_interval = "60s"

Then, after the agent has been running a while:

seekrit agents review nova --days 14
nova — policy v7, 4,983 decision(s) over 14d

proposals
- rule 1 (api.openai.com) permits ANTHROPIC_API_KEY, never injected in 4102 permitted request(s)
- rule 3 (api.github.com) never matched a request, while other rules did
? 12 request(s) to hooks.slack.com matched no rule (POST) — the agent wants this and cannot have it

2 narrowing change(s) can be applied; 1 need a human decision

seekrit agents review nova -o nova.policy.json writes the narrowed rules, and publishing goes through the same signed path as any other change. So the loop closes without ever loosening the security model: the review proposes, a human signs.

The agent's page in the dashboard runs the same review. Activity and grant review shows the counts the proxies reported, the proposals derived from them, and a toggle beside each narrowing change; Apply to draft loads the narrowed rules into the policy editor, where they sit as unpublished changes until you sign and publish them. The proposals are computed in your browser from the counts the API served — seekrit never opines on what your policy should be, because a server that did would have a say in authorization without ever holding a key.

Three properties are worth knowing before you rely on it:

  • Every proposal carries its evidence. "Remove rule 3" alone is a suggestion to ignore; "rule 3 never matched a request, while other rules did" is a decision someone can actually make.
  • Widening is surfaced and never applied. A ? line never reaches the file, even with --out. A review that could broaden policy from observed traffic would let a prompt-injected agent earn permissions by retrying until the denial count looked like a requirement.
  • An empty window proposes nothing. A rule is only called unused when other rules were used in the same window. Otherwise switching reporting on would propose deleting a working policy.
note

What crosses the boundary is counts: hosts, methods, secret names, which rule decided, and how often — never a request path and never a value. Full per-request detail stays in your own collector, which is where a forensic trail belongs. seekrit agents activity nova — or the Show reported decisions table on the agent's dashboard page — shows exactly what we hold.

See what authority is outstanding

A task narrows an agent's policy for one run and expires on its own. The agent's dashboard page lists them under Dispatched runs: what each run was for, the scopes it narrowed to, the policy version it was dispatched against, when it was last seen, and when it expires. Anything still active can be revoked there, which ends its authority at the enforcement point's next introspection.

Runs are dispatched by whoever orchestrates the agent, not from the dashboard: the dispatcher mints the skd_ token itself and registers only its hash, so the credential exists once, in the process that will use it. There is deliberately no button here that would put a live task token on a screen.

Turn an agent off

Disable the identity. The next policy fetch is refused, so a running proxy keeps the bundle it already has until that bundle expires — which is why expiry is the real bound on revocation, and why a shorter lifetime is the knob to reach for when that window matters. Session tickets narrow it further: they are held only in the proxy's memory, so a restart drops them all.

Dispatched tasks are the tightest bound available, because introspection is a live check rather than a cached bundle: a disabled identity stops authorizing any run that has not already been introspected, and seekrit agents revoke kills a specific run immediately.

To retire an identity for good, delete it — Delete agent at the bottom of its page in the dashboard, or seekrit agents rm <agent>. The identity and its whole published history go with it and the slug becomes available again. Deleting is not a faster disable: a proxy already holding a live bundle keeps running on it until that bundle expires either way, so reach for disable plus a shorter lifetime when the window is what matters. The audit_log rows survive the delete, so "who widened this agent's reach, and when" still has an answer.

Versions and rollback

Every publish appends a version; nothing is ever rewritten. Rolling back republishes an old bundle as a new version — no one re-signs anything, so the signature stays valid and the history stays honest. The consequence: the restored version keeps its original expiry, so rolling back to something stale leaves proxies failing closed. Edit and publish instead when that is the case.

The audit trail records agent.policy_published and agent.policy_rolled_back with the signer and version — that row is the durable answer to "who widened this agent's reach, and when". Per-request decisions are not audited by us: they happen in your proxy and go to your own OTLP collector, like every other substitution.