Skip to content

retr0h/kvlt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

36 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

release go report card license build codecov release powered by just conventional commits go reference github commit activity hovnokod

β–ˆβ–„β–€ β–ˆβ–‘β–ˆ β–ˆβ–‘β–‘ β–€β–ˆβ–€
β–ˆβ–‘β–ˆ β–€β–„β–€ β–ˆβ–„β–„ β–‘β–ˆβ–‘

kvlt /kʌlt/ noun β€” pronounced β€œcult”; the v stands in for u, the old-Norse-runic spelling popularized by black-metal aesthetics (cf. trve kvlt).

πŸ” Pluggable secrets vault. Local-first. No daemon.

A single-binary secrets vault for projects that don't have HashiCorp Vault and don't want one. Encrypts with age using your existing SSH keys; named vaults give you a stable call site (kvlt get prod API_KEY) regardless of whether the backend is local age files today, AWS Secrets Manager tomorrow.

✨ Features

  • πŸ” age + SSH keys β€” encrypts with ~/.ssh/id_ed25519.pub, decrypts with the matching private key. Borrows your existing protection chain (passphrase + ssh-agent + Touch ID via secretive); kvlt doesn't reinvent the lock.
  • πŸͺͺ Named vaults β€” kvlt get prod API_KEY, never kvlt get_aws(…); the backend is an implementation detail. Switching from local age files to AWS Secrets Manager later doesn't touch a single call site.
  • πŸ”Œ Pluggable backends β€” Provider interface + factory registry. AWS Secrets Manager (planned) sits behind a //go:build aws guard so the base binary stays dependency-light.
  • πŸ‘₯ Multi-recipient β€” encrypt to N SSH public keys, any one of those private keys can decrypt. The team-sharing escape hatch.
  • 🀫 Stdin / TTY input modes β€” echo $VAL | kvlt put keeps secrets out of shell history; bare kvlt put prompts with echo off.
  • 🐚 Shell-friendly β€” kvlt env vault for eval "$(…)" direnv integration; kvlt run vault -- cmd for scoped env injection like aws-vault exec / op run.
  • 🚫 No service, no daemon β€” pure CLI; nothing listening, nothing persistent.
  • πŸ“¦ Single static binary β€” Go, CGO off, darwin / linux / windows Γ— amd64 / arm64.

πŸ“¦ Install

curl -fsSL https://github.com/retr0h/kvlt/raw/main/install.sh | sh

Installs to ~/.local/bin (or /usr/local/bin as root) β€” SHA256 checksums verified. Override with KVLT_INSTALL_DIR=/some/path or pin a version with KVLT_VERSION=1.1.1.

πŸ”¨ Build from source

git clone https://github.com/retr0h/kvlt.git
cd kvlt
go build -o kvlt .
install -m 755 kvlt ~/.local/bin/kvlt

Cloud backends opt in via build tags: go build -tags aws -o kvlt .

πŸš€ Quick start

kvlt vault create --name dev                                    # bootstrap a vault (encrypts to ~/.ssh/id_ed25519.pub)
kvlt vault create --name prod -p ~/.ssh/team.pub                # encrypt to a non-default public key
kvlt secret put --vault dev --key API_KEY --value sk-1234       # store a secret (lands in shell history)
echo "$DB_PASS" | kvlt secret put --vault dev --key DB_PASS     # stdin β†’ no shell history
kvlt secret put --vault dev --key TOKEN                         # interactive, echo off
kvlt secret import --vault dev --env ~/.env                     # bulk-import a dotenv file
kvlt secret import --vault dev --file ~/kc.yaml --key KC        # one whole file stored as one secret
kvlt secret get --vault dev --key API_KEY                       # decrypt β€” prompts for SSH passphrase if not in agent
kvlt secret get --vault dev --key API_KEY -i ~/work/id_ed25519  # use a specific private key
kvlt secret list --vault dev                                    # names only, never values
kvlt secret delete --vault dev --key OLD_KEY                    # remove a secret (prompts unless --force)
kvlt env --vault dev                                            # all secrets as `export KEY=VALUE` for `eval`
kvlt run --vault dev -- npm start                               # exec child with vault secrets in env
kvlt vault delete --name dev                                    # delete the vault + every secret in it (prompts)

Override the default decrypt key globally with KVLT_PRIVATE_KEY=/path/to/key.

Full recipe collection in docs/recipes.md.

πŸ›‘οΈ Why SSH keys (and why this is meaningfully more secure)

Most secret stuff on a dev laptop is a plaintext file. ~/.aws/credentials, ~/.config/gh/hosts.yml (your GitHub PAT), .env files, npm tokens in ~/.npmrc, GitLab tokens in ~/.config/glab-cli/, every .kube/config β€” all sitting there in plaintext, readable by any process running as you. Once malware has user-level execution on your machine, every one of those is immediate, no friction.

The clever bit isn't kvlt β€” it's delegating the lock to the SSH protection chain, one of the few credential systems on a dev machine that actually has a human-in-the-loop step:

What's on disk Attacker w/ user code-exec does Result
~/.aws/credentials plaintext cat full AWS access, instantly
.env with STRIPE_KEY=… cat Stripe access, instantly
gh / glab / npm tokens cat git host access, instantly
Key-file vault (key.txt next to blobs) cat key.txt && cat blob decrypt, instantly β€” key file is the secret
kvlt + passphrase-locked SSH key reads .age + encrypted key file needs the passphrase
kvlt + ssh-agent (timed unlock) tries to decrypt needs the passphrase to (re-)unlock the agent
kvlt + Secretive on macOS tries to decrypt needs your fingerprint (Touch ID)

Every kvlt decrypt requires something that isn't on disk: your typed passphrase, ssh-agent's in-memory unlock state, or a Touch ID prompt routed through the Secure Enclave. Reading every file under $HOME gets the attacker .age blobs β€” useless without the key β€” and an encrypted private key file, useless without the passphrase. The credentials never exist as plaintext at rest.

This is why a .env -> kvlt swap is a real upgrade, not just a re-shuffle. Vault designs that store the encryption key as a sibling text file in the repo don't help either β€” an attacker grabbing the vault grabs the key. kvlt moves the key out of the filesystem entirely; what's left on disk is useless without something off-disk (your passphrase, the agent's unlocked state, your fingerprint).

Honest about the limits:

  • Cached ssh-agent unlock β€” once the agent is unlocked, anything running as you can sign with it. Mitigate with ssh-add -t 1h for time-limited caching, or skip the agent entirely on macOS by using Secretive (key lives in the Secure Enclave, every signature requires Touch ID).
  • Keylogger on the box captures the passphrase the next time you type it. Beyond software's job.
  • .age blobs are still copyable β€” an attacker with the blobs can sit on them waiting for a future key compromise. Rotate keys and the underlying secrets when threat-modeling demands it.

In short: kvlt is exactly as protective as your SSH private key is, which is far better than "as protective as a text file in $HOME."

Sharing a vault with teammates uses age's multi-recipient model β€” no shared keys, each person decrypts with their own SSH private key. Walkthrough in docs/recipes.md.

βš™οΈ How It Works

kvlt is a CLI; nothing runs between invocations. Each command opens the vault config, talks to the backend, and exits.

  1. πŸͺͺ Pick a vault by name β€” every verb takes a name (dev, prod, …); the name resolves to a backend through .kvlt/vaults/<type>/<id>.yaml
  2. πŸ” Default backend is local (age + SSH keys) β€” kvlt put encrypts to one or more SSH public-key recipients via age; blobs land at .kvlt/secrets/local_encryption/<vault>/<key>.age. Decrypt requires the matching SSH private key β€” passphrase prompt fires on /dev/tty if your key isn't in ssh-agent already.
  3. πŸ”Œ Backends are pluggable β€” Provider interface + factory registry. Adding AWS Secrets Manager is one new file behind a //go:build aws guard; the base binary stays dependency-light.
  4. πŸ” migrate is copy-then-swap β€” list keys, copy each value to the new backend, write the new config, delete the old one. Source stays functional until the very last step. (Planned; the backend abstraction supports it cleanly.)

The contract every backend implements is four methods (Get / Put / List / Name) β€” small on purpose. Anything fancier is layered on top by callers, not pushed into the backend.

πŸ’‘ Inspiration

  • age β€” pure-Go, audited, SSH-key-friendly encryption. kvlt is a vault wrapper around it; the crypto is age's.

πŸ”€ Alternatives

Tool Description
HashiCorp Vault Full-featured secret-management platform
OpenBao Open-source fork of Vault
1Password CLI If you already live in 1Password
pass GPG-encrypted files, the Unix way

kvlt is meant for the gap below "I need a Vault cluster" and above "I have a .env file."

πŸ—ΊοΈ Roadmap

Shipped:

  • πŸͺͺ Project scaffold + CLI tree
  • πŸ” local backend (age + SSH-key recipients)
  • πŸšͺ vault create / secret put / secret get / secret list
  • 🐚 kvlt env / kvlt run for shell + child-process integration
  • πŸ”Œ Pluggable backend registry (factory pattern)

Up next β€” only what earns its keep:

  • 🀝 ssh-agent integration β€” friction-free decrypt; Touch ID via Secretive on macOS without re-prompting per read.
  • πŸ” vault migrate β€” copy-then-swap, named-vault payoff: change backend type without touching call sites.
  • πŸ”Œ AWS Secrets Manager backend (-tags aws) β€” only if a real "dev local β†’ prod cloud" use case shows up. Other tools (Azure Key Vault, 1Password, HashiCorp Vault) are intentionally not on the roadmap; if you live in those, use them directly.

πŸ“š Docs

πŸ“„ License

The MIT License.

About

πŸ” Pluggable secrets vault β€” age + SSH keys by default, cloud backends optional.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors