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 XFCC request authorization support #22

Merged
merged 11 commits into from
Oct 31, 2024
58 changes: 52 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@

This project provides an X509 client certificate lookup implementation for [Envoy proxy](https://www.envoyproxy.io/).
It allows Keycloak to retrieve the client certificate from the `x-forwarded-client-cert` (XFCC) header set by Envoy and use it for authorization.
For more information, refer to [Keycloak's reverse proxy documentation](https://www.keycloak.org/server/reverseproxy) and the section [Enabling client certificate lookup](https://www.keycloak.org/server/reverseproxy#_enabling_client_certificate_lookup).
See also [Envoy's documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-client-cert) on XFCC header.

See [Configuring Kubernetes Ingress Controllers for Client Certificate Forwarding](docs/ingress-controllers.md) for more information on how to configure Kubernetes ingress controllers for client certificate forwarding.
This project was created because the code submitted in [keycloak#33159](https://github.com/keycloak/keycloak/pull/33159) was not accepted.
Instead, Keycloak encourages the development of implementations for different proxies as extensions.


> ⚠️ **Alert:** There are implications that you should be aware of when enabling client certificate lookup in Keycloak.
For more information, see [Understanding Client Certificate Forwarding and Security Implications](docs/security-and-client-cert-forwarding.md).

This project was created because the code submitted in [keycloak#33159](https://github.com/keycloak/keycloak/pull/33159) was not accepted.
Instead, Keycloak encourages the development of implementations for different proxies as extensions.

## Installation

This project is not available on Maven Central.
Expand All @@ -26,6 +23,18 @@ Clone the repository and execute:
The JAR file will be created in the `target` directory.
Copy the JAR file to the `providers` directory in your Keycloak distribution.
For instance, in the official Keycloak Docker image releases, place the JAR file in the `/opt/keycloak/providers/`.

## Configuration

For information on how to use the project, refer to following documents:

* See [here](docs/ingress-controllers.md) on how to configure Kubernetes ingress controllers for client certificate forwarding.
* For more information on the Keycloak feature, refer to [Keycloak's reverse proxy documentation](https://www.keycloak.org/server/reverseproxy) and the section [Enabling client certificate lookup](https://www.keycloak.org/server/reverseproxy#_enabling_client_certificate_lookup).
* See also [Envoy's documentation](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-client-cert) on XFCC header.


### Enable client certificate lookup (mandatory)

Add following command line parameter to `kc.sh` to choose the provider:

```
Expand All @@ -45,6 +54,43 @@ This project may require updates for newer Keycloak versions.
Refer to Keycloak's [Configuring Providers](https://www.keycloak.org/server/configuration-provider) documentation for more information.


### Authorizing clients that are allowed to send XFCC headers (optional)

If Keycloak is deployed in an environment where some clients must bypass the proxy, it is important to ensure that only Envoy can send XFCC headers.
This prevents clients from impersonating other users by sending XFCC headers.
For more information on the verification process, refer to [this section](docs/security-and-client-cert-forwarding.md#mitigation) of the security implications document.

Prerequisites:

* Envoy must TLS and client certificate authentication for its connection to Keycloak.
* Configure the list of expected client certificate subject names that are allowed to send XFCC headers.

The list is configured as a command line parameter to `kc.sh` in the following format:

```
--spi-x509cert-lookup-envoy-cert-path-verify="[ [ <leaf-cert-subject>, <intermediate-cert-subject>, ... ], ... ]"
```
The presence of this parameter is optional and its behavior is as follows:

| Parameter value | Description | Example |
| --- | --- | --- |
| Not set | Any client can send XFCC headers, and they will be processed. | N/A |
| Empty array | XFCC headers will not be processed from any client. | `--spi-x509cert-lookup-envoy-cert-path-verify='[]'` |
| Non-empty array | XFCC headers will be processed only if the client certificate chain matches the specified subject names. | `--spi-x509cert-lookup-envoy-cert-path-verify='[[ "CN=envoy" ]]'` |

The parameter value is a JSON array of arrays.
Each inner array represents a certificate chain, with the first element as the leaf certificate's subject name and subsequent elements as intermediate certificates.
Root certificates should not be included.
For example, to allow a client certificate chain with the subject name `CN=envoy, O=example.com` and an intermediate certificate with the subject name `CN=intermediate, O=example.com`, use:

```
--spi-x509cert-lookup-envoy-cert-path-verify='[[ "CN=envoy, O=example.com", "CN=intermediate, O=example.com" ]]'
```

The subject names must match exactly, as X.500 Distinguished Names (DN) are order-sensitive (`CN=envoy, O=example.com` is not the same as `O=example.com, CN=envoy`).
The path can be partial: verification succeeds if the expected subject names are found in order, even if the received chain has additional certificates.


## Development

This section is for developers who wish to contribute to the project.
Expand Down
34 changes: 33 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,39 @@ services:
keycloak:
image: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION}

#
# Notes:
#
# - Kecyloak 22 does not support
# --spi-x509cert-lookup-envoy-cert-path-verify='[[ "CN=envoy-client" ]]'
# The parameter must be quoted as follows::
# --spi-x509cert-lookup-envoy-cert-path-verify='[[\"CN=authorized-client\"]]'
#
# This is possibly related to https://github.com/keycloak/keycloak/pull/22585
#
# - Keycloak 22 does not support PEM format for
# --https-trust-store-file=/input/target/certs/client-ca.pem
# even though following guide indicated it should work
# https://www.keycloak.org/nightly/server/mutual-tls#_using_a_dedicated_truststore_for_mtls
#
# Keycloak 26 and newer suppport also following
# --truststore-paths=/input/target/certs/client-ca.pem
#

entrypoint: /bin/bash
command:
- -cxe
- |
/opt/keycloak/bin/kc.sh import --file /input/src/test/resources/integration-test/keycloak-realm.json
/opt/keycloak/bin/kc.sh start --spi-x509cert-lookup-provider=envoy
/opt/keycloak/bin/kc.sh start \
--https-certificate-file=/input/target/certs/keycloak.pem \
--https-certificate-key-file=/input/target/certs/keycloak-key.pem \
--https-trust-store-file=/input/target/certs/client-ca-truststore.p12 \
--https-trust-store-password=password \
--https-client-auth=request \
--spi-x509cert-lookup-provider=envoy \
--spi-x509cert-lookup-envoy-cert-path-verify='[[ "CN=envoy-client" ]]' \
--log-level=INFO,io.github.nordix.keycloak.services.x509:debug

environment:
- KEYCLOAK_ADMIN=admin
Expand All @@ -30,3 +57,8 @@ services:
volumes:
- ./:/input:ro
- ./target/keycloak-client-cert-lookup-for-envoy-1.0-SNAPSHOT.jar:/opt/keycloak/providers/keycloak-client-cert-lookup-for-envoy.jar:ro

# Expose Keycloak's HTTPS port to allow test suite to do direct requests.
ports:
- "10080:8080"
- "10443:8443"
4 changes: 4 additions & 0 deletions docs/assets/client-authorization-flow.drawio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 18 additions & 10 deletions docs/security-and-client-cert-forwarding.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# Understanding Client Certificate Forwarding and Security Implications

This document outlines the security implications and risks of enabling client certificate forwarding in reverse proxies.
This document outlines the security implications, risks of enabling client certificate forwarding in reverse proxies and mitigations.
These concerns are not exclusive to Envoy but apply to any reverse proxy that forwards client certificates.
The initial architectural assumption is that all requests originate from the proxy, preventing direct client access to Keycloak.
However, in environments like Kubernetes, some clients may bypass the proxy, leading to additional considerations.


## Overview

The `x-forwarded-client-cert` (XFCC) header is used by Envoy proxy to send the client certificate information to the backend service, such as Keycloak.
Expand All @@ -17,16 +16,15 @@ Keycloak then uses the certificate for authorization purposes.

![image](assets/xfcc-intro.drawio.svg)


## Scenarios

### Scenario 1: Failed Authentication of an Client Running Inside the Proxy's Perimeter

Pre-conditions:

* Keycloak is configured with the X509 client certificate lookup SPI for Envoy proxy.
* A client is created to Keycloak with "X509 client certificate" client authenticator enabled.
* A client running inside Kubernetes cluster connects to Keycloak directly, without going through the Envoy proxy, using mutually authenticated TLS.
- Keycloak is configured with the X509 client certificate lookup SPI for Envoy proxy.
- A client is created to Keycloak with "X509 client certificate" client authenticator enabled.
- A client running inside Kubernetes cluster connects to Keycloak directly, without going through the Envoy proxy, using mutually authenticated TLS.

Scenario:

Expand All @@ -39,10 +37,10 @@ The client certificate information from the TLS layer is not used.

Pre-conditions:

* Keycloak is configured with the X509 client certificate lookup SPI for Envoy proxy.
* A client is created to Keycloak with "X509 client certificate" client authenticator enabled.
* A malicious user has acquired (1) the client ID and (2) the subject name of the client certificate.
* Malicious user has gained access to the cluster e.g., through a compromised pod.
- Keycloak is configured with the X509 client certificate lookup SPI for Envoy proxy.
- A client is created to Keycloak with "X509 client certificate" client authenticator enabled.
- A malicious user has acquired (1) the client ID and (2) the subject name of the client certificate.
- Malicious user has gained access to the cluster e.g., through a compromised pod.

Scenario:

Expand All @@ -51,3 +49,13 @@ This can include a Keycloak admin client to obtain full access to Keycloak.
The forged certificate can be self-generated by the malicious user, as long as it contains the correct subject name.

![image](assets/xfcc-scenario-2.drawio.svg)

## Mitigation

The following diagram demonstrates the logic implemented by the [X509 client certificate lookup SPI for Envoy proxy](https://github.com/Nordix/keycloak-client-cert-lookup-for-envoy).
This mechanism is designed to accept client certificates forwarded by the Envoy proxy while also securely handling clients that bypass the proxy and connect directly to Keycloak.
These direct clients are authenticated using their TLS-level client certificates or other authenticators (e.g. client secret) and are prevented from impersonating other clients by sending XFCC headers.

For this mitigation to be effective, Envoy must be configured to use TLS and client certificate authentication for its connection to Keycloak. Additionally, the X509 client certificate lookup SPI must be configured with the expected client certificate subject names for Envoy proxy.

![image](assets/client-authorization-flow.drawio.svg)
30 changes: 27 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>21</maven.compiler.release>
<!-- Compatibility with Keycloak 20 to 25 -->
<maven.compiler.release>17</maven.compiler.release>

<!-- Versions of dependencies -->
<keycloak.version>26.0.2</keycloak.version>

<junit.jupiter.version>5.11.3</junit.jupiter.version>
<jboss.resteasy.version>6.2.10.Final</jboss.resteasy.version>
<jboss.logmanager.version>3.0.6.Final</jboss.logmanager.version>
<certy.version>0.4.0</certy.version>
<apache.commons.exec.version>1.4.0</apache.commons.exec.version>
<maven.clean.plugin.version>3.4.0</maven.clean.plugin.version>
<maven.resources.plugin.version>3.3.1</maven.resources.plugin.version>
<maven.compiler.plugin.version>3.13.0</maven.compiler.plugin.version>
Expand Down Expand Up @@ -54,6 +57,13 @@
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>
<!-- For HttpRequest if Keycloak version is 23 or below. -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<version>${keycloak.version}</version>
<scope>provided</scope>
</dependency>

<!-- Test dependencies -->
<dependency>
Expand Down Expand Up @@ -86,6 +96,18 @@
<version>${certy.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>${apache.commons.exec.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.logmanager</groupId>
<artifactId>jboss-logmanager</artifactId>
<version>${jboss.logmanager.version}</version>
<scope>test</scope>
</dependency>

</dependencies>

Expand Down Expand Up @@ -117,6 +139,8 @@
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<java.util.logging.config.file>
${project.basedir}/src/test/resources/logging.properties</java.util.logging.config.file>
</systemPropertyVariables>
</configuration>
</plugin>
Expand All @@ -141,7 +165,7 @@
</pluginManagement>

<plugins>
<!-- Integration test configuration -->
<!-- Integration test -->
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven.failsafe.plugin.version}</version>
Expand All @@ -165,7 +189,7 @@
</executions>
</plugin>

<!-- Checkstyle configuration -->
<!-- Checkstyle -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
Expand Down
Loading