Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7f4beeb
migration to jetty 12
yandrey321 Sep 3, 2026
7b7c3bc
fixed issues found by pmd
yandrey321 Sep 3, 2026
c31bdcc
Merge remote-tracking branch 'apache/master' into HDDS-8280
yandrey321 Sep 3, 2026
a1b77d2
fixed the build issue
yandrey321 Sep 3, 2026
4a532cf
fixed CI build issue
yandrey321 Sep 3, 2026
fee1a44
Fixed CI failure
yandrey321 Sep 3, 2026
3cc9bf5
fixed CI issues in integration tests
yandrey321 Sep 3, 2026
61680fc
fixed issues in CI s3 tests
yandrey321 Sep 3, 2026
27c73bf
fixed issue found by CI: content type
yandrey321 Sep 3, 2026
a016819
fixed problem found by CI
yandrey321 Sep 3, 2026
f69f8d2
fixed CI test failure
yandrey321 Sep 4, 2026
381dfaf
Merge remote-tracking branch 'apache/master' into HDDS-8280
yandrey321 Sep 8, 2026
212ea59
Merge remote-tracking branch 'apache/master' into HDDS-8280
yandrey321 Sep 9, 2026
a373f69
Addressed review comments
yandrey321 Sep 9, 2026
8fb4e62
fixed sts test failures
yandrey321 Sep 9, 2026
8a5edeb
fixed review comments part 2
yandrey321 Sep 11, 2026
4b7d636
fixed CI issues
yandrey321 Sep 11, 2026
98b149c
fixed CI failures
yandrey321 Sep 12, 2026
e01c4a3
Merge remote-tracking branch 'apache/master' into HDDS-8280
yandrey321 Sep 14, 2026
51fee25
addressed review comments 3
yandrey321 Sep 15, 2026
396f9a3
addressed review comments 4
yandrey321 Sep 15, 2026
140f844
fixed CI failure
yandrey321 Sep 16, 2026
8a89113
fixed failed test
yandrey321 Sep 16, 2026
5d23280
fixed review comments 5
yandrey321 Sep 17, 2026
78713ea
fix
yandrey321 Sep 18, 2026
d9a9e08
Fixed review comments 6
yandrey321 Sep 18, 2026
9a69784
Addressed review comments 7
yandrey321 Sep 23, 2026
a14759d
Merge remote-tracking branch 'apache/master' into HDDS-8280
yandrey321 Sep 23, 2026
7c99e9f
fixed CI build issue
yandrey321 Sep 23, 2026
b37fb40
fixed CI faulure
yandrey321 Sep 23, 2026
948192b
Declare ozone-client at runtime scope in httpfsgateway
yandrey321 Sep 23, 2026
3d0033d
Merge remote-tracking branch 'apache/master' into HDDS-8280
yandrey321 Sep 25, 2026
a253d75
Addressed review comments 8
yandrey321 Sep 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
import static org.apache.hadoop.hdds.ratis.RatisHelper.HDDS_DATANODE_RATIS_PREFIX_KEY;
import static org.apache.hadoop.ozone.OzoneConfigKeys.OZONE_CONTAINER_COPY_WORKDIR;

