enhance: allow for control over what port health checks are exposed on (#41759)

closes: #39506

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
Steven Hawkins 2025-08-28 04:18:22 -04:00 committed by GitHub
parent bcdbde38dd
commit 565e195f48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 99 additions and 10 deletions

View File

@ -14,6 +14,11 @@ For more information, see the link:{adminguide_link}#_update-email-workflow[Upda
There's a new option `http-management-scheme` that may be set to `http` to force the management interface to use HTTP rather than inheriting the HTTPS settings of the main interface.
= Option to expose health endpoints on the main HTTP(S) ports
With `health-enabled` set to true, you may set the `http-management-health-enabled` to `false` to indicate that health endpoints should be exposed on the main HTTP(s) ports instead of the
management port. When this option is `false` you should block unwanted external traffic to `/health` at your proxy.
= Additional context information for log messages (preview)
You can now add context information to each log message like the realm or the client that initiated the request.
@ -32,6 +37,7 @@ While access logs are often used for debugging and traffic analysis, they are al
For more information, see the https://www.keycloak.org/server/logging[Logging guide].
= Supported passkeys
*Passkeys* integration is now a supported feature. This feature integrates passkeys seamlessly in the {project_name} forms using both conditional and modal UI. Although supported, *passkeys* are disabled by default. To activate the integration in the realm, the option *Enable Passkeys* in the *WebAuthn Passwordless Policy* (*Authentication* → *Policies* → *Webauthn Passwordless Policy*) needs to be enabled.

View File

@ -11,6 +11,9 @@ includedOptions="health-enabled">
{project_name} has built in support for health checks. This {section} describes how to enable and use the {project_name} health checks.
The {project_name} health checks are exposed on the management port `9000` by default. For more details, see <@links.server id="management-interface" />
When the `http-management-health-enabled` option is `false` the health endpoints will remain on the main HTTP(S) ports, rather than being exposed on the management port.
When this option is `false` you should block unwanted external traffic to `/health` at your proxy.
== {project_name} health check endpoints
{project_name} exposes 4 health endpoints:

View File

@ -48,6 +48,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.CacheSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpManagementSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.ProbeSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.SchedulingSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.Truststore;
@ -83,6 +84,9 @@ import static org.keycloak.operator.crds.v2alpha1.deployment.spec.TracingSpec.co
)
public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependentResource<StatefulSet, Keycloak> {
public static final String HTTP_MANAGEMENT_HEALTH_ENABLED = "http-management-health-enabled";
public static final String HTTP_MANAGEMENT_SCHEME = "http-management-scheme";
public static final String POD_IP = "POD_IP";
private static final List<String> COPY_ENV = Arrays.asList("HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY");
@ -326,9 +330,20 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
// Set bind address as this is required for JGroups to form a cluster in IPv6 envionments
containerBuilder.addToArgs(0, "-Djgroups.bind.address=$(%s)".formatted(POD_IP));
boolean tls = isTlsConfigured(keycloakCR);
String protocol = tls ? "HTTPS" : "HTTP";
int port = -1;
if (readConfigurationValue(HTTP_MANAGEMENT_HEALTH_ENABLED, keycloakCR, context).map(Boolean::valueOf).orElse(true)) {
port = HttpManagementSpec.managementPort(keycloakCR);
if (readConfigurationValue(HTTP_MANAGEMENT_SCHEME, keycloakCR, context).filter("http"::equals).isPresent()) {
protocol = "HTTP";
}
} else {
port = tls ? HttpSpec.httpsPort(keycloakCR) : HttpSpec.httpPort(keycloakCR);
}
// probes
var protocol = isManagementHttps(keycloakCR) ? "HTTPS" : "HTTP";
var port = HttpManagementSpec.managementPort(keycloakCR);
var readinessOptionalSpec = Optional.ofNullable(keycloakCR.getSpec().getReadinessProbeSpec());
var livenessOptionalSpec = Optional.ofNullable(keycloakCR.getSpec().getLivenessProbeSpec());
var startupOptionalSpec = Optional.ofNullable(keycloakCR.getSpec().getStartupProbeSpec());
@ -391,11 +406,6 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
.endContainer().endSpec().endTemplate().endSpec().build();
}
private boolean isManagementHttps(Keycloak keycloakCR) {
return isTlsConfigured(keycloakCR) && keycloakCR.getSpec().getAdditionalOptions().stream()
.noneMatch(v -> "http-management-scheme".equals(v.getName()) && "http".equals(v.getValue()));
}
private void handleScheduling(Keycloak keycloakCR, Map<String, String> labels, PodSpecFluent<?> specBuilder) {
SchedulingSpec schedulingSpec = keycloakCR.getSpec().getSchedulingSpec();
if (schedulingSpec != null) {

View File

@ -392,7 +392,7 @@ public class PodTemplateTest {
@Test
public void testHttpManagment() {
var result = getDeployment(null, new StatefulSet(),
spec -> spec.withAdditionalOptions(new ValueOrSecret("http-management-scheme", "http")))
spec -> spec.withAdditionalOptions(new ValueOrSecret(KeycloakDeploymentDependentResource.HTTP_MANAGEMENT_SCHEME, "http")))
.getSpec()
.getTemplate()
.getSpec()
@ -400,6 +400,21 @@ public class PodTemplateTest {
.get(0);
assertEquals("HTTP", result.getReadinessProbe().getHttpGet().getScheme());
assertEquals(9000, result.getReadinessProbe().getHttpGet().getPort().getIntVal());
}
@Test
public void testHealthOnMain() {
var result = getDeployment(null, new StatefulSet(),
spec -> spec.withAdditionalOptions(new ValueOrSecret(KeycloakDeploymentDependentResource.HTTP_MANAGEMENT_HEALTH_ENABLED, "false")))
.getSpec()
.getTemplate()
.getSpec()
.getContainers()
.get(0);
assertEquals("HTTPS", result.getReadinessProbe().getHttpGet().getScheme());
assertEquals(8443, result.getReadinessProbe().getHttpGet().getPort().getIntVal());
}
@Test

View File

@ -31,6 +31,13 @@ public class ManagementOptions {
.hidden()
.build();
public static final Option<Boolean> HTTP_MANAGEMENT_HEALTH_ENABLED = new OptionBuilder<>("http-management-health-enabled", Boolean.class)
.category(OptionCategory.MANAGEMENT)
.description("If health endpoints should be exposed on the management interface. If false, health endpoints will be exposed on the main interface.")
.defaultValue(true)
.buildTime(true)
.build();
public static final Option<Boolean> LEGACY_OBSERVABILITY_INTERFACE = new OptionBuilder<>("legacy-observability-interface", Boolean.class)
.category(OptionCategory.MANAGEMENT)
.deprecated()

View File

@ -40,6 +40,10 @@ public class ManagementPropertyMappers {
.to("quarkus.management.enabled")
.transformer((val, ctx) -> managementEnabledTransformer())
.build(),
fromOption(ManagementOptions.HTTP_MANAGEMENT_HEALTH_ENABLED)
.to("quarkus.smallrye-health.management.enabled")
.isEnabled(() -> isTrue(HealthOptions.HEALTH_ENABLED), "health is enabled")
.build(),
fromOption(ManagementOptions.LEGACY_OBSERVABILITY_INTERFACE)
.build(),
fromOption(ManagementOptions.HTTP_MANAGEMENT_RELATIVE_PATH)
@ -122,7 +126,8 @@ public class ManagementPropertyMappers {
if (isTrue(LEGACY_OBSERVABILITY_INTERFACE)) {
return false;
}
var isManagementOccupied = isTrue(HealthOptions.HEALTH_ENABLED) || isTrue(MetricsOptions.METRICS_ENABLED);
var isManagementOccupied = isTrue(MetricsOptions.METRICS_ENABLED)
|| (isTrue(HealthOptions.HEALTH_ENABLED) && isTrue(ManagementOptions.HTTP_MANAGEMENT_HEALTH_ENABLED));
return isManagementOccupied;
}

View File

@ -937,4 +937,11 @@ public class PicocliTest extends AbstractConfigurationTest {
nonRunningPicocli = pseudoLaunch("start-dev", "--http-access-log-enabled=true", "--http-access-log-exclude='/realms/my-realm/.*");
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
}
@Test
public void healthEnabledRequired() {
var nonRunningPicocli = pseudoLaunch("start-dev", "--http-management-health-enabled=false");
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("Available only when health is enabled"));
}
}

View File

@ -50,7 +50,7 @@ public class HealthDistTest {
@Test
@Launch({ "start-dev", "--health-enabled=true" })
void testHealthEndpoint() {
void testHealthEndpoint(KeycloakDistribution distribution) {
when().get("/health").then()
.statusCode(200);
when().get("/health/live").then()
@ -62,6 +62,18 @@ public class HealthDistTest {
.statusCode(404);
when().get("/lb-check").then()
.statusCode(404);
// still nothing on main
distribution.setRequestPort(8080);
when().get("/health/ready").then()
.statusCode(404);
}
@Test
@Launch({ "start-dev", "--health-enabled=true", "--http-management-health-enabled=false" })
void testHealthEndpointOnMain(KeycloakDistribution distribution) {
distribution.setRequestPort(8080);
when().get("/health/ready").then().statusCode(200);
}
@Test

View File

@ -153,6 +153,10 @@ HTTP Access log:
Management:
--http-management-health-enabled <true|false>
If health endpoints should be exposed on the management interface. If false,
health endpoints will be exposed on the main interface. Default: true.
Available only when health is enabled.
--http-management-port <port>
Port of the management interface. Relevant only when something is exposed on
the management interface - see the guide for details. Default: 9000.

View File

@ -153,6 +153,10 @@ HTTP Access log:
Management:
--http-management-health-enabled <true|false>
If health endpoints should be exposed on the management interface. If false,
health endpoints will be exposed on the main interface. Default: true.
Available only when health is enabled.
--http-management-port <port>
Port of the management interface. Relevant only when something is exposed on
the management interface - see the guide for details. Default: 9000.

View File

@ -366,6 +366,10 @@ Health:
Management:
--http-management-health-enabled <true|false>
If health endpoints should be exposed on the management interface. If false,
health endpoints will be exposed on the main interface. Default: true.
Available only when health is enabled.
--http-management-port <port>
Port of the management interface. Relevant only when something is exposed on
the management interface - see the guide for details. Default: 9000.

View File

@ -367,6 +367,10 @@ Health:
Management:
--http-management-health-enabled <true|false>
If health endpoints should be exposed on the management interface. If false,
health endpoints will be exposed on the main interface. Default: true.
Available only when health is enabled.
--http-management-port <port>
Port of the management interface. Relevant only when something is exposed on
the management interface - see the guide for details. Default: 9000.

View File

@ -366,6 +366,10 @@ Health:
Management:
--http-management-health-enabled <true|false>
If health endpoints should be exposed on the management interface. If false,
health endpoints will be exposed on the main interface. Default: true.
Available only when health is enabled.
--http-management-port <port>
Port of the management interface. Relevant only when something is exposed on
the management interface - see the guide for details. Default: 9000.

View File

@ -364,6 +364,10 @@ Health:
Management:
--http-management-health-enabled <true|false>
If health endpoints should be exposed on the management interface. If false,
health endpoints will be exposed on the main interface. Default: true.
Available only when health is enabled.
--http-management-port <port>
Port of the management interface. Relevant only when something is exposed on
the management interface - see the guide for details. Default: 9000.