After zero raw credentials: hardening the vault the fleet stands on

The last post ended with a verified state: an agent in our fleet holds zero raw fleet or model credentials, leases, a placeholder key, and an environment stripped of a hundred-plus secrets. That state creates a new obligation. Once the vault is load-bearing, its bugs are fleet incidents. So this month we attacked our own tool: fuzzed its attacker-facing parsers, put ceilings above caller discipline, shipped it to npm without a single long-lived token in the release path, and closed the last gap between what we publish and what we run.

Where the migration left us

Quick recap for anyone landing here first: our platform dispatches a fleet of specialist agents against real repos and real APIs, and strongroom (published then as keeper, more on the rename below) is the encrypted vault that hands those agents leases instead of keys: TTL-bound, use-counted, host-scoped, revocable, audited handles, with the raw secret injected only at the egress point. The migration case study covered the three seams: git credentials leased through a keyless redeem-daemon since June 16, the spawn environment cut from 132 keys to 13, the model key brokered since June 27.

Since then the defaults flipped on for good, and the pattern has simply run: every repo execution mints its own short-lived credential and revokes it in cleanup, and the full lifecycle (add → grant → redeem → revoke → remove) lands in the hash-chained audit log, per execution, without a human in the loop. Which is exactly the point where a security tool changes category. It stops being something you evaluate and becomes something you stand on, and the correct response to that is to spend your effort trying to break it.

Fuzzing found what review didn't

The broker is the part of strongroom an agent actually talks to: it parses a URL an agent constructed, matches the path against a per-lease allowlist, and only then redeems the lease and injects the secret upstream. Parsers fed by the least-trusted process in the system are exactly what fuzzers are for, so we wired up coverage-guided fuzzing (Jazzer.js locally, ClusterFuzzLite as a weekly CI guard) over three targets: path canonicalization, the allowlist matcher, and lease-id handling.

The campaign found two real bugs in the path-allowlist layer, both fixed in PR #37 with regression tests:

  • A climbing .. survived canonicalization. Normalization ran before the path was anchored to root, so .. could normalize into /.. instead of being clamped. The broker's URL regex happens to make this unreachable today, but a canonicalizer in a security boundary has to be correct for any input, not just the inputs the current caller produces. The fix anchors first, then normalizes, so a climb clamps at /.
  • A ? in an endpoint glob killed the lease. The glob-to-regex escape set was missing ?, so a lease granted with a ?-containing path pattern threw SyntaxError on every request, a valid lease that 502s on 100% of traffic. It failed closed, which is the right direction, but a lease that can't serve is a denial of service you granted yourself. The fix escapes it, and pattern compilation now sits in a try/catch that fails closed by design: an uncompilable pattern matches nothing, deliberately, rather than throwing incidentally.

Neither bug was in the encryption, the lease accounting, or the audit chain, the parts that get the design attention. Both were in the boring glue that turns an operator's intent (--paths "/v1/chat/*") into an enforcement decision. That's a pattern worth internalizing: the security-critical code you didn't think of as security-critical is where the fuzzer earns its keep.

Ceilings above discipline

The migration's lease parameters, 600-second TTLs, single-digit use counts, were caller discipline: the platform code asks for small leases, so leases are small. Nothing stopped a bug (or a manipulated caller) from asking for a week-long, thousand-use lease and getting it.

Now the vault itself has an opinion. Set KEEPER_MAX_TTL and KEEPER_MAX_USES and no lease minted from that vault, through the CLI or the library, by any caller, can exceed them. Two design decisions matter more than the feature:

  • Over-cap grants are rejected, never silently clamped. A caller that asked for more than policy allows should find out, not receive a quietly smaller lease and misbehave later. The error names the cap it hit, and the attempt is audited as a policy denial, an over-ask is a signal someone should see.
  • Nonsense is rejected everywhere. A NaN TTL previously minted a lease that never expires, the worst possible interpretation of bad input in a tool whose whole premise is expiry. Zero, negative, and non-numeric TTLs and use counts are now refused at grant, ceiling or no ceiling.

