fix: adding a first-class option for trusted proxies
closes: #32135 Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
parent
7472d0ac5e
commit
c9779cfa24
14 changed files with 114 additions and 10 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,10 +35,26 @@ 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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
@ -56,6 +58,25 @@ public class ProxyHostnameV2DistTest {
|
|||
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" })
|
||||
public void testForwardedProxyHeaders() {
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
Loading…
Reference in a new issue