From 6ed656e5618e39d09f8fe944d526725c87860a0b Mon Sep 17 00:00:00 2001 From: Tobias Kantusch Date: Thu, 28 Apr 2022 00:04:18 +0200 Subject: [PATCH 1/2] Add option to enable authentication using Bearer tokens By enabling the `bearerEnabled` setting, authentication on the metrics endpoint using valid Bearer tokens can now be enforced. A client requesting the metrics endpoint must set the `Authorization: Bearer` header with a valid token obtained from Keycloak. The token must originate from the realm configured by the `realm` setting (defaults to `master`) and must have the role configured in the `role` setting (defaults to `prometheus-metrics`). --- README.md | 30 +++++++++-- .../keycloak/metrics/MetricsEndpoint.java | 53 +++++++++++++++++-- .../metrics/MetricsEndpointFactory.java | 15 +++++- 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b70a631..48469c6 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,32 @@ ERROR: Failed to open /opt/keycloak/lib/../providers/keycloak-metrics-spi.jar ``` The endpoint for the metrics is `//realms//metrics` +### External Access + +By default, the metrics endpoint is accessible to **everyone** with access to your Keycloak instance. + +Make sure to enable one of the following settings depending on your setup. + +#### Bearer Authentication + +Note: If you use Keycloak < 17 use the corresponding XML settings instead of the environment variables. + +You can enable protection of the metrics endpoint via Bearer authentication by setting the `KC_SPI_REALM_RESTAPI_EXTENSION_METRICS_BEARER_ENABLED` environment variable (or corresponding command line argument) to `true`. + +By default, the requesting user must be in the `master` realm and have the `prometheus-metrics` role. +However, you can modify the realm using `KC_SPI_REALM_RESTAPI_EXTENSION_METRICS_REALM` and role using `KC_SPI_REALM_RESTAPI_EXTENSION_METRICS_ROLE`. + +To configure your Prometheus instance to obtain an OAuth2 token before querying the metrics endpoint, consult the [official Prometheus OAuth2 configuration](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#oauth2). +Use a client of type `confidential` that has `Service Accounts Enabled` set to `ON`. +Then, make sure to include the role configured above in the `Service Account Roles` of that client. + +#### `DISABLE_EXTERNAL_ACCESS` + +If using Bearer authentication is not an option, you can still protect the endpoint using the `DISABLE_EXTERNAL_ACCESS` environment variable. +Once set, requests in which the header 'X-Forwarded-Host' header is set will be denied. +Thus, to disable access to the metrics the header must be set in a request on your proxy. +This is enabled by default on HA Proxy on Openshift. + ### Enable metrics-listener event - To enable the event listener via the GUI interface, go to _Manage -> Events -> Config_. The _Event Listeners_ configuration should have an entry named `metrics-listener`. @@ -424,10 +450,6 @@ keycloak_request_duration_count{code="200",method="GET",resource="admin,admin/se keycloak_request_duration_sum{code="200",method="GET",resource="admin,admin/serverinfo",uri="",} 19.0 ``` -## External Access - -To disable metrics being externally accessible to a cluster. Set the environment variable 'DISABLE_EXTERNAL_ACCESS'. Once set enable the header 'X-Forwarded-Host' on your proxy. This is enabled by default on HA Proxy on Openshift. - ## Grafana Dashboard You can use this dashboard or create yours https://grafana.com/grafana/dashboards/10441-keycloak-metrics-dashboard/ diff --git a/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpoint.java b/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpoint.java index 4cf1e62..ee3b57b 100644 --- a/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpoint.java +++ b/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpoint.java @@ -2,20 +2,52 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.NotAuthorizedException; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.StreamingOutput; + +import org.jboss.logging.Logger; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.services.resource.RealmResourceProvider; +import org.keycloak.services.managers.AppAuthManager.BearerTokenAuthenticator; +import org.keycloak.services.managers.AuthenticationManager.AuthResult; public class MetricsEndpoint implements RealmResourceProvider { // The ID of the provider is also used as the name of the endpoint public final static String ID = "metrics"; - private static final boolean DISABLE_EXTERNAL_ACCESS = Boolean.parseBoolean(System.getenv("DISABLE_EXTERNAL_ACCESS")); + private static final boolean DISABLE_EXTERNAL_ACCESS = Boolean + .parseBoolean(System.getenv("DISABLE_EXTERNAL_ACCESS")); + + private final static Logger logger = Logger.getLogger(MetricsEndpoint.class); + + private Boolean bearerEnabled; + private String role; + private AuthResult auth; + + public MetricsEndpoint(KeycloakSession session, Boolean bearerEnabled, String realm, String role) { + super(); + + this.bearerEnabled = bearerEnabled; + if (this.bearerEnabled) { + RealmModel realmModel = session.realms().getRealmByName(realm); + if (realmModel == null) { + logger.errorf("Could not find realm with name %s", realm); + return; + } + session.getContext().setRealm(realmModel); + this.auth = new BearerTokenAuthenticator(session).authenticate(); + this.role = role; + } + } @Override public Object getResource() { @@ -25,15 +57,28 @@ public Object getResource() { @GET @Produces(MediaType.TEXT_PLAIN) public Response get(@Context HttpHeaders headers) { + checkAuthentication(headers); + + final StreamingOutput stream = output -> PrometheusExporter.instance().export(output); + return Response.ok(stream).build(); + } + + private void checkAuthentication(HttpHeaders headers) { if (DISABLE_EXTERNAL_ACCESS) { if (!headers.getRequestHeader("x-forwarded-host").isEmpty()) { // Request is being forwarded by HA Proxy on Openshift - return Response.status(Status.FORBIDDEN).build(); //(stream).build(); + throw new NotAuthorizedException("X-Forwarded-Host header is present"); } } - final StreamingOutput stream = output -> PrometheusExporter.instance().export(output); - return Response.ok(stream).build(); + if (this.bearerEnabled) { + if (this.auth == null) { + throw new NotAuthorizedException("Invalid bearer token"); + } else if (this.auth.getToken().getRealmAccess() == null + || !this.auth.getToken().getRealmAccess().isUserInRole(this.role)) { + throw new ForbiddenException("Missing required realm role"); + } + } } @Override diff --git a/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpointFactory.java b/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpointFactory.java index 10881ea..de72841 100644 --- a/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpointFactory.java +++ b/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpointFactory.java @@ -9,13 +9,26 @@ public class MetricsEndpointFactory implements RealmResourceProviderFactory { + private static final String BEARER_ENABLED_CONFIGURATION = "bearerEnabled"; + private static final String REALM_CONFIGURATION = "realm"; + private static final String DEFAULT_REALM = "master"; + private static final String ROLE_CONFIGURATION = "role"; + private static final String DEFAULT_ROLE = "prometheus-metrics"; + + private Boolean bearerEnabled; + private String realm; + private String role; + @Override public RealmResourceProvider create(KeycloakSession session) { - return new MetricsEndpoint(); + return new MetricsEndpoint(session, this.bearerEnabled, this.realm, this.role); } @Override public void init(Config.Scope config) { + this.bearerEnabled = config.getBoolean(BEARER_ENABLED_CONFIGURATION, false); + this.realm = config.get(REALM_CONFIGURATION, DEFAULT_REALM); + this.role = config.get(ROLE_CONFIGURATION, DEFAULT_ROLE); String resteasyVersion = ResteasyProviderFactory.class.getPackage().getImplementationVersion(); if (resteasyVersion.startsWith("3.")) { From 655893ec063c2b4ffd58aeb6e2f623edba16c168 Mon Sep 17 00:00:00 2001 From: Tobias Kantusch Date: Mon, 23 May 2022 10:32:24 +0200 Subject: [PATCH 2/2] Enforce configuring an authentication option by default Security should be enabled by default in projects. We hence enforce that any of the available authentication options are configured before delivering the metrics as a response. If none of the authentication options are viable, authentication can be disabled all together, although that is surely not recommended. --- README.md | 13 +++++++++++-- .../aerogear/keycloak/metrics/MetricsEndpoint.java | 12 +++++++++++- .../keycloak/metrics/MetricsEndpointFactory.java | 5 ++++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 48469c6..a6395c7 100644 --- a/README.md +++ b/README.md @@ -108,9 +108,9 @@ The endpoint for the metrics is `//realms//metri ### External Access -By default, the metrics endpoint is accessible to **everyone** with access to your Keycloak instance. +By default, the metrics endpoint returns HTTP Forbidden responses until authentication has been configured. -Make sure to enable one of the following settings depending on your setup. +To activate the metrics endpoint, one of the following settings depending on your setup must be enabled. #### Bearer Authentication @@ -132,6 +132,15 @@ Once set, requests in which the header 'X-Forwarded-Host' header is set will be Thus, to disable access to the metrics the header must be set in a request on your proxy. This is enabled by default on HA Proxy on Openshift. +#### Disable Authentication + +**NOT recommended**. + +If you are sure what you do and have considered the other authentication options, you can disable authentication completely. +However, this will make your endpoint accessible to everyone with access to your Keycloak instance, if the metrics route is not otherwise protected. + +To disable the authentication, set the `KC_SPI_REALM_RESTAPI_EXTENSION_METRICS_AUTHENTICATION_DISABLED` environment variable (or corresponding command line argument) to `true`. + ### Enable metrics-listener event - To enable the event listener via the GUI interface, go to _Manage -> Events -> Config_. The _Event Listeners_ configuration should have an entry named `metrics-listener`. diff --git a/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpoint.java b/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpoint.java index ee3b57b..9a7b1ff 100644 --- a/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpoint.java +++ b/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpoint.java @@ -29,13 +29,15 @@ public class MetricsEndpoint implements RealmResourceProvider { private final static Logger logger = Logger.getLogger(MetricsEndpoint.class); + private Boolean authenticationDisabled; private Boolean bearerEnabled; private String role; private AuthResult auth; - public MetricsEndpoint(KeycloakSession session, Boolean bearerEnabled, String realm, String role) { + public MetricsEndpoint(KeycloakSession session, Boolean authenticationDisabled, Boolean bearerEnabled, String realm, String role) { super(); + this.authenticationDisabled = authenticationDisabled; this.bearerEnabled = bearerEnabled; if (this.bearerEnabled) { RealmModel realmModel = session.realms().getRealmByName(realm); @@ -78,7 +80,15 @@ private void checkAuthentication(HttpHeaders headers) { || !this.auth.getToken().getRealmAccess().isUserInRole(this.role)) { throw new ForbiddenException("Missing required realm role"); } + return; } + + if (this.authenticationDisabled) { + return; + } + + logger.error("Authentication on metrics endpoint is not configured!"); + throw new ForbiddenException(); } @Override diff --git a/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpointFactory.java b/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpointFactory.java index de72841..d858970 100644 --- a/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpointFactory.java +++ b/src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpointFactory.java @@ -9,23 +9,26 @@ public class MetricsEndpointFactory implements RealmResourceProviderFactory { + private static final String DISABLE_AUTHENTICATION = "disableAuthentication"; private static final String BEARER_ENABLED_CONFIGURATION = "bearerEnabled"; private static final String REALM_CONFIGURATION = "realm"; private static final String DEFAULT_REALM = "master"; private static final String ROLE_CONFIGURATION = "role"; private static final String DEFAULT_ROLE = "prometheus-metrics"; + private Boolean authenticationDisabled; private Boolean bearerEnabled; private String realm; private String role; @Override public RealmResourceProvider create(KeycloakSession session) { - return new MetricsEndpoint(session, this.bearerEnabled, this.realm, this.role); + return new MetricsEndpoint(session, this.authenticationDisabled, this.bearerEnabled, this.realm, this.role); } @Override public void init(Config.Scope config) { + this.authenticationDisabled = config.getBoolean(DISABLE_AUTHENTICATION, false); this.bearerEnabled = config.getBoolean(BEARER_ENABLED_CONFIGURATION, false); this.realm = config.get(REALM_CONFIGURATION, DEFAULT_REALM); this.role = config.get(ROLE_CONFIGURATION, DEFAULT_ROLE);