helm

Plugin Access Manager Helm Chart

Chart Contract

Uninstall keeps the identity data. helm uninstall leaves the bundled database volume (data-<release>-auth-database-0) and its password Secret (<release>-auth-database), so a reinstall under the same release name opens the same users, organizations and applications with the same password. This chart, not the Bitnami subchart, owns that Secret: it takes auth-database.auth.password when set, else the Secret already in the namespace, else a new random password. Deleting the data is a separate, manual step: delete that PVC and that Secret together (the Valkey volume valkey-data-<release>-valkey-primary-0, sessions and cached tokens only, is left too). Deleting only the Secret resets nothing: a reinstall generates a new password that the kept data never learned.

This helm chart installs Plugin Access Manager for Midaz, a high-performance ledger.


Startup validation (chart source after 9.5.8)

The published 9.5.8 package does not contain these render-time guards. A new chart release is required to distribute them; changing this repository does not mutate that package.

The offline regression suite in tests/ is not wired into CI yet. Run it from the repository root; it needs Helm 3 and PyYAML, no cluster:

helm dependency build charts/plugin-access-manager
python3 charts/plugin-access-manager/tests/test_startup_contract.py

Install Plugin Access Manager Helm Chart:

To install Plugin Access Manager using Helm, run the following command:

$ helm install plugin-access-manager oci://registry-1.docker.io/lerianstudio/plugin-access-manager --version <> -n midaz-plugins --create-namespace

This will create a new namespace called midaz-plugins if it doesn’t already exist and deploy the Plugin Access Manager Helm chart.

After installation, you can verify that the release was successful by listing the Helm releases in the midaz-plugins namespaces:

$ helm list -n midaz-plugins

Managed Cloud (global.cloud)

Point this chart at a managed-cloud environment (AWS/GCP/Azure) instead of the bundled in-cluster Caradhras/Postgres/Redis with one knob:

global:
  cloud: "aws"   # aws | gcp | azure — leave unset for the bundled dev topology
  datastores:
    postgres: { host: "my-rds.example.com", user: "access_manager" }
    redis: { host: "my-elasticache.example.com", port: "6379" }
  env:
    name: "production"
  multiTenant:
    enabled: false
  observability:
    enabled: true

# Disable the bundled in-cluster datastores — otherwise they still deploy
# alongside the managed ones above (wasted resources, confusing topology).
auth-database:
  enabled: false
valkey:
  enabled: false

# auth.secrets.DB_PASSWORD/REDIS_PASSWORD are normally single-sourced from the
# bundled datastores' Secrets. With both disabled above, supply the managed
# instances' credentials explicitly (or point auth.existingSecretName at a
# pre-created Secret with the same keys instead of inlining them here):
auth:
  secrets:
    DB_PASSWORD: "CHANGE_ME"
    REDIS_PASSWORD: "CHANGE_ME"

global.cloud sets the connection TOPOLOGY (TLS, SSL mode) for every mask below; only the ENDPOINTS (host/port/user) still come from global.datastores — a cloud preset can’t know your RDS host. A native {auth,identity}.configmap.<KEY> always overrides any mask.

Copy values-template.yaml as your starting point — it documents every global.* mask with a working example. values.yaml is the full power-user reference; values.schema.json validates it.


Configuring Ingress for Different Controllers

This chart has no top-level ingress: block: a top-level ingress.* value is ignored and renders no Ingress. Each exposed component has its own block, with the same shape (enabled, className, annotations, hosts, tls):

Component Values key Service port Host derived from global.ingress.domain
Auth auth.ingress auth.service.port (4000) auth.<domain>
Identity identity.ingress identity.service.port (4001) identity.<domain>
Caradhras (API and admin panel) caradhras.ingress 8000 caradhras.<domain>
Caradhras UI (needs caradhras.ui.enabled) caradhras.ui.ingress caradhras.ui.service.port (8080) caradhras.<domain>

Ingresses are off by default. To set className, annotations, tls and the host domain once for all of them, use global.ingress (className, annotations, tls, domain); a per-component value always wins. With global.ingress.domain set and no explicit hosts, the host is <subdomain>.<domain> as in the last column. The caradhras UI Ingress derives the same caradhras.<domain> host as caradhras.ingress, so give one of them explicit hosts when you enable both.

Keep Caradhras internal. caradhras.ingress and caradhras.ui.ingress must be internal only, on an internal load balancer or a private network, never internet-facing. On AWS ALB that means alb.ingress.kubernetes.io/scheme: internal. Only the Access Manager (auth and identity) and operators call Caradhras; organization admins and end users go through the Access Manager and never reach Caradhras directly. The https rule still holds: a public-CA certificate does not need an internet-facing load balancer.

The examples below configure auth.ingress; use the same keys under identity.ingress, caradhras.ingress or caradhras.ui.ingress.

NGINX Ingress Controller

To use the NGINX Ingress Controller, configure the values.yaml as follows:

auth:
  ingress:
    enabled: true
    className: "nginx"
    # Annotations are key-value pairs read by the controller.
    # See more https://github.com/kubernetes/ingress-nginx/blob/main/docs/user-guide/nginx-configuration/annotations.md
    annotations: {}
    hosts:
      - host: auth.example.com
        paths:
          - path: /
            pathType: Prefix
    tls:
      - secretName: auth-tls  # Ensure this secret exists or is managed by cert-manager
        hosts:
          - auth.example.com

AWS ALB (Application Load Balancer)

For AWS ALB Ingress Controller, use the following configuration:

auth:
  ingress:
    enabled: true
    className: "alb"
    annotations:
      alb.ingress.kubernetes.io/scheme: internal  # Use "internet-facing" for public ALB
      alb.ingress.kubernetes.io/target-type: ip   # Use "instance" if targeting EC2 instances
      alb.ingress.kubernetes.io/group.name: "access-manager"  # Group ALB resources under this name
      alb.ingress.kubernetes.io/healthcheck-path: "/health"  # Auth health check path
      alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'  # Listen on HTTP and HTTPS
    hosts:
      - host: auth.example.com
        paths:
          - path: /
            pathType: Prefix
    tls: []  # TLS is managed by the ALB using ACM certificates

Traefik Ingress Controller

For Traefik, configure the values.yaml as follows:

auth:
  ingress:
    enabled: true
    className: "traefik"
    annotations:
      traefik.ingress.kubernetes.io/router.entrypoints: "web, websecure"  # Entrypoints defined in Traefik
      traefik.ingress.kubernetes.io/router.tls: "true"  # Enable TLS for this route
    hosts:
      - host: auth.example.com
        paths:
          - path: /
            pathType: Prefix
    tls:
      - secretName: auth-tls  # Ensure this secret exists and contains the TLS certificate
        hosts:
          - auth.example.com

Pod Spreading (global.scheduling.spread)

Every Deployment of this chart (auth, identity, caradhras, and the caradhras UI when caradhras.ui.enabled=true) renders topologySpreadConstraints from the lerian-common spread preset. The default is soft (ScheduleAnyway) across nodes, so replicas of the same component are spread across nodes (e.g. spot nodes) whenever possible, but a pod is never left Pending because of it.

Zone spreading is off by default (zone: ""). When scoring a soft spread, the scheduler skips every node that lacks the topology.kubernetes.io/zone label, so on clusters without zone labels (bare-metal, k3s) a zone constraint silently cancels the node spread. Turn it on (zone: ScheduleAnyway) where every node carries zone labels (EKS, GKE, AKS).

global:
  scheduling:
    spread:
      enabled: true            # master switch
      hostname: ScheduleAnyway # kubernetes.io/hostname: ScheduleAnyway | DoNotSchedule | "" (off)
      zone: ""                 # topology.kubernetes.io/zone: same values (off by default)
      maxSkew: 1               # integer >= 1
      minDomains: 0            # DoNotSchedule constraints only; 0 = off
      nodeTaintsPolicy: ""     # Honor | Ignore | "" (Kubernetes default Ignore)
auth:
  spread: { hostname: DoNotSchedule }  # field-level override of the global preset
  topologySpreadConstraints: []        # raw list; non-empty replaces the preset entirely

On EKS + Karpenter use a hard node spread:

global:
  scheduling:
    spread:
      hostname: DoNotSchedule
      zone: ScheduleAnyway
      maxSkew: 1
      minDomains: 2
      nodeTaintsPolicy: Honor

minDomains: 2 is required with DoNotSchedule. Skew is measured only against nodes that already exist, so on a single eligible node all replicas would otherwise share it. With fewer than 2 nodes the extra replica stays Pending until Karpenter launches another node. nodeTaintsPolicy: Honor stops tainted nodes (for example a node being drained) from counting as domains.

Precedence: <component>.topologySpreadConstraints (non-empty, wins entirely) > <component>.spread.<field> > global.scheduling.spread.<field> > off. Each preset constraint selects only the component’s own pods (labelSelector = the Deployment’s spec.selector.matchLabels) of the same ReplicaSet (matchLabelKeys: [pod-template-hash]), so a rolling update is never blocked by the old ReplicaSet’s pods, even with DoNotSchedule. Requires Kubernetes >= 1.27 (matchLabelKeys). Use DoNotSchedule only when the cluster can always provide enough distinct nodes/zones for the replica count (a hard constraint leaves extra pods Pending otherwise). Caradhras keeps taking nodeSelector / affinity / tolerations from auth.* (unchanged), but is spread on its own pods via caradhras.spread / caradhras.topologySpreadConstraints. The init-user Job is not spread.

Upgrade note: the preset is on by default, so upgrading to the chart version that introduces it adds topologySpreadConstraints to the pod template of every Deployment and triggers one rolling restart of each. No values change is required. To keep the previous pod template exactly, set global.scheduling.spread.enabled: false.

Plugin Access Manager Components:

Identity Service

Parameter Description Default
replicaCount Number of replicas for the deployment 1
image.repository Repository for the container image ghcr.io/lerianstudio/plugin-identity
image.pullPolicy Image pull policy Always
image.tag Image tag used for deployment 3.14.0
imagePullSecrets Secrets for pulling images from a private registry []
nameOverride Overrides the default generated name by Helm ""
fullnameOverride Overrides the full name generated by Helm ""
ingress.enabled Enable or disable ingress false
ingress.className Ingress class name ""
ingress.annotations Additional ingress annotations {}
ingress.hosts Ingress host configuration [{"host": "", "paths": [{"path": "/", "pathType": "Prefix"}]}]
ingress.tls TLS configuration for ingress []
service.type Kubernetes service type ClusterIP
service.port Service port 4001
deploymentStrategy Deployment strategy {"type": "RollingUpdate", "rollingUpdate": {"maxSurge": 1, "maxUnavailable": 1}}
podSecurityContext Pod security context {}
securityContext Security context for the container See values.yaml
pdb.enabled Enable or disable PodDisruptionBudget true
pdb.maxUnavailable Maximum number of unavailable pods 1
pdb.minAvailable Minimum number of available pods 0
pdb.annotations Annotations for PodDisruptionBudget {}
resources CPU and memory limits for pods See values.yaml
autoscaling.enabled Enable or disable horizontal pod autoscaling true
autoscaling.minReplicas Minimum number of replicas 1
autoscaling.maxReplicas Maximum number of replicas 3
autoscaling.targetCPUUtilizationPercentage Target CPU utilization percentage for autoscaling 80
autoscaling.targetMemoryUtilizationPercentage Target memory utilization percentage for autoscaling 80
spread Per-component override of global.scheduling.spread (fields: enabled, hostname, zone, maxSkew) {}
topologySpreadConstraints Raw topologySpreadConstraints; non-empty replaces the spread preset []
nodeSelector Node selector for scheduling pods {}
tolerations Tolerations for scheduling on tainted nodes {}
affinity Affinity rules for pod scheduling {}
extraEnvVars Extra environment variables to be added to the deployment {}
configmap.PLUGIN_AUTH_SSO_CALLBACK_URL Literal callback URL for this component, overriding common.sso.*. Must match the auth component — see Single sign-on unset
configmap.GOOGLE_DIRECTORY_ENABLED "true" turns on the Google Workspace directory connection (identity 3.12.0+). Needs the other four GOOGLE_DIRECTORY_* values unset
configmap.GOOGLE_DIRECTORY_CLIENT_ID Client id of Lerian’s Google OAuth client unset
configmap.GOOGLE_DIRECTORY_REDIRECT_URL OAuth redirect URL registered on that client (https) unset
secrets.GOOGLE_DIRECTORY_CLIENT_SECRET Client secret of that Google OAuth client. Rendered only when set ""
secrets.GOOGLE_DIRECTORY_ENCRYPTION_KEY Encrypts each organization’s stored Google refresh token; at least 32 bytes. Changing it forces every connected organization to reconnect. Rendered only when set ""
useExistingSecret Use an existing secret instead of creating a new one false
existingSecretName The name of the existing secret to use ""

