Apache Druid Security on Kubernetes: Authentication & Authorization with OIDC (PAC4J), RBAC, and Azure AD

This guide builds on the infrastructure foundation, deployment preparation, and installation you completed in Parts 1–3. Here we focus on production-grade authentication and authorization patterns for Druid running on Kubernetes.

This post is part of a series: Part 1 covers the infrastructure foundation, Part 2 walks through deployment preparation, and Part 3 completes the production setup with TLS, MiddleManager-less, ZooKeeper-less, and GitOps.

Introduction

Enterprise Druid deployments must protect both machine-to-machine traffic and human access to the web console and APIs. In practice, this means implementing multiple authenticators in a chain, using SSO (OIDC/OAuth 2.0) for people, and a metadata-backed internal authenticator/authorizer for service accounts. In this article, we walk through how to design a practical authentication chain with sensible fallbacks and priorities, add OAuth login via PAC4J (for example with Azure Active Directory), pair it with Druid’s Basic Security for RBAC and the Escalator for safe internal calls, and finally bootstrap credentials.

TL;DR Summary

In short, combine an authenticatorChain such as ["db", "pac4j"] so that internal service accounts continue to work while humans log in via SSO. Remember that PAC4J only authenticates; perform authorization with the Druid Basic Authorizer (or another RBAC/PDP). Enable the druid-basic-security extension, configure metadata-backed authentication and authorization, and rely on the Escalator for safe cluster-internal calls. Keep initial admin and internal credentials in Kubernetes Secrets and serve only over TLS. For Azure AD, register an app with the redirect URI /druid-ext/druid-pac4j/callback and set the OIDC values in Druid accordingly.


Multiple Authentication Chains Architecture

Modern enterprises rarely rely on a single authentication mechanism. Druid supports an authentication chain where each authenticator is tried in order until one succeeds. In a typical setup you place an internal, metadata-backed Basic Authenticator first to handle service-to-service flows and system accounts, and you follow it with PAC4J (OIDC/OAuth) to authenticate human users via SSO. If the first authenticator fails and skip-on-failure is enabled, Druid proceeds to the next one; otherwise it stops early for performance and clarity. As a rule of thumb, put the most common and cheapest authenticator first, enable db.skipOnFailure for resiliency during brief metadata store hiccups, minimize external calls when an internal check could handle the request, and ensure each authenticator emits clear success/failure logs for auditing.

Example chain definition in common.runtime.properties:

# Two authenticators: internal (db) then web SSO (pac4j)
druid.auth.authenticatorChain=["db","pac4j"]

Configuration checklist:

  • Define druid.auth.authenticatorChain=["db","pac4j"].
  • Set druid.auth.authenticator.db.skipOnFailure=true for resiliency.
  • Place the internal authenticator first for common service calls.

We’ll define each authenticator in the following sections.

Internal vs External Authentication

Internal authentication covers service-to-service calls, technical system accounts and automation (for example, ingestion tasks and CI/CD). It typically uses Basic Auth against metadata-backed users and is best paired with the Druid Basic Authorizer for RBAC. External authentication is for humans who access the Druid console and APIs via SSO using OIDC/OAuth with PAC4J, delegating identity to providers such as Azure Active Directory or Google. In practice, Basic credentials live in the Druid metadata store and work as long as that store is available, while PAC4J relies on OAuth/OIDC tokens and a session established with your IdP. Regardless of how users authenticate, authorization is enforced by Druid’s Authorizer.

OAuth Authentication with PAC4J

PAC4J is the Druid extension that enables OIDC/OAuth login for the web console and APIs. It handles the OAuth flow, redirects users to your IdP, and creates a session on return. Important: PAC4J authenticates, it does not authorize.

Reference configuration (adapted from repository pac4j.md):

# PAC4J web user authenticator (OIDC)
druid.auth.authenticator.pac4j.name=pac4j
druid.auth.authenticator.pac4j.type=pac4j

# PAC4J does not enforce authorization; pair with Basic Authorizer for RBAC
druid.auth.authenticator.pac4j.authorizerName=pac4j

# Typical OIDC scopes; include email for consistent identity mapping
druid.auth.pac4j.oidc.scope=openid profile email

# Secrets (store in Kubernetes Secrets and mount as env)
druid.auth.pac4j.cookiePassphrase=<STRONG_RANDOM>
druid.auth.pac4j.oidc.clientID=<CLIENT_ID>
druid.auth.pac4j.oidc.clientSecret=<CLIENT_SECRET>

# For Azure AD (replace <TENANT>)
druid.auth.pac4j.oidc.discoveryURI=https://login.microsoftonline.com/<TENANT>/.well-known/openid-configuration

Notes: The callback path is handled at /druid-ext/druid-pac4j/callback by the extension. Use a long, random cookiePassphrase and rotate it on a schedule. Prefer group or email claims from the identity provider for stable user identification. Because PAC4J sessions are stateful, set reasonable TTLs and enforce HTTPS.

Setup checklist:

  • Add the druid-pac4j extension to your loadList (as covered in Part 3).
  • Configure discoveryURI, clientID, clientSecret and a strong cookiePassphrase via SOPS (see Part 1).
  • Ensure the identity provider uses the exact callback path /druid-ext/druid-pac4j/callback.
  • Validate login via the Druid console and consult server logs for callback errors.

Azure AD Integration with OAuth

For Azure AD, register an application, configure redirect URIs, and grant appropriate permissions.

You can use Terraform to deploy the Azure AD Settings:

locals {
  druid_enterprise_app_name = "k8siunera-auth-druid"
}

module "k8siunera-analyticsinternal-druid" {
  source   = "../modules/oauth-aad"
  username = local.druid_enterprise_app_name

  homepage_url  = "https://druid.example.com"
  redirect_uris = ["https://druid.example.com/druid-ext/druid-pac4j/callback"]
}

What the module provisions (excerpts from terraform/modules/oauth-aad):

# Application with Web platform configured
resource "azuread_application" "default" {
  display_name = var.username

  web {
    homepage_url  = var.homepage_url
    redirect_uris = var.redirect_uris
  }

  # Request standard OIDC scopes on Microsoft Graph
  required_resource_access {
    resource_app_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph
    dynamic "resource_access" {
      for_each = var.required_resource_access_scopes
      content {
        id   = resource_access.value
        type = "Scope"
      }
    }
  }
}

Reference: Terraform azuread_application resource docs: https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application

Required resource access scopes in Azure AD are:

# Defaults include the standard OIDC scopes on Microsoft Graph
variable "required_resource_access_scopes" {
  default = [
    "37f7f235-527c-4136-accd-4a02d197296e", # openid
    "64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0", # email
    "14dad69e-099b-42c9-810b-d002981feec1", # profile
  ]
}

Azure AD portal steps: In the Azure portal, register a new application (Quickstart: Register an application) and choose the supported account types (single-tenant or multi-tenant) that fit your organization. Add a Web redirect URI (Add redirect URI) pointing to https://druid.example.com/druid-ext/druid-pac4j/callback. Generate a client secret and store it in a Kubernetes Secret, mapping it to druid.auth.pac4j.oidc.clientSecret. Capture the Application (client) ID and Directory (tenant) ID and map them to clientID and discoveryURI respectively. Configure the openid, profile and email scopes and, if required, include group claims. If you enforce Conditional Access, confirm the policies apply as intended and ensure the Druid cluster has egress to login.microsoftonline.com.

Reference these in your Druid runtime properties (via env mapping), for example:

# PAC4J OIDC (values supplied via env from the Secret above)
druid.auth.pac4j.oidc.clientID=<DRUID_AUTH_PAC4J_OIDC_CLIENT_ID>
druid.auth.pac4j.oidc.clientSecret=<DRUID_AUTH_PAC4J_OIDC_CLIENT_SECRET>
druid.auth.pac4j.oidc.discoveryURI=<DRUID_AUTH_PAC4J_OIDC_DISCOVERY_URI>
druid.auth.pac4j.cookiePassphrase=<DRUID_AUTH_PAC4J_COOKIEPASSPHRASE>

Operational notes:

  • Configure group claims if you plan to map groups to Druid roles.
  • Enforce Conditional Access as needed.

Multi-tenancy notes: The discoveryURI is tenant-specific; for organizational tenants, prefer a fixed tenant to preserve policy control. If you depend on group membership for permissions, sync groups to Druid roles via the management API or through your own automation.

Troubleshooting: If you encounter an HTTP 400 at the callback, verify that the redirect URI matches exactly and that HTTPS is enforced. For login loops, check the cookiePassphrase and confirm your reverse proxy sets X-Forwarded-Proto=https.

Security Best Practices

Security hardening in production reads better as a set of habits than a checklist. Expose Druid only over HTTPS, disable plaintext ports and enforce HSTS at the ingress. Keep secrets in Kubernetes Secrets or wire in an External Secrets Operator, and rotate client secrets and cookie passphrases on a schedule. Apply least‑privilege RBAC—most humans should have read‑only access while CI/CD and operators receive narrowly scoped elevated rights. Turn on audit logging for authentication and authorization actions and forward the logs to your SIEM, then monitor for spikes in failures, escalator errors or sudden permission changes. Finally, align Druid roles with your data domains and master‑data ownership model (see this article).

Kubernetes-Native Security Integration

Treat Kubernetes as a first-class security boundary. Use NetworkPolicies to isolate Druid pods by role and allow only the egress required for your identity provider and metadata database. Lock down Kubernetes RBAC so that only GitOps controllers (not humans) can patch the ConfigMaps and Secrets that shape Druid’s behavior. Apply Pod Security Standards controls to run as non‑root, drop unnecessary capabilities and, where possible, use a read‑only root filesystem. For a reference point, compare deployment options and defaults with our Helm charts at https://iunera.github.io/helm-charts/.

Druid Basic Security (Authenticators, Authorizers, Escalator)

The druid-basic-security extension provides an internal, metadata-backed Basic Authenticator for service accounts, a Basic Authorizer that applies Druid RBAC over resources (DATASOURCE, STATE, CONFIG and EXTERNAL), and an Escalator used to perform privileged cluster‑internal requests safely.

Enable extension (already shown in Part 3 loadList) and configure properties:

# Enable both internal (db) and SSO (pac4j) authenticators
druid.auth.authenticatorChain=["db","pac4j"]

# Metadata-backed Basic Authenticator (name: db)
druid.auth.authenticator.db.type=basic
druid.auth.authenticator.db.name=db
# Recommended for resiliency: continue to next authenticator if DB temporarily unavailable
druid.auth.authenticator.db.skipOnFailure=true

# Basic Authorizer (name: db)
druid.auth.authorizer.db.type=basic
druid.auth.authorizer.db.name=db
# Escalator: used for internal system calls
druid.escalator.type=basic
druid.escalator.internalClientUsername=<internal-client>
druid.escalator.internalClientPassword=<strong-secret>
druid.escalator.authorizerName=db

Bootstrap checklist:

  • Enable the druid-basic-security extension and restart services.
  • Set initialAdminPassword and internalClientPassword via Kubernetes Secrets.
  • Configure the escalator with type=basic, authorizerName=db, and provide internalClientUsername/password.
  • Bootstrap users and roles with the management API.
  • After configuration, remove initial environment values and rotate the secrets.

Bootstrap initial admin and internal client via environment (use Secrets in Kubernetes):

env:
  - name: druid_auth_authenticator_db_initialAdminPassword
    valueFrom:
      secretKeyRef:
        name: druid-initial-secrets
        key: initialAdminPassword
  - name: druid_escalator_internalClientPassword
    valueFrom:
      secretKeyRef:
        name: druid-initial-secrets
        key: internalClientPassword

After the cluster starts, log in as the initial admin and create users/roles via the management API. Examples adapted from the Postman collection in this repository:

# Create a role
curl -s -X POST \
  -H "Content-Type: application/json" \
  -u admin:*** \
  https://<COORDINATOR_OR_OVERLORD>/druid-ext/basic-security/authorization/db/db/roles/readRole

# List roles
curl -s -u admin:*** \
  https://<COORDINATOR_OR_OVERLORD>/druid-ext/basic-security/authorization/db/db/roles/

# Grant permissions to a role
curl -s -X POST -H "Content-Type: application/json" -u admin:*** \
  https://<COORDINATOR_OR_OVERLORD>/druid-ext/basic-security/authorization/db/db/roles/readRole/permissions \
  -d '[
    {"resource": {"name": ".*", "type": "DATASOURCE"}, "action": "READ"},
    {"resource": {"name": ".*", "type": "STATE"}, "action": "READ"},
    {"resource": {"name": ".*", "type": "CONFIG"}, "action": "READ"},
    {"resource": {"name": "EXTERNAL", "type": "EXTERNAL"}, "action": "READ"}
  ]'

# Create a user and assign a role
curl -s -X POST -H "Content-Type: application/json" -u admin:*** \
  https://<COORDINATOR_OR_OVERLORD>/druid-ext/basic-security/authentication/db/users/alice

curl -s -X POST -H "Content-Type: application/json" -u admin:*** \
  https://<COORDINATOR_OR_OVERLORD>/druid-ext/basic-security/authorization/db/db/users/alice/roles \
  -d '["readRole"]'

Rotation procedure: After bootstrap, remove the initial password environment properties from your manifests. Create new secrets with rotated admin and internal credentials, then roll the pods so they pick up the updated values. Finally, verify access with /status/self and other protected endpoints.

Security hardening: Harden the deployment by enforcing HTTPS-only, disabling plaintext ports and ensuring the reverse proxy sets X-Forwarded-Proto. Limit the escalator account to internal use, store its secret in a dedicated Secret and reference it by name. Finally, enable audit logs on authentication and authorization events.


Troubleshooting

PAC4J/OIDC login issues (400 at callback)

  • Verify the redirect URI exactly matches your IdP app registration (including scheme, host, path /druid-ext/druid-pac4j/callback).
  • Enforce HTTPS end-to-end. Ensure your reverse proxy passes X-Forwarded-Proto: https.
  • Use a long, random druid.auth.pac4j.cookiePassphrase and keep it identical across all pods; rotate carefully (existing sessions become invalid).
  • Check for clock skew between Druid pods and IdP (OIDC tokens are time-bound).
  • Inspect Router/Coordinator logs during the callback to see PAC4J error details.

Login loops after successful IdP authentication

  • Mismatched or rotated cookiePassphrase across nodes can invalidate sessions; ensure it’s uniform via a single Secret.
  • Check domain/path/samesite on the session cookie if you terminate TLS at a proxy.
  • Confirm that the Router path /druid-ext/druid-pac4j/callback isn’t altered or blocked by the proxy.

401/403 when calling APIs

  • 401 usually means no authenticator succeeded: include the proper Authorization header (Basic for service accounts) or establish a PAC4J session for the browser.
  • 403 means authenticated but not authorized: verify the user’s roles and permissions in the Basic Authorizer.
  • Confirm the chain and authorizers:
    • druid.auth.authenticatorChain=["db","pac4j"]
    • druid.auth.authorizers=["db","pac4j"] with db as the RBAC authorizer, pac4j typically allowAll.
  • Use GET /status/self to see the recognized identity and authorizer.

