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].
= 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
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.
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
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;
import java.util.List;
public class ProxyOptions {
public enum Headers {
@ -26,4 +28,9 @@ public class ProxyOptions {
.category(OptionCategory.PROXY)
.defaultValue(Boolean.FALSE)
.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);
}
public static boolean isBlank(Option<String> option) {
public static boolean isBlank(Option<?> option) {
return getOptionalKcValue(option.getKey())
.map(StringUtil::isBlank)
.orElse(true);

View file

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

View file

@ -346,24 +346,34 @@ public class PropertyMapper<T> {
return this;
}
/**
* Set the validator, overwriting the current one.
*/
public Builder<T> validator(BiConsumer<PropertyMapper<T>, ConfigValue> validator) {
this.validator = validator;
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.
* @param isEnabled
* @param enabledWhen
* @return
*/
public Builder<T> validateEnabled(BooleanSupplier isEnabled, String enabledWhen) {
this.validator = (mapper, value) -> {
public Builder<T> appendValidateEnabled(BooleanSupplier isEnabled, String enabledWhen) {
this.appendValidator((mapper, value) -> {
if (!isEnabled.getAsBoolean()) {
throw new PropertyException(mapper.getOption().getKey() + " available only when " + enabledWhen);
}
};
});
this.description = String.format("%s Available only when %s.", this.description, enabledWhen);
return this;
}

View file

@ -1,7 +1,10 @@
package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.common.net.Inet;
import io.smallrye.config.ConfigSourceInterceptorContext;
import org.keycloak.config.ProxyOptions;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import java.util.Optional;
@ -32,9 +35,25 @@ final class ProxyPropertyMappers {
.to("quarkus.http.proxy.allow-x-forwarded")
.mapFrom("proxy-headers")
.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()
};
}
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) {
boolean enabled = false;

View file

@ -25,9 +25,11 @@ import org.apache.http.HttpHeaders;
import org.junit.Assert;
import org.junit.jupiter.api.BeforeAll;
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.RawDistOnly;
import org.keycloak.it.junit5.extension.WithEnvVars;
import org.keycloak.it.utils.KeycloakDistribution;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import static io.restassured.RestAssured.given;
@ -55,6 +57,25 @@ public class ProxyHostnameV2DistTest {
assertForwardedHeaderIsIgnored();
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
@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
might leave the server exposed to security vulnerabilities. Takes precedence
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:

View file

@ -276,6 +276,12 @@ Proxy:
The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence
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:

View file

@ -242,6 +242,12 @@ Proxy:
The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence
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:

View file

@ -277,6 +277,12 @@ Proxy:
The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence
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:

View file

@ -194,6 +194,12 @@ Proxy:
The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence
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:

View file

@ -229,6 +229,12 @@ Proxy:
The proxy headers that should be accepted by the server. Misconfiguration
might leave the server exposed to security vulnerabilities. Takes precedence
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: