|
This version is still in development and is not considered stable yet. For the latest stable version, please use Korvet 0.19! |
Admin API
Korvet exposes an HTTP Admin API under /api/v1 for topic administration, consumer-group inspection, monitoring snapshots, broker security posture, and Kafka SASL credential management.
Authentication
REST API endpoints under /api/v1/ use JWT cookie authentication when
korvet.admin.security-enabled=true (the default). POST /api/v1/auth/login
validates an admin username and password, then sets an HttpOnly session cookie
that the browser sends on later Admin API calls. HTTP Basic credentials are not
accepted by the /api/v1/ REST API chain.
GET /api/v1/auth/status is public and reports whether first-run setup is
required; when the request carries a valid session cookie it also reports the
caller’s username and role. On a fresh secured deployment, create the first
admin user with POST /api/v1/setup/admin while firstRunSetupRequired=true.
Every console user has a role: ADMIN (full read-write, the default) or
VIEWER (read-only — GET succeeds, mutations return 403 Forbidden).
POST /api/v1/users accepts an optional role, and
PUT /api/v1/users/{username} changes the password, the role, or both. The
last remaining ADMIN cannot be deleted or demoted (422 Unprocessable
Entity). See Authentication for details.
When korvet.admin.security-enabled=false, all Admin API routes are public.
Use that mode only for local single-user contexts such as korvet demo.
Swagger UI and the OpenAPI JSON endpoints are protected separately with HTTP Basic authentication so direct browser navigation can use the browser’s native login dialog. Actuator endpoints other than health probes also use HTTP Basic authentication — see Monitoring for details.
Login
POST /api/v1/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "secret123"
}
The login endpoint returns a session cookie valid for 8 hours (configurable via korvet.admin.jwt.expiry).
Login attempts are rate-limited per username and client IP, and
POST /api/v1/auth/logout revokes the session token immediately — see
Authentication for the throttling policy
and session-revocation details.
Broker Security Status
-
GET /api/v1/security/statusreturns read-only TLS and SASL posture for the configured broker listener.
The response is metadata-only: it includes configured listener, TLS, and SASL flags, and never includes private keys, passwords, certificate contents, or local certificate file paths.
Console User Management
The Admin API manages the console users who access the web UI and REST API:
-
GET /api/v1/users— list console users -
GET /api/v1/users/{username}— get details for one user -
POST /api/v1/users— create a console user, with an optionalrole(ADMINby default) -
PUT /api/v1/users/{username}— change the password, the role, or both -
DELETE /api/v1/users/{username}— delete a console user
The last remaining ADMIN cannot be deleted or demoted (422 Unprocessable
Entity). See Authentication for the role
model.
Password Change Security
When changing your own password, you must prove you know the current one:
PUT /api/v1/users/alice
Content-Type: application/json
{
"currentPassword": "old-password",
"password": "new-secure-password"
}
currentPassword is required when changing your own password: omitting it
returns HTTP 400 Bad Request, and an incorrect value returns HTTP
403 Forbidden.
An admin resetting another user’s password must omit currentPassword
(sending it returns an error); this lets admins reset accounts without
knowing the current password while keeping the two flows distinguishable.
The distinction prevents an attacker with a hijacked session from silently
taking over an account by changing the password without proving they know the
current one.
Kafka SASL Credentials
The credentials API manages Kafka SASL service accounts — the usernames and passwords that Kafka clients use to authenticate against the broker.
Supported mechanisms
-
SCRAM-SHA-256(default) — recommended for most deployments. -
PLAIN— requires TLS to be enabled on the broker (korvet.broker.tls=true).
Endpoints
-
POST /api/v1/credentials— create a credential. -
GET /api/v1/credentials— list all credentials. -
GET /api/v1/credentials/{username}— get one credential. -
PUT /api/v1/credentials/{username}— rotate the password or change the mechanism. -
DELETE /api/v1/credentials/{username}— delete a credential.
Create a credential
POST /api/v1/credentials
Content-Type: application/json
{
"username": "kafka-client-1",
"password": "secret123",
"mechanism": "SCRAM-SHA-256"
}
mechanism is optional and defaults to SCRAM-SHA-256.
username must be 3–64 characters and contain only letters, digits, -, _, or ..
password must be 8–128 characters with no whitespace.
ACLs
/api/v1/acls manages the broker’s allow-only authorization rules. A rule grants a
principal an operation (READ/WRITE) on a resource: resourceType is TOPIC (default)
or GROUP (a consumer group id), and resource matches exact, prefix*, or *. The legacy
{principal, topic, operation} request shape still works and creates a TOPIC rule; responses
always carry resourceType and resource, plus topic for topic rules.
A principal is a SASL username or, with federated authentication, an IdP group written as
group:<name> (for example group:payments-team) — the broker grants such a rule to every
OAUTHBEARER connection whose configured groups claim carries <name>. The bare prefix
(group: with no name) is rejected, and local credential/console usernames can never start
with group: (their name pattern excludes :), so group principals cannot collide with
users.
POST /api/v1/acls
Content-Type: application/json
{
"principal": "alice",
"resourceType": "GROUP",
"resource": "billing-consumers",
"operation": "READ"
}
-
GET /api/v1/acls— principals that have rules -
GET /api/v1/acls/{principal}— a principal’s rules -
DELETE /api/v1/acls/{principal}?resourceType=&resource=&operation=— delete one rule (the legacytopicparameter is still accepted for topic rules)
See Authentication for the matching semantics, the enforcement points, and the storage-format migration note.
Topics
-
POST /api/v1/topicscreates a topic. -
GET /api/v1/topicslists topics. -
GET /api/v1/topics/{name}returns one topic. -
GET /api/v1/topics/{name}/statsreturns current per-topic traffic counters. -
GET /api/v1/topics/{name}/partitionsreturns current per-partition stream and offset stats. Each row includessizeBytes— the measured local Redis byte size (MEMORY USAGE) summed over the physical streams currently backing the partition (the logical stream, or every segment with a local copy). It isnullwhen any component measurement is unavailable and0when the partition holds no local data (for example, fully offloaded). The same field appears onGET /api/v1/topic-partitions. -
GET /api/v1/topics/{name}/messagesbrowses stored topic messages without mutating consumer state. -
PUT /api/v1/topics/{name}replaces explicit topic configuration. -
DELETE /api/v1/topics/{name}deletes a topic.
Topic stats
-
GET /api/v1/topics/{name}/statsreturns accumulated topic totals plus current-process traffic counters.
The response includes root topic, accumulated (producedMessages,
producedBytes, nullable messageCount), and current (produceRecords,
produceRequests, fetchRequests). current counters are process-local
Micrometer values: they reset on restart, omit other brokers, and
fetchRequests includes empty polls. Persisted consume totals, last activity
timestamps, auto-created status, and throughput history require core tracking in
a later phase.
Browse topic messages
The message browser endpoint is read-only. It uses existing Korvet storage reads and does not create consumer groups, commit offsets, reset offsets, or advance broker read state.
GET /api/v1/topics/orders/messages?partition=0&seek=earliest&limit=25&decode=utf8
| Parameter | Description |
|---|---|
|
Optional partition number. Omit it to browse across all partitions. |
|
Read cursor. Supported values are |
|
Browse direction. |
|
Required when |
|
Required when |
|
Zero-based page offset. Defaults to |
|
Maximum rows to return. Defaults to |
|
Payload decode mode: |
{
"items": [
{
"topic": "orders",
"partition": 0,
"offset": 42,
"streamId": "1782260333186-0",
"timestamp": 1782260333186,
"recordTimestamp": 1782260333000,
"key": {
"preview": "order-42",
"value": "order-42",
"encoding": "utf8",
"size": 8,
"truncated": false,
"error": null,
"decodeStatus": "decoded"
},
"value": {
"preview": "{\"id\":42}",
"value": "{\"id\":42}",
"encoding": "utf8",
"size": 9,
"truncated": false,
"error": null,
"decodeStatus": "decoded"
},
"headers": [],
"size": 32,
"tier": null
}
],
"total": 1,
"offset": 0,
"limit": 25,
"nextOffset": null
}
headers lists the record’s headers in stored order (duplicate keys preserved);
each entry has a key and a value field decoded like key/value above. It
is [] when the record has no headers and null when the stored header blob
cannot be decoded. timestamp is the stream-id timestamp used by
seek=timestamp; recordTimestamp is the stored Kafka record timestamp and may
be null. offset and limit follow the same page envelope as
GET /api/v1/storage/offload-jobs; total is the number of visible messages
addressable by the bounded offset paginator from the selected seek cursor before
the page offset is applied. It is capped by the offset + limit maximum page
window. nextOffset is the page offset of the next page, or null when the
result set is exhausted. tier is always null.
Each decoded field (key, value, and header values) carries a
decodeStatus:
-
decoded— the payload was decoded with its registered schema. -
raw— the payload was rendered as-is (no schema decoding attempted). -
schema-not-found— the record’s Confluent wire-format schema id was never registered; the field falls back to raw base64 rendering with the reason inerror. Deleting a schema subject or version keeps its schema ids resolvable, so records written before the delete still decode. -
decode-error— the schema was found but decoding failed; the field falls back to raw base64 rendering with the reason inerror.
A decode failure affects only that field: the page never fails because a single record could not be decoded.
Consumer Groups
-
GET /api/v1/consumer-groupslists consumer groups. -
GET /api/v1/consumer-groups/{groupId}returns group state and members. -
GET /api/v1/consumer-groups/{groupId}/offsetsreturns current per-partition offsets, end offsets, undelivered-message lag, and available stream group metadata. Optional filters aretopicandpartition. Lag is storage-reported message count and excludes delivered-but-uncommitted pending messages. -
POST /api/v1/consumer-groups/{groupId}/reset-offsetsclears the group’s committed offsets and stream delivery state; consumers restart according to their ownauto.offset.resetpolicy. Responds204. With?dryRun=truenothing is mutated: the response (200) previews the per-partition state the reset would clear —groupId,partitionCount, andpartitionsrows withtopic,partition,committedOffset,currentOffset,endOffset,pendingCount, andtargetOffset. Offsets are serialized as strings;targetOffsetis alwaysnullbecause the reset clears offset state rather than committing a broker-chosen offset. Only partitions that carry group state are listed. Requires theADMINrole.
Configuration
-
GET /api/v1/configuration/runtime-policiesreturns read-only runtime policy values from the running broker configuration.
currentValue and defaultValue are raw config strings: durations use milliseconds, sizes use
bytes, and enums use their Kafka/config names. User interfaces should format these values for
display.
-
GET /api/v1/configuration/systemreturns read-only, effective startup-bound system configuration for the running broker/API process.
Values are machine-readable: durations use Ms fields, sizes use bytes, and enum/config names are
returned as strings. Secrets are omitted and exposed only as *Configured flags. When
redis.uriConfigured=true, Redis host, port, and credential flags are derived from the URI.
Runtime Logging
-
GET /api/v1/loggersreturns runtime logger levels forROOT, shallow Korvet-owned loggers undercom.redis.korvet.*. -
POST /api/v1/loggers/{name}sets or clears one visible logger’s runtime level.
The response includes supported levels, items with name, nullable
configuredLevel, and effectiveLevel, and total. Send configuredLevel=null
to clear an explicit runtime level. Runtime log-level changes are process-local
and do not survive restart unless also configured at startup.
Monitoring
-
GET /api/v1/healthreturns Spring Boot health underdetailsplus backend-ownedchecksfor the Operations UI. Thedetailstree includesredis(broker Redis reachability, plus the local-storage Redis backend when configured separately) andbroker(local broker liveness) subsystems, sostatusdegrades toDOWNwhen either backing store is unreachable. -
GET /api/v1/metricsreturns a curated metrics snapshot: topic/group counts, currentactiveConnectionsandbackpressureConnectionsgauges, cumulativemessagesIn/bytesIn/bytesOut/produceRequests/fetchRequests/rebalances/brokerFailures/lossyRecordscounters, andrequestLatencypercentiles aggregated across all Kafka API request timers. -
GET /api/v1/metrics/topicsreturns per-topic produce/fetch rates (records/s, bytes/s, requests/s) and produce/fetch latency percentiles (p50Ms/p95Ms/p99Ms), paginated by topic name viaoffset/limit(default 100, max 1000) withtotalfor detecting partial pages, and optionally filtered to a single topic viatopic. Rates are per-step counter deltas from the in-process metrics history store averaged over a rolling lookback window, so they read 0 until the first step (korvet.admin.metrics-history.interval, default 15s) completes; percentile fields arenulluntil the topic has seen traffic. Each entry also carriessamples— the topic’s per-step produced-traffic history (timestamp,produceRecordsPerSecond,produceBytesPerSecond) over the retention window, oldest first, backing the per-topic trend sparklines. The array is empty when the topic has no history series, e.g. it was rejected by thekorvet.admin.metrics-history.max-seriescap or the broker just restarted. -
GET /api/v1/metrics/historyreturns curated per-step metrics captured in-process (defaults: every 15s with 1h retention, configurable viakorvet.admin.metrics-history.*), oldest first. Counter-backed fields are per-second rates over each step, so clients never do delta math. The buffer is in-memory only — it starts empty on every restart and holds at mostretention / intervalsamples — and backs the dashboard sparklines. -
GET /api/v1/metrics/consumer-lagreturns per-group aggregate lag (totalLag,maxLag,partitionsWithUnknownLag,lastCommitTimestamp) in one call, sized for dashboard polling. Totals are lower bounds when some partitions have unknowable lag, andnullwhen none is known. -
GET /api/v1/brokersreturns live broker registry nodes enriched with the runtime metadata each broker publishes with its registry heartbeat:bindHost,bindPort,tls,sasl,saslMechanism,connections,lastHeartbeat(ISO-8601 instant),uptime(ISO-8601 duration), andversion. Every enrichment field is nullable and degrades independently — entries written by brokers on builds that predate per-node metadata carry none of them, in which case the local node falls back to its own listener posture. A node is stale oncelastHeartbeatis older than the registry’s 30-second liveness threshold (stale nodes are not listed). -
GET /api/v1/storage-statsreturns storage diagnostics:queueDepth,segmentsPerMin/bytesPerMinthroughput,archiveLatencypercentiles, cumulativearchiveFailuresby error type, andlagOldestSeconds— the age of the oldest un-archived segment (nulluntil the storage worker reports lag,0when the backlog is empty).
Health checks contain stable id, name, status, severity, scope,
targetLabel, message, nullable durationMs, lastCheckedAt, and actions.
Checks cover Admin API response, the Actuator aggregate, broker liveness, broker
registry, broker Redis connectivity, remote storage diagnostics, and
schema-registry availability when the corresponding runtime beans are present.
When local stream storage is configured with a separate Redis backend, an
additional storage-redis-connectivity check identifies that target separately.
Unsupported actions are reported as an empty actions array.
Offload Jobs
-
GET /api/v1/storage/offload-jobslists segment offload jobs, merged from live tiered segment state and the durable job records the storage worker maintains. Query parameters:status(repeatable;pending,running,done,failed,cancelled),offset(zero-based row offset, default 0), andlimit(default 100, maximum 500). Each item carriesid,topic,partition,segment,status,attempts,maxAttempts, and — once the worker has attempted the job —startedAt,finishedAt,durationMs, anderror(nullotherwise). Thecountsobject tallies every status over the whole population, independent of filter and pagination. -
POST /api/v1/storage/offload-jobs/{id}/retryresets afailedorcancelledjob topendingwith zero attempts so the storage worker’s next offload scan retries the segment. Returns the updated job; responds409for any other status and404for unknown jobs. -
POST /api/v1/storage/offload-jobs/{id}/cancelcancels apendingorfailedjob: the worker skips the segment until the job is retried. The segment’s local data is kept (and, without an offloaded copy, is not freed by local retention). Returns the updated job; responds409forrunning,done, or already-cancelledjobs.
Job lifecycle: a sealed segment surfaces as pending; each worker tick starts
an attempt (running) that ends done or failed. Failed jobs are retried
automatically every tick until attempts reaches maxAttempts
(korvet.storage.worker.offload-jobs.max-attempts, default 5), after which the
job stays failed until retried manually. Finished (done) records are kept
up to korvet.storage.worker.offload-jobs.retention-count (default 1000);
failed and cancelled records are exempt from that cap. Records for
segments already deleted by retention remain visible as historical rows until
evicted. Both POST endpoints require the ADMIN role; VIEWER sessions can
only read.
Redis Streams Inspection
-
GET /api/v1/storage/redis-streamsreturns point-in-time metadata for the physical Redis Streams that currently back configured topic partitions. Each entry carriestopic,partition, the physical streamkey,length,firstId,lastId,memoryBytes, and astatusofactive,empty, orunavailable. The inventory is derived from topic and segment metadata and does not scan Redis, so missing streams stay distinguishable from unavailable metadata reads. -
GET /api/v1/storage/redis-streams/runtimereturns the local write circuit-breaker runtime state as acircuitBreakerobject withstate(disabled,closed,open, orhalf-open) and, only while open,openUntil. Failure details are not exposed.
memoryBytes is the stream’s measured physical Redis memory (MEMORY USAGE)
and is null when the measurement is unavailable. The circuit breaker is the
write-path breaker described in
Production Tuning; while it is open,
produces fail fast with the retriable REQUEST_TIMED_OUT error.
Storage Segments
-
GET /api/v1/topics/{name}/partitions/{partition}/segments/{segmentId}returns detailed information about a specific segment, including local Redis metadata and remote Iceberg metadata when the segment is offloaded.
Segment Detail Response
For offloaded segments with tiered storage enabled, the response includes a remoteStore object describing the segment’s Iceberg data files:
{
"topic": "orders",
"partition": 0,
"segmentId": 1,
"state": "OFFLOADED",
"tier": "remote",
"open": false,
"messageCount": 10000,
"byteSize": 524288,
"startId": "1722153600000-0",
"endId": "1722153900000-42",
"startTimestamp": 1722153600000,
"endTimestamp": 1722153900000,
"localStore": {
"streamKey": "korvet:storage:local:orders:0:1",
"length": 10000,
"firstId": "1722153600000-0",
"lastId": "1722153900000-42"
},
"remoteStore": {
"format": "PARQUET",
"table": "analytics.orders",
"catalog": "rest",
"dataFiles": [
{
"location": "s3://my-bucket/korvet/analytics/orders/data/00000-0.parquet",
"bucket": "my-bucket",
"objectKey": "korvet/analytics/orders/data/00000-0.parquet",
"sizeBytes": 524288,
"recordCount": 10000
}
]
}
}
The remoteStore object includes:
-
format: data file format (
PARQUET) -
table: the Iceberg table name
-
catalog: Iceberg catalog type (e.g.,
rest) -
dataFiles: the segment’s data files, each with
location,bucket,objectKey,sizeBytes, andrecordCount
The remoteStore field is:
-
Present with metadata when the segment is offloaded and remote inspection succeeds
-
nullwhen the segment is not offloaded, remote inspection fails, or times out (not an error — the response still includes complete local metadata)
Similarly, localStore carries the local Redis stream metadata (streamKey, length, firstId, lastId) and is null once a segment exists only on the remote tier.
See Remote Segment Metadata Inspection for inspection behavior, caching, and configuration.