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].
|
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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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" })
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue