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. The consequences are worth being precise about:
- 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. 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.
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 policy-as-code needs a held signing key, which is one
reason file-only policy stays supported.
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.
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.
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.
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.