Desktop client for OpenBao and HashiCorp Vault: sign in, request short-lived database and SSH credentials, and share secrets once, without the CLI.
One Flutter codebase for macOS 13+, Windows 11+ and Linux (Ubuntu 22.04+). Built and tested on macOS so far; Windows and Linux are scaffolded but untested. Works with Vault OSS, Vault Enterprise (namespaces) and OpenBao, which share the same HTTP API.
▶ Watch the short demo on YouTube
You need Flutter 3.32 or newer and Docker.
git clone git@github.com:digitalis-io/transikey.git && cd transikey
make gen # fetch packages, generate Freezed / JSON code
make dev-up # OpenBao (dev mode), PostgreSQL, OpenLDAP, SSH target; fully configured
make run # start the appSign in with address http://127.0.0.1:8200 and one of these dev-only credentials (they exist only in the local, in-memory dev stack):
| Method | Credentials |
|---|---|
| Token | root |
| Userpass | user demo, password transikey-dev |
| LDAP | user ldapdemo, password transikey-dev |
| AppRole | output of make dev-approle |
OIDC needs a real identity provider, so the dev stack does not cover it. The OIDC role on your server must list http://localhost:8250/oidc/callback in allowed_redirect_uris, the same requirement as vault login -method=oidc. Transikey opens your browser, waits for the provider to redirect back to that loopback address, and signs you in.
Run make help for every task.
| Term | Meaning |
|---|---|
| Dynamic credentials | A database user created on request that the server deletes when its lease ends |
| Lease | The lifetime of an issued credential; it can be renewed or revoked early |
| OTP | One-time SSH password, valid for a single login to one target IP |
| Signed key | Your SSH public key turned into a short-lived certificate by the server's CA |
| Response wrapping | The server stores a secret and hands back a single-use token that reveals it once |
| Cubbyhole | A private store tied to one token; it disappears with that token |
| AppRole | Machine login with a role ID and a secret ID |
| Namespace | An isolated tenant inside Vault Enterprise or OpenBao |
-
Open Database Credentials (
Cmd/Ctrl+2). -
Pick the
short-livedrole, then Request credentials. Roles of every database mount the server shows you are listed, grouped by mount (database,cass001,cass002…). -
Reveal or copy the username and password. The clipboard clears after 30 seconds.
-
Watch the lease count down: green (active), yellow (expiring soon), red (expired). Renew or Revoke from the same card or from Lease Management.
-
Use the Connect section of the card, then copy the ready-made command or the connection URI. When your policy allows it (see below), the engine, host, port and database are read from the server. Otherwise choose PostgreSQL, MySQL or Cassandra and type them once: they are remembered per mount. A host you type always wins, because the server often knows the database under a name only it can resolve:
PGPASSWORD='<password>' psql -h '127.0.0.1' -p 5432 -U '<username>' -d 'app'
The password travels in an environment variable, not as an argument, so it does not show up in
ps.cqlshhas no such variable: its command leaves the password out andcqlshasks for it. Copy the password from the card when it prompts. Picking another role, on any mount, clears the card, so credentials of one role never sit next to another.Optional policy that lets the app detect the engine and address behind a role (the server never returns the connection password on these paths):
path "database/roles/*" { capabilities = ["read"] } path "database/config/*" { capabilities = ["read"] }
-
Open Secret Sharing (
Cmd/Ctrl+4), tab Wrap. -
Paste text or a JSON object, choose a time to live, Wrap secret.
-
Press Copy share link and send the link over any channel. It works one time, then it is void:
transikey://unwrap?addr=https%3A%2F%2Fvault.example.com%3A8200&token=hvs.CAES... -
The colleague clicks the link. Transikey opens on the Unwrap tab with server and token filled in; nothing is sent until they press Unwrap. A link to a server other than their configured one asks for confirmation first. No session is needed to unwrap.
-
If someone else got there first, unwrapping fails, so interception is detectable.
No Transikey on the other side? Copy CLI command gives the recipient a one-liner instead.
The same exchange with the CLI:
bao write -wrap-ttl=30m sys/wrapping/wrap secret='s3cr3t' # sender
BAO_ADDR='https://vault.example.com:8200' bao unwrap '<token>' # recipientThe dev stack includes an SSH server (ssh -p 2222 ubuntu@127.0.0.1, container IP 172.30.0.10) that trusts the OpenBao SSH CA and verifies OTPs through vault-ssh-helper.
Signed key:
-
Open SSH Access (
Cmd/Ctrl+3), choose thesignrole, tab Sign public key. -
Upload public key (for example
~/.ssh/id_ed25519.pub), then Sign key. -
Download certificate and save it next to the key as
~/.ssh/id_ed25519-cert.pub. -
Copy the command from the Connect section (user, host, port and private key path are remembered; uploading
id_ed25519.pubfills in the key path):ssh -p 2222 -i '~/.ssh/id_ed25519' -o CertificateFile='/Users/me/.ssh/id_ed25519-cert.pub' 'ubuntu@127.0.0.1'
The dev
signrole issues 30 minute certificates with a terminal (permit-pty).
One-time password:
-
Choose the
otprole, tab One-time password, target IP172.30.0.10(the OTP is bound to the IP of the server it is meant for), then Generate OTP. -
Copy a command from the Connect section and paste the OTP at the password prompt. It works exactly once:
ssh -p 2222 -o PreferredAuthentications=keyboard-interactive -o PubkeyAuthentication=no 'ubuntu@127.0.0.1'With
sshpassinstalled, the second command logs in without a prompt:SSHPASS='<otp>' sshpass -e ssh -p 2222 -o PreferredAuthentications=keyboard-interactive -o PubkeyAuthentication=no 'ubuntu@127.0.0.1'
All settings live in Settings (Cmd/Ctrl+,) and are stored in the OS keystore (Keychain, DPAPI or libsecret).
| Setting | Type | Default | Example |
|---|---|---|---|
Vault / OpenBao URL (VAULT_ADDR) |
URL | empty | https://vault.example.com:8200 |
Namespace (VAULT_NAMESPACE) |
string | empty | team-a/prod |
| Skip TLS verification | bool | off | on, for a test server with a self-signed certificate |
| Custom CA certificate | PEM file, added to the system trust store via Import CA certificate | none | corp-root-ca.pem |
| Lock after inactivity | seconds, 0 = never |
300 |
120 |
| Clear clipboard after | seconds, 0 = never |
30 |
10 |
| Biometric unlock | bool | off | on (Touch ID, Windows Hello) |
| Blur window when it loses focus | bool | on | off |
| Theme | system / light / dark |
system |
dark |
| Database mount | comma separated paths, used only when the server does not list its mounts | database |
cass001, cass002 |
| SSH mount | path | ssh |
ssh-client-signer |
| Userpass mount | path | userpass |
userpass-ops |
| AppRole mount | path | approle |
approle-ci |
| LDAP mount | path | ldap |
ldap-corp |
| OIDC mount | path | oidc |
okta |
| Database client (Connect section) | psql / mysql / cqlsh |
psql |
cqlsh |
| Database host | string | 127.0.0.1 |
pg.internal.example.com |
| Database port | integer | 5432 |
3306 |
| Database name (keyspace for Cassandra) | string | app |
orders |
| SSH user (Connect section) | string | ubuntu |
ops |
| SSH host | string | 127.0.0.1 |
bastion.example.com |
| SSH port | integer | 2222 |
22 |
| SSH private key path | path | ~/.ssh/id_ed25519 |
~/.ssh/work_ed25519 |
The Database and SSH rows are edited in the Connect section of their screens, not under Settings; the defaults match the dev stack. Database values are kept per mount (per connection when the server reveals it); the defaults apply to a mount you have not edited yet.
Dev stack environment variables. Set them in the shell before make dev-up:
| Variable | Default | Example |
|---|---|---|
DEV_BAO_PORT |
8200 |
DEV_BAO_PORT=8210 |
DEV_BAO_ROOT_TOKEN |
root |
DEV_BAO_ROOT_TOKEN=dev-root |
DEV_BAO_IMAGE |
openbao/openbao:latest |
DEV_BAO_IMAGE=openbao/openbao:2.0.0 (must ship the bao CLI) |
DEV_POSTGRES_PORT |
5432 |
DEV_POSTGRES_PORT=5433 |
DEV_POSTGRES_PASSWORD |
transikey-dev |
DEV_POSTGRES_PASSWORD=local-only |
DEV_USER / DEV_USER_PASSWORD |
demo / transikey-dev |
DEV_USER=alice |
DEV_LDAP_USER / DEV_LDAP_USER_PASSWORD |
ldapdemo / transikey-dev |
DEV_LDAP_USER=bob |
DEV_LDAP_ADMIN_PASSWORD |
transikey-dev |
DEV_LDAP_ADMIN_PASSWORD=local-only |
DEV_SSHD_PORT / DEV_SSHD_IP |
2222 / 172.30.0.10 |
DEV_SSHD_PORT=2200 |
DEV_SUBNET |
172.30.0.0/24 |
DEV_SUBNET=10.99.0.0/24 (keep DEV_SSHD_IP inside it) |
The dev stack is for local testing only: dev mode keeps data in memory, uses a fixed root token and binds to 127.0.0.1.
Work with several servers? Save each one as a profile and pick it from the Server dropdown on the sign-in form.
| A profile remembers | A profile never stores |
|---|---|
| Name and colour tag, address, namespace, TLS verification, custom CA | Tokens |
| The six mount paths | Passwords, secret IDs |
| Database and SSH Connect targets | Issued credentials or leases |
| Last sign-in method and username |
- Sign in to a server, then Save server profile (session card or Settings → Server profiles). Name it, for example
prod, and give it a colour tag such as red. - Next time pick
prodfrom the dropdown: address, namespace, method and username are filled in. - Switching while signed in asks first, signs you out of the current server and clears the screen, so nothing issued by one server shows while another is active.
- The status bar shows the active profile and a coloured edge, so prod never looks like dev.
Edits made while a profile is active (mounts, Connect targets, TLS) are saved to that profile. Typing a different address on the sign-in form detaches from the profile instead of overwriting it. Theme, lock timeout, clipboard timeout and biometric unlock are global.
Already use the bao or vault CLI? When Transikey finds its settings, the sign-in form shows Import from CLI. One click fills in the form; nothing is sent or stored until you press Sign in.
| Source | Becomes |
|---|---|
BAO_ADDR, else VAULT_ADDR |
Address |
BAO_NAMESPACE, else VAULT_NAMESPACE |
Namespace |
BAO_CACERT, else VAULT_CACERT |
Custom CA certificate (file is read) |
BAO_SKIP_VERIFY, else VAULT_SKIP_VERIFY |
Skip TLS verification |
BAO_TOKEN, else VAULT_TOKEN, else ~/.vault-token |
Token field (masked), method set to Token |
export BAO_ADDR=https://bao.example.com:8200
bao login -method=oidc # writes ~/.vault-token
make run # or start the app from this shellAn app started from Finder, the Start menu or a desktop launcher does not see variables exported in your shell profile; start it from a terminal for those. The token file is found either way. Save the result as a server profile and you only do this once.
Shortcut (Cmd on macOS, Ctrl elsewhere) |
Action |
|---|---|
+1 … +6 |
Authentication, Database Credentials, SSH Access, Secret Sharing, Lease Management, Settings |
+B |
Collapse or expand the sidebar |
+L |
Lock the session |
+, |
Settings |
- Secrets are masked until you press Reveal, and leave provider state when the session locks or ends.
- The token lives in memory and in the OS keystore only. Lock drops the in-memory copy; unlock needs biometrics, otherwise you sign in again.
- Logs carry method, path, status and latency. Headers and bodies are never logged, and every message passes a redaction filter (tokens, PEM blocks, SSH certificates).
- Sign out revokes tokens minted by a userpass, LDAP, OIDC or AppRole login. A token you pasted in is left valid: it is yours.
- The macOS build runs without the App Sandbox so that it can use the login keychain in unsigned builds. Re-enable the sandbox and the data-protection keychain when you ship a signed build.
lib/
├── app/ bootstrap, providers, routing, navigation shell
├── core/ api (VaultApiClient, Dio), errors, models, security, theme, utils, widgets
└── features/ auth, database, ssh, sharing, leases, settings
each with domain/ (contracts), data/ (repositories), presentation/ (Riverpod + UI)
| Task | Command |
|---|---|
| Analyzer + offline tests | make check |
| Live API tests (the dev stack must be running) | make dev-up && make test-integration |
Regenerate code after a model or .feature change |
make gen |
| Release build for this machine | make build |
Tests follow BDD: Gherkin files in test/bdd/*.feature, step definitions in test/bdd/step/ (bdd_widget_test).
Push a version tag and GitHub Actions builds and publishes packages for all three platforms:
git tag -s v0.1.0 -m "v0.1.0" && git push origin v0.1.0| Platform | Package |
|---|---|
| macOS | transikey-v0.1.0-macos-universal.zip (the .app) |
| Windows | transikey-v0.1.0-windows-x64.zip |
| Linux | transikey-v0.1.0-linux-x64.tar.gz |
SHA256SUMS.txt ships with every release. Packages are unsigned for now: macOS Gatekeeper and Windows SmartScreen will warn on first start. A tag with a suffix (v0.2.0-rc1) is published as a pre-release.
Regenerate the app icons after a logo change with python3 tool/make_icons.py (needs pillow and numpy).
- System tray menu, screenshot prevention and multi-window: interfaces exist, platform code is a stub.
ssh/get-attempt-token: the client calls it as specified, but current OpenBao and Vault releases answer404 unsupported path.transikey://links are registered on macOS only. Windows needs a registry entry and Linux a.desktopfile withx-scheme-handler/transikey; both belong to a future installer.- The Linux window icon is not set yet (macOS and Windows use the Transikey icon).
- Windows and Linux builds are scaffolded but have only been built on macOS so far.
Maintained by Digitalis.io. Support: digitalis.io/contact.
Licensed under the Apache License 2.0.