Auth Service

Parameter Description Default
replicaCount Number of replicas for the deployment 3
image.repository Repository for the console service container image ghcr.io/lerianstudio/plugin-auth
image.pullPolicy Image pull policy Always
image.tag Image tag used for deployment 3.14.0
imagePullSecrets Secrets for pulling images from a private registry []
nameOverride Overrides the default generated name by Helm ""
fullnameOverride Overrides the full name generated by Helm ""
namespaceOverride Overrides the namespace for the service ""
ingress.enabled Enable or disable ingress false
ingress.className Ingress class name ""
ingress.annotations Additional ingress annotations {}
ingress.hosts Ingress host configuration [{"host": "", "paths": [{"path": "/", "pathType": "Prefix"}]}]
ingress.tls TLS configuration for ingress []
service.type Kubernetes service type ClusterIP
service.port Service port 4000
deploymentStrategy Deployment strategy of auth; caradhras always rolls with maxSurge: 1, maxUnavailable: 0 {"type": "RollingUpdate", "rollingUpdate": {"maxSurge": 1, "maxUnavailable": 1}}
podSecurityContext Pod security context {}
securityContext Security context for every auth-side container: auth, caradhras (including its migrate init container) and its UI, the init-user Job, and their init containers See values.yaml
pdb.enabled Enable or disable PodDisruptionBudget true
pdb.maxUnavailable Maximum number of unavailable pods 1
pdb.minAvailable Minimum number of available pods 0
pdb.annotations Annotations for PodDisruptionBudget {}
resources CPU and memory limits for pods See values.yaml
autoscaling.enabled Enable or disable horizontal pod autoscaling true
autoscaling.minReplicas Minimum number of replicas 3
autoscaling.maxReplicas Maximum number of replicas 9
autoscaling.targetCPUUtilizationPercentage Target CPU utilization percentage for autoscaling 80
autoscaling.targetMemoryUtilizationPercentage Target memory utilization percentage for autoscaling 80
spread Per-component override of global.scheduling.spread (fields: enabled, hostname, zone, maxSkew) {}
topologySpreadConstraints Raw topologySpreadConstraints; non-empty replaces the spread preset []
nodeSelector Node selector for scheduling pods {}
tolerations Tolerations for scheduling on tainted nodes {}
affinity Affinity rules for pod scheduling {}
extraEnvVars Extra environment variables to be added to the deployment {}
configmap.MFA_ENABLED Multi-factor authentication gate. Unset omits the key and leaves the application default in force unset
configmap.PLUGIN_AUTH_SSO_CALLBACK_URL Literal callback URL for this component, overriding common.sso.*. Must match the identity component — see Single sign-on unset
useExistingSecret Use an existing secret instead of creating a new one false
existingSecretName The name of the existing secret to use ""

Single sign-on (common.sso.baseUrl)

An SSO login leaves the platform for the identity provider and has to come back. The address it comes back to has two halves, and they belong to different people:

half example whose
scheme + host (+ path prefix) https://console.example.com yours — you map the console on your own domain; the chart cannot know or derive it
callback path /signin/sso/callback ours — the console route the browser lands on; do not change it

So you give the chart the first half and it composes the second:

common:
  sso:
    baseUrl: "https://console.example.com"
    # -> https://console.example.com/signin/sso/callback

Serving the console under a path prefix works the same way — baseUrl: "https://apps.example.com/console" yields https://apps.example.com/console/signin/sso/callback.

