uBix Vault Hits Beta — From Design Notes to a Working Secrets Manager
From Design Notes to a Beta
Last time I wrote about uBix Vault, it was a design-notes post — scaffolding, decision records, and a promise to build the hard parts first. It is now a working beta, tagged v0.2.0-beta.1.
Same disclaimer up front, because it’s a secrets manager and overselling maturity is how people get burned: it is a beta, not production-hardened, and it has not had an external security review. For production secrets management today, use HashiCorp Vault or OpenBao. uBix Vault is usable for real workloads in a sandbox or dev environment — there’s a deployment guide — but I would not put production secrets in it yet, and the repo says so.
With that stated plainly: it works. You can init, unseal, write and read versioned secrets, encrypt data without the key ever leaving the vault, mint short-lived database credentials against a real MariaDB, scope access with policies, and it auto-unseals itself on restart. Every one of those was demonstrated end-to-end against a running binary, not just unit-tested. Here’s the build.
What Shipped
The v0.1.0 MVP delivered the core:
- Encryption barrier — all data at rest is AES-256-GCM encrypted; the storage backend never sees plaintext.
- Shamir seal/unseal — the vault boots sealed; the master key is reconstructed from k-of-n key shares.
- Token auth + ACL policies — default-deny, path-based capabilities, scoped tokens.
- KV v2 — versioned secrets with soft-delete / undelete / destroy.
- Transit — encryption-as-a-service; versioned keys that rotate without breaking old ciphertext.
- Dynamic database credentials — short-lived MariaDB users generated on demand, auto-revoked on lease expiry.
- Audit logging — fail-closed, with the client token HMAC’d so it’s never written in the clear.
Then the beta was nine hardening-and-completeness passes on top of that:
- Token TTLs and expiry (the biggest correctness fix — tokens now actually expire), with renewal.
- Auto-unseal — wrap the master key with a key-encryption key so a restarted server unseals itself.
- Health/readiness endpoint for load balancers and probes.
- Encrypted backup and restore via consistent snapshots.
- TLS enforcement — the server refuses to serve plaintext HTTP on a non-loopback address.
- Root-token regeneration — recover a new root token from a quorum of unseal shares.
- Lease renewal, lookup, and cascading revocation — revoke a token and the credentials it created go with it.
- A Kubernetes auth method — pods exchange a ServiceAccount token for a scoped vault token.
- HCL policy documents — real Vault-style policies, in addition to JSON.
That’s the what. The interesting part is the how.
The Process: Design Docs First, One Slice at a Time
The whole thing was built the way I said it would be — the reasoning as a first-class deliverable. Before a line of code, the repo had a design document, a decision log (architecture decision records), a threat model, and a roadmap that separated committed core from optional extensions. The git history literally reads propose the design → accept the design → implement it. For a security project, that paper trail isn’t overhead; it’s the part that lets someone trust the code.
From there it was one focused, reviewed, CI-green pull request per capability — 35 of them by the beta. Every PR ran the full pipeline: build, race-detector tests, golangci-lint (with the gosec security linter), govulncheck, and — importantly — a real MariaDB integration job so the dynamic-credentials feature is validated against an actual database, not a mock.
The Crypto I Wrote Myself (and the Crypto I Didn’t)
One of the rules I set was “no hand-rolled cryptography — use the standard library.” AES-GCM, HMAC, SHA-256, the CSPRNG: all stdlib, no exceptions.
The one deliberate carve-out is Shamir’s Secret Sharing, which I implemented from scratch over GF(2⁸). The justification, written down as a decision record: Shamir is a secret-sharing scheme, not a cipher — it’s the math that splits the master key into unseal shares — and implementing it is a well-understood ~200-line exercise. But “well-understood” isn’t “safe by default,” so it came with safeguards:
- The field arithmetic is constant-time with respect to its byte operands — no data-dependent branches, no table lookups — to avoid timing side channels.
- Multiplication is validated against the FIPS-197 (AES specification) field test vectors, and the inverse against a full
a · a⁻¹ == 1sweep of the field. - Property tests confirm the security behavior: any threshold of shares reconstructs the secret, and fewer than threshold reconstruct nothing.
That’s the line I hold: I’ll implement a secret-sharing scheme with test vectors and constant-time care, but the actual ciphers stay in the standard library where they belong.
The Bug I’m Most Proud Of Catching
The barrier encrypts every value with AES-256-GCM before it hits storage. My first version authenticated the format-version byte as GCM additional data — but not the storage path. Re-reading it during review, that’s a real weakness: an attacker with write access to the underlying storage could copy a valid encrypted blob from one path to another and it would still decrypt. GCM guarantees the integrity of the bytes; it says nothing about where they live.
The fix is to bind the storage path into the additional authenticated data, so a ciphertext only decrypts at the exact location it was written — which is, not coincidentally, what HashiCorp Vault’s barrier does. It’s a ten-line change plus a test that proves a relocated blob is rejected. Finding it in review, and fixing it with a test that fails without the fix, is exactly the kind of thing that makes me trust the process even on security-critical code.
Auto-Unseal Without Betting the Farm on a Cloud
The most annoying thing about a sealed-by-default vault is that every restart needs a human to feed it unseal shares. The beta added auto-unseal: the master key is wrapped (encrypted) under a 32-byte key-encryption key, stored, and on startup the server unwraps it and unseals itself — no manual step.
I deliberately built this behind a small seal abstraction (the seal config records whether the vault is shamir or auto) rather than hard-wiring a specific cloud KMS. Today you supply the key-encryption key directly; a pluggable cloud-KMS or HSM seal drops into the same seam later. Wrong key fails closed — the GCM authentication rejects it and the barrier stays sealed. I restarted a real server twice to prove it comes back unsealed with the data intact.
Dynamic Secrets Are the Whole Point
If there’s one feature that separates a real secrets manager from an encrypted key-value store, it’s dynamic secrets — credentials that don’t exist until you ask, and that get revoked when their lease expires. uBix Vault does this for MariaDB behind a DatabasePlugin interface: ask for a credential, the plugin runs your creation SQL to make a short-lived user, and a background sweeper drops it when the lease is up.
The beta made that lifecycle complete — leases can be renewed and looked up, and revocation cascades: every lease records the token that created it, so revoking a token revokes the database credentials it minted. A dynamic credential should never outlive the identity that requested it, and now it doesn’t. I tested the whole chain end-to-end against a real MariaDB in CI: the generated user can connect, and after revocation it can’t.
One Dependency
Through the entire MVP, the codebase had zero third-party dependencies — everything on the Go standard library. The beta added exactly one: the MySQL driver, because talking to MariaDB requires its wire protocol and reimplementing that would be absurd. That decision got its own record too, including the note that the driver’s MPL license, as a dependency, doesn’t touch the project’s BSD license. Everything else — the HCL policy parser included — I kept in-house. Not out of stubbornness, but because a small, auditable dependency graph is a feature for a security tool, not a limitation.
Where It Is, and What’s Next
uBix Vault is BSD 3-Clause licensed, on GitHub at github.com/cwolsen7905/uBixVault, tagged v0.2.0-beta.1. It originated to provide secrets management for uBix Core, but it’s framework-agnostic — anything can use it over the HTTP API.
The honest limitations are the road to a 1.0: a pluggable cloud-KMS/HSM seal (the KEK is supplied directly for now), Raft-based HA so it’s more than a single node, an external security review, rate limiting, and metrics. The beta’s known-limitations list is the v1.0 backlog, written down where anyone can see it.
I set out to actually understand how a secrets manager works by building the security-critical parts myself, and to keep it honest at every step about what’s real and what isn’t. A tagged beta that init-unseal-writes-reads-and-auto-unseals against a real database, with the barrier, the Shamir math, and the lease lifecycle all built and tested from scratch — that’s the milestone I wanted. More as the KMS seal and HA come together.