kafkito

module
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 6, 2026 License: Apache-2.0

README

kafkito

kafkito logo

Manage and observe Apache Kafka clusters from a single Go binary — topics, messages, consumer groups, schemas and ACLs in one web UI.

kafkito is a free, open-source web UI for managing and observing Apache Kafka clusters — built in Go as a single binary, with a modern React frontend, Apache 2.0. It's an independent rewrite inspired by provectus/kafka-ui (unmaintained since 2024); for a community-maintained continuation of the original Java codebase, see kafbat/kafka-ui.

Quickstart

Run kafkito only behind TLS. kafkito serves plain HTTP. API requests carry bearer tokens and, for private clusters, broker and Schema Registry credentials in the X-Kafkito-Cluster header. The examples below are for your own machine. Anywhere else, terminate TLS in a reverse proxy or platform router, set Strict-Transport-Security there, and do not expose port 37421 directly on an untrusted network. See Running behind TLS.

Try it locally — no auth setup

The local image ships with auth disabled and a logged-in dev identity, so the UI works immediately. Use it to evaluate kafkito against a local Kafka. Never deploy this variant.

docker run --rm -p 37421:37421 \
  -e KAFKITO_INSECURE_AUTH_OFF=true \
  -e KAFKITO_KAFKA_BROKERS=host.docker.internal:9092 \
  ghcr.io/finkeflo/kafkito:latest-local

Auth mode off only starts on a loopback address. Inside a container kafkito listens on all interfaces, so KAFKITO_INSECURE_AUTH_OFF=true acknowledges that; the -p mapping above still decides who can reach it.

Open http://localhost:37421 in your browser. Connect kafkito to a broker on your host (host.docker.internal:9092), or run a Kafka container alongside it on a shared docker network.

Default image (auth enforced)
docker run --rm -p 37421:37421 \
  -e KAFKITO_AUTH_MODE=oidc \
  -e KAFKITO_AUTH_OIDC_ISSUER_URL=https://idp.example.com/realms/kafkito \
  -e KAFKITO_AUTH_OIDC_AUDIENCE=kafkito-api \
  -e KAFKITO_KAFKA_BROKERS=host.docker.internal:9092 \
  ghcr.io/finkeflo/kafkito:latest

Replace the issuer and audience with your IdP's values; with the example values discovery fails and the container exits with code 2. The default image enforces auth and does not start without KAFKITO_AUTH_MODE. kafkito has no login flow of its own: put an auth proxy (for example oauth2-proxy) in front of it that handles the login and forwards Authorization: Bearer <access token>. In mode oidc kafkito checks that token against your OpenID Connect issuer; the signing keys are found through <issuer>/.well-known/openid-configuration unless KAFKITO_AUTH_OIDC_JWKS_URL is set. Mode mock checks tokens against a signing key kafkito creates in memory and never hands out, so every /api/v1/* request gets 401; use it only to smoke-test the image. See Auth modes.

SAP BTP / XSUAA
docker run --rm -p 37421:37421 \
  -e KAFKITO_AUTH_MODE=xsuaa \
  -e VCAP_SERVICES="$VCAP_SERVICES" \
  ghcr.io/finkeflo/kafkito:latest-btp

Mode xsuaa reads its credentials from an XSUAA service binding in VCAP_SERVICES, which Cloud Foundry sets for a bound app; without it the image does not start. See Auth modes and ADR-0004 for the build-tag rationale.

Build from source

Requires Go 1.26+ and Bun 1.4+:

git clone https://github.com/FinkeFlo/kafkito && cd kafkito
make build
KAFKITO_AUTH_MODE=mock KAFKITO_KAFKA_BROKERS=localhost:9092 ./bin/kafkito

make build produces the default build, which needs an auth mode (see Auth modes). With mock the API answers every request with 401; for a local UI without an IdP, make run-dev runs a devauth build with auth off on 127.0.0.1.

Local development (hot-reload)

