feat(vault-tui): implement KV client and service for managing secrets
- Added internal/vault/client.go for creating a Vault client with configuration settings. - Introduced internal/vault/errors.go to classify Vault API errors for better UI handling. - Created internal/vault/kv.go to manage KV secrets, including listing, reading, writing, and deleting operations. - Implemented internal/vault/mounts.go to list and describe secret engine mounts. - Developed internal/vault/service.go to provide a unified entry point for Vault operations. - Added internal/vault/kv_test.go for comprehensive testing of KV operations. - Introduced internal/ui/toast.go for transient notifications in the UI. - Added renovate.json for dependency management and updates.
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
# vault-tui
|
||||
|
||||
A terminal UI and CLI for HashiCorp Vault, built on [Bubble Tea v2](https://github.com/charmbracelet/bubbletea)
|
||||
and [urfave/cli v3](https://github.com/urfave/cli). Browse mounts and KV secrets, view version
|
||||
history, and log in with (almost) any Vault auth method — with `VAULT_TOKEN`/an existing
|
||||
`vault login` session and browser-based OIDC as the two flows it's built around.
|
||||
|
||||
## Install / build
|
||||
|
||||
```sh
|
||||
make build # dist/vault-tui — default build, no cloud auth
|
||||
make build-cloud # dist/vault-tui-cloud — with AWS/Azure/GCP auth
|
||||
make install # go install into $PREFIX/bin ($HOME/.local/bin by default)
|
||||
make help # every target, with a one-line description of each
|
||||
```
|
||||
|
||||
Cloud auth methods (AWS, Azure, GCP) are excluded from the default build — their SDKs pull in
|
||||
a lot of dependencies most deployments never need. `make build-cloud`/`make install-cloud`
|
||||
add them (equivalent to `-tags cloud`); `make release`/`make release-cloud` cross-compile both
|
||||
for linux/darwin/windows × amd64/arm64 into `dist/`, with checksums. `make check` runs
|
||||
formatting, `go vet`, lint (via `golangci-lint` if installed, else `go vet`), and tests — the
|
||||
same gate you'd wire into CI. Without `make`, the plain Go commands still work:
|
||||
|
||||
```sh
|
||||
go build -o vault-tui ./cmd/vault-tui
|
||||
go build -tags cloud -o vault-tui ./cmd/vault-tui
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```sh
|
||||
./vault-tui # launches the TUI (same as `./vault-tui ui`)
|
||||
```
|
||||
|
||||
On first run, if no config file exists and you're at an interactive terminal, vault-tui walks
|
||||
you through creating one (address, namespace, TLS, auth method) and writes it to
|
||||
`~/.config/vault-tui/config.yaml`. Nothing about your environment is hard-coded into the
|
||||
tool — you can also skip the wizard entirely and drive everything from flags/env vars, or run
|
||||
`vault-tui config init` at any time to (re-)run it explicitly.
|
||||
|
||||
If you already have a valid Vault CLI session (`~/.vault-token`, or whatever `token_helper`
|
||||
your `~/.vault` config points at), vault-tui picks it up automatically — no login needed.
|
||||
|
||||
## Auth methods
|
||||
|
||||
| Method | Notes |
|
||||
|---|---|
|
||||
| `token` | Validates an existing token (`VAULT_TOKEN`, `--token`, or your saved `~/.vault-token`) |
|
||||
| `oidc` | Opens a browser, runs a local callback listener (default `http://localhost:8250/oidc/callback`, same as the Vault CLI) |
|
||||
| `userpass`, `ldap`, `okta`, `radius` | Username/password-shaped logins; Okta supports TOTP and best-effort Okta Verify push polling |
|
||||
| `approle` | `role_id` / `secret_id` |
|
||||
| `github` | Personal access token |
|
||||
| `jwt` | Role + JWT |
|
||||
| `kubernetes` | Reads the projected service-account token from disk |
|
||||
| `cert` | TLS client-certificate auth (configure `tls.client_cert`/`tls.client_key` on the profile) |
|
||||
| `aws`, `azure`, `gcp` | Behind `-tags cloud` (see above) |
|
||||
|
||||
Every method works identically from the TUI, from `vault-tui login -method=<name> [key=value ...]`,
|
||||
and from a profile's `auth:` config block — the same `Method`/`Field` declarations drive the
|
||||
TUI form, the CLI prompts, and env-var lookup (`VAULT_TUI_AUTH_<FIELD>` plus each field's own
|
||||
fallback, e.g. `VAULT_PASSWORD`).
|
||||
|
||||
## CLI commands
|
||||
|
||||
```
|
||||
vault-tui launch the TUI (default)
|
||||
vault-tui login [-method=...] authenticate and save the resulting token
|
||||
vault-tui logout [--revoke] forget (optionally revoke) the saved token
|
||||
vault-tui status show the resolved profile, address, and token info
|
||||
vault-tui list <path> list secrets under a path
|
||||
vault-tui read <path> read a secret
|
||||
vault-tui write <path> k=v ... create/update a secret (needs --write)
|
||||
vault-tui delete <path> delete/destroy a secret (needs --write)
|
||||
vault-tui config init|show|path manage the config file
|
||||
```
|
||||
|
||||
Everything defaults to **read-only**. Pass `--write` (or set `read_only: false` in config) to
|
||||
allow `write`/`delete`, and the same flag applies inside the TUI (write-capable keys are
|
||||
disabled and hidden from help when read-only).
|
||||
|
||||
## Configuration
|
||||
|
||||
Discovery order: `--config` flag → `VAULT_TUI_CONFIG` env → `~/.config/vault-tui/config.yaml`
|
||||
(XDG-aware). Every connection setting resolves through the same precedence:
|
||||
|
||||
```
|
||||
CLI flag > env var (VAULT_ADDR, VAULT_NAMESPACE, VAULT_CACERT, ...) > profile > defaults block > builtin
|
||||
```
|
||||
|
||||
`vault-tui status` / `config show` display the effective value; multi-profile setups that
|
||||
want to ignore a stray `VAULT_ADDR` in the shell can set `ignore_env: true` on a profile, or
|
||||
pass `--no-env` for one run.
|
||||
|
||||
<details>
|
||||
<summary>Example <code>config.yaml</code></summary>
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
current_profile: prod
|
||||
|
||||
ui:
|
||||
appearance: auto # auto | dark | light
|
||||
confirm_destructive: true
|
||||
mask_values: true
|
||||
require_cas: true
|
||||
|
||||
defaults:
|
||||
client:
|
||||
timeout: 60s
|
||||
max_retries: 2
|
||||
token:
|
||||
storage: vault-cli # vault-cli | profile | none
|
||||
auto_renew: true
|
||||
revoke_on_logout: false
|
||||
|
||||
profiles:
|
||||
prod:
|
||||
address: https://vault.example.com:8200
|
||||
namespace: admin/platform
|
||||
auth:
|
||||
method: oidc
|
||||
mount: oidc
|
||||
params:
|
||||
role: engineering
|
||||
oidc:
|
||||
port: 8250
|
||||
callback_host: localhost
|
||||
|
||||
staging:
|
||||
address: https://vault-staging.example.com:8200
|
||||
ignore_env: true # a stray VAULT_ADDR must not hijack this profile
|
||||
auth:
|
||||
method: oidc
|
||||
oidc:
|
||||
port: 8251 # so prod and staging logins can run concurrently
|
||||
token:
|
||||
storage: profile # its own file, doesn't touch ~/.vault-token
|
||||
|
||||
ci:
|
||||
address: https://vault.example.com:8200
|
||||
auth:
|
||||
method: approle
|
||||
params:
|
||||
role_id: 8c3a9b0e-4f21-4a0d-9d0c-1b2c3d4e5f60
|
||||
# secret_id intentionally absent — supply via VAULT_TUI_AUTH_SECRET_ID
|
||||
token:
|
||||
storage: none # never persist a CI token to disk
|
||||
|
||||
keys:
|
||||
quit: ["q", "ctrl+q"] # override any default binding
|
||||
down: ["j", "down", "ctrl+n"]
|
||||
|
||||
theme:
|
||||
border_style: rounded # rounded | normal | thick | double | ascii | hidden
|
||||
colors:
|
||||
accent: {light: "#5A32B0", dark: "#B39DFF"}
|
||||
error: "#FF6B6B" # single value = same in light and dark
|
||||
```
|
||||
</details>
|
||||
|
||||
### Token storage
|
||||
|
||||
- `vault-cli` (default): shares `~/.vault-token` — or whatever external `token_helper` your
|
||||
`~/.vault` file configures — with the real Vault CLI. Logging in via either tool updates
|
||||
the token the other one sees.
|
||||
- `profile`: its own file under `$XDG_STATE_HOME/vault-tui/tokens/<profile>.token`, for
|
||||
running multiple profiles/namespaces side by side without clobbering each other.
|
||||
- `none`: never persisted (CI, scripted one-shots).
|
||||
|
||||
### Keybindings & theme
|
||||
|
||||
`keys:` rebinds any action by name (unknown names or colliding bindings fail at startup, not
|
||||
at the moment you press the key); `theme:` recolors everything via named colors that accept
|
||||
either a single hex value or a `{light, dark}` pair. `theme.mask_char` picks the character
|
||||
secret values are masked with (default `•`). `--no-color`/`NO_COLOR` fall back to a monochrome
|
||||
palette that carries hierarchy through weight instead of color. See the example above and
|
||||
`internal/ui/keys/keymap.go` / `internal/ui/theme/theme.go` for the full action/color lists.
|
||||
|
||||
## Default TUI keybindings
|
||||
|
||||
`j`/`k` or arrows to move, `enter`/`l` to open, `esc`/`h` to go back, `/` to filter a list,
|
||||
`?` for the full keybinding overlay, `q`/`ctrl+c` to quit. `p` switches profiles (when more
|
||||
than one is configured) and `ctrl+l` logs out — both available from anywhere.
|
||||
|
||||
On a secret: `s` shows/hides the selected value, `S` all of them, `y` copies the selected
|
||||
value (auto-cleared from the clipboard after `ui.clipboard_clear_after`, if set), `e` edits,
|
||||
`d`/`D` soft-delete/destroy, `V` opens version history. On version history: `enter` views
|
||||
that version, `u` undeletes it, `R` rolls back to it (creates a new version with its data).
|
||||
Destructive actions render as a centered confirmation overlay, and irreversible ones
|
||||
(destroy, non-KV-v2 delete) require typing the path back to confirm.
|
||||
|
||||
The TUI adapts its layout to the terminal size: a side panel with mount/secret details
|
||||
appears once the terminal is wide enough, and chrome (badge, pills, help text) simplifies
|
||||
progressively as it narrows; below roughly 44×12 cells it shows a plain "too small" notice
|
||||
instead of a garbled layout.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
internal/config YAML schema, precedence engine (flag > env > profile > defaults > builtin)
|
||||
internal/vault api.Client wrapper, KV v1/v2 path mapping, mount discovery
|
||||
internal/token token resolution/storage (vault-cli compatible), lookup, auto-renew
|
||||
internal/auth Method/Field abstraction + every login method (incl. OIDC in auth/oidc)
|
||||
internal/cli urfave/cli/v3 commands — the headless surface
|
||||
internal/ui Bubble Tea v2 TUI, built entirely on internal/vault + internal/auth
|
||||
```
|
||||
|
||||
`internal/vault` and `internal/auth` never import a TUI toolkit; `internal/ui` never imports
|
||||
`hashicorp/vault/api` directly. The CLI and TUI are two front ends over the identical
|
||||
Service/Method abstractions, which is what makes `vault-tui login -method=x` and picking
|
||||
the same method in the TUI behave identically.
|
||||
|
||||
### Mount discovery
|
||||
|
||||
`sys/mounts` requires broad permissions and commonly 403s for ordinary tokens. vault-tui
|
||||
tries it first (richer detail when it works) and falls back to `sys/internal/ui/mounts` —
|
||||
the same endpoint the Vault web UI uses, scoped to what your token can actually see.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Okta Verify push-number display is best-effort: the `verify/<nonce>` polling endpoint isn't
|
||||
part of Vault's documented public API, so a poll failure is silently ignored and the login
|
||||
falls back to waiting on the original request.
|
||||
|
||||
## A note on how this was built
|
||||
|
||||
This tool was written almost entirely by Claude (Anthropic) as a side project scratch-my-own-itch
|
||||
— avoiding the Vault web UI from inside WSL. I've reviewed and tested it for my own use, but I
|
||||
have no interest in maintaining it as a hand-written codebase and don't plan to write code in
|
||||
this repo myself. I'll happily look at and merge pull requests, but there's no support and no
|
||||
warranty of any kind — use at your own risk.
|
||||
Reference in New Issue
Block a user