Why the chart composes it instead of taking the whole URL. A mistyped path is the most likely mistake here and by far the hardest to diagnose: Caradhras rejects a redirect_uri that is not on its allow-list without saying which part was wrong, so /signin/sso/calback produces a login that fails with an error naming nothing. Composing takes that mistake off the table.

Setting this is required to configure an SSO provider at all. Without it, identity refuses to save the provider and answers the request with an error; the provider would otherwise look saved while no login through it could ever complete.

Passing the whole URL literally. common.sso.callbackUrl takes the finished URL instead of composing one — but the host is the only part it may change: the chart refuses a value whose path is not /signin/sso/callback, naming the expected path and the one received. A near-miss (/sso/callback, /signin/callback, /signin/sso/calback) otherwise renders clean and surfaces only at the first login.

common:
  sso:
    callbackUrl: "https://console.example.com/signin/sso/callback"

The escape hatch, and it says so. A deployment that genuinely answers SSO on another path — a console mounted on a custom route, or a proxy that rewrites it — lifts that check explicitly:

common:
  sso:
    callbackUrl: "https://console.example.com/custom/sso/return"
    allowCustomCallbackPath: true   # leaving the supported path, on purpose

Off this path the chart can no longer tell you whether the URL is right; it must match whatever actually serves the console callback, and a mismatch shows up only as a failed login. The absolute-http(s)-with-a-path requirement still applies — that one is the binary’s, not the chart’s.

baseUrl and callbackUrl are alternatives, not layers — one asks the chart to append the route, the other supplies the finished URL — and the chart refuses to render when both are set. It also refuses a baseUrl that already ends in /signin/sso/callback, which is what a full URL pasted into the wrong field looks like and would otherwise double the path.

What the chart validates before you deploy. The resolved URL must be an absolute http(s) URL carrying a concrete path — the same rule the binary applies (isAbsoluteCallbackURL). A host with no path is refused on purpose: Casdoor treats an allow-list entry without a path as a wildcard over every path on that host and its subdomains, so a half-formed value would widen the allow-list instead of authorising one endpoint. On top of that the chart checks the path is the console route, unless allowCustomCallbackPath says otherwise. Failing at helm template names the values field; failing at runtime is a provider that saves and never completes a login.

One value, both components, and the chart enforces it. identity writes the URL into the Caradhras provider’s redirect allow-list; auth then sends the same URL as the redirect_uri of the code relay, and Caradhras rejects any redirect_uri the allow-list does not carry. Two different values therefore deploy cleanly and break at the first login, with an error that names neither component. The shared common.sso.* fields give that for free; the per-component {identity,auth}.configmap.PLUGIN_AUTH_SSO_CALLBACK_URL overrides exist for migration, and the chart refuses to render whenever the two resolve differently.

The value is never derived from PLUGIN_AUTH_ADDRESS. That address is how the components reach each other inside the cluster; this one has to be reachable by the end user’s browser, and they are not the same host.

Multi-factor authentication (auth.configmap.MFA_ENABLED)

MFA_ENABLED gates multi-factor authentication on the auth component. It is unset by default: the key is then absent from the ConfigMap and the application’s own default stays in force. Set auth.configmap.MFA_ENABLED: "true" to turn it on. The MFA_SESSION_TTL_SEC / MFA_REMEMBER_TTL_SEC / MFA_MAX_ATTEMPTS / MFA_MAX_RESEND_ATTEMPTS keys tune it and already have chart defaults.

Moving these keys off extraEnvVars

Both keys were previously deliverable only through extraEnvVars. That still works and nothing breaks on upgrade. But setting a key through both channels is refused: the named key and extraEnvVars render into the same ConfigMap data map, so the key would be emitted twice and the surviving value is whatever the YAML parser keeps — the chart refuses rather than shipping an install whose effective configuration nobody can read off the values file. When migrating, delete the extraEnvVars entry in the same change that adds the named key.

# before
auth:
  extraEnvVars:
    PLUGIN_AUTH_SSO_CALLBACK_URL: "https://console.example.com/signin/sso/callback"
    MFA_ENABLED: "true"
identity:
  extraEnvVars:
    PLUGIN_AUTH_SSO_CALLBACK_URL: "https://console.example.com/signin/sso/callback"

