fix: adding a first-class option for trusted proxies

closes: #32135

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
Steve Hawkins 2024-08-28 13:14:52 -04:00 committed by Alexander Schwartz
parent 7472d0ac5e
commit c9779cfa24
14 changed files with 114 additions and 10 deletions

View file

@ -138,6 +138,10 @@ The deprecated hostname v1 feature was removed. This feature was deprecated in {
The deprecated `proxy` option was removed. This option was deprecated in {project_name} 24 and replaced by the `proxy-headers` option in combination with hostname options as needed. For more details, see https://www.keycloak.org/server/reverseproxy[using a reverse proxy] and https://www.keycloak.org/docs/latest/upgrading/index.html#deprecated-proxy-option[the initial migration guide]. The deprecated `proxy` option was removed. This option was deprecated in {project_name} 24 and replaced by the `proxy-headers` option in combination with hostname options as needed. For more details, see https://www.keycloak.org/server/reverseproxy[using a reverse proxy] and https://www.keycloak.org/docs/latest/upgrading/index.html#deprecated-proxy-option[the initial migration guide].
= Option `proxy-trusted-addresses` added
The `proxy-trusted-addresses` can be used when the `proxy-headers` option is set to specify a allowlist of trusted proxy addresses. If the proxy address for a given request is not trusted, then the respective proxy header values will not be used.
= Property `origin` in the `UserRepresentation` is deprecated = Property `origin` in the `UserRepresentation` is deprecated
The `origin` property in the `UserRepresentation` is deprecated and planned to be removed in future releases. The `origin` property in the `UserRepresentation` is deprecated and planned to be removed in future releases.

View file

@ -132,6 +132,14 @@ As it's true that the `js` path is needed for internal clients like the account
We assume you run {project_name} on the root path `/` on your reverse proxy/gateway's public API. 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. If not, prefix the path with your desired one.
== Trusted Proxies
To ensure that proxy headers are used only from proxies you trust, set the `proxy-trusted-addresses` option to a comma separated list of IP addresses (IPv4 or IPv6) or Classless Inter-Domain Routing (CIDR) notations.
For example:
<@kc.start parameters="--proxy-headers forwarded --proxy-trusted-addresses=192.168.0.32,127.0.0.0/8"/>
== Enabling client certificate lookup == Enabling client certificate lookup
When the proxy is configured as a TLS termination proxy the client certificate information can be forwarded to the server through specific HTTP request headers and then used to authenticate When the proxy is configured as a TLS termination proxy the client certificate information can be forwarded to the server through specific HTTP request headers and then used to authenticate

View file

@ -1,5 +1,7 @@
package org.keycloak.config; package org.keycloak.config;
import java.util.List;
public class ProxyOptions { public class ProxyOptions {
public enum Headers { public enum Headers {
@ -26,4 +28,9 @@ public class ProxyOptions {
.category(OptionCategory.PROXY) .category(OptionCategory.PROXY)
.defaultValue(Boolean.FALSE) .defaultValue(Boolean.FALSE)
.build(); .build();
public static final Option<List<String>> PROXY_TRUSTED_ADDRESSES = OptionBuilder.listOptionBuilder("proxy-trusted-addresses", String.class)
.category(OptionCategory.PROXY)
.description("A comma separated list of trusted proxy addresses. If set, then proxy headers from other addresses will be ignored. By default all addresses are trusted. A trusted proxy address is specified as an IP address (IPv4 or IPv6) or Classless Inter-Domain Routing (CIDR) notation.")
.build();
} }

View file

@ -56,7 +56,7 @@ public final class Configuration {
return getOptionalBooleanValue(propertyName).orElse(false); return getOptionalBooleanValue(propertyName).orElse(false);
} }
public static boolean isBlank(Option<String> option) { public static boolean isBlank(Option<?> option) {
return getOptionalKcValue(option.getKey()) return getOptionalKcValue(option.getKey())
.map(StringUtil::isBlank) .map(StringUtil::isBlank)
.orElse(true); .orElse(true);

View file

@ -18,7 +18,6 @@
package org.keycloak.quarkus.runtime.configuration.mappers; package org.keycloak.quarkus.runtime.configuration.mappers;
import org.keycloak.config.BootstrapAdminOptions; import org.keycloak.config.BootstrapAdminOptions;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalKcValue; import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalKcValue;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
@ -36,7 +35,7 @@ public final class BootstrapAdminPropertyMappers {
return new PropertyMapper[]{ return new PropertyMapper[]{
fromOption(BootstrapAdminOptions.USERNAME) fromOption(BootstrapAdminOptions.USERNAME)
.paramLabel("username") .paramLabel("username")
.validateEnabled(BootstrapAdminPropertyMappers::isPasswordSet, PASSWORD_SET) .appendValidateEnabled(BootstrapAdminPropertyMappers::isPasswordSet, PASSWORD_SET)
.build(), .build(),
fromOption(BootstrapAdminOptions.PASSWORD) fromOption(BootstrapAdminOptions.PASSWORD)
.paramLabel("password") .paramLabel("password")
@ -48,7 +47,7 @@ public final class BootstrapAdminPropertyMappers {
.build(),*/ .build(),*/
fromOption(BootstrapAdminOptions.CLIENT_ID) fromOption(BootstrapAdminOptions.CLIENT_ID)
.paramLabel("client id") .paramLabel("client id")
.validateEnabled(BootstrapAdminPropertyMappers::isClientSecretSet, CLIENT_SECRET_SET) .appendValidateEnabled(BootstrapAdminPropertyMappers::isClientSecretSet, CLIENT_SECRET_SET)
.build(), .build(),
fromOption(BootstrapAdminOptions.CLIENT_SECRET) fromOption(BootstrapAdminOptions.CLIENT_SECRET)
.paramLabel("client secret") .paramLabel("client secret")

View file

@ -346,24 +346,34 @@ public class PropertyMapper<T> {
return this; return this;
} }
/**
* Set the validator, overwriting the current one.
*/
public Builder<T> validator(BiConsumer<PropertyMapper<T>, ConfigValue> validator) { public Builder<T> validator(BiConsumer<PropertyMapper<T>, ConfigValue> validator) {
this.validator = validator; this.validator = validator;
return this; return this;
} }
public Builder<T> appendValidator(BiConsumer<PropertyMapper<T>, ConfigValue> validator) {
var current = this.validator;
this.validator = (mapper, value) -> {
validator.accept(mapper, value);
current.accept(mapper, value);
};
return this;
}
/** /**
* Similar to {@link #enabledWhen}, but uses the condition as a validator. This allows the option * Similar to {@link #enabledWhen}, but uses the condition as a validator that is appended to the current one. This allows the option
* to appear in help. * to appear in help.
* @param isEnabled
* @param enabledWhen
* @return * @return
*/ */
public Builder<T> validateEnabled(BooleanSupplier isEnabled, String enabledWhen) { public Builder<T> appendValidateEnabled(BooleanSupplier isEnabled, String enabledWhen) {
this.validator = (mapper, value) -> { this.appendValidator((mapper, value) -> {
if (!isEnabled.getAsBoolean()) { if (!isEnabled.getAsBoolean()) {
throw new PropertyException(mapper.getOption().getKey() + " available only when " + enabledWhen); throw new PropertyException(mapper.getOption().getKey() + " available only when " + enabledWhen);
} }
}; });
this.description = String.format("%s Available only when %s.", this.description, enabledWhen); this.description = String.format("%s Available only when %s.", this.description, enabledWhen);
return this; return this;
} }

View file

@ -1,7 +1,10 @@
package org.keycloak.quarkus.runtime.configuration.mappers; package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.common.net.Inet;
import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigSourceInterceptorContext;
import org.keycloak.config.ProxyOptions; import org.keycloak.config.ProxyOptions;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import java.util.Optional; import java.util.Optional;
@ -32,9 +35,25 @@ final class ProxyPropertyMappers {
.to("quarkus.http.proxy.allow-x-forwarded") .to("quarkus.http.proxy.allow-x-forwarded")
.mapFrom("proxy-headers") .mapFrom("proxy-headers")
.transformer((v, c) -> proxyEnabled(ProxyOptions.Headers.xforwarded, v, c)) .transformer((v, c) -> proxyEnabled(ProxyOptions.Headers.xforwarded, v, c))
.build(),
fromOption(ProxyOptions.PROXY_TRUSTED_ADDRESSES)
.to("quarkus.http.proxy.trusted-proxies")
.validator((mapper, value) -> mapper.validateExpectedValues(value,
(c, v) -> validateAddress(v)))
.appendValidateEnabled(() -> !Configuration.isBlank(ProxyOptions.PROXY_HEADERS), "proxy-headers is set")
.paramLabel("trusted proxies")
.build() .build()
}; };
} }
private static void validateAddress(String address) {
if (Inet.parseCidrAddress(address) != null) {
return;
}
if (Inet.parseInetAddress(address) == null) {
throw new PropertyException(address + " is not a valid IP address (IPv4 or IPv6) nor valid CIDR notation.");
}
}
private static Optional<String> proxyEnabled(ProxyOptions.Headers testHeader, Optional<String> value, ConfigSourceInterceptorContext context) { private static Optional<String> proxyEnabled(ProxyOptions.Headers testHeader, Optional<String> value, ConfigSourceInterceptorContext context) {
boolean enabled = false; boolean enabled = false;

View file

@ -25,9 +25,11 @@ import org.apache.http.HttpHeaders;
import org.junit.Assert; import org.junit.Assert;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.keycloak.it.junit5.extension.CLIResult;
import org.keycloak.it.junit5.extension.DistributionTest; import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.RawDistOnly; import org.keycloak.it.junit5.extension.RawDistOnly;
import org.keycloak.it.junit5.extension.WithEnvVars; import org.keycloak.it.junit5.extension.WithEnvVars;
import org.keycloak.it.utils.KeycloakDistribution;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.given;
@ -55,6 +57,25 @@ public class ProxyHostnameV2DistTest {
assertForwardedHeaderIsIgnored(); assertForwardedHeaderIsIgnored();
assertXForwardedHeadersAreIgnored(); assertXForwardedHeadersAreIgnored();
} }
@Test
void testTrustedProxiesWithoutProxyHeaders(KeycloakDistribution distribution) {
CLIResult result = distribution.run("start-dev", "--proxy-trusted-addresses=1.0.0.0");
result.assertError("proxy-trusted-addresses available only when proxy-headers is set");
}
@Test
void testTrustedProxiesWithInvalidAddress(KeycloakDistribution distribution) {
CLIResult result = distribution.run("start-dev", "--proxy-headers=xforwarded", "--proxy-trusted-addresses=1.0.0.0:8080");
result.assertError("1.0.0.0:8080 is not a valid IP address (IPv4 or IPv6) nor valid CIDR notation.");
}
@Test
@Launch({ "start-dev", "--hostname-strict=false", "--proxy-headers=xforwarded", "--proxy-trusted-addresses=1.0.0.0" })
public void testProxyNotTrusted() {
assertForwardedHeaderIsIgnored();
assertForwardedHeaderIsIgnored();
}
@Test @Test
@Launch({ "start-dev", "--hostname-strict=false", "--proxy-headers=forwarded" }) @Launch({ "start-dev", "--hostname-strict=false", "--proxy-headers=forwarded" })

View file

@ -241,6 +241,12 @@ Proxy:
The proxy headers that should be accepted by the server. Misconfiguration The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence might leave the server exposed to security vulnerabilities. Takes precedence
over the deprecated proxy option. Possible values are: forwarded, xforwarded. over the deprecated proxy option. Possible values are: forwarded, xforwarded.
--proxy-trusted-addresses <trusted proxies>
A comma separated list of trusted proxy addresses. If set, then proxy headers
from other addresses will be ignored. By default all addresses are trusted.
A trusted proxy address is specified as an IP address (IPv4 or IPv6) or
Classless Inter-Domain Routing (CIDR) notation. Available only when
proxy-headers is set.
Vault: Vault:

View file

@ -276,6 +276,12 @@ Proxy:
The proxy headers that should be accepted by the server. Misconfiguration The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence might leave the server exposed to security vulnerabilities. Takes precedence
over the deprecated proxy option. Possible values are: forwarded, xforwarded. over the deprecated proxy option. Possible values are: forwarded, xforwarded.
--proxy-trusted-addresses <trusted proxies>
A comma separated list of trusted proxy addresses. If set, then proxy headers
from other addresses will be ignored. By default all addresses are trusted.
A trusted proxy address is specified as an IP address (IPv4 or IPv6) or
Classless Inter-Domain Routing (CIDR) notation. Available only when
proxy-headers is set.
Vault: Vault:

View file

@ -242,6 +242,12 @@ Proxy:
The proxy headers that should be accepted by the server. Misconfiguration The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence might leave the server exposed to security vulnerabilities. Takes precedence
over the deprecated proxy option. Possible values are: forwarded, xforwarded. over the deprecated proxy option. Possible values are: forwarded, xforwarded.
--proxy-trusted-addresses <trusted proxies>
A comma separated list of trusted proxy addresses. If set, then proxy headers
from other addresses will be ignored. By default all addresses are trusted.
A trusted proxy address is specified as an IP address (IPv4 or IPv6) or
Classless Inter-Domain Routing (CIDR) notation. Available only when
proxy-headers is set.
Vault: Vault:

View file

@ -277,6 +277,12 @@ Proxy:
The proxy headers that should be accepted by the server. Misconfiguration The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence might leave the server exposed to security vulnerabilities. Takes precedence
over the deprecated proxy option. Possible values are: forwarded, xforwarded. over the deprecated proxy option. Possible values are: forwarded, xforwarded.
--proxy-trusted-addresses <trusted proxies>
A comma separated list of trusted proxy addresses. If set, then proxy headers
from other addresses will be ignored. By default all addresses are trusted.
A trusted proxy address is specified as an IP address (IPv4 or IPv6) or
Classless Inter-Domain Routing (CIDR) notation. Available only when
proxy-headers is set.
Vault: Vault:

View file

@ -194,6 +194,12 @@ Proxy:
The proxy headers that should be accepted by the server. Misconfiguration The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence might leave the server exposed to security vulnerabilities. Takes precedence
over the deprecated proxy option. Possible values are: forwarded, xforwarded. over the deprecated proxy option. Possible values are: forwarded, xforwarded.
--proxy-trusted-addresses <trusted proxies>
A comma separated list of trusted proxy addresses. If set, then proxy headers
from other addresses will be ignored. By default all addresses are trusted.
A trusted proxy address is specified as an IP address (IPv4 or IPv6) or
Classless Inter-Domain Routing (CIDR) notation. Available only when
proxy-headers is set.
Vault: Vault:

View file

@ -229,6 +229,12 @@ Proxy:
The proxy headers that should be accepted by the server. Misconfiguration The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence might leave the server exposed to security vulnerabilities. Takes precedence
over the deprecated proxy option. Possible values are: forwarded, xforwarded. over the deprecated proxy option. Possible values are: forwarded, xforwarded.
--proxy-trusted-addresses <trusted proxies>
A comma separated list of trusted proxy addresses. If set, then proxy headers
from other addresses will be ignored. By default all addresses are trusted.
A trusted proxy address is specified as an IP address (IPv4 or IPv6) or
Classless Inter-Domain Routing (CIDR) notation. Available only when
proxy-headers is set.
Vault: Vault: