{"id":52095264,"url":"https://github.com/ranjbar-dev/mini-vault","last_synced_at":"2026-08-04T11:01:00.805Z","repository":{"id":369482210,"uuid":"1283996961","full_name":"ranjbar-dev/mini-vault","owner":"ranjbar-dev","description":null,"archived":false,"fork":false,"pushed_at":"2026-07-05T14:10:14.000Z","size":99,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-05T16:07:15.208Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ranjbar-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-29T12:33:26.000Z","updated_at":"2026-07-05T14:10:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ranjbar-dev/mini-vault","commit_stats":null,"previous_names":["ranjbar-dev/mini-vault"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/ranjbar-dev/mini-vault","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ranjbar-dev%2Fmini-vault","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ranjbar-dev%2Fmini-vault/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ranjbar-dev%2Fmini-vault/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ranjbar-dev%2Fmini-vault/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ranjbar-dev","download_url":"https://codeload.github.com/ranjbar-dev/mini-vault/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ranjbar-dev%2Fmini-vault/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":36273623,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-08-04T02:00:06.901Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-08-04T11:00:59.989Z","updated_at":"2026-08-04T11:01:00.785Z","avatar_url":"https://github.com/ranjbar-dev.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mini-vault\n\nMinimal Go microservice that holds a set of named secrets in memory and serves\nthem to authenticated clients over mutually authenticated TLS (mTLS) gRPC.\n\n\u003e **Run mini-vault on a separate server from the services that consume secrets.**\n\u003e A compromise of a consumer server must not expose the secrets themselves.\n\nSee [`llm.txt`](llm.txt) for a condensed, machine-oriented summary of this\nrepo (architecture, gRPC contract, auth model, env vars, conventions) — it\nexists so an AI coding assistant can work on this repo without reading\nevery source file first. Keep it and this README updated together whenever\nthe API, auth, config, or client package changes.\n\n---\n\n## How it works\n\n```\n[mini-vault server]           [your application]\n┌───────────────────┐  mTLS  ┌────────────────────┐\n│  mini-vault       │───────►│  your-service      │\n│                   │◄───────│                    │\n│  secrets map in  │  value  │  uses secret to    │\n│  heap RAM        │        │  connect / sign /  │\n└───────────────────┘        │  authenticate ...  │\n                             └────────────────────┘\n```\n\nAt startup the operator enters a passphrase (or sets `VAULT_PASSPHRASE`).\nmini-vault uses Argon2id to derive an unwrapping key, decrypts\n`data/secrets.bin` (embedded in the binary at build time), and holds the\nplaintext secrets in a `map[string][]byte` protected by a `sync.RWMutex`.\nThe passphrase and all intermediate key material are zeroed immediately after\ndecryption. Nothing is written to disk at runtime.\n\nEvery gRPC client must present a mutual TLS certificate. Before returning\nany secret the server checks:\n1. Client certificate CN matches `VAULT_CLIENT_CN`\n2. Request count is within `VAULT_RATE_LIMIT_RPM` per 60 s\n\nIf both pass, the requested secret value is returned over the encrypted\nchannel. If the name does not exist, `NOT_FOUND` is returned.\n\n---\n\n## First-time setup\n\n### 1. Generate TLS certificates (offline workstation — never on a server)\n\n```sh\n# Internal CA\nopenssl genrsa -out keys/ca.key 4096\nopenssl req -x509 -new -nodes -key keys/ca.key -sha256 -days 3650 \\\n    -subj \"/CN=mini-vault-ca\" -out keys/ca.crt\n\n# mini-vault server cert — SAN is required, Go rejects CN-only certs (Go \u003e= 1.15)\nopenssl genrsa -out keys/server.key 4096\nopenssl req -new -key keys/server.key -subj \"/CN=mini-vault\" -out keys/server.csr\nprintf \"subjectAltName=DNS:mini-vault,DNS:localhost,IP:127.0.0.1\" \u003e keys/server.ext\n# Edit keys/server.ext to list every hostname/IP this server will actually be dialed by.\nopenssl x509 -req -in keys/server.csr -CA keys/ca.crt -CAkey keys/ca.key \\\n    -CAcreateserial -days 3650 -sha256 -extfile keys/server.ext -out keys/server.crt\n\n# Client cert for your application — CN must match VAULT_CLIENT_CN\nopenssl genrsa -out keys/client.key 4096\nopenssl req -new -key keys/client.key -subj \"/CN=vault-client\" -out keys/client.csr\nopenssl x509 -req -in keys/client.csr -CA keys/ca.crt -CAkey keys/ca.key \\\n    -CAcreateserial -days 3650 -sha256 -out keys/client.crt\n```\n\nPlace `ca.crt`, `server.crt`, `server.key` in `keys/`.\nCopy `ca.crt`, `client.crt`, `client.key` to each application server that\nneeds to call mini-vault.\nStore `ca.key` in cold storage — **never on any server**.\n\n### 2. Define secrets (offline workstation)\n\nCreate `data/secrets.json` — a flat JSON object, string keys and string values:\n\n```json\n{\n  \"db_password\":  \"correct-horse-battery-staple\",\n  \"api_key\":      \"sk-live-abc123\",\n  \"signing_key\":  \"0f1e2d3c4b5a...\"\n}\n```\n\n**`data/secrets.json` must never be committed.** It is plaintext and already\ngitignored.\n\nValues are always strings. Encode binary material as hex or base64 before\nputting it here; your application decodes it after receiving it.\n\n### 3. Encrypt secrets\n\n```sh\ngo run ./cmd/vault-encrypt -in data/secrets.json -out data/secrets.bin\n```\n\n- Type a strong passphrase twice (no echo). The passphrase is the only thing\n  standing between a leaked repo and your secrets — use a long, generated one.\n- `data/secrets.bin` is created. **Commit it** — it is AES-256-GCM encrypted\n  and safe to store. It is useless without the passphrase.\n- **Delete `data/secrets.json` now.** The plaintext file lingering on the\n  workstation is the most likely leak in this whole system.\n\n### 4. Build\n\n```sh\ngo build -ldflags=\"-s -w\" -o bin/mini-vault ./cmd/mini-vault\n```\n\n`data/secrets.bin`, `ca.crt`, `server.crt`, and `server.key` are all\nembedded in the binary at build time. The running binary needs no files on\ndisk.\n\n### 5. Deploy and run\n\n```sh\nscp bin/mini-vault user@vault-host:/usr/local/bin/\n\n# On the vault server — interactive passphrase prompt:\nmini-vault\n```\n\nFor non-interactive startup, set `VAULT_PASSPHRASE` from a root-only file\n(see the systemd `EnvironmentFile` example below). **Never pass it inline on\nthe command line** — `VAULT_PASSPHRASE=... mini-vault` writes the passphrase\nto your shell history, and environment variables are readable from\n`/proc/\u003cpid\u003e/environ` for the life of the process.\n\nSuccessful startup logs:\n```json\n{\"level\":\"INFO\",\"msg\":\"mini-vault ready\",\"secrets_count\":3,\"port\":\"9000\"}\n```\n\nWrong passphrase → immediate exit, no retry.\n\n---\n\n## Configuration\n\nAll configuration is via environment variables. No config file.\n\n| Variable | Default | Description |\n|---|---|---|\n| `VAULT_PASSPHRASE` | *(empty)* | Passphrase to decrypt secrets. If unset, prompted interactively on stdin. |\n| `VAULT_PORT` | `9000` | gRPC listen port |\n| `VAULT_CLIENT_CN` | `vault-client` | Expected CN on the client certificate |\n| `VAULT_RATE_LIMIT_RPM` | `5` | Max `GetSecret` calls per 60 s per client |\n| `VAULT_LOG_LEVEL` | `info` | Log level: `debug` / `info` / `warn` / `error` |\n\n---\n\n## gRPC API\n\nSingle service over mTLS (TLS 1.3 minimum). No other ports are opened.\n\n```protobuf\nservice VaultService {\n  rpc GetSecret(GetSecretRequest) returns (GetSecretResponse);\n  rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);\n}\n\nmessage GetSecretRequest  { string name  = 1; }\nmessage GetSecretResponse { bytes value  = 1; string name = 2; }\nmessage HealthCheckRequest {}\nmessage HealthCheckResponse { bool loaded = 1; int32 count = 2; }\n```\n\n| gRPC code | Cause |\n|---|---|\n| `PERMISSION_DENIED` | Client cert CN does not match `VAULT_CLIENT_CN` |\n| `RESOURCE_EXHAUSTED` | Rate limit exceeded |\n| `NOT_FOUND` | Requested secret name does not exist in the store |\n\n---\n\n## Using mini-vault from a Go application\n\nUse the `client` package — it wraps the mTLS dial and gRPC calls so you\ndon't have to.\n\n### Install\n\n```sh\ngo get github.com/ranjbar-dev/mini-vault/client\n```\n\nFor raw proto access instead (advanced use), `go get\ngithub.com/ranjbar-dev/mini-vault/proto/minivault/v1` and call\n`pb.NewVaultServiceClient` directly.\n\n### Example client\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n\n    \"github.com/ranjbar-dev/mini-vault/client\"\n)\n\nfunc main() {\n    c, err := client.NewFromFiles(\"vault-host:9000\", \"mini-vault\", // ServerName must match the CN in server.crt\n        \"ca.crt\", \"client.crt\", \"client.key\")\n    if err != nil {\n        log.Fatal(err)\n    }\n    defer c.Close()\n\n    password, err := c.GetSecretString(context.Background(), \"db_password\")\n    if err != nil {\n        log.Fatalf(\"GetSecret: %v\", err)\n    }\n    fmt.Println(\"got secret, length:\", len(password))\n}\n```\n\nFor highly sensitive values, use `c.GetSecret(ctx, name)` to get a `[]byte`\nand `client.Zero(b)` it after use — Go strings can't be zeroed.\n\n### Fetching multiple secrets at startup\n\n```go\nfunc loadSecrets(ctx context.Context, c *client.Client) (map[string]string, error) {\n    names := []string{\"db_password\", \"api_key\", \"signing_key\"}\n    out := make(map[string]string, len(names))\n\n    for _, name := range names {\n        val, err := c.GetSecretString(ctx, name)\n        if err != nil {\n            return nil, fmt.Errorf(\"fetch %q: %w\", name, err)\n        }\n        out[name] = val\n    }\n    return out, nil\n}\n```\n\n### Health check\n\n```go\nloaded, count, err := c.HealthCheck(context.Background())\nif err != nil {\n    log.Fatal(err)\n}\nfmt.Printf(\"vault loaded=%v secrets=%d\\n\", loaded, count)\n```\n\n### Handling errors\n\n`GetSecret`/`GetSecretString` return gRPC status errors. Check them with the\npackage's helpers instead of importing `grpc/status` yourself:\n\n```go\nif client.IsNotFound(err) { ... }        // unknown secret name\nif client.IsPermissionDenied(err) { ... } // client cert CN not allowed\nif client.IsRateLimited(err) { ... }      // VAULT_RATE_LIMIT_RPM exceeded\n```\n\n---\n\n## Local dev / CI\n\nGenerate a test `data/secrets.bin` with hardcoded values and passphrase\n(never for production):\n\n```sh\ngo run ./cmd/gentest-secrets   # writes data/secrets.json + data/secrets.bin\ngo build ./...\n\n# Run with the test passphrase via env var:\nVAULT_PASSPHRASE=test-passphrase-change-before-production ./bin/mini-vault\n```\n\n---\n\n## Systemd unit\n\nSee `deploy/mini-vault.service` for the canonical unit file (kept in sync\nwith this section):\n\n```ini\n[Unit]\nDescription=mini-vault secret distribution service\nAfter=network.target\n\n[Service]\nType=simple\nUser=vault-svc\nExecStart=/usr/local/bin/mini-vault\nStandardInput=tty\nTTYPath=/dev/tty\nRestart=no\nLimitCORE=0\nLimitMEMLOCK=infinity\nNoNewPrivileges=true\nProtectSystem=strict\nProtectHome=true\nPrivateTmp=true\nPrivateDevices=true\nCapabilityBoundingSet=\nRestrictAddressFamilies=AF_INET AF_INET6\nSystemCallFilter=@system-service\nLockPersonality=true\nRestrictNamespaces=true\nMemoryDenyWriteExecute=true\n\n[Install]\nWantedBy=multi-user.target\n```\n\n`Restart=no` is intentional — automatic restart would require a cached\npassphrase, which defeats the protection model. An operator must SSH in and\nrestart the service manually after any crash.\n\n`StandardInput=tty` lets the systemd unit prompt for the passphrase on the\noperator's terminal. Alternatively, set `VAULT_PASSPHRASE` in an\n`EnvironmentFile` with `0600` permissions:\n\n```ini\nEnvironmentFile=/etc/mini-vault/passphrase.env\n```\n\n```sh\n# /etc/mini-vault/passphrase.env  (chmod 600, owned by vault-svc)\nVAULT_PASSPHRASE=your-strong-passphrase\n```\n\n---\n\n## Disaster recovery\n\n**Required materials:**\n1. Source code — private git repo (contains `data/secrets.bin` and the\n   public certs `ca.crt`, `server.crt`)\n2. Passphrase — operator's memory or password manager\n3. `server.key` — cold storage alongside `ca.key` (private keys are\n   gitignored and must never be committed). If it is lost, generate a new\n   server cert from the CA before rebuilding.\n\n**Steps:**\n1. Provision a new server with the same firewall rules\n2. Clone the private git repo\n3. `go build -ldflags=\"-s -w\" -o bin/mini-vault ./cmd/mini-vault`\n4. Deploy binary; start; enter passphrase\n5. Verify `HealthCheck` returns `loaded: true`\n6. Update firewall rules if the server IP changed\n\n**If the passphrase is permanently lost:** edit `data/secrets.json` with the\noriginal values (from other secure storage), re-run `vault-encrypt`, rebuild,\nand redeploy.\n\n---\n\n## Security\n\n### OS hardening (mandatory)\n\n- Run as a dedicated non-root user\n- Disable core dumps: `LimitCORE=0` in the unit file\n- Disable swap: `swapoff -a` — prevents heap pages from hitting disk\n- Set `kernel.yama.ptrace_scope=1`\n- Firewall: allow **only** inbound TCP on `VAULT_PORT` from known client IPs\n\n### Threat model\n\n| Threat | Protection |\n|---|---|\n| Network interception | mTLS — all traffic encrypted; MITM requires compromising the CA |\n| Client impersonation | Mutual TLS — client must present a CA-signed cert with matching CN |\n| Consumer server compromised | Secrets live on a separate server; attacker must also compromise mini-vault |\n| mini-vault server compromised at rest | Passphrase required at startup; never stored on disk |\n| Binary stolen | `data/secrets.bin` is AES-256-GCM encrypted; useless without passphrase |\n\n### What mini-vault never does\n\n- Does not expose any HTTP endpoint\n- Does not log secret values at any log level\n- Does not connect to any database\n- Does not support runtime secret updates (requires rebuild to change secrets)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Franjbar-dev%2Fmini-vault","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Franjbar-dev%2Fmini-vault","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Franjbar-dev%2Fmini-vault/lists"}