The credential you never created can't leak

2026-08-25 · Nikhil “Nick” Harinath · ~4 min

Most guidance for reaching object storage from a serverless function looks the same: create an access key pair, store it as an encrypted environment secret, sign S3 requests with it. Every step is correct. The whole thing is still worse than necessary.

Because you have now created a long-lived credential that grants bucket access, and it lives somewhere your code can read it. Encrypting it at rest doesn't change that — at runtime it's a string in your process, and anything that achieves code execution or output injection can read it out.

The alternative: don't have one

Several platforms let you attach storage to a function as a binding rather than an endpoint. Instead of an HTTP client pointed at a URL and signed with keys, you get an object in your runtime:

// no endpoint, no region, no access key, no signature
const obj = await env.BLOBS.get("reports/q3.json");
await env.ARCHIVE.put("claude/notes.md", body);

Authorization happens at the platform layer — the binding is declared in configuration and enforced by the infrastructure. Traffic goes over internal networking rather than the public internet. There is no key material in the function, so there is nothing for a compromise to steal, exfiltrate, or accidentally log.

Why this beats "store the key properly"

Compare the two failure modes honestly.

With S3-style keys: an attacker who achieves code execution, or who finds a path that echoes environment state into output, walks away with a credential valid against your bucket from anywhere on the internet, until you notice and rotate. The credential outlives the request, the process, and often your awareness of the breach.

With bindings: the same attacker can call the same operations while inside your function, and gets nothing portable. There is no artifact to carry away. The moment the request ends, so does the access.

That's a meaningful reduction in blast radius, and it costs less code than doing it the other way.

What you give up

Be honest about the constraint: bindings only work on the platform that provides them. You are trading portability for the removal of a secret.

If you might move this workload to another provider next quarter, the S3 API is a genuine lingua franca and worth the key management. If the function is already platform-specific — it runs at that edge, using that runtime, alongside that storage — then the portability you're "preserving" is theoretical, and you're paying for it with a real credential.

Pair it with server-side containment

Removing the credential is necessary, not sufficient. Whatever can call your function can still do whatever your function permits. So constrain the operations themselves:

  • Force writes under a single prefix
  • Refuse deletes outside that prefix
  • Never accept a caller-supplied full path without validation

Then verify by test. Attempt a delete outside the allowed prefix and confirm it's refused. "I intended it to be contained" and "I confirmed it is contained" are different claims, and only one of them belongs in a security discussion.

The general shape

The strongest thing you can do with a secret is not need one. Before designing careful handling for a credential, check whether the platform offers an identity-based or binding-based path that removes it from the picture — managed identities, workload identity federation, native bindings, IAM roles attached to compute.

Careful secret handling is the fallback. Structural elimination is the goal.

Related notes

  • Capability URLs vs OAuth for a personal MCP server
    OAuth is right for a product. For a single-user MCP server it can be why the thing never ships. What a capability URL actually costs you, stated plainly.
  • The five rungs of agent engineering
    Prompt, context, loop, graph, ownership. Most teams stall on rung one and wonder why their AI never leaves the demo. A ladder for what to fix next.
  • Make your agent fail closed
    The worst agent bug isn't a wrong answer — it's a confident answer built on a tool that quietly returned nothing. A design rule, and where to enforce it.

← All notes  ·  Work with me