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:
| Field | Meaning | Empty means |
|---|---|---|
| Host | Bare hostname — no scheme, port, or wildcard | — (required) |
| Methods | HTTP methods this rule covers | any method |
| Paths | Glob patterns (* within a segment, ** across) | any path |
| Injectable secrets | Names substitutable toward this host | no 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.
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.
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:
- The agent hits a tool it has no credential for and gets a
403naming the constraint that refused it. - You add the secret to the environment and a rule for it here, then publish.
- 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.
--scopemust 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.
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.
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.