A from-scratch secrets manager in Go. It stores and generates secrets behind an encrypted, sealed barrier — and speaks a Vault-compatible HTTP API, so tools you already have — the vault CLI, External Secrets Operator, anything built on HashiCorp's Go client — can talk to it unchanged.
Integrated Raft storage: start with -storage raft and three replicas replicate every write to a majority before acknowledging it. No MySQL to provision, patch or lose. It ships as experimental — we run it on our own dev and then prod first, and the label comes off after that soak. MySQL and file storage are unchanged and remain the defaults.
operator raft adds peers, join tokens, snapshots and recovery, and the endpoints use Vault's response envelope, so vault operator raft list-peers works.| Event | Result |
|---|---|
| Leader's node frozen | New leader in 6–7 s. |
| Leader partitioned from the majority | New leader in 11–17 s; the isolated leader refused writes (503). Every acknowledged write read back in all three runs. |
| Whole cluster restarted (volumes kept, auto-unseal) | Serving again in 18 s, nothing lost. |
| One replica's volume lost | Back as a voter in 19 s (PVC loss on a replica). |
| MySQL → Raft migration | 48 of 48 entries intact; outage 33–35 s, from freezing the old release to the first read served by Raft. |
| Rollback to MySQL | Serving in 41–46 s, with a write made on Raft intact. |
First node (-0) on a wiped volume | Refuses to bootstrap a split cluster; rejoins with a join token. No rejoin time measured. |
| Jepsen-style testing | Three 2-minute runs, no violation found. Not a full proof: the checker could not decide all of the history. |
Measured on kind, three replicas, a client writing every 100 ms with no retries, every acknowledged write checked afterwards; ranges span three chaos runs. These are CI-VM numbers — a regression baseline, not capacity planning.
Integrated storage is a new engine and experimental. Migrate when you have a window and a recent snapshot, and keep the database through the soak. uBixVault has had no independent security audit.
Run 3 or 5 replicas (the chart refuses an even number) on block storage with real fsync, not NFS/SMB. Auto-unseal is the practical mode: a sealed replica does not vote, so a full restart under Shamir shares needs a quorum unsealed first. It is not Vault's integrated storage byte for byte — autopilot covers membership and leader only.
# new cluster from the chart (3 replicas, PVC each) $ helm install vault ./deploy/charts/ubixvault \ --set storage.type=raft --set replicaCount=3 $ ubixvault operator raft list-peers $ ubixvault operator raft join-token # single-use, 15 min # migrate from MySQL (offline window), then verify $ ubixvault operator raft export-mysql mysql.snap $ ubixvault operator raft digest -against mysql.snap cluster.snap # last resort, lost quorum: stage peers.json on a stopped node $ ubixvault operator raft recover -data <dir> -peers peers.json # rotate the cluster CA, then restart and unseal every node $ ubixvault operator raft rotate-ca
ubixvault server -data <dir> [-audit-log <file>] [-tls-cert <f> -tls-key <f>] ubixvault operator init | unseal | seal-status
All data at rest is AES-256-GCM encrypted — storage never sees plaintext, and the storage path is bound into the ciphertext so blobs can't be relocated.
The vault boots sealed. Its master key is reconstructed from Shamir k-of-n key shares before any secret is readable — or, given a key-encryption key, the vault can auto-unseal itself on restart, with recovery keys to regenerate a lost root token. Access is default-deny, path-based tokens with ACL policies, and audit logging is fail-closed — the client token is HMAC'd, never logged in the clear.
A built-in web console at /ui/ — initialize and unseal the vault, sign in with a username and password, LDAP, or a token — manages secrets (with version history), policies, tokens, and PKI certificates, and a Prometheus metrics endpoint exposes seal state and request counts for scraping.
# build and run in dev mode (plain HTTP, audit on) $ go build -o bin/ubixvault ./cmd/ubixvault $ ./bin/ubixvault server -data ./data -audit-log ./audit.log
# 3 unseal shares, threshold 2 $ curl -s -X POST $VAULT/v1/sys/init \ -d '{"secret_shares":3,"secret_threshold":2}' -> {"keys":["k0","k1","k2"], "root_token":"uv...."}
$ curl -s -X POST $VAULT/v1/sys/unseal -d '{"key":"k0"}' $ curl -s -X POST $VAULT/v1/sys/unseal -d '{"key":"k1"}' $ curl -sH "$T" -X POST $VAULT/v1/secret/data/app \ -d '{"data":{"api_key":"sk-123"}}'
# StatefulSet, auto-unseal, TLS $ kubectl create secret generic uv-kek \ --from-literal=auto-unseal-key=$(head -c32 /dev/urandom|xxd -p|tr -d '\n') $ helm install vault ./deploy/charts/ubixvault \ --set tls.existingSecret=uv-tls --set autoUnseal.existingSecret=uv-kek # high availability: 3 replicas over MySQL, one active, standbys forward $ helm upgrade vault ./deploy/charts/ubixvault --reuse-values \ --set storage.type=mysql --set storage.mysql.dsnSecret=uv-dsn \ --set ha.enabled=true --set replicaCount=3
$ curl -s $VAULT/v1/sys/health # readiness (200/429 standby/503/501) $ curl -s $VAULT/v1/sys/leader # which replica is active (HA) $ curl -s $VAULT/v1/sys/metrics # Prometheus text format # manage it from the web console at $VAULT/ui/
sudo) and cannot outlive it — revoking a token revokes the tokens it delegated — renewal is capped, and each auth role can set its own token_max_ttl./ui/ — initialize and unseal the vault; sign in with userpass, LDAP, or a token — to manage KV secrets (with version history), ACL policies, tokens, and PKI (issue certificates, manage roles and the CA) — served from the binary, no external assets./v1/sys/livez) that stays green while sealed — so probes never crash-loop a vault waiting to be unsealed.429 at the limit; health, metrics, and the console are exempt.sys/leader, sys/step-down and the health standby codes work as in Vault.The settings you'll actually reach for — the full reference lives in the deployment docs.
| Setting | Default | Notes |
|---|---|---|
| server -storage <file|mysql|raft> | file | mysql keeps the encrypted store in a MySQL/MariaDB database — durable, replaceable node, and the basis for HA. raft (experimental) replicates it across the replicas' own volumes; HA is implied. |
| server -raft-node-id / -raft-advertise-addr / -raft-bootstrap | — · — · true | Raft node identity and peer address; set -raft-bootstrap=false on nodes that will join. |
| server -raft-retry-join / -raft-join-token-file / -raft-leader-ca-cert | — | Join an existing cluster declaratively with a single-use token; the leader's API certificate is verified against the given CA. |
| operator raft recover -data <dir> -peers <file> | — | Stage a peers.json on a stopped node to recover from lost quorum; applied once on next start. Writes acknowledged only by dropped members can be lost. |
| server -ha | off | Active/standby HA over -storage mysql; the replica holding the lock is active, the rest forward to it. |
| server -ha-cluster-listen / -ha-cluster-addr | :8201 · https://$POD_IP:8201 | Replica-to-replica port, mutual TLS from a CA the vault keeps itself — nothing to configure. |
| server -ha-lock-ttl / -ha-retry-interval | 15s · 500ms | Bound on an unplanned failover / how often standbys try for the lock. |
| server -data <dir> | ./data | Directory for the encrypted store (-storage file). |
| server -storage-mysql-dsn | $UBIXVAULT_STORAGE_DSN | DSN for -storage mysql. Passed via env, never on the command line. |
| operator rekey init / update | — | Rotate the Shamir unseal shares live — no downtime, no data re-encryption. |
| server -tls-cert / -tls-key | — | Enable HTTPS. Without them the server logs a loud no-TLS warning (dev only). |
| server -max-token-ttl | 768h | How far renewal may extend a token whose own TTL is shorter. A token deliberately issued for longer keeps its full TTL; no token can renew itself past its ceiling. |
| server -log-tls-handshake-aborts | false | Log every TLS handshake a client abandons. By default these — TCP health checks from load balancers and ingress controllers, mostly — are counted and logged as one summary line every 5 minutes; other handshake failures are always logged. |
| server -audit-log <path> | — | Turns on fail-closed audit logging to that file. |
| server -auto-unseal-key <hex> | $UBIXVAULT_AUTO_UNSEAL_KEY | Hex 32-byte key-encryption key; enables auto-unseal on restart. |
| operator init -shares / -threshold | 5 / 3 | Shamir k-of-n split created at initialization. |
| -address | $UBIXVAULT_ADDR | Client → server address for the CLI. |
| -token | $UBIXVAULT_TOKEN | Client auth token for the CLI. |
| -ca-cert / -tls-skip-verify | — | Client TLS trust. Skipping verification is insecure — dev only. |
| Value | Default | Notes |
|---|---|---|
| containerPort / service.port | 8200 | Vault-compatible API port. |
| storage.type / storage.mysql.dsnSecret | file · — | mysql uses a database; the DSN comes from a Secret (never a chart value) and the data PVC is dropped. raft (experimental) runs integrated storage on a PVC per replica. |
| storage.raft.persistence.size | 10Gi | Per-replica volume for storage.type=raft; block storage with honest fsync only. replicaCount must be odd. |
| storage.raft.join.tokenSecret / leaderCASecret | <fullname>-raft-join · — | Secret with one join token per joining pod (<fullname>-1, -2), and the CA the joiners verify the leader against. |
| storage.networkPolicy.enabled | false | NetworkPolicy for Raft pods: API port from the cluster, cluster port from the pods themselves. |
| persistence.size / mountPath | 1Gi · /var/lib/ubixvault | Encrypted data volume (StatefulSet PVC), for storage.type=file. |
| backup.enabled / schedule | false · 0 */6 * * * | Scheduled off-node snapshot CronJob using a least-privilege token. |
| ha.enabled / replicaCount | false · 1 | Active/standby HA (requires storage.type=mysql); 3 replicas recommended. Adds a PodDisruptionBudget and soft anti-affinity. |
| autoUnseal.existingSecret | — | Secret holding the KEK; auto-unseal is on by default. |
| tls.existingSecret / tls.certManager | — | Bring your own cert, or issue one via cert-manager. |
| audit.enabled | false | Fail-closed audit log to its own PVC. |
| rateLimit.enabled / perSecond | false · 20 | Per-client token-bucket limit (burst 40). |
| kubernetesAuth.enabled | true | Pods authenticate with their ServiceAccount token. |
| metrics.serviceMonitor.enabled | false | Scrape via the Prometheus Operator. |
| ingress.enabled | false | Expose over an ingress with TLS. |
Every request passes down through the same layers, and everything at rest sits below the barrier.
A request enters through the Vault-compatible HTTP API (or the ubixvault CLI), is authenticated to a token, and checked against default-deny path ACLs before any secrets engine runs. Every request and response passes through the audit broker on the way through.
Beneath all of it sits the barrier: everything written to storage is AES-256-GCM encrypted, with the storage path and format version bound in as authenticated data — so a ciphertext only decrypts at the exact path it was written to, and can't be relocated.
The vault boots sealed, holding only ciphertext. Unsealing reconstructs the master key from Shamir k-of-n shares. The key hierarchy — unseal shares → master key → barrier key → data — lets the barrier key rotate without re-sharing.