The first time a support engineer asked for MCP access so she could pull customer records without bugging the on-call, we handed her a gateway key. The second PM asked for the same thing, we gave him the same key. By the end of the month, five non-engineers were sharing one long-lived credential with full read on every operation in our OpenAPI surface.
That worked until the support person left. The incident response instinct was to rotate the gateway secret — and we did, at 9am on a Tuesday. Every Cursor session, every Cline setup, every internal agent that had that key baked in broke at the same time. We spent the next two hours cycling keys back into configs that various engineers kept in various places.
We had built a blast radius sized for a five-person team on top of a credential we expected to live forever.
The primitive is wrong, not the access
The mistake wasn't giving support and PMs agent access. That's a good thing — a support rep who can ask an agent to pull a refund status is a support rep who isn't paging me at 2am. The mistake was treating the gateway key as a team artifact.
A team-wide key conflates four things that should be separate:
- Identity. Which human is actually making the call? An audit log that says "support-team" when a refund was issued is not an audit log.
- Scope. A support rep looking up a ticket does not need the operation that issues a full refund. A PM reading a metrics endpoint does not need the one that triggers a reindex.
- Rate. A PM exploring in Cursor for an afternoon can accidentally generate 800 calls in 20 minutes if the agent loops on a bad query. A team-wide key has no way to absorb that.
- Revocation. When someone leaves, you need to kill their access without touching anyone else's. Rotating a shared key is a coordinated outage.
What we changed
We moved to issuing one key per human, with three fields attached:
- A list of allowed operations, drawn from the OpenAPI spec by the role — support gets customer lookup + refund status, PM gets metrics read, nobody gets write except on-call.
- A per-key rate limit (100 req/min) so an agent loop can't take down prod by itself.
- A key name that is the human's email, so the audit log reads like a log.
When someone leaves, we delete their key. Nothing else moves. When a key leaks, we rotate only that one. When support needs a new operation, we add it to their scope — we don't widen the team key.
This is the same reason a gateway should sit in front of the MCP surface at all: the gateway is the place where the OpenAPI spec is the source of truth for what operations exist, and where per-key scoping is enforced against that spec rather than reconstructed in every agent config. We built this into Powerduck because we kept re-implementing the same allowlist by hand — the spec already knows which operations are which, the gateway should be the only place that enforces it.
The non-obvious cost of team-wide keys
The visible cost is the outage when you rotate. The quieter cost is that you stop auditing. Once the key is shared, the audit log is useless — you see 4,000 calls from "support-team" and you have no idea which of the five humans made the refund that ended up on a customer's invoice. The next time support needs access to a sensitive operation, you can't point to a history of who actually used the read-only path, so you say no. That's how non-engineer agent access quietly dies: not with a bang, but with a slow refusal to expand scope because nobody trusts the shared credential.
Per-human keys are a small operational overhead — you issue one when someone joins, delete one when they leave — but they're the difference between "agent access for the whole company" and "agent access for the three engineers who are comfortable rotating keys."
Top comments (1)
Per-human keys are the right primitive and the revocation argument alone carries it. One sentence in here is doing more work than it can hold though.
"A key name that is the human's email, so the audit log reads like a log."
The key name is editable. On most providers the update endpoint for a key accepts exactly name and status, which means the field your audit log is reading is the one field anybody with key admin can rewrite after the fact. You have rebuilt the registry, just stored at the provider. The fields worth keying on are the ones the provider refuses to let anyone change: created_by, created_at, the principal the key is bound to. Issue each person a service account and bind their key to it, and the identity is a fact about issuance rather than a label.
The other half is one payments never gets to skip. A per-human key tells you whose authority the call borrowed, not who was present when it ran. Your support rep's key sitting inside an agent that retries a refund at 3am is still her in the log and she is asleep. That is the whole reason strong customer authentication separates the party who authorises from the process that executes: the authorisation is scoped to an act, with an amount and a payee, not to a credential that stays warm.
So scope is the field I would make expire, not the key. Support gets refund-status for as long as they are support, and gets the operation that issues the refund for the ticket they are on.