Everyone is building AI agents. Most are not thinking about how to secure them.
An agent that sends emails, executes trades, controls infrastructure, or manages files is not just a chatbot. It's a system that takes real actions with real consequences. And when something goes wrong — a compromised agent, a replayed action, a forged instruction — the question isn't just "what happened?" It's "can you prove it, and can you stop it?"
This post covers three practical problems in agent security and how to solve them with ML-DSA-65 (NIST FIPS 204) signatures.
The problem with agent actions
When an agent executes an action, you need to answer three questions:
- Was this action actually authorized? Not just "did the agent do it" but "was the agent authorized to do it at this specific moment?"
- Can you invalidate it? If the agent is compromised mid-session, can you stop future actions without waiting for a token to expire?
- How do you verify identity at scale? When you have dozens of agents communicating with each other, how does Agent B know it's actually talking to Agent A and not an impersonator?
JWT with RS256 answers none of these well. It has no revocation. Its signatures are vulnerable to Shor's algorithm. And there's no concept of agent identity — a token proves a session, not an entity.
Part 1 — Sign every action
The simplest and most useful pattern: every action an agent takes gets a signed token before execution.
import { PQAuth } from 'fipsign-sdk'
const pq = new PQAuth(process.env.FIPSIGN_API_KEY)
// Agent is about to send an email
const { token } = await pq.sign({
sub: 'agent_email_sender',
action: 'send_email',
to: 'user@example.com',
subject: 'Your weekly report',
authorizedBy: 'workflow_xyz',
expiresInSeconds: 300, // this action is valid for 5 minutes
})
// Execute the action — pass the token along
await emailService.send({ token, ...emailData })
// On the receiving side — verify before executing
const { valid, payload } = await pq.verify(token)
if (!valid) throw new Error('Unauthorized agent action')
console.log(payload.sub) // 'agent_email_sender'
console.log(payload.action) // 'send_email'
console.log(payload.authorizedBy) // 'workflow_xyz'
Same pattern in Python:
from fipsign import PQAuth
pq = PQAuth(os.environ["FIPSIGN_API_KEY"])
result = pq.sign(
"agent_email_sender",
action="send_email",
to="user@example.com",
authorized_by="workflow_xyz",
expires_in_seconds=300,
)
token = result.token
# Verify before executing
result = pq.verify(token)
if not result.valid:
raise PermissionError("Unauthorized agent action")
What you gain: every action has a cryptographic proof of authorization. The signature is ML-DSA-65 — no known quantum attack. The payload is tamper-proof. If someone intercepts and modifies action: "delete_files" from action: "send_email", verification fails.
Part 2 — Revoke immediately when an agent is compromised
This is where JWT breaks down completely.
With JWT, if an agent is compromised, you can't invalidate its tokens. You wait for expiry. For a token with a 1-hour TTL, that's up to 60 minutes of a compromised agent still passing authentication.
With FIPSign, revocation is immediate and permanent:
// Agent_X is compromised at 14:32
// Revoke its active token immediately
await pq.revoke(agentToken, 'agent compromised — security incident')
// Any subsequent verify() call returns valid: false
const { valid, error } = await pq.verify(agentToken)
console.log(valid) // false
console.log(error) // 'Token has been revoked'
The revocation is backed by a blacklist in Cloudflare D1. Every remote verify() call checks it before returning a result. There's no TTL to wait out.
The failure mode @valentin_monteiro mentioned — agents failing in production — almost always involves a window of time where a compromised agent keeps operating because nothing invalidated its credentials. Revocation closes that window to zero.
Part 2.5 — Closing the detection gap
Revocation is immediate — but only from the moment you call pq.revoke(). The real problem is the window between when an agent is compromised and when someone detects it and triggers the revoke. That gap can be minutes, hours, or days depending on your setup.
Three mechanisms to close it:
1. Short TTL as a damage ceiling
TTL isn't a substitute for revocation — it's a limit on how much damage can happen if detection is slow. A compromised agent signing actions with a 5-minute TTL has at most 5 minutes of undetected exposure per token. Keep high-risk actions under 5 minutes. Keep low-risk actions under 60 minutes. Never use TTLs over a few hours for agent tokens.
This is already in the checklist above — but the reasoning matters: short TTL is your last line of defense when everything else fails.
2. Wire token.rejected to automatic revocation
FIPSign emits a token.rejected webhook every time a verification fails. A compromised agent that starts replaying tokens, sending malformed payloads, or operating outside its expected scope will generate rejections. You can wire that signal directly to an auto-revoke:
// Your webhook handler
app.post('/fipsign-webhook', verifyWebhookSignature, async (req) => {
const { event, data } = req.body
if (event === 'token.rejected') {
const { reason, projectId } = data
// A project generating repeated rejections is a signal worth acting on
if (activeTokensByProject[projectId]) {
await pq.revoke(activeTokensByProject[projectId], 'auto-revoked: anomaly detected')
await alertSecurityTeam({ projectId, reason })
}
}
})
This closes the detection gap to seconds — no human in the loop required. FIPSign provides the signal. Your system decides what to do with it.
3. Use limit.warning as an indirect signal
A compromised agent that starts spamming actions will consume tokens faster than expected. FIPSign emits a limit.warning webhook when usage reaches 80% of your monthly limit. It's not a security signal by design — but if you're nowhere near your normal consumption and limit.warning fires unexpectedly, something is wrong.
if (event === 'limit.warning') {
// Unexpected spike — investigate
await alertSecurityTeam({
message: 'Unusual token consumption spike',
freeRemaining: data.freeRemaining,
month: data.month,
})
}
Not a substitute for proper anomaly detection — but it's a signal you already have for free.
The model: FIPSign provides the cryptographic primitives and the signals. Your system provides the context and the response logic. Neither layer can do the other's job — and they shouldn't try.
Part 3 — Agent identity with Private CA
For simple agents, signed action tokens are enough. But when you have persistent agents — a fleet of IoT devices, a set of microservices, a group of agents that communicate with each other — you need something more durable than a session token.
That's where a Private CA comes in.
The model: each agent gets an ML-DSA-65 certificate issued by your project's CA. The certificate contains the agent's public key, its identity, and its expiry. It's signed by the CA's private key — which never leaves the server.
import { PQAuth, generateKeyPair } from 'fipsign-sdk'
const pq = new PQAuth(process.env.FIPSIGN_API_KEY)
// At agent provisioning time: generate a key pair for the agent
const { publicKey, secretKey } = await generateKeyPair()
// secretKey stays on the agent — never transmitted
// Issue a certificate for this agent
const { certificate } = await pq.ca.issue({
subject: 'agent-data-processor-001',
publicKey,
expiresInSeconds: 30 * 24 * 60 * 60, // 30 days
meta: {
role: 'data_processor',
team: 'analytics',
version: '2.1.0',
},
})
// Store certificate on the agent
// certificate.id, certificate.publicKey, certificate.signature, certificate.expiresAt
Now when Agent B receives a message from Agent A, it can verify Agent A's identity offline:
import rootCert from './root-cert.json' assert { type: 'json' }
// No API call — purely local ML-DSA-65 verification
const result = pq.ca.verifyCert(agentACertificate, rootCert)
if (!result.valid) {
console.error(result.error) // 'Invalid certificate signature', 'CERT_EXPIRED', etc.
return reject('Agent not authorized')
}
console.log(result.cert.subject) // 'agent-data-processor-001'
console.log(result.cert.expiresAt) // Unix timestamp
And if Agent A is decommissioned or compromised:
// Revoke the certificate immediately
await pq.ca.revokeCert(certificate.id, 'agent decommissioned')
// Check revocation before trusting
const { crl } = await pq.ca.getCrl()
if (pq.ca.isCertRevoked(agentACertificate, crl)) {
return reject('Agent certificate revoked')
}
The full security model for agents
Putting it together:
| Concern | Solution |
|---|---|
| Was this action authorized? | Sign every action with /sign
|
| Can I stop a compromised agent? | Revoke with /revoke — immediate, permanent |
| How do agents identify themselves? | Private CA — one certificate per agent |
| How do agents verify each other? |
verifyCert() offline — no API call needed |
| Is the agent's identity still valid? | Check CRL with getCrl() + isCertRevoked()
|
| Are signatures quantum-resistant? | ML-DSA-65 — NIST FIPS 204 |
Security checklist for agents
Before deploying an agent that takes real actions:
- [ ] Every action produces a signed token before execution
- [ ] Token expiry is short — 5 minutes maximum for high-risk actions
- [ ] Revocation is wired in — if the agent is compromised, you can stop it in seconds
- [ ] Persistent agents have certificates, not just session tokens
- [ ] Agents verify each other's certificates before trusting inter-agent messages
- [ ] CRL is checked periodically — don't rely on certificate expiry alone
- [ ] Signatures use ML-DSA-65 — not RS256, not ES256
Why post-quantum matters for agents specifically
Most of the "harvest now, decrypt later" discussion focuses on data encryption. But signatures are equally at risk.
An attacker recording your agents' signed actions today can — once quantum computers are available — forge signatures for actions that never happened. That means fabricated audit trails, forged authorizations, and retroactive tampering with your agent's history.
ML-DSA-65 has no known quantum attack. Migrating now, while your agent system is being built, costs one install command. Migrating later, after you've deployed thousands of agents with RS256-signed credentials, costs months.
Get started
npm install fipsign-sdk
# or
pip install fipsign-sdk
Free tier: 10,000 tokens/month, no credit card. Private CA included — create one from the dashboard in seconds.
If you're building agents and hitting security questions I didn't cover — drop them in the comments. Happy to go deeper on specific failure modes.
Top comments (0)