An Eight Mile project · open source · MIT

vaultlet

A gRPC control plane for secrets you already store somewhere else. vaultlet does not hold a single secret: it sits in front of the Bitwarden Secrets Manager you already run and adds one access model, a policy that denies by default, and one audit log — over TLS and nothing else.

Runtime

Go 1.26 · gRPC

Backend

Bitwarden Secrets Manager

Transport

TLS 1.3 · Basic auth

Surface

5 RPCs · ~2,100 LOC

one request

interceptor chain

handshakeUNAUTHENTICATEDPERMISSION_DENIEDCLI · CI JOBtls 1.3nothing plaintextauthbcrypt · basicpolicydeny by defaultauditone record, no valuesbitwardenthe backend you already run

See it decide

Watch a request get checked.

Three scripted sessions, replayed exactly as the server handles them: a read that is allowed, audited and served; a key outside the policy that never reaches the backend; and a write against a backend that only reads.

vaultlet-cli

principal=ci-payments

$

 

One read: authenticated, checked against policy, served by Bitwarden, and audited.

What it solves

Six ways secrets access usually goes wrong.

Every one of these is a design decision in the code, not a setting you have to remember to switch on.

One machine token shared by every job.

A principal per caller, checked on every call.

Each CI job or operator authenticates as its own user, verified against a bcrypt hash on every RPC. The backend credential stays on the server and is never handed to a pipeline.

Access decided by whoever holds the backend token.

Policy that denies by default.

A rule names a namespace and the actions allowed under it, and a principal with no matching rule gets nothing. Rules are compiled at boot, so a typo fails the start rather than becoming a silent denial.

Who read what, scattered across vendor consoles.

One audit log, without the values.

Every get, put, list and delete writes one structured record: principal, action, key, decision and outcome. Denials are recorded too — a burst of them for one principal is the signal worth alerting on.

A plaintext port that was "just for dev".

TLS 1.3, or no connection at all.

The server has no plaintext mode to forget to turn off, and the client refuses to send a token over anything but TLS. Both sides pin the same floor.

A second vault to back up, rotate and lose.

Nothing stored, nothing to leak.

vaultlet holds no secrets. Values are created and edited in the Bitwarden Secrets Manager you already run, and every read goes to it. Take vaultlet away and nothing is lost.

A value printed by a log line nobody meant to write.

Values that cannot be logged by accident.

A secret is bytes, copied on the way in and on the way out, and its String method prints the key and the version only. Listings and events carry metadata and never the value.

By the numbers

as the repository states them

0

Secrets stored

every value lives in the backend

5

RPCs

one service, one proto file

1

Backend today

Bitwarden Secrets Manager

8

Namespace depth

segments, lowercase and hyphens

1.3

TLS floor

pinned on both sides of the dial

10 s

Graceful drain

then a forced stop, under SIGTERM

What you get

Built like infrastructure, not a wrapper.

The parts that matter in production — typed keys, honest versions, one audited path for values and a service that starts and stops cleanly — are in the repository, not on a roadmap.

Keys with a grammar

A key is namespace/name, validated once at the edge. The fields are unexported, so an invalid key cannot exist anywhere below the parser.

Opaque versions

A version compares for equality and nothing else. Bitwarden has none of its own, so the revision date is padded to fixed width and used as one.

Listings without values

List and watch return metadata only. Reading a rotated value is a separate, policy-checked, audited call — never a long-lived stream.

Status codes, not error fields

NOT_FOUND, PERMISSION_DENIED, FAILED_PRECONDITION: a caller reads the code and never learns which backend answered.

A CLI that is only a client

get, put, list, delete and watch over the same gRPC surface any client uses, with -o json as a stable contract for scripts. There is no back door.

Runs like a service should

Config from YAML, .env and environment in that order, validated before the backend opens. JSON logs to stderr. SIGTERM drains in-flight calls.

Where your secrets live

In the backend you already run. vaultlet only decides who may ask.

Secrets are created and edited in Bitwarden’s own UI and read through it on every call. vaultlet inherits the blast radius of that one backend credential, and nothing more — which is why the README tells you to scope it tightly.

vaultlet stores nothing and caches nothing

Values travel over TLS 1.3 and never over a stream

Audit records carry keys and decisions, never values

Scope the machine account to the projects it serves

Behind the project

Built by Eight Mile in London, as part of our infrastructure and cloud work — the same engineers who build and run systems like it for clients.