# after
common:
  sso:
    baseUrl: "https://console.example.com"
auth:
  configmap:
    MFA_ENABLED: "true"

Caradhras Service (auth backend)

Caradhras is a top-level component, sibling to identity/auth (formerly the nested auth.backend, which ran the legacy Casdoor image). A legacy auth.backend.* override in your OWN values file is still honored as a fallback for image.repository/image.tag/image.pullPolicy/service.port/ replicaCount — see docs/UPGRADE-8.6-to-9.2.md.

Parameter Description Default
caradhras.replicaCount Number of replicas 1
caradhras.name Name of the caradhras component <release>-caradhras
caradhras.image.repository Repository for the caradhras container image ghcr.io/lerianstudio/caradhras
caradhras.image.tag Image tag used for deployment 1.8.6
caradhras.service.port Service port 8000
caradhras.ingress.enabled Expose Caradhras itself (API and admin panel, port 8000) through an Ingress; className/annotations/hosts/tls take the same shape as auth.ingress. The way to reach it over https in production — see below false
caradhras.autoscaling Autoscaling configuration See values.yaml
caradhras.spread Per-component override of global.scheduling.spread (fields: enabled, hostname, zone, maxSkew). nodeSelector / affinity / tolerations for caradhras still come from auth.* {}
caradhras.topologySpreadConstraints Raw topologySpreadConstraints; non-empty replaces the spread preset []
caradhras.migrations.image.repository Repository for the caradhras-migrations container image ghcr.io/lerianstudio/caradhras-migrations
caradhras.migrations.image.tag Image tag — independent of caradhras.image.tag: override both when changing versions 1.8.6
caradhras.migrations.skipRBACChain Render MIGRATIONS_SKIP_RBAC_CHAIN=true on the migrate init container and on the caradhras container: the migrate applies the schema chain and skips the migrations-rbac chain (Lerian product permissions/roles/groups under owner lerian), and the caradhras boot schema check skips the same chain. For installs whose permissions arrive only via RI manifests (e.g. cross). Needs caradhras and caradhras-migrations images that understand the variable; false renders nothing false
caradhras.migrations.extraEnvVars Extra NAME: value env vars for the migrate init container only, rendered as name/value pairs. Chart-owned names (POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_SSLMODE, MIGRATIONS_SKIP_RBAC_CHAIN) are refused; names must be valid env var names ([-._a-zA-Z][-._a-zA-Z0-9]*); not for secrets {}
caradhras.ui.enabled Enable the Caradhras UI (SPA console) sub-resource false
caradhras.ui.image.repository Repository for the caradhras-ui container image ghcr.io/lerianstudio/caradhras-ui
caradhras.ui.image.tag Image tag used for deployment 1.4.0
caradhras.ui.service.port Service port 8080
caradhras.ui.ingress.enabled Enable ingress for the UI false
caradhras.ui.spread Per-component override of global.scheduling.spread (fields: enabled, hostname, zone, maxSkew) unset ({})
caradhras.ui.topologySpreadConstraints Raw topologySpreadConstraints; non-empty replaces the spread preset unset ([])
caradhras.configmap.redisEndpoint Shared session store for a Redis without AUTH, host:port with no space. Empty keeps sessions in a file on each pod’s own filesystem. Required above one replica — see below. Refused when it carries a password ""
caradhras.secrets.redisEndpoint Shared session store for a Redis with AUTH: the full beego connection string host:port,poolsize,password[,dbnum]. Delivered by Secret, never by ConfigMap ""
caradhras.secrets.LICENSE_KEY Caradhras license key (env LICENSE_KEY, by Secret). Required by images with the license gate, which refuse to boot without it. Issued for product caradhras, not the auth/identity key ""
caradhras.useExistingSecret Manage the caradhras Secret yourself; the chart creates none false
caradhras.existingSecretName Name of that Secret. Carries LICENSE_KEY, plus redisEndpoint unless caradhras.configmap.redisEndpoint is set ""
caradhras.redisPassword.enabled Inject the Redis AUTH password as the redisPassword config key, by secretKeyRef. Lets the endpoint stay a bare host:port false
caradhras.redisPassword.secretName Secret holding that password. Empty uses the auth Secret ""
caradhras.redisPassword.secretKey Key inside that Secret REDIS_PASSWORD
caradhras.configmap.redisTls Reach the session store over TLS ("true"/"false"). Only emitted when an endpoint is configured; also settable once for every component via global.datastores.redis.tls "" (resolves to false)

