Before reporting an issue
Area
dist/quarkus
Describe the bug
Database passwords and other sensitive configuration values containing $$ or ${...} patterns are mangled by SmallRye Config's expression evaluation before reaching the database driver, causing authentication failures.
This represents a reliability risk for automated systems where any rotation could produce an incompatible password, causing unexpected downtime.
Root cause: SmallRye Config's ExpressionConfigSourceInterceptor processes ALL configuration values, including passwords:
$$ → $ (interpreted as escape sequence)
${VAR} → attempts variable substitution (fails or returns wrong value depending on Keycloak version)
${ (unclosed) → expression parser sees ${ as an unclosed expression and fails to resolve it, resulting in an empty/null password being passed to the JDBC driver
Why escaping is not a viable solution:
The current documentation suggests escaping $ characters with \. However, this is not practical for enterprise deployments using secrets management systems like:
- AWS Secrets Manager with automatic rotation
- HashiCorp Vault
- Azure Key Vault
- Kubernetes Secrets
In these environments:
- Secrets are opaque by design - security best practices dictate that humans should never see or modify secrets directly; they flow directly from the secrets manager to the application via environment variables
- Password rotation is automated - no human intervention to escape special characters
- Password policies require special characters - including
$ which is commonly in allowed character sets
Real-world impact: We have experienced this issue in production when AWS Secrets Manager rotation generated a database password containing $$. The database itself picked up the password correctly, but Keycloak failed to connect to the DB - we had to manually trigger another rotation to get a compatible password. Through systematic testing, we confirmed that $$ was the only pattern from our allowed character set that caused this failure. We saw that ${...} could also cause this issue for others if they allow brackets in their password policies (though most probably wouldn't).
Version
26.5.4 (also affects 22.x and earlier versions using SmallRye Config)
Regression
Expected behavior
Passwords and other sensitive properties (those marked with isMasked(true) in /quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java) should pass through unchanged, regardless of whether they contain $ characters.
For example:
KC_DB_PASSWORD="my$$password" should authenticate to the DB with my$$password
KC_DB_PASSWORD="test${var}pass" should authenticate to the DB with test${var}pass
Non-sensitive properties should continue to support expression evaluation for legitimate use cases like ${DB_HOST} in URLs.
Thoughts on why sensitive properties may not need expression evaluation:
Escaping $ with \ wasn’t a viable solution for us (more details above).
However, disabling expression evaluation on sensitive properties seems reasonable, but I'm open to feedback if there are other use cases not considered here:
-
Existing externalization mechanisms: As far as I can tell, there is no practical scenario where a password value would need to reference another configuration property. For externalizing passwords, dedicated mechanisms already exist:
- Environment variables (e.g.,
KC_DB_PASSWORD)
- Java KeyStore configuration source
- Kubernetes Secrets mounted as env vars
-
Automated rotation processes: Autorotation processes setting KC_DB_PASSWORD='my$$password' would expect that exact string to be the password without evaluation.
-
CLI arguments are already evaluated by the shell: If a user passes a password via CLI arguments (e.g., --db-password='my$$password'), the shell evaluates $ characters before Keycloak even receives the value. So expression evaluation at the Keycloak/SmallRye level would be redundant for CLI arguments, and only affects environment variables and config files where the literal value is preserved.
-
Alignment with existing masking: Properties marked isMasked(true) are already treated specially for display purposes. Extending this to disable expression evaluation could be a natural fit, as both behaviors serve the same goal: treating sensitive values as opaque data rather than interpretable configuration.
Actual behavior
KC_DB_PASSWORD="my$$password" → Keycloak tries to connect to the DB with my$password (authentication fails)
KC_DB_PASSWORD="test${VAR}pass" → Keycloak attempts variable substitution (fails or wrong value)
KC_DB_PASSWORD="test${pass" → Keycloaks tries to connect to the DB with an empty/null password (authentication fails)
How to Reproduce?
- Create a
docker-compose.yml file:
services:
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${TEST_PASSWORD}
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
ports:
- "5432:5432"
keycloak:
image: keycloak/keycloak:26.5.4
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: ${TEST_PASSWORD}
command: start-dev
ports:
- "8080:8080"
depends_on:
- postgres
- Run with a password containing
$$:
TEST_PASSWORD='my$$password' docker compose up
- Observe authentication failure: Keycloak tries to connect to the DB with
my$password instead of my$$password
Error logs (Keycloak v26.5.4):
2026-02-24 15:16:06,053 WARN [io.agroal.pool] (agroal-11) Datasource '<default>': FATAL: password authentication failed for user "keycloak"
2026-02-24 15:16:06,054 WARN [org.hibernate.orm.jdbc.error] (JPA Startup Thread) FATAL: password authentication failed for user "keycloak"
2026-02-24 15:16:06,055 WARN [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator] (JPA Startup Thread) HHH000342: Could not obtain connection to query JDBC database metadata: org.hibernate.exception.AuthException: Unable to obtain isolated JDBC connection [FATAL: password authentication failed for user "keycloak"] [n/a]
...
Caused by: org.postgresql.util.PSQLException: FATAL: password authentication failed for user "keycloak"
...
2026-02-24 15:16:06,433 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: Failed to start server in (development) mode
2026-02-24 15:16:06,434 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: Failed to obtain JDBC connection
2026-02-24 15:16:06,434 ERROR [org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler] (main) ERROR: FATAL: password authentication failed for user "keycloak"
Anything else?
Affected properties: All 17 properties currently marked with isMasked(true):
db-password
https-key-store-password
https-trust-store-password
bootstrap-admin-password
bootstrap-admin-client-secret
- And others (truststore passwords, vault secrets, etc.)
Proposed solution approach: Create a ConfigSourceInterceptor at higher priority than ExpressionConfigSourceInterceptor that wraps context.proceed() in Expressions.withoutExpansion() for properties where mapper.isMask() == true. This:
- Reuses the existing
isMask() flag (no new property list to maintain)
- Preserves expression evaluation for non-sensitive properties
- Requires no changes to SmallRye Config itself
Related issues:
I have a working implementation and can submit a PR if this approach is acceptable!
cc @amarbat @Anoopgopi @cgurtshaw
Before reporting an issue
Area
dist/quarkus
Describe the bug
Database passwords and other sensitive configuration values containing
$$or${...}patterns are mangled by SmallRye Config's expression evaluation before reaching the database driver, causing authentication failures.This represents a reliability risk for automated systems where any rotation could produce an incompatible password, causing unexpected downtime.
Root cause: SmallRye Config's
ExpressionConfigSourceInterceptorprocesses ALL configuration values, including passwords:$$→$(interpreted as escape sequence)${VAR}→ attempts variable substitution (fails or returns wrong value depending on Keycloak version)${(unclosed) → expression parser sees${as an unclosed expression and fails to resolve it, resulting in an empty/null password being passed to the JDBC driverWhy escaping is not a viable solution:
The current documentation suggests escaping
$characters with\. However, this is not practical for enterprise deployments using secrets management systems like:In these environments:
$which is commonly in allowed character setsReal-world impact: We have experienced this issue in production when AWS Secrets Manager rotation generated a database password containing
$$. The database itself picked up the password correctly, but Keycloak failed to connect to the DB - we had to manually trigger another rotation to get a compatible password. Through systematic testing, we confirmed that$$was the only pattern from our allowed character set that caused this failure. We saw that${...}could also cause this issue for others if they allow brackets in their password policies (though most probably wouldn't).Version
26.5.4 (also affects 22.x and earlier versions using SmallRye Config)
Regression
Expected behavior
Passwords and other sensitive properties (those marked with
isMasked(true)in/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java) should pass through unchanged, regardless of whether they contain$characters.For example:
KC_DB_PASSWORD="my$$password"should authenticate to the DB withmy$$passwordKC_DB_PASSWORD="test${var}pass"should authenticate to the DB withtest${var}passNon-sensitive properties should continue to support expression evaluation for legitimate use cases like
${DB_HOST}in URLs.Thoughts on why sensitive properties may not need expression evaluation:
Escaping
$with\wasn’t a viable solution for us (more details above).However, disabling expression evaluation on sensitive properties seems reasonable, but I'm open to feedback if there are other use cases not considered here:
Existing externalization mechanisms: As far as I can tell, there is no practical scenario where a password value would need to reference another configuration property. For externalizing passwords, dedicated mechanisms already exist:
KC_DB_PASSWORD)Automated rotation processes: Autorotation processes setting
KC_DB_PASSWORD='my$$password'would expect that exact string to be the password without evaluation.CLI arguments are already evaluated by the shell: If a user passes a password via CLI arguments (e.g.,
--db-password='my$$password'), the shell evaluates$characters before Keycloak even receives the value. So expression evaluation at the Keycloak/SmallRye level would be redundant for CLI arguments, and only affects environment variables and config files where the literal value is preserved.Alignment with existing masking: Properties marked
isMasked(true)are already treated specially for display purposes. Extending this to disable expression evaluation could be a natural fit, as both behaviors serve the same goal: treating sensitive values as opaque data rather than interpretable configuration.Actual behavior
KC_DB_PASSWORD="my$$password"→ Keycloak tries to connect to the DB withmy$password(authentication fails)KC_DB_PASSWORD="test${VAR}pass"→ Keycloak attempts variable substitution (fails or wrong value)KC_DB_PASSWORD="test${pass"→ Keycloaks tries to connect to the DB with an empty/null password (authentication fails)How to Reproduce?
docker-compose.ymlfile:$$:TEST_PASSWORD='my$$password' docker compose upmy$passwordinstead ofmy$$passwordError logs (Keycloak v26.5.4):
Anything else?
Affected properties: All 17 properties currently marked with
isMasked(true):db-passwordhttps-key-store-passwordhttps-trust-store-passwordbootstrap-admin-passwordbootstrap-admin-client-secretProposed solution approach: Create a
ConfigSourceInterceptorat higher priority thanExpressionConfigSourceInterceptorthat wrapscontext.proceed()inExpressions.withoutExpansion()for properties wheremapper.isMask() == true. This:isMask()flag (no new property list to maintain)Related issues:
{character causes startup failure)--db-passwordoption #19831 (similar special character issues)I have a working implementation and can submit a PR if this approach is acceptable!
cc @amarbat @Anoopgopi @cgurtshaw