import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Unmarshaller;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -40,13 +47,6 @@
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hdds.HddsConfigKeys;
import org.apache.hadoop.hdds.annotation.InterfaceAudience;
Expand Down
29 changes: 23 additions & 6 deletions hadoop-hdds/common/src/main/resources/ozone-default.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2224,6 +2224,23 @@
to enable Kerberos authentication for Ozone HTTP web consoles
is enabled using the SPNEGO protocol. When this property is
set, ozone.security.http.kerberos.enabled should be set to true.
Filters registered by these initializers run inside a Jetty EE10
(jakarta.servlet) server. A filter that still implements the legacy
javax.servlet.Filter API is run through a bridge and is supported when it
only acts on the request and response it is given - AuthenticationFilter
(the hadoop-auth family, which establishes an authenticated principal),
StaticUserFilter, CrossOriginFilter (CORS response headers) and
RestCsrfPreventionFilter. A filter that wraps the request or response and
forwards the wrapper downstream (for example XFrameOptionsFilter) is not
supported, because the bridge does not carry that wrapper. Subclasses of
those four filters are supported, but the bridge carries only the
authentication result (getRemoteUser, getUserPrincipal, getAuthType,
isUserInRole) of a request a subclass forwards downstream: any other
override on that request, such as a substituted header, parameter or remote
address, is silently dropped, while a forwarded response wrapper fails the
request. A javax.servlet filter outside the supported set aborts HTTP
server startup, so provide jakarta.servlet.Filter implementations for any
other filter.
</description>
</property>
<property>
Expand Down Expand Up @@ -3825,12 +3842,12 @@
<value/>
<tag>OZONE, OM, SCM, MANAGEMENT</tag>
<description>
The base dir for HTTP Jetty server to extract contents. If this property
is not configured, by default, Jetty will create a directory inside the
directory named by the ${ozone.metadata.dirs}/webserver. While in production environment,
it's strongly suggested instructing Jetty to use a different parent directory by
setting this property to the name of the desired parent directory. The value of the
property will be used to set Jetty context attribute 'org.eclipse.jetty.webapp.basetempdir'.
The base dir for the HTTP Jetty server. If this property is not configured,
by default the directory named by ${ozone.metadata.dirs}/webserver is used.
While in a production environment, it's strongly suggested to point this at a
different parent directory by setting this property. Under this directory Ozone
creates and reuses a per-server subdirectory (named after the server) as Jetty's
persistent temp directory; it is kept across restarts rather than deleted on stop.
The directory named by this property must exist and be writeable.
</description>
</property>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
import org.apache.hadoop.hdds.security.x509.certificate.client.DNCertificateClient;
import org.apache.hadoop.hdds.server.OzoneAdmins;
import org.apache.hadoop.hdds.server.http.HttpConfig;
import org.apache.hadoop.hdds.server.http.HttpServerConfigurationException;
import org.apache.hadoop.hdds.server.http.RatisDropwizardExports;
import org.apache.hadoop.hdds.tracing.TracingConfig;
import org.apache.hadoop.hdds.utils.HddsServerUtil;
Expand Down Expand Up @@ -351,6 +352,10 @@ public String getNamespace() {
serviceRuntimeInfo.setHttpsPort(String.valueOf(httpsPort));
}

} catch (HttpServerConfigurationException ex) {
// A filter/HTTP misconfiguration will never succeed on retry; fail fast
// instead of silently starting the datanode without a web server.
throw ex;
} catch (Exception ex) {
LOG.error("HttpServer failed to start.", ex);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.File;
Expand All @@ -43,6 +44,9 @@
import org.apache.hadoop.hdds.protocol.DatanodeDetails;
import org.apache.hadoop.hdds.scm.ScmConfigKeys;
import org.apache.hadoop.hdds.server.http.HttpConfig;
import org.apache.hadoop.hdds.server.http.HttpServer2;
import org.apache.hadoop.hdds.server.http.HttpServerConfigurationException;
import org.apache.hadoop.hdds.server.http.TestHttpServer2;
import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
import org.apache.hadoop.ozone.container.common.ContainerTestUtils;
import org.apache.hadoop.ozone.container.common.SCMTestUtils;
Expand Down Expand Up @@ -210,6 +214,31 @@ void testHttpPorts(HttpConfig.Policy policy) {
}
}

/**
* Verifies that {@link HddsDatanodeService#start} re-throws
* {@link HttpServerConfigurationException} instead of swallowing it through
* the generic {@code catch (Exception ex)} handler. A non-bridgeable
* javax filter (one that {@code ServletElementsFactory} cannot adapt to
* Jetty EE10) is injected via {@code ozone.http.filter.initializers}; the
* datanode HTTP server builder detects it and throws during construction.
*/
@Test
public void startThrowsOnNonBridgeableFilter() {
conf.set(HttpServer2.FILTER_INITIALIZER_PROPERTY,
TestHttpServer2.NonBridgeableFilterInitializer.class.getName());
try {
assertThrows(HttpServerConfigurationException.class, () -> service.start(conf));
} finally {
// The DatanodeStateMachine -- volumes, RocksDB handles under the temp directory, executors --
// is constructed just before the web server that throws, so it has to be released here or it
// outlives the test in this fork while its temp directory is deleted underneath it. Unlike
// the tests above there is no join(): the state machine's daemon was never started.
service.stop();
service.close();
DefaultMetricsSystem.shutdown();
}
}