Production: reach Caradhras over HTTPS

Since application 3.3.0, auth and identity fetch the Caradhras JWKS over https only, unless ENV_NAME is development, staging or local; no setting relaxes this. Precisely: identity also accepts http to a loopback host and fetches no JWKS when PLUGIN_AUTH_ENABLED is false, auth has no loopback exemption, and images below 3.3.0 are unaffected. Caradhras serves plain http, and the default AUTHORIZER_ADDRESS is its in-cluster Service, so a production install has to put TLS in front of it with caradhras.ingress, using a certificate from a public CA, and set auth.configmap.AUTHORIZER_ADDRESS and identity.configmap.AUTHORIZER_ADDRESS to the same https URL (identity also pins that address as the M2M token issuer). Keep that ingress internal: it exposes the Caradhras admin panel and API.

The chart refuses to render an ENV_NAME outside that list together with a JWKS URL the application would reject, because the pods would otherwise crash-loop before logging anything to your collector. With service discovery on (SD_ENABLED=true) the chart cannot see the address auth resolves, so it does not check auth; discovery must still resolve Caradhras to https. Worked example, pre-upgrade check and caveats: docs/UPGRADE-9.5.8.md.

Session store (required above one replica)

Caradhras is beego, and beego writes a login session to a file under the pod’s own working directory unless redisEndpoint names a Redis. With one pod that works. With two, a login that starts on one pod and finishes on another does not find its session and fails with unknown authentication type. The failure is intermittent by nature: it depends on which pod the load balancer picks.

A session store is therefore mandatory whenever caradhras is pinned above one replica (replicaCount > 1 with autoscaling off, or autoscaling.minReplicas > 1). The chart refuses to render in that state rather than deploying a login that fails intermittently. When autoscaling can merely reach more than one pod (autoscaling.maxReplicas > 1, the chart default), the install notes carry a warning instead — failing there would break every default install.

Two channels, and they are mutually exclusive. Beego’s endpoint is positional — host:port,poolsize,password,dbnum — so the password, when there is one, is part of the same string the chart has to deliver:

The Redis Set Delivered as
needs no AUTH caradhras.configmap.redisEndpoint: "valkey:6379" ConfigMap key, via envFrom
requires AUTH caradhras.secrets.redisEndpoint: "valkey:6379,100,<password>" Secret key, via secretKeyRef
requires AUTH, password already in a Secret caradhras.configmap.redisEndpoint: "valkey:6379" plus caradhras.redisPassword.enabled: true endpoint in the ConfigMap, password as env redisPassword, via secretKeyRef

The ConfigMap channel refuses a value with a third field: a ConfigMap gives no Secret-equivalent protection, so any principal that can read it would recover the Redis password. Setting both channels is also refused — caradhras reads a single env named redisEndpoint, and defining it twice is undefined behavior.

The third row is the one to prefer when the password already lives in a Secret. Caradhras reads redisPassword as its own config key and resolves the session store and the rate limiter from that one reading, so the credential never has to be spliced into a connection string — and the endpoint, carrying no password, can ride the ConfigMap. The password defaults to the auth Secret’s REDIS_PASSWORD key, which is where the caradhras DB_PASSWORD already reads from; point caradhras.redisPassword.secretName/.secretKey elsewhere for a different topology.

That channel requires the endpoint to be a bare host:port, and the chart refuses to render otherwise. Caradhras compares the endpoint’s own password field with redisPassword and refuses to boot when they differ — and an endpoint that carries fields with an empty password field differs from any password, so valkey:6379,100 plus redisPassword crashloops even though neither value looks wrong on its own. Set one or the other, never a mix.

To keep the password out of your values file entirely, set caradhras.useExistingSecret=true and point caradhras.existingSecretName at a Secret you manage (external-secrets, sealed-secrets, or kubectl create secret) carrying the keys redisEndpoint and LICENSE_KEY. The chart then creates no Secret of its own. With a bare host:port in caradhras.configmap.redisEndpoint, that Secret needs only LICENSE_KEY.

