← All churts

uBix Vault, Beta 7 — A Full Auth Suite, and Its Own CA

uBix Vault, Beta 7 — A Full Auth Suite, and Its Own CA

Who Are You, and Can I Prove It?

Last time, uBix Vault crossed from a binary into a service I actually run — a Helm chart on my own cluster, a console, metrics, a recovery story. That post ended at beta.3. Four betas have landed since, and if beta 3 was about operating the thing, betas 4 through 7 are about the question every secrets manager eventually has to answer well: who is asking, and how do I know?

Same disclaimer up front, because it’s a secrets manager and overselling maturity is how people get burned: it is a beta, single-node, not production-hardened, and it has not had an external security review. For production, use HashiCorp Vault or OpenBao. I run this on my own internal cluster, deliberately, with that understood.

A Login for Everything

When beta 3 shipped there was exactly one way in: a root or hand-minted token you carried around yourself. That’s fine for poking at a demo and useless for a system that automation and people are both supposed to reach. So the through-line of these four betas is a complete set of auth methods, each one exchanging some credential you already have for a scoped, policy-bound token:

  • AppRole (beta 4) — a role_id plus a secret_id for automation that can’t present a platform identity. Secret IDs are stored only as a SHA-256 hash.
  • Kubernetes (earlier, hardened since) — a pod logs in with its own ServiceAccount token, which the vault validates through the cluster’s TokenReview API. No shared secret to distribute.
  • userpass (beta 6) — the boring, necessary one: a human logs in with a username and password. Passwords are stored only as a PBKDF2-HMAC-SHA256 hash — 600k iterations, a per-user salt — verified in constant time, with the timing deliberately equalized for unknown users so you can’t probe which usernames exist.
  • JWT/OIDC (beta 7, just landed) — present a signed JWT from an identity provider and, if it verifies and matches a role’s claim bindings, you get a token.

That last one is the piece I want to talk about, because it’s the one where the “write the small, boring thing yourself” rule got tested hardest.

Verifying a JWT Without a JWT Library

The obvious way to add JWT auth is to reach for a JWT library. I didn’t, for the same reason I didn’t reach for a crypto library or a metrics client back at the start: this is a security tool, and every dependency I add is code I’m asking you to trust that I haven’t read.

A JWT, stripped of mystique, is three base64url chunks joined by dots: a header, a payload of claims, and a signature over the first two. Verifying it is just — decode the parts, recompute the signing input, and check the signature with the algorithm the header names. The Go standard library already has every primitive that requires:

  • RS256/384/512 verify with rsa.VerifyPKCS1v15 over the matching SHA digest.
  • ES256/384/512 are ECDSA — the JWS encoding is the raw R‖S, each half the signature, which you split and hand to ecdsa.Verify.
  • The public keys come either from a static PEM I configured, or from a JWKS the provider publishes — RSA n/e or EC crv/x/y, which are just base64url big-endian integers you rebuild into a key.

So the whole method is stdlib crypto and encoding/json, and it added zero new dependencies. The one operational subtlety worth encoding: providers rotate their signing keys, so on a signature miss against the cached JWKS the method refetches once before giving up — enough to survive a rotation without turning every login into an outbound fetch.

And then the part that actually matters for security isn’t the signature at all — it’s everything after you trust it. A valid signature on a token that says nothing useful is worthless, so login validates exp and nbf (with a little clock-skew leeway), the bound issuer, the audience, and any per-claim bindings the role requires before it will mint a token. A signed token from the right issuer that isn’t for you still gets rejected.

The whole method is under test the way the crypto was: real RSA and EC keys signing real tokens, and a rejection case for every failure mode — expired, wrong issuer, wrong audience, claim mismatch, wrong signing key, tampered signature.

It Can Be Its Own CA Now

The other headline across these betas is a built-in PKI engine (beta 5). uBix Vault can generate an internal root CA — an EC or RSA key that never leaves the barrier — and then issue short-lived, role-constrained leaf certificates on demand: a role pins the allowed domains, whether subdomains are permitted, the maximum TTL, and the key type, and issuance refuses anything outside those lines. It’s crypto/x509 and nothing else, and it slots in beside Transit as another thing the vault can do with keys it holds rather than just store.

There’s a console panel for it too, so you can generate the CA, define roles, and issue a cert without touching curl — the issued cert, key, and CA chain render as copy-able PEM, with the private key flagged “shown once.”

And a Second Way to Auto-Unseal

Smaller, but I like it: beta 5 also generalized auto-unseal. Beta 3 could unseal itself from a key-encryption key you supplied directly. The problem with that is the KEK has to live somewhere on the host. So there’s now a transit seal option: the vault unwraps its own master key by calling another vault’s Transit engine, which means the key that guards this vault never sits on this vault’s machine at all. Shamir shares, a local KEK, or a transit seal — same Seal interface underneath, your choice of trust model.

The Rule Held

Four betas — four auth methods, a certificate authority, a second seal mode, a rate limiter before that — and the dependency count did not move. uBix Vault still has essentially one third-party dependency, the MySQL driver. The Shamir split, the AEAD barrier, the metrics exporter, the PBKDF2 hashing, and now the entire JWS verification path are all standard-library code I wrote and tested myself.

For most projects that would be stubbornness. For a security tool it’s the whole point: a dependency graph you can read in an afternoon is not a limitation, it’s the feature. The thing guarding your secrets should be a thing you can actually audit.

Where It Is, and What’s Next

uBix Vault is BSD 3-Clause, on GitHub at github.com/cwolsen7905/uBixVault, tagged v0.2.0-beta.7 and deployed on my own cluster.

The road to a real 1.0 is unchanged and still written down where anyone can see it: Raft-based HA so it’s more than a single node, a pluggable cloud-KMS/HSM seal, and — the one that actually gates production use — an external security review. Until those land, the disclaimer at the top stands.

But the question I opened with has a real answer now. Three posts ago uBix Vault could keep a secret. Now it can decide, five different ways, who gets to ask for it — and hand out its own certificates while it’s at it. And the parts I built myself are still the parts I understand best.

← All churts