- 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.
232 lines
10 KiB
Markdown
232 lines
10 KiB
Markdown
# 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.
|