The config keys are the beego literals in camelCase on purpose: caradhras resolves them with conf.GetConfigString, which looks the exact key up in the environment. REDIS_HOST / REDIS_TLS are the auth/identity names, read by lib-commons, and are silently ignored by caradhras.

The endpoint is never derived from global.datastores.redis.*: the mask knows only host and port, so a derived value would be silently unauthenticated against any Redis that requires AUTH. Set it explicitly. redisTls does come from the mask, so a managed-cloud profile that sets redis.tls once covers auth and caradhras both.

Auth Database (PostgreSQL)

Parameter Description Default
auth-database.enabled Enable the database dependency true
auth-database.auth.enabled Enable authentication for the database true
auth-database.auth.enablePostgresUser Enable the default postgres user false
auth-database.auth.username Username for the database auth
auth-database.auth.password Password for the database (generated by this chart when left empty, and kept across uninstall) ""
auth-database.auth.existingSecret Your own Secret with the database password (key password), replacing the one this chart keeps. LDAP and custom secretKeys need it the chart’s <release>-auth-database
auth-database.auth.database Name of the database casdoor
auth-database.primary.persistence.size Persistence size for the primary node 8Gi
auth-database.primary.resourcesPreset Resource preset for the primary node large
auth-database.primary.extendedConfiguration Extended PostgreSQL configuration See values.yaml
auth-database.primary.extraEnvVars Extra environment variables for the database See values.yaml

Valkey (Redis)

Parameter Description Default
valkey.enabled Enable the Valkey (Redis) dependency true
valkey.architecture Architecture for Valkey deployment standalone
valkey.auth.enabled Enable authentication for Valkey false

OTEL Collector

Parameter Description Default
otel-collector-lerian.enabled Inject HOST_IP (node IP) and OTEL_EXPORTER_OTLP_ENDPOINT=$(HOST_IP):4317 into the auth, identity and caradhras containers, so telemetry goes to a node-local OTLP collector. This chart declares no collector dependency and deploys none: install otel-collector-lerian (or any collector listening on the node IP, port 4317) separately. Set false to omit the two variables true
auth.deploymentStrategy.rollingUpdate.maxSurge Maximum number of pods that can be created over the desired number of pods. 1
auth.deploymentStrategy.rollingUpdate.maxUnavailable Maximum number of pods that can be unavailable during the update. 1
auth.pdb.enabled Specifies whether PodDisruptionBudget is enabled. true
auth.pdb.minAvailable Minimum number of available pods. 0
auth.pdb.maxUnavailable Maximum number of unavailable pods. 1
auth.pdb.annotations Annotations for the PodDisruptionBudget. {}
auth.resources.limits.cpu CPU limit allocated for the pods. 1
auth.resources.limits.memory Memory limit allocated for the pods. "756Mi"
auth.resources.requests.cpu Minimum CPU request for the pods. "500m"
auth.resources.requests.memory Minimum memory request for the pods. "256Mi"
auth.autoscaling.enabled Specifies whether autoscaling is enabled. true
auth.autoscaling.minReplicas Minimum number of replicas for autoscaling. 3
auth.autoscaling.maxReplicas Maximum number of replicas for autoscaling. 9
auth.autoscaling.targetCPUUtilizationPercentage Target CPU utilization percentage for autoscaling. 80
auth.autoscaling.targetMemoryUtilizationPercentage Target memory utilization percentage for autoscaling. 80
auth.spread Per-component override of global.scheduling.spread (fields: enabled, hostname, zone, maxSkew). {}
auth.topologySpreadConstraints Raw topologySpreadConstraints; non-empty replaces the spread preset. []
auth.nodeSelector Node selectors for pod scheduling. {}
auth.tolerations Tolerations for pod scheduling. {}
auth.affinity Affinity rules for pod scheduling. {}
auth.configmap Additional configurations in ConfigMap. See default values in the configuration.
auth.secrets Additional secrets for the service. See default values in the configuration.

Dependencies:

PostgreSQL

Valkey