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:

  1. Groups → Create group: create payments-team and add your service-account clients (or users) to it.

  2. 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.

  3. Point the broker at the claim:

    korvet:
      broker:
        sasl:
          oauth:
            groups-claim: groups
  4. 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:

  1. Clients → Create client: client ID korvet-pipeline, client authentication On, service accounts roles On (this enables the client-credentials grant).

  2. Add an audience mapper so tokens carry the broker audience: Client scopes → dedicated scope → Add mapper → Audience, included client audience korvet-broker.

  3. 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):

  1. Groups: create korvet-admins and korvet-viewers, and assign members.

  2. Clients → Create client: client ID korvet-console, client authentication Off (public client), standard flow On. Set the valid redirect URI to https://<console-host>/api/v1/auth/oidc/callback. Under Advanced, set Proof Key for Code Exchange Code Challenge Method to S256.

  3. 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.

  4. Point the console at the realm with the configuration above. Keycloak sets the id_token audience to the client ID, so audience: korvet-console is 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);

Deleting Credentials

credentialStore.deleteCredential("alice");

Or using Redis CLI:

redis-cli DEL korvet:broker:credentials:alice

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: GET requests succeed, every mutation (POST/PUT/DELETE, including user, credential, and ACL management, topic changes, and log-level changes) is rejected with 403 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-failures consecutive 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 Requests with a Retry-After header

  • A successful login or a max-lockout period 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/logout stores the token’s jti in 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) or GROUP (a consumer group id)

  • resource - the resource name; see matching below

  • operation - READ or WRITE. For topics, READ gates fetch and WRITE gates produce. For consumer groups, READ gates the group cycle: JoinGroup, OffsetCommit, and OffsetFetch for 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:

  1. exact — the resource name as-is;

  2. prefix — a name ending in (for example logs-) matches every resource whose name starts with the part before the *;

  3. 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 topics map and load unchanged (each entry is treated as a literal topic rule). A groups map is added to a principal’s document only when its first GROUP rule is created. Brokers older than this format cannot parse documents that contain groups, so upgrade every broker before adding GROUP (or prefix) rules. No action is needed for deployments that keep using topic-only rules.

Prefix matching behavior change. Before prefix rules existed, a stored topic rule whose name ends in (for example logs-) matched only a topic literally so named — a name no conforming Kafka client can create or use, since is not a legal topic-name character. After upgrading, such a rule is interpreted as a prefix rule and grants its operations on every topic starting with logs-. Audit existing rules for trailing- names before upgrading if that widening is not intended.

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 PLAIN and OAUTHBEARER (SASL_SSL); recommended for SCRAM

  • Network isolation - Deploy in private networks

  • Firewall rules - Restrict access to Korvet port

Credential Management

  • Principle of least privilege - Create separate credentials per application

  • Audit access - Monitor authentication attempts

  • Revoke unused credentials - Delete credentials for decommissioned applications

Troubleshooting

Authentication Failures

Invalid Credentials

ERROR Authentication failed for user 'alice': Invalid password

Solution: Verify the username and password are correct.

User Not Found

ERROR Authentication failed for user 'bob': User not found

Solution: Create the credential using the credential store.

Mechanism Not Supported

ERROR Unsupported SASL mechanism: SCRAM-SHA-512

Solution: Use a supported mechanism (PLAIN or SCRAM-SHA-256).

Connection Issues

Client Configuration

Verify the client is configured with:

  • security.protocol=SASL_PLAINTEXT for SCRAM-SHA-256, or SASL_SSL for PLAIN (PLAIN requires TLS)

  • sasl.mechanism=SCRAM-SHA-256 (default) or sasl.mechanism=PLAIN

  • Correct JAAS configuration with username and password

Server Configuration

Verify SASL is enabled in Korvet:

# Check environment variable
echo $KORVET_BROKER_SASL_ENABLED

# Should output: true

Debugging

Enable debug logging for authentication:

logging:
  level:
    com.redis.korvet.broker.auth: DEBUG
    com.redis.korvet.broker.kafka.SaslHandshakeHandler: DEBUG
    com.redis.korvet.broker.kafka.SaslAuthenticateHandler: DEBUG

Migration Guide

Enabling Authentication on Existing Deployment

Enabling authentication will break existing unauthenticated clients.
  1. Create credentials for all existing applications

  2. Update client configurations with SASL settings

  3. Test authentication with a subset of clients

  4. Enable SASL in Korvet configuration

  5. Monitor for authentication failures

  6. Update remaining clients

Disabling Authentication

To disable authentication:

korvet:
  broker:
    sasl:
      enabled: false

Clients can then connect without authentication.