Skip to content

Commit 0e53879

Browse files
committed
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`).
1 parent cd08e02 commit 0e53879

3 files changed

Lines changed: 92 additions & 8 deletions

File tree

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Two distinct providers are defined:
1111

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

1517
## License
1618

@@ -433,7 +435,33 @@ keycloak_request_duration_sum{code="200",method="GET",resource="admin,admin/real
433435
434436
## External Access
435437
436-
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.
438+
By default, the metrics endpoint is **unprotected** and can be queried by everyone.
439+
You should consider enabling one of the following settings.
440+
441+
#### Bearer Token (recommended)
442+
443+
You can configure authorization on the metrics endpoint using standard Bearer token authentication via the following environment variables (or corresponding CLI arguments).
444+
445+
Environment Variable | Default | Description
446+
---|---|---
447+
`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.
448+
`KC_SPI_REALM_RESTAPI_EXTENSION_METRICS_REALM` | `master` | Realm of the requesting user.
449+
`KC_SPI_REALM_RESTAPI_EXTENSION_METRICS_ROLE` | `prometheus-metrics` | Role that the requesting user must have to be able to query the metrics.
450+
451+
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).
452+
In Keycloak, use a client of type `confidential` that has `Service Accounts Enabled` set to `ON`.
453+
Then, make sure to include the role configured above in the `Service Account Roles` of that client.
454+
455+
#### Presence of the `X-Forwarded-Host` HTTP header
456+
457+
**Note**: The following requires careful setup of your reverse proxy headers. Please consider [Bearer authentication](#bearer-authentication-recommended) first.
458+
459+
To deny requests which have the `X-Forwarded-Host` header set, set the `DISABLE_EXTERNAL_ACCESS` environment variable to `true`.
460+
Then, configure your reverse proxy to set the `X-Forwarded-Host` header (which is enabled by default on HA Proxy on Openshift).
461+
462+
Only requests which **don't** have the `X-Forwarded-Host` header set will be able to access the metrics.
463+
If you configured your proxy correctly to set this by default, all requests going through that proxy won't have access to the metrics.
464+
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).
437465
438466
## Grafana Dashboard
439467

src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpoint.java

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,51 @@
22

33
import jakarta.ws.rs.GET;
44
import jakarta.ws.rs.Produces;
5+
import jakarta.ws.rs.ForbiddenException;
6+
import jakarta.ws.rs.NotAuthorizedException;
57
import jakarta.ws.rs.core.Context;
68
import jakarta.ws.rs.core.HttpHeaders;
79
import jakarta.ws.rs.core.MediaType;
810
import jakarta.ws.rs.core.Response;
9-
import jakarta.ws.rs.core.Response.Status;
1011
import jakarta.ws.rs.core.StreamingOutput;
12+
13+
import org.jboss.logging.Logger;
14+
15+
import org.keycloak.models.KeycloakSession;
16+
import org.keycloak.models.RealmModel;
1117
import org.keycloak.services.resource.RealmResourceProvider;
18+
import org.keycloak.services.managers.AppAuthManager.BearerTokenAuthenticator;
19+
import org.keycloak.services.managers.AuthenticationManager.AuthResult;
1220

1321
public class MetricsEndpoint implements RealmResourceProvider {
1422

1523
// The ID of the provider is also used as the name of the endpoint
1624
public final static String ID = "metrics";
1725

18-
private static final boolean DISABLE_EXTERNAL_ACCESS = Boolean.parseBoolean(System.getenv("DISABLE_EXTERNAL_ACCESS"));
26+
private static final boolean DISABLE_EXTERNAL_ACCESS = Boolean
27+
.parseBoolean(System.getenv("DISABLE_EXTERNAL_ACCESS"));
28+
29+
private final static Logger logger = Logger.getLogger(MetricsEndpoint.class);
30+
31+
private final Boolean bearerEnabled;
32+
private String bearerRole;
33+
private AuthResult bearerTokenAuth;
34+
35+
public MetricsEndpoint(KeycloakSession session, Boolean bearerEnabled, String bearerRealm, String bearerRole) {
36+
super();
37+
38+
this.bearerEnabled = bearerEnabled;
39+
if (this.bearerEnabled) {
40+
RealmModel realmModel = session.realms().getRealmByName(bearerRealm);
41+
if (realmModel == null) {
42+
logger.errorf("Could not find realm with name %s", bearerRealm);
43+
return;
44+
}
45+
session.getContext().setRealm(realmModel);
46+
this.bearerTokenAuth = new BearerTokenAuthenticator(session).authenticate();
47+
this.bearerRole = bearerRole;
48+
}
49+
}
1950

2051
@Override
2152
public Object getResource() {
@@ -25,15 +56,28 @@ public Object getResource() {
2556
@GET
2657
@Produces(MediaType.TEXT_PLAIN)
2758
public Response get(@Context HttpHeaders headers) {
59+
checkAuthentication(headers);
60+
61+
final StreamingOutput stream = output -> PrometheusExporter.instance().export(output);
62+
return Response.ok(stream).build();
63+
}
64+
65+
private void checkAuthentication(HttpHeaders headers) {
2866
if (DISABLE_EXTERNAL_ACCESS) {
2967
if (!headers.getRequestHeader("x-forwarded-host").isEmpty()) {
3068
// Request is being forwarded by HA Proxy on Openshift
31-
return Response.status(Status.FORBIDDEN).build(); //(stream).build();
69+
throw new ForbiddenException("X-Forwarded-Host header is present");
3270
}
3371
}
3472

35-
final StreamingOutput stream = output -> PrometheusExporter.instance().export(output);
36-
return Response.ok(stream).build();
73+
if (this.bearerEnabled) {
74+
if (this.bearerTokenAuth == null) {
75+
throw new NotAuthorizedException("Invalid bearer token");
76+
} else if (this.bearerTokenAuth.getToken().getRealmAccess() == null
77+
|| !this.bearerTokenAuth.getToken().getRealmAccess().isUserInRole(this.bearerRole)) {
78+
throw new ForbiddenException("Missing required realm role");
79+
}
80+
}
3781
}
3882

3983
@Override

src/main/java/org/jboss/aerogear/keycloak/metrics/MetricsEndpointFactory.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,26 @@
88

99
public class MetricsEndpointFactory implements RealmResourceProviderFactory {
1010

11+
private static final String BEARER_ENABLED_CONFIGURATION = "bearerEnabled";
12+
private static final String BEARER_REALM_CONFIGURATION = "realm";
13+
private static final String DEFAULT_BEARER_REALM = "master";
14+
private static final String BEARER_ROLE_CONFIGURATION = "role";
15+
private static final String DEFAULT_BEARER_ROLE = "prometheus-metrics";
16+
17+
private Boolean bearerEnabled;
18+
private String bearerRealm;
19+
private String bearerRole;
20+
1121
@Override
1222
public RealmResourceProvider create(KeycloakSession session) {
13-
return new MetricsEndpoint();
23+
return new MetricsEndpoint(session, this.bearerEnabled, this.bearerRealm, this.bearerRole);
1424
}
1525

1626
@Override
1727
public void init(Config.Scope config) {
18-
// nothing to do
28+
this.bearerEnabled = config.getBoolean(BEARER_ENABLED_CONFIGURATION, false);
29+
this.bearerRealm = config.get(BEARER_REALM_CONFIGURATION, DEFAULT_BEARER_REALM);
30+
this.bearerRole = config.get(BEARER_ROLE_CONFIGURATION, DEFAULT_BEARER_ROLE);
1931
}
2032

2133
@Override

0 commit comments

Comments
 (0)