Repo: github.com/arkenforge/arken-core
If you've built an MCP server or an API that agents call, you've run into this problem.
Your agent needs to call a tool. That tool needs to know who's calling. So you do what everyone does: generate an API key, paste it in an env var, and move on.
It works. Until it doesn't.
The key gets committed to a repo. Or it gets shared across a whole team's agents, so every agent looks identical in your logs. Or someone leaves and you realize you have no idea which credentials they had. Or you're running 50 agents and managing 50 sets of credentials per tool.
This isn't a process problem. It's an architectural one. API keys were designed for humans. They were never designed for machines.
What agents actually need
Think about what authentication is supposed to do: prove that the entity making a request is who they say they are.
For humans, passwords work (barely) because you can reset them, remember them, and tie them to a person. For machines, a static shared secret is just a liability — it has to exist somewhere, which means it can leak from somewhere.
What agents need is proof of identity without a credential that can be stolen. That's a solved problem in cryptography. It's called a challenge-response. The server issues a one-time nonce. The client signs it with a private key. The server verifies the signature. No credential ever travels over the wire — only a signature that proves the client holds the key.
SSH works this way. TLS works this way. There's no reason agent authentication shouldn't.
What Arken does
Arken is an open-source implementation of this for HTTP APIs and MCP servers.
Each agent gets a key pair. The did:key identifier derived from that key pair is the agent's identity — a string that looks like did:key:zQ3sh.... It's deterministic from the private key, portable across every Arken-protected endpoint, and requires no registration with any central authority.
Server side — one line of Express middleware:
const { arkenAuth } = require('@arkenforge/arken-auth');
app.get('/tool', arkenAuth(), (req, res) => {
res.json({ did: req.user.did });
});
An unsigned request gets a 401 with a one-time nonce:
{
"challenge": {
"nonce": "a3f9...",
"statement": "Sign this message to prove you control this DID."
}
}
The client signs the nonce and retries with an Authorization: Signature header. The server verifies the ECDSA signature in Rust, consumes the nonce, and sets req.user.did. No credentials stored. Nonce expires in 60 seconds.
Agent side — a drop-in fetch wrapper:
const { createClient } = require('arken-client');
const { fetch, did } = createClient(process.env.AGENT_PRIVATE_KEY);
console.log(did); // did:key:zQ3s… — same every time for this key
const res = await fetch('https://your-mcp-server.com/tool');
// 401 → sign → retry happens automatically
// your code never sees the auth flow
If no key is provided, one is generated. For agents that need a stable identity across runs, generate once and store the private key.
What this looks like in practice
Every agent in your fleet has a unique did:key. When it calls a tool, the server logs exactly which DID called — not "some agent using the shared service token."
You can do things that were previously impossible with API keys:
- Per-agent audit trail — you know exactly which agent called which tool and when
- No credential rotation — there are no credentials to rotate
- No credential storage — the server holds nothing. There's no credential database to breach
- Fleet identity management — each agent in a 100-agent fleet has its own cryptographic identity, issued at creation time, with no manual key management
FastAPI
from arken_auth import arken_auth
@app.get("/tool")
async def tool(user = Depends(arken_auth())):
return {"did": user["did"]}
Production deployments
By default nonces are stored in memory. For multi-instance deployments, pass a Redis client:
const { arkenAuth, RedisNonceStore } = require('@arkenforge/arken-auth');
const { createClient } = require('redis');
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
app.use('/tool', arkenAuth({ store: new RedisNonceStore(redis) }));
Install
npm install @arkenforge/arken-auth # server
npm install arken-client # agent
pip install arken-auth # FastAPI
pip install arken-client # Python agents
Pre-1.0 — the API may change before the stable release. Apache 2.0.
Why this matters beyond MCP
Agents proliferating without per-agent identity is going to cause problems. Most enterprises don't know which agents exist inside their infrastructure, what they're authorized to do, or what they've accessed. A shared service token doesn't tell you any of that.
A did:key per agent is the primitive that makes visibility possible — not just for security, but for billing, auditing, and access control.
If you're building MCP servers, agent backends, or APIs that agents call — especially if you've hit the credentials problem in production — I'd like to hear about your setup.
Top comments (25)
The 60-second nonce is the part that keeps this honest. A did:key per agent plus that audit trail beats a shared key nobody can trace back to one caller.
That's the part most people skip past but it's doing a lot of work. A replayable signature is just a bearer token with extra steps — the nonce is what makes each authentication event discrete and attributable. Without it you've proven the agent holds the key once, not that this specific request was authorized.
That distinction helps. A nonce on its own is just freshness, while the signature ties it to one agent, and that per-agent audit trail is what makes the extra step worth keeping.
Exactly — freshness plus identity in one primitive. The nonce prevents replay, the signature proves which key signed it, and the did:key makes that key a persistent identity rather than an anonymous public key. All three have to be present for the audit trail to mean anything.
The did:key part is what stuck with me there. A stable identity across runs gives that audit trail a name that survives the next run.
Exactly. Same key, same DID, every run — so the audit trail accumulates against one identity over time rather than starting fresh.
Curious what you're building — are you running agents across multiple services or mostly one integration?
I'm on email infra, not agents, so the multi-service side is still the open question for me.
Email is a close analogue. DKIM is the same idea: the sender signs, the receiver verifies against a public key. The difference is DKIM publishes the key in DNS, while did:key embeds it in the identifier, so there's nothing to look up. Have you dealt with key rotation on the DKIM side?
the overlap window is the messy part, old key still valid while the new one propagates.
The 'API keys were designed for humans' line is the whole argument — a static secret that has to exist somewhere will leak from somewhere. Deriving identity from the key pair instead of a central registry is the elegant part: no credential database to breach, and every call in the log names an actual agent. My question is revocation — if an agent's private key is compromised, how do you kill that did:key across every endpoint without a registry to publish the revocation to?
Short answer: there's no global revocation, and that's the cost of having no registry. A server can deny a DID, so you revoke per endpoint. Across a fleet you'd need shared state, like a deny list in Redis that every server checks, which is a registry in everything but name. I'd offer that as opt-in rather than bake it in.
Are you thinking about fleets inside one org, or open endpoints anyone can call? The answer is very different for each.
The honest answer is that it splits by scope: inside one org, a shared deny list is fine — the org already has central control, so calling it 'not a registry' is just branding. For open endpoints anyone can call, per-endpoint deny is the only answer that doesn't smuggle a registry back in. So the design question becomes whether you ship one mechanism with two modes, or accept that open endpoints simply revoke slower?
Ok, the challenge-response logic makes sense, but the private key still has to live somewhere on the agent host. If that ends up in an env var like every other credential, did we really escape the leak problem or just move it somewhere quieter? Would love to hear how you store the signing key
Fair challenge. The private key still has to live somewhere, and an env var is still an env var.
The difference is what's on the other side. With an API key, the server stores a copy too, so a leak on either end compromises the credential. With a keypair, the server stores nothing and only ever sees a signature. A leaked private key is still bad, but the exposure is one-sided, which removes half the attack surface.
For storing the signing key, in roughly increasing order of rigor:
KMS is the cleanest option because it removes key exfiltration from the agent host. It doesn't remove key misuse: an attacker who compromises the host can still call KMS with the host's IAM credentials and sign nonces while they have access. Scope the IAM permission to signing with that one key, and log and alert on sign calls.
How are you running your agents: containers, serverless, something else? The right answer depends a lot on the runtime.
Per-agent cryptographic identity solves an important visibility problem, but identity and authorization need to remain separate. Knowing which agent signed a request doesn't automatically establish whether it should be allowed to perform that action.
I'd take this a step further with short-lived, task-scoped permissions layered on top of the DID. That way, even if an agent's private key is compromised, its identity alone doesn't grant unrestricted access to every tool or resource. The combination of verifiable identity, explicit authorization, and auditable actions is what makes agent authentication practical in larger production environments.
Agreed. The DID is deliberately only the identity layer. It answers "who signed this", and req.user.did is where authorization attaches. Task-scoped permissions need something to issue them, and that part isn't built yet. Would you issue them from a policy service, or check inline at the tool server?
I'd separate workload identity from delegated user authority. A key or signing key can tell the API which agent runtime made the call, but it doesn't say whose permissions the call should exercise. For user-driven actions, I'd use a short-lived, audience-bound delegation carrying the actor, tenant and scopes. System-owned jobs should use a separate service principal. Otherwise, per-agent credentials improve attribution but can still leave authorization too broad.
That split is right. The DID is workload identity, and delegated user authority is a separate claim that shouldn't ride on the same key. The hard part is audience-binding, so a delegation signed for server A can't be replayed against server B. Are you carrying that as a JWT from your IdP, or something custom?
Challenge-response is the right primitive for agent identity. The server holds nothing to steal. The client proves possession without transmitting a secret. That inversion of the trust model is what makes it work for machines, not just humans.
The part I keep circling back to is what comes after identity. A did:key tells you who asked, but not whether the answer should be yes or no. When a tool call triggers a payment or signs something, you need per-agent authorization limits on top of the identity layer. Authentication is clean now. Authorization for real spending decisions, with per-agent caps and intent checks, is where the open problems are.
Clean authentication makes authorization tractable, because every decision has a named subject. Per-agent spend caps keyed on the DID fit naturally, but I'd keep them in a policy layer behind the middleware, not in it. Where do you enforce caps today: at the tool server or at the payment gateway?
One important aspect of agent authentication is separating identity verification from authorization. A cryptographic signature can prove which agent is making a request, but it doesn't automatically determine what that agent should be allowed to access. Combining unique agent identities with scoped permissions, short-lived credentials, and execution-level audit logs creates a much stronger security model. The discussion around KMS-backed signing is particularly interesting because protecting the private key is just as important as verifying the signature.
Agreed. Key custody is where the real risk sits. The signature check is the easy part, which is why the KMS thread ended up being the most useful bit of this post.
The 401-challenge pattern is clean for machine identity, but sequential tool loops expose the round-trip overhead fast. If an agent runs a dozen tool calls in a row, paying a 401-then-sign retry on every step doubles network latency unless you exchange that first signed nonce for a short-lived scoped token. Separating the initial DID proof from a five-minute session ticket keeps the audit trail without bloating the loop time.
The round-trip overhead is real, and you've named the right fix. The initial DID proof is the expensive step. Once the agent has proven it controls the key, there's no reason to repeat the full challenge-response on every call in a session.
The session ticket model makes sense. The first call does the full 401 → sign → verify flow, and the server issues a short-lived scoped token (say 5 minutes, tied to the DID, single agent). Subsequent calls in that session present the token instead of signing again. The audit trail stays intact because the token is issued to a specific DID and every call in the session is attributed to it.
The tradeoff is that you've reintroduced a bearer token, which is what the protocol was trying to avoid. The mitigation is that it's short-lived and scoped, so the blast radius of a stolen token is bounded. A leaked 5-minute token is a different risk profile than a leaked API key that never expires.
I haven't built this yet. Right now every request pays the full round trip. For single tool calls or low-frequency integrations the overhead is fine, but a tight tool loop would feel it. Session tokens are the next thing to design.
What does your tool loop look like: hitting the same server repeatedly, or fanning out across multiple endpoints?
Agent authentication needs both an identity and a delegated intent. A useful credential should say who issued it, which agent instance holds it, which audience and action it covers, and when it expires. Without that binding, a bearer token turns every tool call into an attribution problem after the fact.