Requires Go 1.26+, Bun 1.4+, and Docker.

make worktree-init    # writes .env.dev with a free port pair
make dev              # Compose + backend (air) + frontend (Vite), one command

Open the Vite URL printed in the [frontend] stream (default http://localhost:37422). Backend changes under cmd/, internal/, or api/ rebuild automatically; frontend changes hot-reload through Vite. Press Ctrl-C in the make dev terminal to stop both processes (the Compose stack stays up — tear it down with make dev-down). The backend listens on 127.0.0.1 only, because auth mode off requires a loopback address; Vite proxies /api to it.

Multiple git worktrees can run make dev in parallel; each calls make worktree-init once to claim a free port pair, and they share the same Compose-backed Kafka and Schema Registry.

From an IDE, running air directly works too — .air.toml loads .env.dev itself, as long as you've run make worktree-init once.

Configuration

kafkito reads an optional YAML file (--config or KAFKITO_CONFIG) and KAFKITO_* environment variables. Later sources win: built-in defaults → YAML file → KAFKITO_* variables → $PORT.

Variable YAML key Default Notes
KAFKITO_SERVER_ADDR server.addr :37421 Listen address.
PORT — — If set and non-empty, overrides server.addr with :$PORT (Cloud Foundry / Heroku).
KAFKITO_TEST_CONNECTION_TIMEOUT server.test_connection_timeout 15s Go duration (30s, 2m) for the private-cluster "Test connection" probe. 0 means the default; invalid or negative values fail startup.
KAFKITO_SERVER_FRAME_ANCESTORS server.frame_ancestors 'none' CSP frame-ancestors source list, see Security headers.
KAFKITO_PRIVATE_CLUSTERS private_clusters.mode on Who may use private clusters: on (every user), off (nobody) or role (RBAC subjects with the permission private_cluster:use; needs rbac.enabled). Case-insensitive; a YAML boolean counts as on/off. Other values fail startup.
KAFKITO_PRIVATE_CLUSTERS_ALLOW_PLAIN_WITHOUT_TLS private_clusters.allow_plain_without_tls false true lets private clusters use SASL/PLAIN without TLS, which sends the password in clear text; by default such a definition gets 400 plain_without_tls. Meant for local development brokers. Configured clusters are not affected; kafkito logs a startup warning for each configured cluster with SASL/PLAIN without TLS or insecure_skip_verify. Values other than a boolean fail startup.
KAFKITO_KAFKA_BROKERS — — Env-only shortcut: when no clusters are configured, defines one cluster named local from a comma-separated broker list.
KAFKITO_AUTH_MODE auth.mode off oidc or mock (every build), xsuaa (-btp build) or off (served only by -tags devauth builds such as the -local image). A mode the build cannot serve fails startup, so the default and -btp builds need this set. See Auth modes.
KAFKITO_AUTH_OIDC_ISSUER_URL auth.oidc.issuer_url — Mode oidc, required: the exact iss of accepted tokens and the base for discovery. https (plain http only on a loopback host), no query or fragment.
KAFKITO_AUTH_OIDC_AUDIENCE auth.oidc.audience — Mode oidc, required: a value the token's aud must contain. Use the identifier of the kafkito API, not the client ID of the auth proxy.
KAFKITO_AUTH_OIDC_JWKS_URL auth.oidc.jwks_url — Mode oidc: the signing-key (JWKS) URL. Empty means discovery at startup. Same URL rule as the issuer.
KAFKITO_AUTH_OIDC_REQUIRED_TYP auth.oidc.required_typ — Mode oidc: when set (for example at+jwt), tokens must carry this typ header.
KAFKITO_AUTH_OIDC_ALLOWED_AZP auth.oidc.allowed_azp — Mode oidc: when set, a comma-separated list (YAML: a list) of accepted azp values; tokens without azp are rejected.
KAFKITO_INSECURE_AUTH_OFF — — true allows mode off on a non-loopback address. Ignored on Cloud Foundry, where off always fails startup.

An invalid listen address (for example PORT=abc) fails startup with exit code 2.

Per-cluster data masking rules (clusters[].data_masking, for record values, keys and headers) are described in docs/data-masking.md.

Auth modes

KAFKITO_AUTH_MODE (YAML auth.mode, case-sensitive, default off) picks how kafkito checks the bearer token on /api/v1/*; /healthz and /readyz are never authenticated. kafkito builds the validator before it listens and then logs auth initialised with the mode. An unknown mode, or one the build cannot serve, logs auth init failed at error and exits with code 2.

Mode Builds Startup IdP outage
off served only by -tags devauth builds (-local image); other builds exit with code 2 Every request gets the synthetic principal dev-user. Exits with code 2 (insecure auth configuration) always on Cloud Foundry (VCAP_APPLICATION set), and on a non-loopback address unless KAFKITO_INSECURE_AUTH_OFF=true. No IdP involved.
mock every build Starts an issuer on a random 127.0.0.1 port with a signing key created at startup and loads its keys. No client can get a token, so every API request gets 401. No external IdP: the issuer runs inside the process.
oidc every build Needs KAFKITO_AUTH_OIDC_ISSUER_URL and KAFKITO_AUTH_OIDC_AUDIENCE; a missing value, or an issuer or JWKS URL that is not https (plain http only on a loopback host), exits with code 2. Without KAFKITO_AUTH_OIDC_JWKS_URL, reads <issuer>/.well-known/openid-configuration (10 s timeout, 1 MB limit, at most 5 redirects, none from https to http, each target following the same URL rule); its issuer must equal the configured issuer exactly and its jwks_uri must follow the same URL rule (and be https when the issuer is). If that fails, kafkito logs auth init failed and exits with code 2. Then loads the keys, waiting at most 5 s, and logs auth initialised with issuer, audience and JWKS URL. At startup: when discovery fails, exits with code 2 (see Startup). When the keys do not load (with KAFKITO_AUTH_OIDC_JWKS_URL set, or after a successful discovery), logs auth: JWKS warm-up failed; startup continues at warn and serves; API requests get 401 until the keys load, and /readyz does not change. Later: requests keep working with the keys already loaded; a token signed with a new key gets 401 until the IdP answers again.
xsuaa -btp builds Reads the first xsuaa binding from VCAP_SERVICES; a missing variable or binding, invalid JSON, or a binding without url, uaadomain or xsappname (or with a scheme or port in uaadomain) exits with code 2 (auth init failed). Then loads the keys from <url>/token_keys, waiting at most 5 s. At startup: logs auth: JWKS warm-up failed; startup continues at warn (also when <url>/token_keys breaks the jku rules below) and serves; API requests get 401 until the keys load, and /readyz does not change. Later: requests keep working with the keys already loaded; a token signed with a new key, or naming a jku first seen during the outage, gets 401 until the IdP answers again.

How mock, oidc and xsuaa load keys:

  • One load per key URL runs at a time and all requests share it. A request waits at most 5 s for it and then gets 401; the load itself may take up to 10 s, so a slow IdP still fills the cache for later requests.
  • A load starts at most once per minute per URL: for the first keys, when a token names a kid the cached keys lack (key rotation), or in the background on the first request after the keys are 15 min old. Once that load has ended, requests in the rest of the minute do not wait: without keys they get 401 at once, and a token with an unknown kid is checked against the cached keys and rejected.
  • Nothing retries on a timer: after a failed start the next load begins with the first API request at least a minute later.
  • A failed load logs jwks fetch failed at warn and keeps the previous keys. The URL is logged for the configured key URL only; key URLs taken from a token (xsuaa) are not.
  • A rejected token is logged only at debug (auth validate failed, with the rule that failed and no claim values); the 401 itself appears in the request log at info.

Tokens (mock, oidc and xsuaa) must meet all of these:

  • A compact JWS with exactly one signature, a kid header and a JSON claim set as payload.
  • Header alg is one of RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512 or EdDSA (HMAC and none are always rejected) and equals the key's alg when the key has one.
  • exp is present and not past; nbf and iat, when present, are checked too, with 60 s clock skew allowed.
  • sub is a non-empty string and aud is present.
  • mock: iss equals the embedded issuer URL exactly, aud contains mock-client.
  • oidc: iss equals KAFKITO_AUTH_OIDC_ISSUER_URL exactly (no normalization, so a trailing slash counts), and aud contains KAFKITO_AUTH_OIDC_AUDIENCE. With KAFKITO_AUTH_OIDC_REQUIRED_TYP set, the typ header must match it (case-insensitive, an application/ prefix is ignored); with KAFKITO_AUTH_OIDC_ALLOWED_AZP set, azp must be one of the listed values. Both are off by default. Set the audience to the identifier of the kafkito API rather than the client ID of the auth proxy: many IdPs put the client ID into the aud of ID tokens, and required_typ (at+jwt for RFC 9068 access tokens) keeps ID tokens out where the IdP sets typ. The RBAC identity is user_name, else preferred_username, else sub; check that your IdP does not let users change the claim you map RBAC subjects to.
  • xsuaa: iss is the binding url or a path below it, aud contains the binding clientid or xsappname, zid equals the binding identityzoneid when that is set, and the jku header is https://<uaadomain or a subdomain>/token_keys (a DNS name, no port other than 443, no user info, query or fragment). At most 16 different jku URLs, the binding's own included, are accepted per process; tokens naming further ones get 401.
Logging

kafkito logs to stdout via log/slog, one line per event:

Variable Values Default
KAFKITO_LOG_LEVEL debug, info, warn, error info
KAFKITO_LOG_FORMAT json, text json

The YAML equivalents are log.level and log.format; make dev defaults to text. Every API request gets a request id (from X-Vcap-Request-Id, traceparent or X-Request-Id, else generated), echoed in the X-Request-Id response header and attached to its log lines. Server errors (5xx) log at warn, client errors (4xx) and requests slower than 2s at info, all other requests at debug; health probes and static assets are not logged, and query strings, headers and bodies never are.

Warnings and errors can name the broker and Schema Registry addresses of the clusters involved, private clusters included (see Private clusters); restrict who can read the logs accordingly.

To see every request on SAP BTP Cloud Foundry temporarily:

cf set-env <app> KAFKITO_LOG_LEVEL=debug && cf restart <app>
Security headers

Every response (API, UI and static files) carries:

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
X-Content-Type-Options: nosniff
Referrer-Policy: no-referrer
Cross-Origin-Opener-Policy: same-origin
X-Frame-Options: DENY

Private-cluster credentials are kept in the browser (remembered passwords in localStorage, the others in the open tab), so the policy allows no inline scripts or styles and no third-party origins.

API responses (/api/..., errors and the copy event stream included) also carry Cache-Control: no-store, so neither the browser nor a proxy keeps a copy of cluster data. The UI keeps its own caching: hashed files under /assets/ are cached for a year, index.html is revalidated (no-cache).

  • Embedding in an iframe (e.g. an SAP BTP launchpad): set KAFKITO_SERVER_FRAME_ANCESTORS (YAML server.frame_ancestors) to a space-separated CSP source list, e.g. 'self' https://*.launchpad.example.com. X-Frame-Options: DENY is only sent while the value is 'none', because it cannot express an allow-list.
  • HSTS is not set by kafkito: TLS is terminated by the upstream proxy or router, which should send Strict-Transport-Security (see Running behind TLS).
  • make dev serves the UI through Vite, which sets none of these headers; the policy only applies when the Go binary serves the UI (make build, images, make e2e).

Why kafkito?

  • Single static binary — no JVM, no side-car containers, ~50 MB RAM footprint.
  • Graceful with limited permissions — works with read-only ACLs on individual topics; does not require cluster-admin rights.
  • Built-in RBAC & data masking — YAML-policy based, OSS, no enterprise gating.
  • Powerful message browser — JavaScript-DSL filters, JSON/text/binary values, Schema-Registry aware (Avro and JSON Schema decoding; Protobuf payloads are detected but not decoded yet).
  • Cloud-native ready — stateless, 12-Factor, JWT auth (generic OIDC or XSUAA), distroless image.

Tech Stack

Layer Tech
Backend Go 1.26 · Chi · OpenAPI 3.1 (oapi-codegen, kin-openapi) · twmb/franz-go + kadm · dop251/goja · knadh/koanf · log/slog
Frontend React 19 · Vite · TanStack Router · TanStack Query · Tailwind · Radix UI primitives · Bun
Distribution Single Go binary (//go:embed-ed SPA) · distroless multi-arch Docker image

Project Status

See docs/adr/ for Architecture Decision Records.

  • ADR-0001: Greenfield Apache-2.0
  • ADR-0002: Tech Stack
  • ADR-0003: Cloud Foundry Readiness
  • ADR-0004: XSUAA as a build-tagged plugin
  • ADR-0005: OpenAPI 3.1 as the HTTP contract

Contributing

Pull requests are welcome. See CONTRIBUTING.md for the DCO sign-off requirement, local checks, and style rules.

License

Apache License 2.0 — see LICENSE and NOTICE.

Acknowledgements

  • provectus/kafka-ui — original Apache-2.0 reference for features (RBAC, masking, graceful degradation). Unmaintained since 2024. We may port code from there with attribution.
  • kafbat/kafka-ui — community-maintained continuation of provectus/kafka-ui (Apache-2.0, Java/Spring). Different stack, shared goals. Worth a look if you want a maintained fork of the original codebase.

Directories

Path Synopsis
Package api embeds the OpenAPI 3.1 document that is the single source of truth for kafkito's HTTP contract (see docs/adr/0005-openapi-contract.md).
Package api embeds the OpenAPI 3.1 document that is the single source of truth for kafkito's HTTP contract (see docs/adr/0005-openapi-contract.md).
cmd
kafkito command
Command kafkito starts the kafkito HTTP server.
Command kafkito starts the kafkito HTTP server.
Package frontend embeds the built SPA assets and exposes them as an http.Handler.
Package frontend embeds the built SPA assets and exposes them as an http.Handler.
internal
auth
Package auth provides OIDC-based authentication primitives: a Validator interface plus a generic OIDCValidator implementation, principal extraction from a verified JWT and chi middleware that runs the validation.
Package auth provides OIDC-based authentication primitives: a Validator interface plus a generic OIDCValidator implementation, principal extraction from a verified JWT and chi middleware that runs the validation.
config
Package config loads kafkito runtime configuration from a YAML file (optional) and environment variables (prefix KAFKITO_).
Package config loads kafkito runtime configuration from a YAML file (optional) and environment variables (prefix KAFKITO_).
connerr
Package connerr sorts errors from connecting to a broker or a Schema Registry into a few stable classes with fixed texts.
Package connerr sorts errors from connecting to a broker or a Schema Registry into a few stable classes with fixed texts.
kafka
Package kafka wraps the franz-go client/kadm admin for kafkito.
Package kafka wraps the franz-go client/kadm admin for kafkito.
masking
Package masking applies per-cluster data masking rules to the value, key and header values of Kafka records.
Package masking applies per-cluster data masking rules to the value, key and header values of Kafka records.
rbac
Package rbac implements the YAML-driven role-based access control policy engine used by kafkito.
Package rbac implements the YAML-driven role-based access control policy engine used by kafkito.
server
Package server wires the kafkito HTTP router and top-level handlers.
Package server wires the kafkito HTTP router and top-level handlers.
server/api
Package api provides primitives to interact with the openapi HTTP API.
Package api provides primitives to interact with the openapi HTTP API.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL