Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to require bearer token authentication on the endpoint #138

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,41 @@ ERROR: Failed to open /opt/keycloak/lib/../providers/keycloak-metrics-spi.jar
```
The endpoint for the metrics is `<url>/<http_relative_path>/realms/<realm>/metrics`

### External Access

By default, the metrics endpoint returns HTTP Forbidden responses until authentication has been configured.

To activate the metrics endpoint, one of the following settings depending on your setup must be enabled.

#### 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.

#### 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`.
Expand Down Expand Up @@ -424,10 +459,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/
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,54 @@

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 authenticationDisabled;
private Boolean bearerEnabled;
private String role;
private AuthResult auth;

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);
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() {
Expand All @@ -25,15 +59,36 @@ 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");
}
return;
}

if (this.authenticationDisabled) {
return;
}

logger.error("Authentication on metrics endpoint is not configured!");
throw new ForbiddenException();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,29 @@

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();
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);

String resteasyVersion = ResteasyProviderFactory.class.getPackage().getImplementationVersion();
if (resteasyVersion.startsWith("3.")) {
Expand Down