A modern, simple UI for OpenBao — Infisical-style UX, built with Next.js, Tailwind CSS, and coss ui (copy-paste components on Base UI). The UI and OpenBao ship in a single Docker image.
Create a shareable, app-scoped access group across an environment group — with a live preview of the exact OpenBao policy it generates:
Give your service an app identity (AppRole) so it gets a short-lived, scoped token in production — the guide is wired to your environment and secret path:
Next.js is the single front door (a BFF), and OpenBao runs alongside it inside the same image, reachable only on loopback:
┌───────────────────────── single Docker image ─────────────────────────┐
browser ───► │ Next.js (:3000) │
│ ├── /ui2/* → React pages (coss ui — this app) │
│ ├── /ui2/api/* → BFF route handlers (auth, session) │
│ ├── /ui/* → rewrite/proxy ─────────► OpenBao stock UI │
│ └── /v1/* → rewrite/proxy ─────────► OpenBao (127.0.0.1:8200) │
└──────────────────────────────────────────────────────────────────────────┘
/ui2/*— this application's pages (served underbasePath: /ui2). The root/redirects here./ui2/api/*— custom API routes. Login validates credentials against OpenBao server-side and stores the token in an httpOnly cookie — it never reaches client JS./ui/*— OpenBao's own stock UI, proxied through untouched so both UIs coexist (ui = trueindocker/openbao.hcl; auto-on in dev mode). A Classic UI link in the sidebar points here./v1/*— transparently proxied to the embedded OpenBao (next.config.ts→rewrites()).
Only port 3000 is exposed; OpenBao stays internal.
Run OpenBao (or the bundled mock) and the UI separately:
# 1. Backend (one terminal) — real OpenBao …
bao server -dev -dev-root-token-id=root # listens on :8200
# … or the dependency-free mock (in-memory KV v2, no install needed):
pnpm mock:bao # listens on :8200, token: root
# 2. UI (another terminal)
pnpm install
pnpm dev # http://localhost:3000Open http://localhost:3000 → you'll be redirected to /ui2/login. Sign in with
the Token method using root. The Overview page shows live seal status, your
token's policies, and the enabled secret engines; Secrets lets you browse KV
engines, create/edit secrets, view version history, and roll back — all fetched
through the proxy, proving the frontend ↔ backend wiring end to end.
Config via env (see .env.example): OPENBAO_ADDR, BAO_COOKIE_NAME, and the
optional OPENBAO_UI_PUBLIC_URL (behind a reverse proxy).
Interactive pages use TanStack Query against an authenticated BFF proxy
(/ui2/api/bao/<path>). The proxy injects the httpOnly token and the
X-Vault-Namespace header server-side, so the token is never exposed to client
JS while the UI still gets live, cacheable reads/writes.
docker build -t openbao-ui .
docker run --rm -p 3000:3000 -e BAO_DEV=1 -e BAO_DEV_ROOT_TOKEN_ID=root openbao-ui
# or:
docker compose up --buildThen visit http://localhost:3000 and log in with token root.
- Dev mode (
BAO_DEV=1, default): in-memory, auto-unsealed, fixed root token — great for trying it out, not for production. - Non-dev (
BAO_DEV=0): boots fromdocker/openbao.hcl(file storage). The instance starts sealed/uninitialized — the UI detects this at/ui2/loginand walks you through the built-in initialize → save keys → unseal flow. Mount a volume at/bao/fileto persist storage.
The app derives its public origin (used for the OIDC redirect_uri and the
post-login redirect) from each request's Host / X-Forwarded-Host +
X-Forwarded-Proto headers. That works out of the box when your proxy forwards
those headers.
If your proxy/load balancer rewrites or drops them — symptom: "Sign in with
Google" fails with "OpenBao returned no authorization URL…" pointing at an
unexpected host like 0.0.0.0:3000 — set the public URL explicitly:
docker run … -e OPENBAO_UI_PUBLIC_URL=https://bao.example.com openbao-uiThis pins the origin to that exact value regardless of request headers. It is
also echoed to the OIDC setup wizard, so the role's allowed_redirect_uris is
registered with the same URL the login flow sends — keep this value in sync
with the redirect URI registered in your Google/OIDC provider. Only the origin
(scheme://host[:port]) is used; any path is ignored.
The published image embeds a specific OpenBao version and is tagged to match it, so picking a tag picks the OpenBao inside:
Our image (ghcr.io/hasdfa/openbao-ui) |
Embedded OpenBao |
|---|---|
:2.5.5, :2.5, :2 |
quay.io/openbao/openbao:2.5.5 |
:latest |
newest released OpenBao |
:sha-<commit> |
immutable, per-commit |
- The version is pinned in
.openbao-version(single source of truth). CI, the published image, and localdocker buildall read it, so the OpenBao binary inside always matches the tag. Override per build with--build-arg OPENBAO_VERSION=<x.y.z>. - Auto-release:
.github/workflows/sync-openbao.ymlruns weekly — when OpenBao cuts a new release it publishes a matchingopenbao-ui:<version>image and opens a PR bumping the pin. So every new OpenBao release gets a matching UI image automatically.publish.ymlalso ships on every push tomain(re-publishing:latest+ the current version tags).
Components live in components/ui/*. The coss registry is configured in
components.json (@coss), so real coss/Base-UI components can be pulled in via
the shadcn CLI as the UI grows:
npx shadcn@latest add @coss/<component>A full management UI, not just a viewer — everything below is implemented and talks to OpenBao live through the proxy:
Secrets
- KV v2 and v1 — browse, create, and edit, with version history, one-click rollback, and soft-delete / destroy
- Environments — friendly names, colors, and shareable env groups layered on your KV mounts; create or disable an environment right from the UI
- Compare a secret path across environments side by side
- Dashboards for Transit, PKI, SSH, Database, and Cubbyhole
Access
- Team — members (identity entities), role templates, and scoped access roles: grant an env group + app at a chosen level with a live preview of the exact policy, materialized as a real OpenBao policy + identity group
- Auth methods — enable/configure userpass, LDAP, AppRole, and a guided Google (OIDC) sign-in wizard
- Identity (entities & groups), policies & capabilities, tokens, leases, and MFA
Operations
- Seal status / health, quotas, plugins, and an audit log viewer
- Built-in initialize → save keys → unseal bootstrap flow for fresh instances
Settings
- Login customization / branding, server config, password policies, preferences, and multi-namespace switching
Plus a command palette, dark mode, copy-paste integration guides for your app, and a first-run onboarding checklist.
Early but real, and actively developed. Feedback, issues, and PRs are very welcome.







