k9s for Okta — a keyboard-driven terminal UI for exploring and auditing Okta Workforce Identity.
ota lets IAM operators, security auditors, and SREs navigate Users, Groups,
Group Rules, Policies, and System Logs without leaving the terminal. It brings
the keyboard-first "press :, type a resource, drill down" workflow that k9s
established for Kubernetes to Okta's identity model.
Status: v0.1.2 — read-only. Every action is a GET; ota cannot mutate your tenant. Write actions are planned for v0.2 — see Roadmap.
Contents · Install · Quick start · Configuration · Keyboard reference · Supported resources · Troubleshooting · Roadmap · Contributing
Okta's Admin Console takes many clicks to correlate a user's group membership
with their recent sign-in events, and ad-hoc curl | jq snippets lose context
fast. ota collapses those loops into a few keystrokes:
| Question | In ota |
|---|---|
Why can't alice@example.com reach this app? |
:users → /alice → Enter → Groups tab |
| What sign-ins failed in the last 24h? | :logs → P → preset Failed Sign-ins (24h) |
| Is this Group Rule still valid? | :grouprules → look for the red ⚠ INVALID badge |
Pick whichever fits your setup — all channels ship the same binary for macOS (Intel + Apple Silicon), Linux (amd64 + arm64), and Windows (amd64).
brew install tedilabs/tap/otaUpgrade with brew upgrade ota. The tap also carries
tfvault.
Install through the github
backend, which resolves release archives straight from GitHub:
mise use -g github:tedilabs/ota # latest
mise use -g github:tedilabs/ota@0.1.2 # pinnedOr add it to a project's mise.toml:
[tools]
"github:tedilabs/ota" = "latest"An aqua-registry entry is pending review. Once it merges,
mise use -g aqua:tedilabs/otawill work as well and will additionally verify the release checksums — the same pair of backendstfvaultoffers.
Detects your OS/arch, verifies the SHA256 checksum against checksums.txt, and
drops the binary in /usr/local/bin (falling back to ~/.local/bin when that
isn't writable):
curl -fsSL https://raw.githubusercontent.com/tedilabs/ota/main/install.sh | sh
# pin a version, or choose the install directory
curl -fsSL https://raw.githubusercontent.com/tedilabs/ota/main/install.sh | sh -s -- --version v0.1.2
curl -fsSL https://raw.githubusercontent.com/tedilabs/ota/main/install.sh | sh -s -- --bin-dir ~/.local/bindocker run --rm -it \
-e OKTA_ORG_URL -e OKTA_API_TOKEN \
ghcr.io/tedilabs/ota:latestMulti-arch (linux/amd64, linux/arm64), distroless, runs as non-root. To
reuse your profiles, mount the config directory and point XDG at it:
docker run --rm -it \
-e OKTA_ORG_URL -e OKTA_API_TOKEN \
-e XDG_CONFIG_HOME=/config \
-v ~/.config/ota:/config/ota:ro \
ghcr.io/tedilabs/ota:latestGrab a tarball from the releases page.
Each release ships per-arch archives, a macOS universal binary
(ota_<ver>_macos_all.tar.gz), checksums.txt, and SBOMs.
go install github.com/tedilabs/ota/cmd/ota@latest # needs Go 1.24+go install builds without release ldflags, so ota --version reports dev.
For a stamped build, clone and use the Makefile:
git clone https://github.com/tedilabs/ota.git && cd ota
make build && ./bin/ota --version- In the Okta Admin Console, create a Read-Only Administrator account (Security → Administrators). No Okta tenant yet? Start with a free developer org.
- Sign in as that read-only admin, then go to Security → API → Tokens → Create Token.
- Name it
ota-readonlyand copy the value — Okta shows it only once.
Why a read-only admin? ota only issues GETs, but scoping the token to a read-only role means a leaked token still can't mutate your tenant. Tokens are never written to disk and are scrubbed from debug logs and panic traces.
export OKTA_ORG_URL="https://dev-NNNNNN.okta.com" # or your custom domain
export OKTA_API_TOKEN="<token from step 1>"Both are required. OKTA_ORG_URL accepts <org>.okta.com,
<org>.oktapreview.com, and custom domains.
otaYou land on the Users list. Press ? for context-aware help, : for the
command palette, q to quit.
Connectivity trouble? ota --check probes the API once and prints a plain-text
diagnostic without starting the TUI.
| Flag | Purpose |
|---|---|
--profile <name> |
Select a tenant profile from the config file |
--config <path> |
Explicit config path (overrides XDG lookup) |
--token-env <VAR> |
Read the token from a different env var |
--check |
One-shot API diagnostic, no TUI |
--poll-interval <sec> |
Override the Logs tail interval |
--debug |
Write a debug log to ~/.cache/ota/debug.log |
--version |
Print version and exit |
ota reads an optional YAML config from $XDG_CONFIG_HOME/ota/config.yaml
(default ~/.config/ota/config.yaml). Environment variables alone are enough
for a single tenant — the config file exists for multi-tenant setups and
preference overrides.
Example ~/.config/ota/config.yaml
profiles:
dev:
org_url: "https://dev-123456.okta.com"
api_token_env: "OKTA_API_TOKEN" # env var holding the token
default_log_filter: ""
prod:
org_url: "https://acme.okta.com"
api_token_env: "OKTA_PROD_API_TOKEN"
ui:
theme: "dark" # dark | high_contrast | monochrome
pii_masking:
enabled: true # phone/email masked by default
default_unmask_on_copy: false
logs_actor_email: false # stricter compliance mode
keybindings: # override any key ID
nav.down: "j"
nav.up: "k"
app.quit: "q"
search.open: "/"
logs:
poll_interval_seconds: 7 # tail interval (5–10 recommended)
debug: false # writes ~/.cache/ota/debug.logSelect a profile at startup with --profile <name>. Keybinding IDs come from
internal/keys/keys.go; the full catalogue lives in
docs/CONVENTIONS.md §7 and
docs/TUI_DESIGN.md §3.
Defaults are k9s-compatible with Vim navigation. Press ? in any screen for the
live, context-aware list.
| Key | Action |
|---|---|
: |
Command palette |
/ |
Incremental search (lists only) |
? |
Help modal |
Esc |
Cancel current mode/modal |
q |
Close screen / quit (with confirm) |
Ctrl-c |
Soft quit; double-tap to force-exit |
Ctrl-l |
Force redraw (after a tmux resize) |
| Key | Action |
|---|---|
j k / ↓ ↑ |
Down / up one row |
h l / ← → |
Tab or column left / right |
gg / G |
Top / bottom of list |
Ctrl-d Ctrl-u |
Half-page down / up |
Ctrl-f Ctrl-b |
Full-page down / up |
Enter or d |
Open detail (all attributes, incl. [Raw] tab) |
Esc |
Back to the list |
Tab Shift-Tab |
Cycle detail tabs |
Shift+<letter> cycles a column off → ascending → descending → off. The
active column shows ↑ / ↓ beside its header.
| Key | Sorts by | Available in |
|---|---|---|
Shift+S |
STATUS (operational rank — INVALID/LOCKED_OUT first) |
Users, Group Rules |
Shift+N |
NAME (alphabetical) | Users, Groups, Group Rules |
Shift+L |
LAST LOGIN | Users |
Shift+C |
CREATED / CHANGED | Users |
| Key | Action |
|---|---|
R |
Refresh current resource (invalidates cache) |
r |
Detail: toggle [Raw] JSON tab · Lists: rich ↔ raw (Policies, Logs) |
y / yy / yf |
Copy selection / row / focused field to clipboard |
o |
Open the Admin Console link in a browser |
e |
Expand / collapse detail sections (e.g. Factor IDs) |
s / f |
Logs: toggle tail mode / auto-follow |
| Command | Effect |
|---|---|
:users :user :u |
Users list |
:groups :group :g |
Groups list |
:grouprules :rules :gr (+ hyphen/underscore variants) |
Group Rules list |
:policies [TYPE] |
Policy type selector, or jump straight to e.g. OKTA_SIGN_ON |
:logs :l |
Logs search / tail |
:search <SCIM-expr> |
Server-side search (Users, Groups) |
:filter <SCIM-expr> |
Server-side filter (Groups, Apps, Logs) |
:unmask <field> / :mask |
Reveal / re-hide masked PII for this session |
:raw |
Toggle raw JSON in detail screens |
:refresh |
Drop cache and reload |
:about |
App, token, and rate-limit summary |
:errors |
Session error history |
:debug open |
Print the debug log path |
:help :? · :quit :q |
Help · Quit |
:searchon Users is eventually consistent — freshly created users can take minutes to surface. Use:filterwhen you need read-after-write.
Full key map and screen catalogue: docs/TUI_DESIGN.md §3 & §4.
All read-only in v0.1.2.
| Resource | List | Detail | Highlights |
|---|---|---|---|
| Users | ✅ | ✅ 6 tabs | Profile (fixed + custom fields), Credentials, Timestamps, Groups, Factors (PII masked), Recent activity |
| Groups | ✅ | ✅ 4 tabs | Type icons for OKTA_GROUP / APP_GROUP / BUILT_IN; RULE / SYS / LARGE badges; Members tab loads progressively (Esc stops) |
| Group Rules | ✅ | ✅ | ACTIVE / INACTIVE / INVALID colour-coded, INVALID counter banner, expression in monospace |
| Policies | ✅ per type | ✅ | All 7 types listed; rich rendering for OKTA_SIGN_ON, ACCESS_POLICY, PASSWORD, MFA_ENROLL; raw JSON for the other three |
| System Logs | ✅ + tail | ✅ | Adaptive ~7s polling (stretches to 15s on low-quota tenants), hole-free resume after a 429, 5 filter presets |
Authentication: Okta SSWS API tokens via environment variables, optionally
mapped per-profile with api_token_env.
Tracked in PRD §11.3.1:
- Token input is environment-only — interactive prompt lands in v0.2.
- No runtime
:profileswitch — choose with--profileand relaunch. :ratelimit/:healthcheckmodals are partial, and the[RL]header badge may be missing in some builds.- Config file permissions aren't validated — ota stores no tokens, but a
warn-only
0600check is queued. PROFILE_ENROLLMENT,POST_AUTH_SESSION,IDP_DISCOVERYpolicies render as raw JSON only — pressr.- Detail runs inline inside the list model; the routing refactor is deferred to v0.2 with no user-visible change.
- Per-field
M!unmask isn't exposed — the Raw tab masks at projection time and annotates masked lines with# masked.
| Symptom | What's happening |
|---|---|
E0000004 / 401 |
Token invalid or revoked — rotate it in Security → API → Tokens |
E0000006 / 403 |
Token's role lacks read scope for that resource; check :about |
E0000007 / 404 |
Resource was deleted by another admin; press R to refresh |
429 Rate limited |
ota auto-pauses tail polling and resumes from the last published timestamp — no events are lost |
| Garbled output after a tmux resize | Press Ctrl-l to force a redraw |
| Anything else | Run :errors in-app, or start with --debug and read ~/.cache/ota/debug.log |
Can't connect at all? ota --check prints a one-shot diagnostic without the TUI.
v0.1.x — config permission warning, rate-limit numeric display + :ratelimit modal.
v0.2 (targeting Q3 2026)
- Apps resource (list, detail, User → Apps tab)
- Interactive token prompt and runtime
:profileswitching - First write actions, in ascending order of blast radius:
- Group static member add / remove
- User lifecycle —
unlock,unsuspend,activate,deactivate - Group Rule activate / deactivate (double-confirm + impact estimate)
- Rich renderers for the remaining 3 policy types
- OAuth 2.0 Service App (Private Key JWT) auth
:unmask <field>selective PII reveal
v0.3+ — custom view DSL, Event Hook pseudo-streaming, shareable URI scheme.
Issues and PRs welcome at https://github.com/tedilabs/ota/issues. Conventions,
architecture, and testing rules live in docs/; release mechanics are
in docs/RELEASING.md.
When filing a bug, please include:
ota --versionoutput- Okta tenant type (Developer Free / Production / Preview)
- Steps to reproduce, plus expected vs. observed behaviour
- Relevant lines from
~/.cache/ota/debug.log(tokens are auto-redacted)
Apache License 2.0 — see LICENSE.
- k9s for popularising the resource-navigation TUI pattern.
- Bubble Tea, Bubbles, and Lip Gloss — the Charm stack behind ota's UI.
- The Okta Core API and System Log references.