This guide covers how to configure and use authentication in AxonOps Schema Registry. Authentication controls who can access the registry API. When combined with role-based access control (RBAC), it also determines what each user can do.
- Overview
- Enabling Authentication
- Bootstrap Admin User
- Basic Authentication
- API Key Authentication
- LDAP / Active Directory
- OIDC (OpenID Connect)
- JWT (JSON Web Token)
- mTLS (Mutual TLS)
- Roles and Permissions
- User Management API
- API Key Management API
- Admin CLI
- Combining Authentication Methods
- Related Documentation
Authentication is optional but recommended for production deployments. When enabled, the registry supports multiple authentication methods simultaneously. Requests are evaluated against each configured method in the order they appear in the methods list until one succeeds:
- API Key -- header, query parameter, or Confluent-compatible Basic Auth format
- Basic Auth -- database users with bcrypt-hashed passwords, with LDAP fallback if configured
- OIDC Bearer Token -- OpenID Connect token validation
- JWT Bearer Token -- static key or JWKS-based token validation
- mTLS (Transport Only) -- client certificate verification at the transport layer (not an auth method)
When authentication is disabled (the default), all endpoints are accessible without credentials. The health check (/) and metrics (/metrics) endpoints are always unauthenticated regardless of configuration.
Add the security.auth section to your configuration file:
security:
auth:
enabled: true
methods:
- api_key
- basic
rbac:
enabled: true
default_role: readonlyThe methods list defines which authentication methods are active and the order in which they are tried. Valid values are api_key, basic, jwt, and oidc. mTLS is transport-level security configured via security.tls.*, not an auth method.
When a request fails all configured methods, the registry returns 401 Unauthorized with appropriate WWW-Authenticate headers for each enabled method.
When deploying for the first time with an empty user database, you need an initial admin account. The bootstrap mechanism solves this chicken-and-egg problem.
Add the bootstrap section to your configuration:
security:
auth:
enabled: true
methods:
- basic
bootstrap:
enabled: true
username: admin
password: ${ADMIN_PASSWORD}
email: admin@example.comSet the password via environment variable:
export ADMIN_PASSWORD='a-strong-random-password'The bootstrap process is idempotent. If one or more users already exist in the database, the bootstrap step is skipped entirely. The created user is assigned the super_admin role.
Bootstrap credentials can also be set entirely through environment variables:
| Variable | Description |
|---|---|
SCHEMA_REGISTRY_BOOTSTRAP_ENABLED |
Set to true to enable bootstrap |
SCHEMA_REGISTRY_BOOTSTRAP_USERNAME |
Admin username |
SCHEMA_REGISTRY_BOOTSTRAP_PASSWORD |
Admin password |
SCHEMA_REGISTRY_BOOTSTRAP_EMAIL |
Admin email (optional) |
The schema-registry-admin CLI tool can bootstrap an admin user by connecting directly to the database, bypassing the API entirely:
schema-registry-admin init \
--storage-type postgresql \
--pg-host localhost --pg-port 5432 \
--pg-database schema_registry \
--pg-user postgres --pg-password dbpass \
--admin-username admin \
--admin-password 'a-strong-random-password' \
--admin-email admin@example.comThis approach is useful when you want to create the admin user before starting the registry for the first time.
Basic Auth validates credentials against the database using bcrypt-hashed passwords. If LDAP is configured and allow_fallback is true (the default), credentials for users not found in LDAP are tried against the database as a fallback. Users that exist in LDAP but provide the wrong password are rejected immediately — no fallback occurs. Set allow_fallback: false to disable fallback entirely.
security:
auth:
enabled: true
methods:
- basic
basic:
realm: "Schema Registry"The realm value appears in the WWW-Authenticate: Basic realm="..." response header.
curl -u admin:password http://localhost:8081/subjectscurl -H "Authorization: Basic $(echo -n admin:password | base64)" \
http://localhost:8081/subjectsWhen a request arrives with Basic Auth credentials, the registry evaluates them in this order:
- API key lookup -- the username is checked as a potential API key value (for Confluent client compatibility)
- LDAP -- if an LDAP provider is configured, credentials are validated against the directory
- Database -- credentials are validated against the local user database (bcrypt comparison)
- Config-based users -- legacy fallback to users defined directly in the YAML config
API keys provide programmatic access without exposing user passwords. They are scoped to a role, have optional expiration, and are stored as SHA-256 hashes (or HMAC-SHA256 when a server secret is configured).
1. Confluent-compatible Basic Auth format
This is the recommended method for Kafka producers, consumers, and tools that already support Confluent Schema Registry authentication. The API key is sent as the username; the password field is ignored.
curl -u "sr_live_abc123def456:x" http://localhost:8081/subjects2. X-API-Key header
curl -H "X-API-Key: sr_live_abc123def456" http://localhost:8081/subjects3. Query parameter
curl "http://localhost:8081/subjects?api_key=sr_live_abc123def456"security:
auth:
enabled: true
methods:
- api_key
api_key:
header: X-API-Key
query_param: api_key
key_prefix: "sr_live_"
secret: "${API_KEY_SECRET}"
cache_refresh_seconds: 60| Field | Description | Default |
|---|---|---|
header |
HTTP header name for API key lookup | X-API-Key |
query_param |
Query parameter name for API key lookup | api_key |
key_prefix |
Prefix prepended to generated keys for identification | "" |
secret |
HMAC-SHA256 secret for key hashing (defense-in-depth) | "" (plain SHA-256) |
cache_refresh_seconds |
How often the key cache is refreshed from the database | 60 |
- Hashed at rest -- keys are stored as SHA-256 hashes (or HMAC-SHA256 with a server secret)
- Shown once -- the raw key is returned only at creation time and is never retrievable afterward
- Optional HMAC pepper -- when
secretis configured, even a database compromise does not allow key verification without the secret - Validated on use -- enabled status and expiration are checked on every request
- Cached for performance -- validated keys are cached in memory and refreshed from the database at the configured interval, ensuring cluster-wide consistency
curl -u admin:password -X POST http://localhost:8081/admin/apikeys \
-H "Content-Type: application/json" \
-d '{
"name": "ci-pipeline",
"role": "developer",
"expires_in": 2592000
}'The expires_in field is the key lifetime in seconds (2592000 = 30 days). The response includes the raw key:
{
"id": 1,
"key": "sr_live_a1b2c3d4e5f6...",
"key_prefix": "a1b2c3d4",
"name": "ci-pipeline",
"role": "developer",
"user_id": 1,
"username": "admin",
"enabled": true,
"created_at": "2026-02-16T10:00:00Z",
"expires_at": "2026-03-18T10:00:00Z"
}Save the key value immediately. It cannot be retrieved later.
LDAP authentication integrates with existing directory services. Users authenticate with their directory credentials, and LDAP group memberships are mapped to registry roles.
security:
auth:
enabled: true
methods:
- basic
ldap:
enabled: true
url: "ldap://ldap.example.com:389"
bind_dn: "cn=service-account,ou=Services,dc=example,dc=com"
bind_password: "${LDAP_BIND_PASSWORD}"
base_dn: "dc=example,dc=com"
user_search_base: "ou=Users,dc=example,dc=com"
user_search_filter: "(sAMAccountName=%s)"
username_attribute: "sAMAccountName"
email_attribute: "mail"
group_attribute: "memberOf"
role_mapping:
"CN=SchemaAdmins,OU=Groups,DC=example,DC=com": "admin"
"CN=Developers,OU=Groups,DC=example,DC=com": "developer"
"SchemaAdmins": "admin"
"Developers": "developer"
default_role: "readonly"
start_tls: true
ca_cert_file: "/etc/ssl/certs/ldap-ca.pem"
client_cert_file: "/etc/ssl/certs/ldap-client.pem"
client_key_file: "/etc/ssl/private/ldap-client-key.pem"
allow_fallback: true # Set to false for strict LDAP-only auth
connection_timeout: 10
request_timeout: 30- The registry connects to the LDAP server using the service account (
bind_dn). - It searches for the user with the configured
user_search_filter, substituting%swith the provided username. - If the user is found, the registry re-binds with the user's own DN and supplied password to verify credentials.
- On successful authentication, the user's group memberships are extracted from the
group_attribute. - Groups are matched against
role_mappingto determine the registry role. Both full DNs and Common Names (CN) are supported for matching, with case-insensitive comparison on CNs.
For encrypted connections:
- LDAPS -- use
ldaps://in the URL (e.g.,ldaps://ldap.example.com:636) - StartTLS -- set
start_tls: truewith a plainldap://URL
Both methods support custom CA certificates via ca_cert_file and client certificate authentication (mTLS) via client_cert_file / client_key_file. For development and testing environments, insecure_skip_verify: true disables certificate validation (not recommended for production).
Security warning: If LDAP is configured without TLS (
ldap://URL andstart_tls: false), the registry logs a warning at startup and emits asecurity_warningaudit event withreason: ldap_no_tls. Bind credentials and user passwords are transmitted in plaintext over non-TLS connections, which MUST be avoided in production.
| Field | Description | Default |
|---|---|---|
url |
LDAP server URL | (required) |
bind_dn |
Service account DN for searches | (required) |
bind_password |
Service account password | (required) |
base_dn |
Base DN for searches | (required) |
user_search_base |
DN to start user searches from | Same as base_dn |
user_search_filter |
LDAP filter for finding users (%s = username) |
(sAMAccountName=%s) |
username_attribute |
Attribute containing the username | sAMAccountName |
email_attribute |
Attribute containing the email | mail |
group_attribute |
Attribute containing group memberships | memberOf |
role_mapping |
Map of LDAP group names/DNs to registry roles | {} |
default_role |
Role assigned when no group mapping matches | readonly |
start_tls |
Upgrade connection to TLS via StartTLS | false |
ca_cert_file |
Path to CA certificate for TLS verification | "" |
client_cert_file |
Path to client certificate for mTLS | "" |
client_key_file |
Path to client private key for mTLS | "" |
insecure_skip_verify |
Skip TLS certificate verification | false |
allow_fallback |
Allow fallback to DB/htpasswd when user is not found in LDAP | true |
connection_timeout |
Connection timeout in seconds | 10 |
request_timeout |
Search request timeout in seconds | 30 |
By default (allow_fallback: true), when an LDAP user search returns "user not found", the registry falls back to other configured authentication methods (database users, htpasswd). This allows mixed environments where some users are in LDAP and others are local accounts (e.g. a bootstrap admin user).
Security note: Fallback only occurs when the user does not exist in LDAP. If the user exists in LDAP but provides the wrong password, the request is rejected immediately with HTTP 401 — no fallback occurs. This prevents users from bypassing LDAP password policies (complexity requirements, expiry, account lockout, MFA) by authenticating against a local database password instead.
Set allow_fallback: false for strict LDAP-only authentication. In this mode, all LDAP failures (user not found, wrong password, server unreachable) result in an immediate HTTP 401 — no fallback to database or htpasswd users occurs.
LDAP-authenticated requests use auth_method: ldap in audit events. Users who authenticated via DB/htpasswd fallback (not found in LDAP) use auth_method: ldap_fallback, distinguishing them from direct database authentication (auth_method: basic). See Audit Logging for details.
OIDC authentication validates Bearer tokens against an OpenID Connect provider. This works with Keycloak, Okta, Auth0, Azure AD, and any standards-compliant OIDC provider.
security:
auth:
enabled: true
methods:
- oidc
oidc:
enabled: true
issuer_url: "https://auth.example.com/realms/myorg"
client_id: "schema-registry"
username_claim: "preferred_username"
roles_claim: "realm_access.roles"
role_mapping:
"schema-admin": "admin"
"schema-developer": "developer"
"schema-reader": "readonly"
default_role: "readonly"
required_audience: "schema-registry"
allowed_algorithms:
- RS256
- ES256Obtain a token from your OIDC provider and pass it as a Bearer token:
TOKEN=$(curl -s -X POST "https://auth.example.com/realms/myorg/protocol/openid-connect/token" \
-d "grant_type=client_credentials" \
-d "client_id=schema-registry" \
-d "client_secret=your-client-secret" | jq -r '.access_token')
curl -H "Authorization: Bearer $TOKEN" http://localhost:8081/subjects- The registry fetches the OIDC discovery document from
issuer_urlat startup. - For each request with a Bearer token, the token is verified against the provider's public keys.
- The
client_idis checked against the token's audience. - If
required_audienceis set, theaudclaim must contain that value. - The username is extracted from the configured
username_claim(with fallback tosub). - Roles are extracted from
roles_claim, which supports dot notation for nested claims (e.g.,realm_access.rolesfor Keycloak). - Extracted roles are mapped to registry roles via
role_mapping.
| Field | Description | Default |
|---|---|---|
issuer_url |
OIDC provider URL (must serve .well-known/openid-configuration) |
(required) |
client_id |
Client ID for token audience validation | (required) |
username_claim |
Token claim containing the username | sub |
roles_claim |
Token claim containing roles (supports dot notation) | "" |
role_mapping |
Map of OIDC roles to registry roles | {} |
default_role |
Role assigned when no role mapping matches | readonly |
required_audience |
Required value in the aud claim |
"" |
allowed_algorithms |
Restrict accepted signing algorithms | [] (all supported) |
skip_issuer_check |
Skip issuer validation (testing only) | false |
skip_expiry_check |
Skip token expiry validation (testing only) | false |
JWT authentication validates tokens using a static public key file or a JWKS (JSON Web Key Set) URL. Use this when you have a custom token issuer that does not implement full OIDC discovery.
security:
auth:
enabled: true
methods:
- jwt
jwt:
public_key_file: "/etc/schema-registry/jwt-public.pem"
algorithm: "RS256"
issuer: "https://auth.example.com"
audience: "schema-registry"
claims_mapping:
role: "custom_role_claim"
roles: "custom_roles_claim"security:
auth:
enabled: true
methods:
- jwt
jwt:
jwks_url: "https://auth.example.com/.well-known/jwks.json"
algorithm: "RS256"
issuer: "https://auth.example.com"
audience: "schema-registry"curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
http://localhost:8081/subjects- The token is parsed and the signing method is validated against the configured
algorithm. - If a
public_key_fileis configured, it is used for signature verification. Supported formats: RSA (PEM), ECDSA (PEM), or HMAC (raw bytes). - If a
jwks_urlis configured, the key is looked up by the token'skidheader. The JWKS key set is cached for 5 minutes and refreshed automatically. - Standard claims are validated:
iss(issuer),aud(audience),exp(expiration). - The username is extracted from
sub,preferred_username, oremail(in that order). - The role is extracted from the
roleorrolesclaim, with support for custom claim names viaclaims_mapping.
| Field | Description | Default |
|---|---|---|
public_key_file |
Path to PEM-encoded public key file | "" |
jwks_url |
URL to JWKS endpoint | "" |
algorithm |
Expected signing algorithm (RS256, RS384, RS512, ES256, ES384, ES512, HS256, HS384, HS512) | "" |
issuer |
Expected token issuer (iss claim) |
"" |
audience |
Expected token audience (aud claim) |
"" |
claims_mapping |
Map of standard claim names to custom claim names | {} |
mTLS is transport-level security only. It verifies that connecting clients present a valid certificate signed by a trusted CA, but it does NOT provide authentication or authorization. To get user identity and RBAC, layer mTLS with an authentication method such as basic, jwt, or oidc.
Important:
mtlsis NOT a valid value forsecurity.auth.methods. It is configured exclusively viasecurity.tls.*settings.
Enable TLS with client certificate verification:
security:
tls:
enabled: true
cert_file: "/etc/schema-registry/server.crt"
key_file: "/etc/schema-registry/server.key"
ca_file: "/etc/schema-registry/ca.crt"
client_auth: "verify"
min_version: "TLS1.3"To combine mTLS with authentication and RBAC:
security:
tls:
enabled: true
cert_file: "/etc/schema-registry/server.crt"
key_file: "/etc/schema-registry/server.key"
ca_file: "/etc/schema-registry/ca.crt"
client_auth: "verify"
auth:
enabled: true
methods:
- basic
rbac:
enabled: true
default_role: readonly| Mode | Behavior |
|---|---|
none |
No client certificate requested |
request |
Client certificate requested but not required |
require |
Client certificate required but not verified against CA |
verify |
Client certificate required and verified against the CA in ca_file |
For mTLS transport security, use verify to ensure clients present valid certificates signed by your CA.
curl --cert client.crt --key client.key --cacert ca.crt \
https://localhost:8081/subjectsWhen mTLS is active, all audit events include "transport_security": "mtls". When TLS is used without client certificates, audit events include "transport_security": "tls". Without TLS, events include "transport_security": "none".
The registry uses a fixed set of roles with predefined permissions. Roles cannot be customized, but the super_admins list in the RBAC configuration grants unrestricted access to specific usernames regardless of their assigned role.
| Role | Schema Read | Schema Write | Schema Delete | Config Read | Config Write | Mode Read | Mode Write | User Mgmt |
|---|---|---|---|---|---|---|---|---|
super_admin |
Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
admin |
Yes | Yes | Yes | Yes | Yes | Yes | Yes | Read only |
developer |
Yes | Yes | No | Yes | No | Yes | No | No |
readonly |
Yes | No | No | Yes | No | Yes | No | No |
security:
auth:
rbac:
enabled: true
default_role: readonly
super_admins:
- admin
- ops-leadUsers listed in super_admins have all permissions regardless of their assigned role. The default_role is applied when an authentication method does not provide a role (e.g., config-based basic auth).
User management requires the admin:write permission (super_admin role). For complete request and response schemas, see the API Reference.
curl -u admin:password -X POST http://localhost:8081/admin/users \
-H "Content-Type: application/json" \
-d '{
"username": "jane",
"password": "secure-password",
"email": "jane@example.com",
"role": "developer",
"enabled": true
}'curl -u admin:password http://localhost:8081/admin/userscurl -u admin:password http://localhost:8081/admin/users/1curl -u admin:password -X PUT http://localhost:8081/admin/users/1 \
-H "Content-Type: application/json" \
-d '{
"role": "admin",
"enabled": true
}'curl -u admin:password -X DELETE http://localhost:8081/admin/users/1Any authenticated user can change their own password via the self-service endpoint:
curl -u jane:old-password -X POST http://localhost:8081/me/password \
-H "Content-Type: application/json" \
-d '{
"old_password": "old-password",
"new_password": "new-secure-password"
}'API key management requires the admin:write permission. Keys are created for the currently authenticated user by default. Super admins can create keys for other users by specifying for_user_id.
curl -u admin:password -X POST http://localhost:8081/admin/apikeys \
-H "Content-Type: application/json" \
-d '{
"name": "production-pipeline",
"role": "developer",
"expires_in": 7776000
}'The expires_in value is in seconds (7776000 = 90 days).
curl -u admin:password http://localhost:8081/admin/apikeysFilter by user:
curl -u admin:password "http://localhost:8081/admin/apikeys?user_id=1"Rotation atomically creates a new key with the same settings and revokes the old one:
curl -u admin:password -X POST http://localhost:8081/admin/apikeys/1/rotate \
-H "Content-Type: application/json" \
-d '{
"expires_in": 7776000
}'The response includes both the new key (with the raw key value) and the ID of the revoked key.
Revocation disables a key without deleting it, preserving the audit trail:
curl -u admin:password -X POST http://localhost:8081/admin/apikeys/1/revokecurl -u admin:password -X DELETE http://localhost:8081/admin/apikeys/1The schema-registry-admin tool provides command-line management of users, API keys, and roles. It communicates with the registry over HTTP, so the server must be running (except for the init command, which connects directly to the database).
The CLI supports Basic Auth and API key authentication:
# Basic Auth
schema-registry-admin -u admin -p password user list
# API Key
schema-registry-admin -k sr_live_abc123... user list
# Custom server URL
schema-registry-admin -s https://registry.example.com:8081 -u admin -p password user listschema-registry-admin user list
schema-registry-admin user get <id>
schema-registry-admin user create --name jane --pass secret --role developer --email jane@example.com
schema-registry-admin user update <id> --role admin
schema-registry-admin user update <id> --disabled
schema-registry-admin user delete <id>schema-registry-admin apikey list
schema-registry-admin apikey list --user-id 1
schema-registry-admin apikey get <id>
schema-registry-admin apikey create --name ci-key --role developer --expires-in 720h
schema-registry-admin apikey create --name ops-key --role admin --expires-in 8760h --for-user-id 2
schema-registry-admin apikey update <id> --role admin
schema-registry-admin apikey revoke <id>
schema-registry-admin apikey rotate <id> --expires-in 720h
schema-registry-admin apikey delete <id>schema-registry-admin role listThe CLI supports table (default) and JSON output:
schema-registry-admin -o json user list
schema-registry-admin -o json apikey get 1The init command creates the initial admin user by connecting directly to the database:
schema-registry-admin init \
--storage-type postgresql \
--pg-host localhost --pg-port 5432 \
--pg-database schema_registry \
--pg-user postgres --pg-password dbpass \
--admin-username admin \
--admin-password 'secure-password'Supported storage types: postgresql, mysql, cassandra, memory.
Multiple authentication methods can be enabled simultaneously. The registry tries each method in the order listed in the methods array and accepts the first successful authentication.
A common production configuration combines API keys for programmatic access with Basic Auth for interactive use and LDAP for enterprise directory integration:
security:
auth:
enabled: true
methods:
- api_key
- basic
basic:
realm: "Schema Registry"
api_key:
header: X-API-Key
query_param: api_key
key_prefix: "sr_"
cache_refresh_seconds: 60
ldap:
enabled: true
url: "ldaps://ldap.example.com:636"
bind_dn: "cn=svc-schema-registry,ou=Services,dc=example,dc=com"
bind_password: "${LDAP_BIND_PASSWORD}"
base_dn: "dc=example,dc=com"
user_search_filter: "(sAMAccountName=%s)"
role_mapping:
"SchemaAdmins": "admin"
"Developers": "developer"
default_role: "readonly"
rbac:
enabled: true
default_role: readonly
super_admins:
- adminWith this configuration:
- Kafka producers and consumers use API keys via the Confluent-compatible
-u "API_KEY:x"format - DevOps teams authenticate using their LDAP credentials with
curl -u username:password - CI/CD pipelines use API keys via the
X-API-Keyheader - The bootstrap admin user retains full access via
super_admins
- Configuration -- full configuration reference
- Security -- TLS, rate limiting, and audit logging
- API Reference -- complete API endpoint documentation