Metadata store outages affecting Basic Auth

  • Set druid.auth.authenticator.db.skipOnFailure=true so requests can fall through to the next authenticator in brief outages.
  • Monitor the metadata database health; repeated failures will still impact internal users.

Escalator/internal client fails (403)

  • Ensure:
    • druid.escalator.type=basic
    • druid.escalator.authorizerName=db
    • Valid druid.escalator.internalClientUsername and druid.escalator.internalClientPassword (from Secrets) and corresponding user exists.
  • Grant that internal user the minimal required roles/permissions.

TLS-only cluster gotchas

  • With druid.enablePlaintextPort=false, update all health probes and clients to use HTTPS (see component ports in Part 3).
  • Provide proper truststores for outbound HTTPS (simple-client-sslcontext extension) and set X-Forwarded-* headers at the proxy.

Initial admin bootstrap problems

  • druid.auth.authenticator.db.initialAdminPassword is only used on first startup; after creating users, rotate and remove it from manifests.
  • If you can’t log in as admin afterward, verify the metadata store contents and the environment variables/secrets in your deployment.

Quick diagnostics

Check identity:

curl -sk https://<ROUTER_OR_COORDINATOR>/status/self -u <user>:<pass>

List roles and users via the Basic Security endpoints to validate RBAC configuration.

Frequently Asked Questions

Is pac4j required?

No. You can operate with Basic Auth (metadata-backed) for service accounts and add pac4j later for human SSO. This guide shows how to run both together via the authenticator chain.

Does pac4j provide authorization?

No. pac4j authenticates only. Use the Druid Basic Authorizer (RBAC) or another PDP for authorization.

How do I support both SSO users and service accounts?

Use druid.auth.authenticatorChain=["db","pac4j"]. Put db first for fast internal calls; pac4j handles browser SSO. Authorize via the Basic Authorizer (name db).

How do I map IdP (e.g., Azure AD) groups to Druid roles?

Druid doesn’t automatically map groups. Read group/email claims from the IdP and sync them to Druid roles via the management API or your automation pipeline.

Where should I store client secrets and the cookie passphrase?

In Kubernetes Secrets managed via GitOps/secret encryption (e.g., SOPS). Mount them as environment variables as shown in this article. Rotate regularly.

What happens to sessions if I rotate the cookie passphrase?

Existing PAC4J sessions become invalid. Perform a rolling restart and expect users to re-authenticate.

What if the metadata store goes down—will logins still work?

With db.skipOnFailure=true, requests can fall through to pac4j during brief outages. Internal Basic-auth users may be impacted until the store recovers.

How can I see which user Druid thinks I am?

Call GET /status/self (via Router/Coordinator) with your credentials or PAC4J session; it returns the authenticated identity and authorizer information.

Conclusion

Combining PAC4J for user SSO with the Druid Basic Security extension for internal auth and RBAC delivers a pragmatic, enterprise-grade security posture. The authenticator chain pattern lets you support both service accounts and human users safely. By enforcing TLS, automating secret rotation, and integrating Kubernetes-native controls, your Druid cluster is well-positioned for secure, compliant analytics at scale.

Finally, if you’re building advanced AI assistants over time-series with Druid, treat identity and authorization as first-class architecture concerns. See our Druid MCP Server project for ideas and guardrails: https://www.iunera.com/kraken/projects/apache-druid-mcp-server-conversational-ai-for-time-series/


References

For deeper dives, see the Druid Basic Security documentation (Authenticator/Authorizer/Escalator): https://druid.apache.org/docs/latest/development/extensions-core/druid-basic-security/, the operations guide on user authentication: https://druid.apache.org/docs/latest/operations/security-user-auth/, a gist with Management API examples: https://gist.github.com/davidagee/c0c839cd23f047b838e8a3ea73320346, and our Helm charts for comparing defaults and values: https://iunera.github.io/helm-charts/.