fix: adding support for xforwarded prefix (#45699)

closes: #35298

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
Steven Hawkins
2026-01-26 12:14:54 -05:00
committed by GitHub
parent e414050524
commit 77704a91b6
7 changed files with 34 additions and 11 deletions

View File

@@ -12,6 +12,10 @@ In minor or patch releases, {project_name} will only introduce breaking changes
Notable changes may include internal behavior changes that prevent common misconfigurations, bugs that are fixed, or changes to simplify running {project_name}.
It also lists significant changes to internal APIs.
=== `X-Forwarded-Prefix` Header is now supported
With `proxy-headers` set to `xforwarded`, the server can determine the proxy context path from the `X-Forwarded-Prefix` header.
=== New database indexes on the `BROKER_LINK` table
The `BROKER_LINK` table now contains two additional indexes `IDX_BROKER_LINK_USER_ID` and `IDX_BROKER_LINK_IDENTITY_PROVIDER` to improve performance.

View File

@@ -66,7 +66,7 @@ The `proxy-headers` option can be also used to resolve the URL partially dynamic
<@kc.start parameters="--hostname my.keycloak.org --proxy-headers xforwarded"/>
In this case, scheme, and port are resolved dynamically from X-Forwarded-* headers, while hostname is statically defined as `my.keycloak.org`.
In this case, scheme, port, and context-path are resolved dynamically from X-Forwarded-* headers, while hostname is statically defined as `my.keycloak.org`.
=== Fixed URLs

View File

@@ -30,7 +30,7 @@ You only need to proxy port `8443` (or `8080`) even when you use different host
* By default if the option is not specified, no reverse proxy headers are parsed. This should be used when no proxy is in use or with https passthrough.
* `forwarded` enables parsing of the `Forwarded` header as per https://www.rfc-editor.org/rfc/rfc7239.html[RFC 7239].
* `xforwarded` enables parsing of non-standard `X-Forwarded-*` headers, such as `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, and `X-Forwarded-Port`.
* `xforwarded` enables parsing of non-standard `X-Forwarded-*` headers, such as `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Forwarded-Port`, and `X-Forwarded-Prefix`.
NOTE: If you are using a reverse proxy for anything other than https passthrough and do not set the `proxy-headers` option, then by default you will see 403 Forbidden responses to requests via the proxy that perform origin checking.
@@ -47,14 +47,14 @@ NOTE: When using the `xforwarded` setting, the `X-Forwarded-Port` takes preceden
NOTE: If the TLS connection is terminated at the reverse proxy (edge termination), enabling HTTP through the `http-enabled` setting is required.
== Different context-path on reverse proxy
== Different context path on reverse proxy
{project_name} assumes it is exposed through the reverse proxy under the same context path as {project_name} is configured for. By default {project_name} is exposed through the root (`/`), which means it expects to be exposed through the reverse proxy on `/` as well.
You can use a full URL for the `hostname` option in these cases, for example using `--hostname=https://my.keycloak.org/auth` if {project_name} is exposed through the reverse proxy on `/auth`.
For more details on exposing {project_name} on different hostname or context-path incl. Administration REST API and Console, see <@links.server id="hostname"/>.
Alternatively you can also change the context path of {project_name} itself to match the context path for the reverse proxy using the `http-relative-path` option, which will change the context-path of {project_name} itself to match the context path used by the reverse proxy.
By default {project_name} is exposed through the root context path (`/`). If the proxy is using a different context path than {project_name}, one of the following must be done:
- Use a simple hostname for the `hostname` option, `xforwarded` for the `proxy-headers` option, and have the proxy set the `X-Forwarded-Prefix` header.
- Use a full URL for the `hostname` option including the proxy context path, for example using `--hostname=https://my.keycloak.org/auth` if {project_name} is exposed through the reverse proxy on `/auth`.
- Change the context path of {project_name} itself to match the context path for the reverse proxy using the `http-relative-path` option.
For more details on exposing {project_name} on different hostname or context path incl. Administration REST API and Console, see <@links.server id="hostname"/>.
== Enable sticky sessions
@@ -141,7 +141,7 @@ The following table shows the recommended paths to expose.
We assume you run {project_name} on the root path `/` on your reverse proxy/gateway's public API.
If not, prefix the path with your desired one.
NOTE: If you configured a `http-relative-path` on the server, proceed as follows to use discovery wih RFC 8414: Configure a reverse proxy to map the `/.well-known/` path without the prefix to the path with the prefix on the server.
NOTE: If you configured a `http-relative-path` on the server, proceed as follows to use discovery with RFC 8414: Configure a reverse proxy to map the `/.well-known/` path without the prefix to the path with the prefix on the server.
== Trusted Proxies

View File

@@ -35,6 +35,11 @@ public class ProxyOptions {
.defaultValue(Boolean.FALSE)
.build();
public static final Option<Boolean> PROXY_X_FORWARDED_PREFIX_HEADER_ENABLED = new OptionBuilder<>("proxy-allow-x-forwarded-prefix-header", Boolean.class)
.category(OptionCategory.PROXY)
.defaultValue(Boolean.FALSE)
.build();
public static final Option<Boolean> PROXY_TRUSTED_HEADER_ENABLED = new OptionBuilder<>("proxy-trusted-header-enabled", Boolean.class)
.category(OptionCategory.PROXY)
.defaultValue(Boolean.FALSE)

View File

@@ -41,6 +41,10 @@ final class ProxyPropertyMappers implements PropertyMapperGrouping{
.to("quarkus.http.proxy.allow-x-forwarded")
.mapFrom(ProxyOptions.PROXY_HEADERS, (v, c) -> proxyEnabled(ProxyOptions.Headers.xforwarded, v, c))
.build(),
fromOption(ProxyOptions.PROXY_X_FORWARDED_PREFIX_HEADER_ENABLED)
.to("quarkus.http.proxy.enable-forwarded-prefix")
.mapFrom(ProxyOptions.PROXY_HEADERS, (v, c) -> proxyEnabled(ProxyOptions.Headers.xforwarded, v, c))
.build(),
fromOption(ProxyOptions.PROXY_TRUSTED_HEADER_ENABLED)
.to("quarkus.http.proxy.enable-trusted-proxy-header")
.mapFrom(ProxyOptions.PROXY_HEADERS, (v, c) -> proxyEnabled(null, v, c))

View File

@@ -27,7 +27,8 @@ public class ConstantsDebugHostname {
"X-Forwarded-Host",
"X-Forwarded-Proto",
"X-Forwarded-Port",
"X-Forwarded-For"
"X-Forwarded-For",
"X-Forwarded-Prefix"
};
public static final String FORWARDED_PROXY_HEADER = "Forwarded";

View File

@@ -121,6 +121,14 @@ public class ProxyHostnameV2DistTest {
assertXForwardedHeaders();
}
@Test
@Launch({ "start-dev", "--hostname=fixed", "--proxy-headers=xforwarded" })
public void testXForwardedProxyHeadersWithHostname() {
given().header("X-Forwarded-Prefix", "/prefix").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("http://fixed:8080/prefix/admin"));
given().header("X-Forwarded-Host", "test:123").when().get("https://localhost:8443").then().header(HttpHeaders.LOCATION, containsString("https://fixed:123/admin"));
given().header("X-Forwarded-Proto", "https").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("https://fixed/admin"));
}
private void assertForwardedHeader() {
// trigger a login error
assertForwardedHeader("http://mykeycloak.org:8080/realms/master/protocol/openid-connect/auth?client_id=security-admin-console", "https://test:1234/admin", ADDRESS);
@@ -139,6 +147,7 @@ public class ProxyHostnameV2DistTest {
}
private void assertXForwardedHeaders() {
given().header("X-Forwarded-Prefix", "/prefix").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("http://localhost:8080/prefix/admin"));
given().header("X-Forwarded-Host", "test:123").when().get("http://mykeycloak.org:8080").then().header(HttpHeaders.LOCATION, containsString("http://test:123/admin"));
given().header("X-Forwarded-Host", "test:123").when().get("http://localhost:8080").then().header(HttpHeaders.LOCATION, containsString("http://test:123/admin"));
given().header("X-Forwarded-Host", "test:123").when().get("https://localhost:8443").then().header(HttpHeaders.LOCATION, containsString("https://test:123/admin"));