static class MockService implements ServicePlugin {

@Override
Expand Down
2 changes: 1 addition & 1 deletion hadoop-hdds/docs/content/design/s3-performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public class OzoneClientProducer {
}
```

As we can see here, the producer is *request* scoped (see the annotation on the class), which means that the `OzoneClient` bean will be created for each request. If the client couldn't be created a specific exception will be thrown by the CDI framework (!) as one bean couldn't be injected with CDI. This error is different from the regular business exceptions therefore the normal exception handler (`OS3ExceptionMapper` implements `javax.ws.rs.ext.ExceptionMapper`) -- which can transform exceptions to HTTP error code -- doesn't apply. It can cause strange 500 error instead of some authentication error.
As we can see here, the producer is *request* scoped (see the annotation on the class), which means that the `OzoneClient` bean will be created for each request. If the client couldn't be created a specific exception will be thrown by the CDI framework (!) as one bean couldn't be injected with CDI. This error is different from the regular business exceptions therefore the normal exception handler (`OS3ExceptionMapper` implements `jakarta.ws.rs.ext.ExceptionMapper`) -- which can transform exceptions to HTTP error code -- doesn't apply. It can cause strange 500 error instead of some authentication error.

## Caching

Expand Down
70 changes: 70 additions & 0 deletions hadoop-hdds/docs/content/security/SecuringOzoneHTTP.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,76 @@ ozone.http.filter.initializers | org.apache.hadoop.security.AuthenticationFilter
After that, individual component needs to configure properly to completely enable
SPNEGO or SIMPLE authentication.

### Filter initializer compatibility

Ozone's HTTP servers run on Jetty 12 (EE10, `jakarta.servlet`). Filters registered
through `ozone.http.filter.initializers` must use `jakarta.servlet.Filter`, or must
belong to one of these bridged `javax.servlet` families:

* `AuthenticationFilter` (hadoop-auth: Kerberos/SPNEGO and simple authentication)
* `StaticUserFilter` (static user for unsecured consoles)
* `CrossOriginFilter` (CORS response headers)
* `RestCsrfPreventionFilter` (CSRF prevention)

Any other `javax.servlet.Filter` — including Hadoop's `XFrameOptionsFilter` and
site-specific wrapper filters — is not bridgeable. Registering one causes the
daemon (OM, SCM, Datanode, S3G, Recon, HttpFS) to **abort start-up**.

Subclasses of the four families above are accepted, because Hadoop's own bridged filters are
subclasses — `ProxyUserAuthenticationFilter`, `DelegationTokenAuthenticationFilter`, and the
HttpFS and Recon authentication filters built on them. If you register a site-specific
subclass, note what the bridge carries back into the jakarta chain from a request the filter
wraps and forwards downstream: **only the authentication result** — `getRemoteUser`,
`getUserPrincipal`, `getAuthType` and `isUserInRole`. Request attributes the filter sets do
propagate, and a forwarded response wrapper fails the request with an error rather than being
dropped, but any **other** override on that forwarded request — a substituted header,
parameter, or remote address — is **silently lost**. All four bridged families, and Hadoop's
subclasses of them, override only the principal methods, so this affects custom subclasses
only.

**Upgrade note:** before the Jetty 12 migration, a failed HTTP server start was logged
and the daemon continued without a web UI. After it, the daemon refuses to start so
that a misconfigured filter is never silently skipped. If your cluster sets
`ozone.http.filter.initializers` to a custom filter, migrate it to
`jakarta.servlet.Filter` before upgrading.

### Jetty 12 URI compliance

Jetty 12 answers `400 Bad Request` for ambiguous URI constructs that Jetty 9.4 accepted:
empty path segments (e.g., `bucket//key`), ambiguous percent-encodings, encoded path
separators, and suspicious path characters (a decoded backslash, `DEL`, or a C0 control
byte).

Because S3 object keys and WebHDFS paths legitimately contain these sequences, the **S3
Gateway REST endpoint** and **HttpFS** relax exactly those four checks, so they keep serving
the URIs they served before the migration. Every other Ozone HTTP server — OM, SCM, Datanode,
Recon, and the S3 Gateway's web-admin and STS endpoints — uses the Jetty 12 defaults and
answers `400 Bad Request` for such URIs.

Genuinely illegal URI characters that RFC 3986 forbids in unencoded form — such as `[`, `]`,
`{`, `}`, and `|` — are rejected with `400 Bad Request` on every server, including the two
that relax the ambiguity checks. Conforming S3 and WebHDFS clients already percent-encode
these characters; a client that sends them unencoded must be updated before upgrading.

### HttpFS `/conf` response media type

`GET /conf` is served by Ozone's own `HddsConfServlet` on every HTTP server. OM, SCM, Datanode,
Recon, and the S3 Gateway already registered that servlet before the Jetty 12 migration, so only
**HttpFS** -- which until now fell through to Hadoop's `ConfServlet` -- changes, and only for the
XML form of the response:

Accept header | Before (HttpFS) | After
-----------------------------------|--------------------------------------|-------------------
contains `json` | `application/json;charset=utf-8` | `application/json;charset=utf-8`
anything else, or absent | `text/xml;charset=utf-8` | `application/xml;charset=utf-8`

The format is still chosen from the `Accept` header alone, and the documents themselves are
unchanged -- both servlets render them with Hadoop's `Configuration.dumpConfiguration` and
`Configuration.writeXml`. Clients that parse the header are unaffected; monitoring or scripts that
compare the XML `Content-Type` against a literal string need updating. HttpFS additionally gains
the Ozone-only `/conf?cmd=getOzoneTags` and `/conf?cmd=getPropertyByTag&tags=...` commands, which
Hadoop's servlet did not serve.

### Enable SPNEGO authentication for OM HTTP
Property| Value
-----------------------------------|-----------------------------------------
Expand Down
34 changes: 34 additions & 0 deletions hadoop-hdds/docs/content/security/SecuringOzoneHTTP.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,40 @@ ozone.http.filter.initializers | org.apache.hadoop.security.AuthenticationFilter

之后,各个组件需要正确配置才能完全启用 SPNEGO 或 SIMPLE 身份验证。

### Filter initializer 兼容性

Ozone 的 HTTP 服务运行在 Jetty 12(EE10,`jakarta.servlet`)上。通过 `ozone.http.filter.initializers` 注册的过滤器必须使用 `jakarta.servlet.Filter`,或者属于以下已桥接的 `javax.servlet` 过滤器系列之一:

* `AuthenticationFilter`(hadoop-auth:Kerberos/SPNEGO 和简单身份验证)
* `StaticUserFilter`(未启用安全的控制台使用的静态用户过滤器)
* `CrossOriginFilter`(CORS 响应头)
* `RestCsrfPreventionFilter`(CSRF 防护)

其他任何 `javax.servlet.Filter` 实现——包括 Hadoop 的 `XFrameOptionsFilter` 及站点自定义的包装过滤器——均不可桥接。注册此类过滤器将导致守护进程(OM、SCM、Datanode、S3G、Recon、HttpFS)**启动中止**。

上述四个系列的子类也被接受,因为 Hadoop 自身已桥接的过滤器就是子类——`ProxyUserAuthenticationFilter`、`DelegationTokenAuthenticationFilter`,以及基于它们构建的 HttpFS 和 Recon 认证过滤器。如果您注册站点自定义的子类,请注意桥接会将过滤器包装并向下游传递的请求中的哪些内容带回 jakarta 链:**仅限认证结果**——`getRemoteUser`、`getUserPrincipal`、`getAuthType` 和 `isUserInRole`。过滤器设置的请求属性(attribute)会传递,而向下游传递的响应包装器会以错误方式使请求失败(而非被丢弃);但该转发请求上的**其他任何**覆写——替换的请求头、参数或远程地址——都会被**静默丢弃**。上述四个已桥接系列,以及 Hadoop 对它们的子类,均仅覆写上述认证相关方法,因此此限制只影响自定义子类。

**升级注意事项:** 在迁移到 Jetty 12 之前,HTTP 服务启动失败仅会被记录日志,守护进程会在没有 Web UI 的情况下继续运行。迁移后,守护进程将拒绝启动,以确保配置错误的过滤器不会被静默跳过。如果您的集群将 `ozone.http.filter.initializers` 设置为自定义过滤器,请在升级前将其迁移至 `jakarta.servlet.Filter`。

### Jetty 12 URI 合规性

Jetty 12 会拒绝 Jetty 9.4 曾接受的语义模糊的 URI 结构,并返回 `400 Bad Request`:空路径段(如 `bucket//key`)、模糊的百分号编码、编码的路径分隔符以及可疑路径字符(解码后的反斜杠、`DEL` 或 C0 控制字符)。

由于 S3 对象键和 WebHDFS 路径本身就可能包含这些序列,**S3 Gateway REST 端点**和 **HttpFS** 放宽了上述四项检查,因此它们仍能提供迁移前所能提供的 URI。其他所有 Ozone HTTP 服务——OM、SCM、Datanode、Recon,以及 S3 Gateway 的 web 管理端点和 STS 端点——均使用 Jetty 12 的默认设置,对此类 URI 返回 `400 Bad Request`。

RFC 3986 明确禁止以未编码形式出现在 URI 中的非法字符——例如 `[`、`]`、`{`、`}` 和 `|`——在所有服务上都会被拒绝并返回 `400 Bad Request`,包括上述放宽了模糊性检查的两个服务。符合规范的 S3 和 WebHDFS 客户端已对这些字符进行百分号编码;如果客户端以未编码形式发送这些字符,则必须在升级前更新客户端。

### HttpFS `/conf` 响应的媒体类型

迁移后,所有 HTTP 服务的 `GET /conf` 均由 Ozone 自己的 `HddsConfServlet` 提供。OM、SCM、Datanode、Recon 以及 S3 Gateway 在 Jetty 12 迁移之前就已注册该 servlet,因此只有 **HttpFS** 会发生变化——它此前一直回落到 Hadoop 的 `ConfServlet`——而且变化仅限于响应的 XML 形式:

Accept 请求头 | 变更前(HttpFS) | 变更后
-----------------------------------|--------------------------------------|-------------------
包含 `json` | `application/json;charset=utf-8` | `application/json;charset=utf-8`
其他值或未设置 | `text/xml;charset=utf-8` | `application/xml;charset=utf-8`

响应格式仍然仅依据 `Accept` 请求头选择,文档内容本身没有变化——两个 servlet 都使用 Hadoop 的 `Configuration.dumpConfiguration` 和 `Configuration.writeXml` 生成内容。解析该响应头的客户端不受影响;但按字面字符串比较 XML 响应头 `Content-Type` 的监控或脚本需要更新。此外,HttpFS 还新增了 Hadoop 的 servlet 未提供的、仅 Ozone 支持的 `/conf?cmd=getOzoneTags` 和 `/conf?cmd=getPropertyByTag&tags=...` 命令。

### 为 OM HTTP 启用 SPNEGO 身份验证
参数 | 值
-----------------------------------|-----------------------------------------
Expand Down
19 changes: 18 additions & 1 deletion hadoop-hdds/docs/content/security/SecuringS3.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ the AWS credentials. The values will be printed out on the screen. You can
set these values up in your _.aws_ file for automatic access while working
against Ozone S3 buckets.

If a secret already exists for the user, the endpoint responds with HTTP
`400 Bad Request` and the error code `S3_SECRET_ALREADY_EXISTS` in the
plain-text response body. The error code is carried in the body, not in the
HTTP status line (reason phrase), so clients must read the body rather than
parse the status message:
```bash
curl -X PUT --negotiate -u : -v https://localhost:9879/secret
# < HTTP/1.1 400 Bad Request
# S3_SECRET_ALREADY_EXISTS
```

<div class="alert alert-danger" role="alert">
Please note: These S3 credentials are like your Kerberos passwords
that give complete access to your buckets.
Expand Down Expand Up @@ -121,7 +132,13 @@ curl -X DELETE --negotiate -u : -v "http://localhost:9879/secret?username=testus
### Response

- **Success:** Returns HTTP `200 OK` along with a confirmation message in JSON format.
- **Failure:** Returns an appropriate HTTP error status and message if there are issues (e.g., authentication failures).
- **Failure:** Returns an appropriate HTTP error status if there are issues (e.g., authentication failures). If no secret exists for the user, the endpoint responds with HTTP `404 Not Found` and the error code `S3_SECRET_NOT_FOUND` in the plain-text response body. The error code is carried in the body, not in the HTTP status line (reason phrase), so clients must read the body rather than parse the status message:

```bash
curl -X DELETE --negotiate -u : -v http://localhost:9879/secret
# < HTTP/1.1 404 Not Found
# S3_SECRET_NOT_FOUND
```

### Testing and Verification

Expand Down
Loading
Loading