Skip to content

Commit

Permalink
Add option to enable authentication using Bearer tokens
Browse files Browse the repository at this point in the history
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`).
  • Loading branch information
sirkrypt0 committed Nov 10, 2024
1 parent cd08e02 commit 0e53879
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 8 deletions.
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Two distinct providers are defined:

The endpoint is available under `<base url>/realms/<realm>/metrics` (Quarkus).
It will return data for all realms, no matter which realm you use in the URL.
By default, the metrics endpoint is **unprotected** and can be queried by everyone.
See [External Access](#external-access) to protect your metrics endpoint.

## License

Expand Down Expand Up @@ -433,7 +435,33 @@ keycloak_request_duration_sum{code="200",method="GET",resource="admin,admin/real
## 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.
By default, the metrics endpoint is **unprotected** and can be queried by everyone.
You should consider enabling one of the following settings.
#### Bearer Token (recommended)
You can configure authorization on the metrics endpoint using standard Bearer token authentication via the following environment variables (or corresponding CLI arguments).
Environment Variable | Default | Description
---|---|---
`KC_SPI_REALM_RESTAPI_EXTENSION_METRICS_BEARER_ENABLED` | `false` | Set to `true` to require a valid Bearer token from a user in the configured realm which has the configured role assigned.
`KC_SPI_REALM_RESTAPI_EXTENSION_METRICS_REALM` | `master` | Realm of the requesting user.
`KC_SPI_REALM_RESTAPI_EXTENSION_METRICS_ROLE` | `prometheus-metrics` | Role that the requesting user must have to be able to query the metrics.
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).
In Keycloak, 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.
#### Presence of the `X-Forwarded-Host` HTTP header
**Note**: The following requires careful setup of your reverse proxy headers. Please consider [Bearer authentication](#bearer-authentication-recommended) first.
To deny requests which have the `X-Forwarded-Host` header set, set the `DISABLE_EXTERNAL_ACCESS` environment variable to `true`.
Then, configure your reverse proxy to set the `X-Forwarded-Host` header (which is enabled by default on HA Proxy on Openshift).
Only requests which **don't** have the `X-Forwarded-Host` header set will be able to access the metrics.
If you configured your proxy correctly to set this by default, all requests going through that proxy won't have access to the metrics.
Instead, you'll need to access the metrics endpoint e.g. via the internal IP of the Keycloak host (since this won't have the `X-Forwarded-Host` header set).
## Grafana Dashboard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,51 @@

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 final Boolean bearerEnabled;
private String bearerRole;
private AuthResult bearerTokenAuth;

public MetricsEndpoint(KeycloakSession session, Boolean bearerEnabled, String bearerRealm, String bearerRole) {
super();

this.bearerEnabled = bearerEnabled;
if (this.bearerEnabled) {
RealmModel realmModel = session.realms().getRealmByName(bearerRealm);
if (realmModel == null) {
logger.errorf("Could not find realm with name %s", bearerRealm);
return;
}
session.getContext().setRealm(realmModel);
this.bearerTokenAuth = new BearerTokenAuthenticator(session).authenticate();
this.bearerRole = bearerRole;
}
}

@Override
public Object getResource() {
Expand All @@ -25,15 +56,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 ForbiddenException("X-Forwarded-Host header is present");
}
}

final StreamingOutput stream = output -> PrometheusExporter.instance().export(output);
return Response.ok(stream).build();
if (this.bearerEnabled) {
if (this.bearerTokenAuth == null) {
throw new NotAuthorizedException("Invalid bearer token");
} else if (this.bearerTokenAuth.getToken().getRealmAccess() == null
|| !this.bearerTokenAuth.getToken().getRealmAccess().isUserInRole(this.bearerRole)) {
throw new ForbiddenException("Missing required realm role");
}
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,26 @@

public class MetricsEndpointFactory implements RealmResourceProviderFactory {

private static final String BEARER_ENABLED_CONFIGURATION = "bearerEnabled";
private static final String BEARER_REALM_CONFIGURATION = "realm";
private static final String DEFAULT_BEARER_REALM = "master";
private static final String BEARER_ROLE_CONFIGURATION = "role";
private static final String DEFAULT_BEARER_ROLE = "prometheus-metrics";

private Boolean bearerEnabled;
private String bearerRealm;
private String bearerRole;

@Override
public RealmResourceProvider create(KeycloakSession session) {
return new MetricsEndpoint();
return new MetricsEndpoint(session, this.bearerEnabled, this.bearerRealm, this.bearerRole);
}

@Override
public void init(Config.Scope config) {
// nothing to do
this.bearerEnabled = config.getBoolean(BEARER_ENABLED_CONFIGURATION, false);
this.bearerRealm = config.get(BEARER_REALM_CONFIGURATION, DEFAULT_BEARER_REALM);
this.bearerRole = config.get(BEARER_ROLE_CONFIGURATION, DEFAULT_BEARER_ROLE);
}

@Override
Expand Down

0 comments on commit 0e53879

Please sign in to comment.