|
This version is still in development and is not considered stable yet. For the latest stable version, please use Korvet 0.19! |
Authentication
This guide covers configuring SASL authentication to secure access to Korvet.
Overview
Korvet supports SASL (Simple Authentication and Security Layer) authentication to control access to the Kafka protocol endpoint. When enabled, clients must authenticate before producing or consuming messages.
Supported Mechanisms
-
SASL/SCRAM-SHA-256 - Challenge-response authentication using stored SCRAM keys. This is the default mechanism.
-
SASL/PLAIN - Username and password authentication. Must be opted in explicitly and requires TLS (see below).
-
SASL/OAUTHBEARER - Federated authentication with OAuth 2.0 bearer tokens issued by an external identity provider (see below).
-
SASL/GSSAPI - Kerberos authentication with service tickets issued by a KDC such as MIT Kerberos or Active Directory (see below).
By default, only SCRAM-SHA-256 is advertised to clients. The advertised mechanisms are controlled by korvet.broker.sasl.mechanisms (default SCRAM-SHA-256).
Enabling Authentication
Configuration
Enable SASL authentication in your Korvet configuration:
korvet:
broker:
sasl:
enabled: true
mechanisms:
- SCRAM-SHA-256
Or using environment variables:
KORVET_BROKER_SASL_ENABLED=true
KORVET_BROKER_SASL_MECHANISMS=SCRAM-SHA-256
Enabling PLAIN
PLAIN is not advertised by default. To use it, add it explicitly to korvet.broker.sasl.mechanisms. Because PLAIN transmits credentials without encryption, Korvet requires TLS to be enabled when PLAIN is advertised. Starting the broker with PLAIN advertised while korvet.broker.tls=false fails validation at startup with:
korvet.broker.sasl: PLAIN mechanism requires korvet.broker.tls=true
Clients using PLAIN must therefore connect with the SASL_SSL security protocol, not SASL_PLAINTEXT.
korvet:
broker:
tls: true
sasl:
enabled: true
mechanisms:
- SCRAM-SHA-256
- PLAIN
Federated Authentication (OAUTHBEARER)
Instead of managing per-client passwords in Korvet, clients can authenticate with OAuth 2.0 bearer tokens issued by an external identity provider (IdP) such as Keycloak, Microsoft Entra ID, or Okta. The broker validates each token locally: the signature is checked against the IdP’s JWKS signing keys (cached, with automatic re-fetch on key rotation), and the issuer, audience, and expiry claims are enforced. Password-based mechanisms keep working alongside OAUTHBEARER, so locally stored credentials remain available as a break-glass fallback.
See the design document docs/design/FEDERATED_AUTHENTICATION.adoc for the full federation
roadmap. Console OIDC login against the same IdP is covered in
Console Single Sign-On; IdP-based ACL principals are a future
phase.
Configuration
Add OAUTHBEARER to the advertised mechanisms and configure the identity provider. Because
bearer tokens travel in the SASL exchange and are replayable if intercepted, Korvet requires TLS
to be enabled when OAUTHBEARER is advertised — same as PLAIN. Starting the broker with
OAUTHBEARER advertised while korvet.broker.tls=false fails validation at startup with:
korvet.broker.sasl: OAUTHBEARER mechanism requires korvet.broker.tls=true
korvet:
broker:
tls: true
sasl:
enabled: true
mechanisms:
- SCRAM-SHA-256
- OAUTHBEARER
oauth:
issuer-uri: https://keycloak.example.com/realms/korvet
audience: korvet-broker
# jwks-uri: optional override; discovered from the issuer by default
# principal-claim: sub
# groups-claim: groups # map group claims to group: ACL principals
# tenant-claim: tenant # carry a tenant id on the connection (no enforcement yet)
# clock-skew: 30s
# max-reauth: 5m # re-authentication deadline for live sessions
issuer-uri and audience are required when OAUTHBEARER is advertised. The JWKS endpoint is
resolved from <issuer-uri>/.well-known/openid-configuration unless jwks-uri is set. Only
asymmetrically signed tokens (RSA and EC families) are accepted; unsigned (alg=none) and
HMAC-signed tokens are rejected.
Principal Mapping
The claim named by korvet.broker.sasl.oauth.principal-claim (default sub) becomes the
connection principal. Topic ACL rules apply to that principal exactly as they do for SCRAM
usernames. Many IdPs use an opaque identifier for sub; point principal-claim at a
human-friendly claim such as client_id or preferred_username if you prefer to write ACL
rules against those.
Group Claims and ACL Federation
Writing one ACL rule per workload principal does not scale when the IdP already organizes
workloads into groups. Setting korvet.broker.sasl.oauth.groups-claim maps a token’s group
claim onto ACL principals: each value g of the claim contributes the rules granted to the
principal group:g, unioned with the rules granted to the principal claim’s value. Grants are
allow-only, so the union is order-insensitive — an operation is allowed if the principal or any
of its groups has been granted it. The policy is resolved once per (re)authentication.
The group: prefix is a reserved principal namespace: local SASL credentials and console
usernames may not contain :, so a group principal can never collide with a literal username.
SCRAM and PLAIN connections are unaffected — they always resolve exactly one principal.
Walkthrough with Keycloak groups:
-
Groups → Create group: create
payments-teamand add your service-account clients (or users) to it. -
Add a group-membership mapper so access tokens carry the claim: Client scopes → dedicated scope → Add mapper → Group Membership, token claim name
groups, full group path Off. -
Point the broker at the claim:
korvet: broker: sasl: oauth: groups-claim: groups -
Grant the group instead of individual principals:
curl -u admin:admin-password -X POST http://localhost:8080/api/v1/acls \ -H 'Content-Type: application/json' \ -d '{"principal":"group:payments-team","topic":"orders","operation":"WRITE"}'
Every token whose groups claim contains payments-team may now produce to orders;
membership changes are managed entirely in the IdP and take effect at the next
(re-)authentication — see the revocation window below.
Token Expiry and Re-Authentication
The authenticated session is bounded by the smaller of the token’s exp claim and the
korvet.broker.sasl.oauth.max-reauth cap (default 5 minutes). The broker returns the session
lifetime in the SASL response (session_lifetime_ms), so compliant Kafka clients (2.2+)
re-authenticate on the same connection with a fresh token before the deadline (KIP-368). A
connection that keeps sending requests past the deadline without a successful
re-authentication is closed.
Because tokens are validated locally against the IdP’s signing keys, IdP-side changes —
disabling a client, removing it from a group — only take effect when the broker sees the next
token. max-reauth bounds that revocation window: lower it for faster revocation at the cost
of more frequent re-authentications. A per-token denylist is intentionally not provided.
Keycloak Example
Create a confidential client with the client-credentials grant in your realm:
-
Clients → Create client: client ID
korvet-pipeline, client authentication On, service accounts roles On (this enables the client-credentials grant). -
Add an audience mapper so tokens carry the broker audience: Client scopes → dedicated scope → Add mapper → Audience, included client audience
korvet-broker. -
Point Korvet at the realm:
korvet:
broker:
tls: true
sasl:
enabled: true
mechanisms:
- OAUTHBEARER
oauth:
issuer-uri: https://keycloak.example.com/realms/korvet
audience: korvet-broker
principal-claim: client_id
Kafka Client Configuration
The standard Java client fetches tokens itself using the client-credentials grant:
security.protocol=SASL_SSL
sasl.mechanism=OAUTHBEARER
sasl.oauthbearer.token.endpoint.url=https://keycloak.example.com/realms/korvet/protocol/openid-connect/token
sasl.login.callback.handler.class=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginCallbackHandler
sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required \
clientId="korvet-pipeline" \
clientSecret="<client secret>";
The client obtains a token from the IdP’s token endpoint at login (and again before each re-authentication); the broker never sees the client secret, only the resulting signed token.
Kerberos Authentication (GSSAPI)
Clients in Kerberos environments (MIT Kerberos, FreeIPA, Active Directory) can authenticate with
service tickets instead of stored passwords. Unlike the password-based mechanisms, GSSAPI
verifies no credential stored in Korvet: the broker itself is a Kerberos principal —
conventionally kafka/<host>@REALM — whose long-term keys live in a keytab, and the KDC vouches
for clients by issuing them tickets for that service principal.
Broker Configuration
Create the broker’s service principal and keytab in your KDC, then add GSSAPI to the advertised
mechanisms and point Korvet at the keytab:
korvet:
broker:
sasl:
enabled: true
mechanisms:
- SCRAM-SHA-256
- GSSAPI
gssapi:
keytab-file: /etc/korvet/korvet.keytab
principal: kafka/broker1.example.com@EXAMPLE.COM
# service-name: kafka # must match the clients' sasl.kerberos.service.name
keytab-file and principal are required when GSSAPI is advertised; the broker logs in from
the keytab at startup and fails fast if the keytab, principal, or Kerberos configuration
(/etc/krb5.conf, or the file named by the java.security.krb5.conf system property) is
invalid. GSSAPI tokens are not replayable secrets, so — matching Kafka — TLS is not mandatory for
GSSAPI, though it is still recommended to encrypt message traffic. Only quality-of-protection
auth is supported (no GSSAPI integrity or confidentiality layers), and tokens must arrive in
SaslAuthenticate frames (any client from Kafka 0.10 on).
Principal Mapping
The authenticated Kerberos identity is mapped to its short name by keeping only the primary
component: alice@EXAMPLE.COM and alice/host.example.com@EXAMPLE.COM both become alice.
Topic ACL rules apply to the short name exactly as they do for SCRAM usernames — grant ACLs to
alice, not the full principal. Kerberos carries no tenant identifier, so GSSAPI connections
have no tenant id.
Client Configuration
security.protocol=SASL_PLAINTEXT
sasl.mechanism=GSSAPI
sasl.kerberos.service.name=kafka
sasl.jaas.config=com.sun.security.auth.module.Krb5LoginModule required \
useKeyTab=true \
storeKey=true \
keyTab="/etc/security/keytabs/client.keytab" \
principal="client@EXAMPLE.COM";
Use SASL_SSL instead of SASL_PLAINTEXT when the broker listener has TLS enabled. The client
requests a ticket for <service-name>/<broker-host>@REALM, so every advertised broker host must
have a matching service principal in the KDC (or in the broker keytab when all brokers share
one).
Console Single Sign-On (OIDC)
Humans can sign in to the admin console through the same external identity provider that issues
broker tokens, using the OIDC authorization-code flow with PKCE. After the IdP authenticates the
user, the console validates the returned id_token exactly like the broker validates bearer
tokens (JWKS signature, issuer, audience, expiry — plus the login nonce), maps the user’s groups
claim to a console role, and issues the regular session cookie. The IdP token is consumed
server-side during the callback and never reaches the browser.
Local password login stays available as a break-glass fallback: when single sign-on is enabled the login page leads with the Sign in with … button and tucks the password form behind a secondary action. First-run setup still creates the local break-glass admin even when OIDC is configured, and federated users get no rows in the local user store — their identity lives only in their session.
Configuration
korvet:
admin:
oauth:
enabled: true
issuer-uri: https://keycloak.example.com/realms/korvet
client-id: korvet-console
audience: korvet-console
roles:
admin-group: korvet-admins
viewer-group: korvet-viewers
# provider-name: Keycloak # label for the login button (default "SSO")
# scopes: openid profile
# username-claim: preferred_username
# roles.claim: groups
# redirect-uri: https://korvet.example.com/api/v1/auth/oidc/callback
# client-secret: only for IdPs that demand a confidential client
# jwks-uri: optional override; discovered from the issuer by default
# clock-skew: 30s
When enabled is true, issuer-uri, client-id, audience, and at least one of
roles.admin-group / roles.viewer-group are required; startup fails fast with a clear message
otherwise. Endpoint and JWKS discovery is lazy, so the console starts (and password login works)
even while the IdP is unreachable — only single sign-on attempts fail in that window.
Role Mapping
The claim named by roles.claim (default groups) is read from the validated id_token. A
user whose groups contain roles.admin-group signs in as ADMIN; otherwise a match on
roles.viewer-group grants VIEWER; a user matching neither is rejected with a clear error
on the login page. Role changes at the IdP take effect on the next login — the console session
keeps the role it was minted with until it expires or is logged out.
Keycloak Walkthrough
In your realm (for example korvet):
-
Groups: create
korvet-adminsandkorvet-viewers, and assign members. -
Clients → Create client: client ID
korvet-console, client authentication Off (public client), standard flow On. Set the valid redirect URI tohttps://<console-host>/api/v1/auth/oidc/callback. Under Advanced, set Proof Key for Code Exchange Code Challenge Method toS256. -
Group membership mapper: on the client’s dedicated scope, Add mapper → Group Membership, token claim name
groups, Full group path Off, Add to ID token On. -
Point the console at the realm with the configuration above. Keycloak sets the
id_tokenaudience to the client ID, soaudience: korvet-consoleis correct without an extra mapper.
Sign-out clears and revokes the local console session; IdP single logout is currently not propagated (signing in again while the IdP session is still alive will not prompt for credentials).
Managing Credentials
Credential Storage
Credentials are stored in Redis using secure PBKDF2 password hashing:
-
Algorithm: PBKDF2WithHmacSHA256
-
Iterations: 10,000
-
Salt: 128-bit random per credential
-
Hash: 256-bit output
Creating User Credentials
Use the Korvet admin API or Redis CLI to create user credentials. The admin API (see Deployment for enabling it) exposes credential management over HTTP; the Redis CLI approach below is shown for direct access.
Using Redis CLI
# Store a SCRAM-SHA-256 credential for user "alice" in tenant "tenant1"
redis-cli HSET korvet:broker:credentials:alice \
mechanism SCRAM-SHA-256 \
password_hash <base64-encoded-StoredKey> \
server_key <base64-encoded-ServerKey> \
salt <base64-encoded-salt> \
iterations 10000 \
tenant_id tenant1
For SCRAM, password_hash stores the Base64-encoded StoredKey and server_key
stores the Base64-encoded ServerKey. Do not store the salted password.
To create a PLAIN credential (only usable when PLAIN is advertised and TLS
is enabled), use mechanism PLAIN with password_hash, salt, and
iterations fields.
Programmatic Creation
import com.redis.korvet.broker.redis.RedisCredentialStore;
// Create credential store
RedisCredentialStore credentialStore =
new RedisCredentialStore(redisClient, "korvet");
PasswordHasher passwordHasher = new PasswordHasher();
// Hash the password
PasswordHasher.HashedPassword hashed =
passwordHasher.hashPassword("secret-password");
// Store the credential
StoredCredential credential = StoredCredential.builder()
.username("alice")
.mechanism("PLAIN")
.passwordHash(hashed.getHash())
.salt(hashed.getSalt())
.iterations(hashed.getIterations())
.tenantId("tenant1")
.build();
credentialStore.storeCredential(credential);
Updating Credentials
To update a user’s password, store a new credential with the same username:
// Hash new password
PasswordHasher.HashedPassword newHashed =
passwordHasher.hashPassword("new-password");
// Update credential
StoredCredential updated = StoredCredential.builder()
.username("alice")
.mechanism("PLAIN")
.passwordHash(newHashed.getHash())
.salt(newHashed.getSalt())
.iterations(newHashed.getIterations())
.tenantId("tenant1")
.build();
credentialStore.storeCredential(updated);
Client Configuration
Kafka Producer
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
StringSerializer.class.getName());
// SASL configuration (default SCRAM-SHA-256 mechanism)
props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_PLAINTEXT");
props.put(SaslConfigs.SASL_MECHANISM, "SCRAM-SHA-256");
props.put(SaslConfigs.SASL_JAAS_CONFIG,
"org.apache.kafka.common.security.scram.ScramLoginModule required " +
"username=\"alice\" " +
"password=\"secret-password\";");
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
For PLAIN clients, use the SASL_SSL security protocol (PLAIN requires TLS) and Kafka’s PLAIN login module:
props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_SSL");
props.put(SaslConfigs.SASL_MECHANISM, "PLAIN");
props.put(SaslConfigs.SASL_JAAS_CONFIG,
"org.apache.kafka.common.security.plain.PlainLoginModule required " +
"username=\"alice\" " +
"password=\"secret-password\";");
Kafka Consumer
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "my-group");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());
// SASL configuration (default SCRAM-SHA-256 mechanism)
props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_PLAINTEXT");
props.put(SaslConfigs.SASL_MECHANISM, "SCRAM-SHA-256");
props.put(SaslConfigs.SASL_JAAS_CONFIG,
"org.apache.kafka.common.security.scram.ScramLoginModule required " +
"username=\"alice\" " +
"password=\"secret-password\";");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
For PLAIN, set security.protocol=SASL_SSL, sasl.mechanism=PLAIN, and use the PlainLoginModule (PLAIN requires TLS).
Command Line Tools
# kafka-console-producer
kafka-console-producer \
--bootstrap-server localhost:9092 \
--topic test \
--producer-property security.protocol=SASL_PLAINTEXT \
--producer-property sasl.mechanism=SCRAM-SHA-256 \
--producer-property 'sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="alice" password="secret-password";'
# kafka-console-consumer
kafka-console-consumer \
--bootstrap-server localhost:9092 \
--topic test \
--from-beginning \
--consumer-property security.protocol=SASL_PLAINTEXT \
--consumer-property sasl.mechanism=SCRAM-SHA-256 \
--consumer-property 'sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="alice" password="secret-password";'
Console Roles
Admin console users (the accounts used for the web UI, the admin REST API, Swagger, and actuator Basic auth) carry one of two roles:
-
ADMIN — full read-write access. The default for every user created without an explicit role, so existing single-admin deployments upgrade with no behavior change.
-
VIEWER — read-only access:
GETrequests succeed, every mutation (POST/PUT/DELETE, including user, credential, and ACL management, topic changes, and log-level changes) is rejected with403 Forbidden. The web UI hides mutating actions for viewers, but the server is the enforcement point.
The role is stored on the user, embedded in the session cookie at login (sessions issued before
roles existed keep full admin rights until they expire), and reported by
GET /api/v1/auth/status so the UI can adapt. Assign a role at creation
(POST /api/v1/users with "role": "VIEWER") or change it later
(PUT /api/v1/users/{username} with "role": …). The last remaining ADMIN can be neither
deleted nor demoted, so a console cannot lose its last administrator.
Console roles are the mapping target for federated-auth phase 2 (IdP group claims → roles); see
docs/design/FEDERATED_AUTHENTICATION.adoc.
Console Login Security
Console password logins (POST /api/v1/auth/login) are protected against
brute force and session theft.
Login Rate Limiting
Login attempts are rate-limited per username and client IP address:
-
After
korvet.admin.login-throttle.max-failuresconsecutive failures (default: 10), the username+IP pair is locked out -
Initial lockout duration:
korvet.admin.login-throttle.initial-lockout(default: 30 seconds) -
Lockout duration doubles with each additional failure, up to
korvet.admin.login-throttle.max-lockout(default: 15 minutes) -
Locked-out login attempts return HTTP
429 Too Many Requestswith aRetry-Afterheader -
A successful login or a
max-lockoutperiod without failed attempts clears the failure counter
korvet:
admin:
login-throttle:
max-failures: 10 # Failures before lockout
initial-lockout: 30s # First lockout duration
max-lockout: 15m # Maximum lockout duration
Session Revocation
Logging out revokes the session token immediately:
-
Session JWTs carry a unique
jti(JWT ID) claim -
POST /api/v1/auth/logoutstores the token’sjtiin a Redis denylist with a TTL matching the token’s expiration -
Subsequent requests with a revoked token are rejected, even if the token has not expired, so stolen or copied session cookies become invalid immediately upon logout
-
Password and role changes through
PUT /api/v1/users/{username}likewise revoke the user’s existing sessions
Legacy tokens without a jti claim (issued by versions before session
revocation existed) are rejected and cannot be used.
Changing your own password requires the current password in the request
(currentPassword); admin resets of other accounts must omit it. See
Admin API for the endpoint details.
Authorization (ACLs)
SASL authentication alone only prevents unauthenticated access — once connected, every authenticated principal can produce to and consume from any topic. ACLs add per-user authorization on top of authentication, for topics and for consumer groups.
Enabling ACL Enforcement
korvet:
broker:
sasl:
enabled: true
acl:
enabled: true
korvet.broker.acl.enabled requires korvet.broker.sasl.enabled=true; enabling ACLs without
SASL fails validation at startup.
Authorization Model
ACL rules are allow-only grants of the form (principal, resourceType, resource, operation):
-
principal - the SASL username, or
group:<name>for an IdP group (see Group Claims and ACL Federation) -
resourceType -
TOPIC(the default) orGROUP(a consumer group id) -
resource - the resource name; see matching below
-
operation -
READorWRITE. For topics,READgates fetch andWRITEgates produce. For consumer groups,READgates the group cycle:JoinGroup,OffsetCommit, andOffsetFetchfor that group id.
Resource names match in one of three ways. The precedence below is documentation of intent — because rules are allow-only, a grant by any match allows the operation:
-
exact — the resource name as-is;
-
prefix — a name ending in
(for examplelogs-) matches every resource whose name starts with the part before the*; -
wildcard — the lone
*matches every resource of the rule’s type.
When ACL enforcement is enabled, a principal may only perform operations it has been granted;
everything else — including resources with no rules at all — is denied with the standard Kafka
error: TOPIC_AUTHORIZATION_FAILED (TopicAuthorizationException) for topics,
GROUP_AUTHORIZATION_FAILED (GroupAuthorizationException) for consumer groups. Consuming
through a group therefore needs both a TOPIC/READ grant for the topic and a GROUP/READ
grant for the group id.
Rules are stored in Redis in a single JSON document at {namespace}:broker:acls, one member
per principal. The principal’s policy is resolved once per connection at authentication time,
so rule changes apply to connections established afterwards.
|
Storage-format migration note. Documents written before consumer-group rules existed hold only
the per-principal Prefix matching behavior change. Before prefix rules existed, a stored topic rule whose name
ends in |
Managing ACL Rules
ACL rules are managed through the admin REST API. The pre-group request shape
({principal, topic, operation}) still works and creates a TOPIC rule:
# Grant alice WRITE on orders (legacy shape, implies resourceType=TOPIC)
curl -u admin:admin-password -X POST http://localhost:8080/api/v1/acls \
-H 'Content-Type: application/json' \
-d '{"principal":"alice","topic":"orders","operation":"WRITE"}'
# Grant bob READ on all topics
curl -u admin:admin-password -X POST http://localhost:8080/api/v1/acls \
-H 'Content-Type: application/json' \
-d '{"principal":"bob","resourceType":"TOPIC","resource":"*","operation":"READ"}'
# Grant alice READ on every topic starting with logs-
curl -u admin:admin-password -X POST http://localhost:8080/api/v1/acls \
-H 'Content-Type: application/json' \
-d '{"principal":"alice","resourceType":"TOPIC","resource":"logs-*","operation":"READ"}'
# Let alice consume through the billing-consumers group
curl -u admin:admin-password -X POST http://localhost:8080/api/v1/acls \
-H 'Content-Type: application/json' \
-d '{"principal":"alice","resourceType":"GROUP","resource":"billing-consumers","operation":"READ"}'
# List alice's rules
curl -u admin:admin-password http://localhost:8080/api/v1/acls/alice
# Delete a rule (legacy topic param, or resourceType + resource)
curl -u admin:admin-password -X DELETE \
'http://localhost:8080/api/v1/acls/alice?topic=orders&operation=WRITE'
curl -u admin:admin-password -X DELETE \
'http://localhost:8080/api/v1/acls/alice?resourceType=GROUP&resource=billing-consumers&operation=READ'
Multi-Tenancy
Each credential is associated with a tenant ID. For OAUTHBEARER connections, the tenant ID can
instead come from a token claim named by korvet.broker.sasl.oauth.tenant-claim; it is
extracted and carried on the connection context, but no enforcement is attached to it yet
(multi-tenancy is tracked in issue
#1383). When a client
authenticates, the tenant ID is attached to the connection and can be used for:
-
Data isolation - Separate topics and consumer groups per tenant
-
Resource quotas - Limit resources per tenant
-
Access control - Restrict access to tenant-specific resources
Tenant Mapping
// User "alice" belongs to "tenant1"
StoredCredential credential = StoredCredential.builder()
.username("alice")
.tenantId("tenant1")
// ... other fields
.build();
// User "bob" belongs to "tenant2"
StoredCredential credential2 = StoredCredential.builder()
.username("bob")
.tenantId("tenant2")
// ... other fields
.build();
Security Best Practices
Password Security
-
Use strong, randomly generated passwords
-
Rotate passwords regularly
-
Never commit passwords to version control
-
Use environment variables or secret management systems
Network Security
SASL/PLAIN transmits credentials in base64 encoding (not encrypted). Korvet therefore requires TLS whenever PLAIN is advertised, so PLAIN clients must use the SASL_SSL security protocol. SCRAM-SHA-256 does not send the password and can be used over SASL_PLAINTEXT, though TLS is still recommended in production:
-
Use TLS - Required for
PLAINandOAUTHBEARER(SASL_SSL); recommended for SCRAM -
Network isolation - Deploy in private networks
-
Firewall rules - Restrict access to Korvet port
Troubleshooting
Authentication Failures
Invalid Credentials
ERROR Authentication failed for user 'alice': Invalid password
Solution: Verify the username and password are correct.
Migration Guide
Enabling Authentication on Existing Deployment
| Enabling authentication will break existing unauthenticated clients. |
-
Create credentials for all existing applications
-
Update client configurations with SASL settings
-
Test authentication with a subset of clients
-
Enable SASL in Korvet configuration
-
Monitor for authentication failures
-
Update remaining clients