The broker got the same treatment on its time axis: the upstream request is now bounded by a timeout (30 seconds to first response headers by default). Before, a black-holed upstream hung the request forever, and because in-flight requests count against a lease's concurrency cap, a hung upstream could wedge the lease shut. Now it gets a 504, audited, and the slot frees. The bound is to headers, so a healthy streaming response is never cut off mid-stream. And because the control plane scripts against all of this, the CLI grew a machine contract: grant, leases, ls, and audit take --json and put exactly one JSON value on stdout, no ANSI, no prose, no scraping.

The rename, and a release path with no secret to steal

keeper became strongroom, the name change bought an unclaimed npm namespace, and nothing else moved: the keeper CLI alias still works, KEEPER_* environment variables are unchanged, and the old GitHub URL redirects. Compatibility across a rename is cheap to promise and easy to break, so it's pinned the same way everything else is: the package ships both bins, and the fleet ran across the rename without an edit.

The npm release itself is the part I'd show off. @askalf/strongroom publishes via OIDC trusted publishing: the registry trusts a specific workflow in a specific repo, CI proves its identity per-release, and no npm token exists anywhere, not in CI secrets, not on a laptop, nothing to leak and nothing to rotate. Releases are Sigstore-signed and carry npm provenance attestation, and the whole flow is one version-bump PR: merging it tags the release, cuts the changelog, and publishes. There is a certain rightness to a secrets vault shipping through a release path that holds no long-lived secret of its own, the same argument the tool makes, applied to the tool.

The last pin: run what you ship

One gap survived all of the above. The fleet's platform still consumed strongroom as a git dependency pinned to a commit hash, a pin from before the rename, before the fuzz fixes, before the ceilings. Dogfooding has a completeness condition: it doesn't fully count until you run the artifact you publish, through the same supply chain your users get.

This week's platform change (our platform is a private monorepo, so it isn't linkable, every strongroom-side artifact above is) swapped the pin for the published package: @askalf/strongroom@^0.2.1, integrity-hashed and provenance-attested, delivering the broker parser fixes and the upstream timeout to the exact code path that brokers our model traffic in production every day.

The swap also handed us a free supply-chain lesson. Regenerating the lockfile revealed that one sibling dependency was referenced as a floating git ref, no commit pin, so any unrelated lockfile regeneration would silently advance it to whatever HEAD happened to be. That's a code change riding into production inside someone else's diff, unreviewed. It's pinned now, and the rule generalizes: a floating ref is a write you didn't review. If a dependency moves, it should move because a human chose the commit.

What still isn't leased

Scope, as always. The leases cover the credentials our agents touch, git, the model key, the brokered HTTP APIs. The platform itself still holds raw long-lived credentials for some infrastructure: gateway-style tokens that authenticate a persistent connection rather than a request, and a handful of provider tokens that predate the vault. Those are exactly the ones that still cost us manual rotation ceremonies when they age out or need re-rolling, the leased paths have needed none, because nothing lives long enough to rotate. The contrast is the roadmap, and it's also the scope line worth stating plainly: strongroom shrinks the agent's blast radius today; the platform's own credential hygiene is a longer road we're walking in public.

What's next

Two directions, both pulled by the same trend: agents that spawn agents. First, lease delegation: a parent agent should be able to attenuate its own lease into a narrower one for a sub-agent (shorter TTL, fewer uses, tighter paths, never wider), with the parentage recorded in the audit chain. Multi-agent trees need least privilege between agents, not just between agent and platform. Second, first-class MCP packaging, promoting the repo's worked MCP example into an installable server, because MCP servers are where credentials are being stuffed into environment variables right now, and “tools return leases, not keys” deserves to be a one-line install.

All claims are verifiable at github.com/askalf/strongroom and npmjs.com/package/@askalf/strongroom (as of 2026-07-16).

We build the boundaries that make agents safe to hand real capability: secrets, tools, browsers, skills. If your agents hold keys you'd rather they didn't, that's the kind of problem we go deep on.

Start a conversation →
← All writing