Files
f.weber ae30ba1240 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.
2026-08-14 11:09:03 +02:00

232 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.