This is an automated email from the ASF dual-hosted git repository.
smengcl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new 43663479f20 HDDS-16347. Stop expanding _HOST to the bind host in HTTP
SPNEGO principals (#11464)
43663479f20 is described below
commit 43663479f207c1abfa190564235baa7392f7e32d
Author: rjgoyln <[email protected]>
AuthorDate: Wed Oct 14 03:48:18 2026 +0800
HDDS-16347. Stop expanding _HOST to the bind host in HTTP SPNEGO principals
(#11464)
Co-authored-by: Wei-Chiu Chuang <[email protected]>
Co-authored-by: Copilot Autofix powered by AI
<[email protected]>
---
.../hadoop/hdds/server/http/BaseHttpServer.java | 35 +++++
.../hadoop/hdds/server/http/HttpServer2.java | 2 +-
.../hdds/server/http/TestBaseHttpServer.java | 150 +++++++++++++++++++--
.../hadoop/ozone/s3/S3GatewayWebAdminServer.java | 2 +-
.../hadoop/ozone/s3/TestS3GatewayHttpServers.java | 82 +++++++++++
5 files changed, 256 insertions(+), 15 deletions(-)
diff --git
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/server/http/BaseHttpServer.java
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/server/http/BaseHttpServer.java
index ec709a5c183..25cdaba4f01 100644
---
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/server/http/BaseHttpServer.java
+++
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/server/http/BaseHttpServer.java
@@ -36,10 +36,15 @@
import static
org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_SERVER_HTTPS_TRUSTSTORE_PASSWORD_KEY;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.net.InetAddresses;
import java.io.File;
import java.io.IOException;
+import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
@@ -118,6 +123,11 @@ public BaseHttpServer(MutableConfigurationSource conf,
String name)
// Ozone config prefix must be set to avoid AuthenticationFilter
// fall back to default one form hadoop.http.authentication.
builder.authFilterConfigurationPrefix(getHttpAuthConfigPrefix());
+ // Without an explicit host, HttpServer2 expands _HOST in the SPNEGO
+ // principal to the host of its first endpoint, which is the bind
+ // address: a wildcard or interface-specific bind host does not name
+ // this node and matches no keytab entry.
+ builder.hostName(getSpnegoHost());
if (httpAuthType.equals("kerberos")) {
builder.setSecurityEnabled(true);
builder.setUsernameConfKey(getSpnegoPrincipal());
@@ -329,6 +339,31 @@ protected InetSocketAddress getBindAddress(String
bindHostKey,
getHostPortString(hostName, addressPort.orElse(bindPortdefault)));
}
+ /**
+ * Returns the host that replaces {@code _HOST} in this server's SPNEGO
+ * principal: the host of the advertised address of an enabled scheme, HTTP
+ * before HTTPS, when it is a name, otherwise the canonical name of the local
+ * host.
+ */
+ protected String getSpnegoHost() throws UnknownHostException {
+ List<String> addressKeys = new ArrayList<>(2);
+ if (policy.isHttpEnabled()) {
+ addressKeys.add(getHttpAddressKey());
+ }
+ if (policy.isHttpsEnabled()) {
+ addressKeys.add(getHttpsAddressKey());
+ }
+ for (String addressKey : addressKeys) {
+ Optional<String> host = getHostNameFromConfigKeys(conf, addressKey);
+ // An IP literal, wildcard or not, names no host, so it falls back to the
+ // local name exactly as the default wildcard bind address did before.
+ if (host.isPresent() && !InetAddresses.isInetAddress(host.get())) {
+ return host.get();
+ }
+ }
+ return InetAddress.getLocalHost().getCanonicalHostName();
+ }
+
/**
* Retrieve the socket address that should be used by clients to connect
* to the HTTPS web interface.
diff --git
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/server/http/HttpServer2.java
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/server/http/HttpServer2.java
index 1568d26fcfe..e1d05567f93 100644
---
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/server/http/HttpServer2.java
+++
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/server/http/HttpServer2.java
@@ -221,7 +221,7 @@ public final class HttpServer2 implements FilterContainer {
// gets stored.
public static final String CONF_CONTEXT_ATTRIBUTE = "hadoop.conf";
private static final String ADMINS_ACL = "admins.acl";
- private static final String SPNEGO_FILTER = "SpnegoFilter";
+ static final String SPNEGO_FILTER = "SpnegoFilter";
private static final String NO_CACHE_FILTER = "NoCacheFilter";
private static final String BIND_ADDRESS = "bind.address";
diff --git
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/server/http/TestBaseHttpServer.java
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/server/http/TestBaseHttpServer.java
index 0362e6ece40..868abc1e34d 100644
---
a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/server/http/TestBaseHttpServer.java
+++
b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/server/http/TestBaseHttpServer.java
@@ -27,18 +27,25 @@
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.file.Path;
-import org.apache.commons.lang3.NotImplementedException;
+import java.util.stream.Stream;
import org.apache.hadoop.hdds.conf.MutableConfigurationSource;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.ozone.OzoneConfigKeys;
+import org.apache.hadoop.ozone.OzoneConsts;
+import
org.apache.hadoop.security.authentication.server.KerberosAuthenticationHandler;
import org.apache.ozone.test.GenericTestUtils.PortAllocator;
+import org.eclipse.jetty.ee8.servlet.FilterHolder;
import org.eclipse.jetty.ee8.webapp.WebAppContext;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
/**
* Test Common ozone/hdds web methods.
@@ -54,6 +61,10 @@ class TestBaseHttpServer {
private static final int BIND_PORT_HTTP_DEFAULT =
PortAllocator.getFreePort();
private static final int BIND_PORT_HTTPS_DEFAULT =
PortAllocator.getFreePort();
private static final String ENABLED_KEY = "enabled";
+ private static final String HTTP_AUTH_TYPE_KEY = "http.auth.type";
+ private static final String HTTP_AUTH_CONFIG_PREFIX = "http.auth.";
+ private static final String KEYTAB_KEY = "http.kerberos.keytab";
+ private static final String SPNEGO_PRINCIPAL_KEY = "http.kerberos.principal";
private String hostname;
@@ -206,10 +217,7 @@ void usesPerServerTempSubdirectory() throws Exception {
BaseHttpServer subject = new TestingHttpServer(conf);
try {
subject.start();
- Field field = BaseHttpServer.class.getDeclaredField("httpServer");
- field.setAccessible(true);
- WebAppContext webAppContext =
- ((HttpServer2) field.get(subject)).getWebAppContext();
+ WebAppContext webAppContext = httpServer2Of(subject).getWebAppContext();
assertEquals(new File(tempDir.toFile(), "testing").getCanonicalFile(),
webAppContext.getTempDirectory().getCanonicalFile());
assertTrue(webAppContext.isPersistTempDirectory());
@@ -232,10 +240,7 @@ void honoursOperatorConfiguredTempDir() throws Exception {
BaseHttpServer subject = new TestingHttpServer(conf);
try {
subject.start();
- Field field = BaseHttpServer.class.getDeclaredField("httpServer");
- field.setAccessible(true);
- WebAppContext webAppContext =
- ((HttpServer2) field.get(subject)).getWebAppContext();
+ WebAppContext webAppContext = httpServer2Of(subject).getWebAppContext();
assertEquals(operatorTempDir.getCanonicalFile(),
webAppContext.getTempDirectory().getCanonicalFile());
} finally {
@@ -243,6 +248,112 @@ void honoursOperatorConfiguredTempDir() throws Exception {
}
}
+ static Stream<Arguments> advertisedAddressesNamingNoHost() {
+ return Stream.of(
+ Arguments.of(HttpConfig.Policy.HTTP_AND_HTTPS, null, null),
+ Arguments.of(HttpConfig.Policy.HTTP_AND_HTTPS, "0.0.0.0:9874",
"0.0.0.0:9875"),
+ Arguments.of(HttpConfig.Policy.HTTP_AND_HTTPS, "[::]:9874",
"[::]:9875"),
+ Arguments.of(HttpConfig.Policy.HTTP_AND_HTTPS, "127.0.0.1:9874",
"[::1]:9875"),
+ Arguments.of(HttpConfig.Policy.HTTP_AND_HTTPS, "192.0.2.1:9874",
"[2001:db8::1]:9875"),
+ Arguments.of(HttpConfig.Policy.HTTP_AND_HTTPS,
"[fe80::1%nosuchif0]:9874", null),
+ Arguments.of(HttpConfig.Policy.HTTP_ONLY, "0.0.0.0:9874",
"om1.example.com:9875"),
+ Arguments.of(HttpConfig.Policy.HTTPS_ONLY, "om1.example.com:9874",
"0.0.0.0:9875"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("advertisedAddressesNamingNoHost")
+ void spnegoHostFallsBackToLocalHostWithoutAdvertisedName(
+ HttpConfig.Policy policy, String httpAddress, String httpsAddress)
+ throws Exception {
+ BaseHttpServer subject = newDisabledServer(policy, httpAddress,
httpsAddress);
+
+ assertEquals(InetAddress.getLocalHost().getCanonicalHostName(),
+ subject.getSpnegoHost());
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "HTTP_ONLY, om1.example.com:9874, , om1.example.com",
+ "HTTP_AND_HTTPS, 0.0.0.0:9874, om1.example.com:9875, om1.example.com",
+ "HTTP_AND_HTTPS, om1.example.com:9874, om1-tls.example.com:9875,
om1.example.com",
+ "HTTPS_ONLY, om1.example.com:9874, om1-tls.example.com:9875,
om1-tls.example.com",
+ })
+ void spnegoHostIsAdvertisedNameOfEnabledScheme(HttpConfig.Policy policy,
+ String httpAddress, String httpsAddress, String expectedHost)
+ throws Exception {
+ BaseHttpServer subject = newDisabledServer(policy, httpAddress,
httpsAddress);
+
+ assertEquals(expectedHost, subject.getSpnegoHost());
+ }
+
+ /**
+ * The bind host is what {@link HttpServer2} expands {@code _HOST} to when
+ * it is not told otherwise; none of these name the host the keytab was
+ * issued for.
+ */
+ @ParameterizedTest
+ @ValueSource(strings = {"0.0.0.0", "::", "127.0.0.1"})
+ void spnegoPrincipalNamesAdvertisedHostNotBindHost(String bindHost)
+ throws Exception {
+ MutableConfigurationSource conf = newConfig(HttpConfig.Policy.HTTP_ONLY);
+ conf.set(BIND_HOST_HTTP_KEY, bindHost);
+ conf.set(ADDRESS_HTTP_KEY, "om1.example.com:" + BIND_PORT_HTTP_DEFAULT);
+ conf.set(HTTP_AUTH_TYPE_KEY, "kerberos");
+ conf.set(SPNEGO_PRINCIPAL_KEY, "HTTP/[email protected]");
+
+ try (BaseHttpServer subject = new SecureTestingHttpServer(conf)) {
+ FilterHolder spnegoFilter = httpServer2Of(subject).getWebAppContext()
+ .getServletHandler().getFilter(HttpServer2.SPNEGO_FILTER);
+ assertEquals("HTTP/[email protected]",
+
spnegoFilter.getInitParameter(KerberosAuthenticationHandler.PRINCIPAL));
+ }
+ }
+
+ /**
+ * Secure clusters register {@code AuthenticationFilterInitializer} through
+ * {@code ozone.http.filter.initializers}; it expands {@code _HOST} from the
+ * {@code bind.address} that {@link HttpServer2} fills with the same host.
+ */
+ @Test
+ void filterInitializerPrincipalNamesAdvertisedHostNotBindHost()
+ throws Exception {
+ MutableConfigurationSource conf = newConfig(HttpConfig.Policy.HTTP_ONLY);
+ conf.set(BIND_HOST_HTTP_KEY, "::");
+ conf.set(ADDRESS_HTTP_KEY, "om1.example.com:" + BIND_PORT_HTTP_DEFAULT);
+ conf.set(HTTP_AUTH_TYPE_KEY, "kerberos");
+ conf.set(HTTP_AUTH_CONFIG_PREFIX + KerberosAuthenticationHandler.PRINCIPAL,
+ "HTTP/[email protected]");
+ conf.set(OzoneConfigKeys.OZONE_HTTP_FILTER_INITIALIZERS_KEY,
+ OzoneConsts.OZONE_HTTP_FILTER_INITIALIZERS_SECURE);
+
+ try (BaseHttpServer subject = new SecureTestingHttpServer(conf)) {
+ FilterHolder authFilter = httpServer2Of(subject).getWebAppContext()
+ .getServletHandler().getFilter("authentication");
+ assertEquals("HTTP/[email protected]",
+
authFilter.getInitParameter(KerberosAuthenticationHandler.PRINCIPAL));
+ }
+ }
+
+ private BaseHttpServer newDisabledServer(HttpConfig.Policy policy,
+ String httpAddress, String httpsAddress) throws IOException {
+ MutableConfigurationSource conf = newConfig(policy);
+ conf.setBoolean(ENABLED_KEY, false);
+ if (httpAddress != null) {
+ conf.set(ADDRESS_HTTP_KEY, httpAddress);
+ }
+ if (httpsAddress != null) {
+ conf.set(ADDRESS_HTTPS_KEY, httpsAddress);
+ }
+ return new TestingHttpServer(conf);
+ }
+
+ private static HttpServer2 httpServer2Of(BaseHttpServer server)
+ throws ReflectiveOperationException {
+ Field field = BaseHttpServer.class.getDeclaredField("httpServer");
+ field.setAccessible(true);
+ return (HttpServer2) field.get(server);
+ }
+
private MutableConfigurationSource newConfig(HttpConfig.Policy policy) {
MutableConfigurationSource conf = new OzoneConfiguration();
conf.set(OzoneConfigKeys.OZONE_HTTP_BASEDIR, tempDir.toString());
@@ -293,12 +404,12 @@ protected int getHttpsBindPortDefault() {
@Override
protected String getKeytabFile() {
- throw new NotImplementedException();
+ return KEYTAB_KEY;
}
@Override
protected String getSpnegoPrincipal() {
- throw new NotImplementedException();
+ return SPNEGO_PRINCIPAL_KEY;
}
@Override
@@ -308,12 +419,25 @@ protected String getEnabledKey() {
@Override
protected String getHttpAuthType() {
- throw new NotImplementedException();
+ return HTTP_AUTH_TYPE_KEY;
}
@Override
protected String getHttpAuthConfigPrefix() {
- throw new NotImplementedException();
+ return HTTP_AUTH_CONFIG_PREFIX;
+ }
+ }
+
+ /** Reports HTTP security as enabled without a Kerberos login. */
+ private static final class SecureTestingHttpServer extends TestingHttpServer
{
+
+ SecureTestingHttpServer(MutableConfigurationSource conf) throws
IOException {
+ super(conf);
+ }
+
+ @Override
+ public boolean isSecurityEnabled() {
+ return true;
}
}
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/S3GatewayWebAdminServer.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/S3GatewayWebAdminServer.java
index b75b34dc3fc..13206dda168 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/S3GatewayWebAdminServer.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/S3GatewayWebAdminServer.java
@@ -126,7 +126,7 @@ private void
addSecretAuthentication(MutableConfigurationSource conf)
conf.get(OZONE_S3G_WEB_AUTHENTICATION_KERBEROS_PRINCIPAL);
if (!Strings.isNullOrEmpty(principalInConf)) {
params.put("kerberos.principal", SecurityUtil.getServerPrincipal(
- principalInConf,
conf.get(OZONE_S3G_WEBADMIN_HTTP_BIND_HOST_KEY)));
+ principalInConf, getSpnegoHost()));
}
String httpKeytab = conf.get(OZONE_S3G_KEYTAB_FILE);
if (!Strings.isNullOrEmpty(httpKeytab)) {
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/TestS3GatewayHttpServers.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/TestS3GatewayHttpServers.java
index bcf3c7ce8c0..9dd90214c70 100644
---
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/TestS3GatewayHttpServers.java
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/TestS3GatewayHttpServers.java
@@ -23,12 +23,20 @@
import java.io.IOException;
import java.nio.file.Path;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.CommonConfigurationKeysPublic;
import org.apache.hadoop.hdds.conf.MutableConfigurationSource;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.ozone.OzoneConfigKeys;
+import org.apache.hadoop.ozone.s3secret.S3SecretConfigKeys;
import org.apache.hadoop.ozone.s3sts.S3STSConfigKeys;
+import org.apache.hadoop.security.UserGroupInformation;
+import
org.apache.hadoop.security.authentication.server.KerberosAuthenticationHandler;
import org.apache.ozone.test.GenericTestUtils.PortAllocator;
+import org.eclipse.jetty.ee8.servlet.FilterHolder;
import org.eclipse.jetty.ee8.webapp.WebAppContext;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -46,6 +54,36 @@ class TestS3GatewayHttpServers {
@TempDir
private Path tempDir;
+ /**
+ * Switching {@link UserGroupInformation} to Kerberos reads the default
+ * realm, which the JDK resolves from these properties when there is no
+ * krb5.conf, as in CI.
+ */
+ private static String previousKerberosRealm;
+ private static String previousKerberosKdc;
+
+ @BeforeAll
+ static void fakeKerberosRealm() {
+ previousKerberosRealm = System.getProperty("java.security.krb5.realm");
+ previousKerberosKdc = System.getProperty("java.security.krb5.kdc");
+ System.setProperty("java.security.krb5.realm", "EXAMPLE.COM");
+ System.setProperty("java.security.krb5.kdc", "localhost");
+ }
+
+ @AfterAll
+ static void clearKerberosRealm() {
+ if (previousKerberosRealm == null) {
+ System.clearProperty("java.security.krb5.realm");
+ } else {
+ System.setProperty("java.security.krb5.realm", previousKerberosRealm);
+ }
+ if (previousKerberosKdc == null) {
+ System.clearProperty("java.security.krb5.kdc");
+ } else {
+ System.setProperty("java.security.krb5.kdc", previousKerberosKdc);
+ }
+ }
+
/**
* An enabled server gets Jetty's CDI integration attribute, without which
* Weld falls through to its legacy Jetty container and fails to start the
@@ -77,6 +115,36 @@ void disabledStsServerIsCreatedWithoutCdiIntegration() {
assertDoesNotThrow(() -> new S3STSHttpServer(conf, "s3g-sts"));
}
+ /**
+ * The secret endpoint expands {@code _HOST} in its SPNEGO principal from the
+ * advertised web admin address, not from the bind host, which here is the
+ * IPv6 wildcard that no keytab entry can match.
+ */
+ @Test
+ void secretEndpointPrincipalNamesAdvertisedHost() throws Exception {
+ OzoneConfiguration conf = newConf();
+ conf.set(S3GatewayConfigKeys.OZONE_S3G_WEBADMIN_HTTP_BIND_HOST_KEY, "::");
+ conf.set(S3GatewayConfigKeys.OZONE_S3G_WEBADMIN_HTTP_ADDRESS_KEY,
+ "s3g1.example.com:" + PortAllocator.getFreePort());
+ conf.setBoolean(S3SecretConfigKeys.OZONE_S3G_SECRET_HTTP_ENABLED_KEY,
true);
+
conf.set(S3GatewayConfigKeys.OZONE_S3G_WEB_AUTHENTICATION_KERBEROS_PRINCIPAL,
+ "HTTP/[email protected]");
+
+ Configuration hadoopConf = new Configuration();
+
hadoopConf.set(CommonConfigurationKeysPublic.HADOOP_SECURITY_AUTHENTICATION,
+ "kerberos");
+ UserGroupInformation.setConfiguration(hadoopConf);
+ try (ExposedS3GatewayWebAdminServer server =
+ new ExposedS3GatewayWebAdminServer(conf)) {
+ FilterHolder secretFilter = server.context().getServletHandler()
+ .getFilter("secretAuthentication");
+ assertEquals("HTTP/[email protected]",
+
secretFilter.getInitParameter(KerberosAuthenticationHandler.PRINCIPAL));
+ } finally {
+ UserGroupInformation.reset();
+ }
+ }
+
private OzoneConfiguration newConf() {
OzoneConfiguration conf = new OzoneConfiguration();
conf.set(OzoneConfigKeys.OZONE_HTTP_BASEDIR,
@@ -97,4 +165,18 @@ WebAppContext context() {
return getWebAppContext();
}
}
+
+ /** Exposes the web app context, which BaseHttpServer keeps protected. */
+ private static final class ExposedS3GatewayWebAdminServer
+ extends S3GatewayWebAdminServer {
+
+ ExposedS3GatewayWebAdminServer(MutableConfigurationSource conf)
+ throws IOException {
+ super(conf, "s3g-web");
+ }
+
+ WebAppContext context() {
+ return getWebAppContext();
+ }
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]