Apache Ozone: Improper authentication when generating S3 secrets
Description
Improper authentication of an HTTP endpoint in the S3 Gateway of Apache Ozone 1.4.0 allows any authenticated Kerberos user to revoke and regenerate the S3 secrets of any other user. This is only possible if: * ozone.s3g.secret.http.enabled is set to true. The default value of this configuration is false. * The user configured in ozone.s3g.kerberos.principal is also configured in ozone.s3.administrators or ozone.administrators.
Users are recommended to upgrade to Apache Ozone version 1.4.1 which disables the affected endpoint.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Apache Ozone 1.4.0 S3 Gateway allows authenticated Kerberos users to revoke/regenerate others' S3 secrets when non-default configs are set.
Vulnerability
Description
In Apache Ozone 1.4.0, the S3 Gateway exposes HTTP endpoints to revoke and generate S3 secrets for any user. Due to improper authentication, any authenticated Kerberos user can call these endpoints to manipulate the secrets of other users [1][3]. The vulnerable endpoints are POST /secret/revoke and POST /secret/generate, which accept an optional target username [4].
Exploitation
Conditions
The vulnerability is only exploitable if two non-default configuration settings are enabled: ozone.s3g.secret.http.enabled must be set to true (default is false), and the Kerberos principal configured in ozone.s3g.kerberos.principal must also be listed in either ozone.s3.administrators or ozone.administrators [1][3]. An attacker must be an authenticated Kerberos user but does not need to be an administrator.
Impact
An attacker can revoke existing S3 secrets and generate new ones for arbitrary users, effectively gaining control over their S3 credentials. This can lead to unauthorized access to S3 buckets and data stored in Ozone, potentially resulting in data compromise and privilege escalation [1][3].
Mitigation
Users should upgrade to Apache Ozone version 1.4.1, which disables the affected HTTP endpoint [1][3]. As a workaround, ensure that the configuration flags ozone.s3g.secret.http.enabled remains false (the default) or that the Kerberos principal is not listed in the administrator groups.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.ozone:ozoneMaven | >= 1.4.0, < 1.4.1 | 1.4.1 |
Affected products
3- Apache Software Foundation/Apache Ozonev5Range: 1.4.0
Patches
14ee6c07acfdfHDDS-9203. Allow generating/revoking S3 secret for other users via REST (#5233)
8 files changed · +121 −81
hadoop-hdds/docs/content/security/SecuringS3.md+2 −2 modified@@ -42,10 +42,10 @@ The user needs to `kinit` first and once they have authenticated via kerberos ozone s3 getsecret ``` -* Or by sending request to /secret/generate S3 REST endpoint. +* Or by sending request to /secret S3 REST endpoint. ```bash -curl -X POST --negotiate -u : https://localhost:9879/secret/generate +curl -X PUT --negotiate -u : https://localhost:9879/secret ``` This command will talk to ozone, validate the user via Kerberos and generate
hadoop-hdds/docs/content/security/SecuringS3.zh.md+2 −2 modified@@ -36,10 +36,10 @@ icon: cloud ozone s3 getsecret ``` -* 或者通过向 /secret/generate S3 REST 端点发送请求。 +* 或者通过向 /secret S3 REST 端点发送请求。 ```bash -curl -X POST --negotiate -u : https://localhost:9879/secret/generate +curl -X PUT --negotiate -u : https://localhost:9879/secret ``` 这条命令会与 Ozone 进行通信,对用户进行 Kerberos 认证并生成 AWS 凭据,结果会直接打印在屏幕上,你可以将其配置在 _.aws._ 文件中,这样可以在操作 Ozone S3 桶时自动进行认证。
hadoop-ozone/dist/src/main/smoketest/s3/secretgenerate.robot+11 −1 modified@@ -31,7 +31,17 @@ ${ENDPOINT_URL} http://s3g:9878 S3 Gateway Generate Secret Run Keyword if '${SECURITY_ENABLED}' == 'true' Kinit HTTP user - ${result} = Execute curl -X POST --negotiate -u : -v ${ENDPOINT_URL}/secret/generate + ${result} = Execute curl -X PUT --negotiate -u : -v ${ENDPOINT_URL}/secret + IF '${SECURITY_ENABLED}' == 'true' + Should contain ${result} HTTP/1.1 200 OK ignore_case=True + Should Match Regexp ${result} <awsAccessKey>.*</awsAccessKey><awsSecret>.*</awsSecret> + ELSE + Should contain ${result} S3 Secret endpoint is disabled. + END + +S3 Gateway Generate Secret By Username + Run Keyword if '${SECURITY_ENABLED}' == 'true' Kinit test user testuser testuser.keytab + ${result} = Execute curl -X PUT --negotiate -u : -v ${ENDPOINT_URL}/secret/testuser2 IF '${SECURITY_ENABLED}' == 'true' Should contain ${result} HTTP/1.1 200 OK ignore_case=True Should Match Regexp ${result} <awsAccessKey>.*</awsAccessKey><awsSecret>.*</awsSecret>
hadoop-ozone/dist/src/main/smoketest/s3/secretrevoke.robot+10 −1 modified@@ -32,7 +32,16 @@ ${SECURITY_ENABLED} true S3 Gateway Revoke Secret Run Keyword if '${SECURITY_ENABLED}' == 'true' Kinit HTTP user - ${result} = Execute curl -X POST --negotiate -u : -v ${ENDPOINT_URL}/secret/revoke + ${result} = Execute curl -X DELETE --negotiate -u : -v ${ENDPOINT_URL}/secret + IF '${SECURITY_ENABLED}' == 'true' + Should contain ${result} HTTP/1.1 200 OK ignore_case=True + ELSE + Should contain ${result} S3 Secret endpoint is disabled. + END + +S3 Gateway Revoke Secret By Username + Run Keyword if '${SECURITY_ENABLED}' == 'true' Kinit test user testuser testuser.keytab + ${result} = Execute curl -X DELETE --negotiate -u : -v ${ENDPOINT_URL}/secret/testuser2 IF '${SECURITY_ENABLED}' == 'true' Should contain ${result} HTTP/1.1 200 OK ignore_case=True ELSE
hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3secret/S3SecretGenerateEndpoint.java+0 −49 removed@@ -1,49 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * <p> - * http://www.apache.org/licenses/LICENSE-2.0 - * <p> - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - * - */ - -package org.apache.hadoop.ozone.s3secret; - -import org.apache.hadoop.ozone.audit.S3GAction; -import org.apache.hadoop.ozone.om.helpers.S3SecretValue; - -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.core.Response; -import java.io.IOException; - -/** - * Endpoint to generate and return S3 secret. - */ -@Path("/secret/generate") -@S3SecretEnabled -public class S3SecretGenerateEndpoint extends S3SecretEndpointBase { - @POST - public Response generate() throws IOException { - S3SecretResponse s3SecretResponse = new S3SecretResponse(); - S3SecretValue s3SecretValue = generateS3Secret(); - s3SecretResponse.setAwsSecret(s3SecretValue.getAwsSecret()); - s3SecretResponse.setAwsAccessKey(s3SecretValue.getAwsAccessKey()); - AUDIT.logReadSuccess(buildAuditMessageForSuccess( - S3GAction.GENERATE_SECRET, getAuditParameters())); - return Response.ok(s3SecretResponse).build(); - } - - private S3SecretValue generateS3Secret() throws IOException { - return getClient().getObjectStore().getS3Secret(userNameFromRequest()); - } -}
hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3secret/S3SecretManagementEndpoint.java+54 −11 renamed@@ -20,31 +20,74 @@ import org.apache.hadoop.ozone.audit.S3GAction; import org.apache.hadoop.ozone.om.exceptions.OMException; +import org.apache.hadoop.ozone.om.helpers.S3SecretValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.ws.rs.POST; +import javax.annotation.Nullable; +import javax.ws.rs.DELETE; +import javax.ws.rs.PUT; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.core.Response; import java.io.IOException; import static javax.ws.rs.core.Response.Status.NOT_FOUND; /** - * Revoke secret endpoint. + * Endpoint to manage S3 secret. */ -@Path("/secret/revoke") +@Path("/secret") @S3SecretEnabled -public class S3SecretRevokeEndpoint extends S3SecretEndpointBase { - +public class S3SecretManagementEndpoint extends S3SecretEndpointBase { private static final Logger LOG = - LoggerFactory.getLogger(S3SecretRevokeEndpoint.class); + LoggerFactory.getLogger(S3SecretManagementEndpoint.class); + + @PUT + public Response generate() throws IOException { + return generateInternal(null); + } + @PUT + @Path("/{username}") + public Response generate(@PathParam("username") String username) + throws IOException { + return generateInternal(username); + } - @POST + private Response generateInternal(@Nullable String username) + throws IOException { + S3SecretResponse s3SecretResponse = new S3SecretResponse(); + S3SecretValue s3SecretValue = generateS3Secret(username); + s3SecretResponse.setAwsSecret(s3SecretValue.getAwsSecret()); + s3SecretResponse.setAwsAccessKey(s3SecretValue.getAwsAccessKey()); + AUDIT.logReadSuccess(buildAuditMessageForSuccess( + S3GAction.GENERATE_SECRET, getAuditParameters())); + return Response.ok(s3SecretResponse).build(); + } + + private S3SecretValue generateS3Secret(@Nullable String username) + throws IOException { + String actualUsername = username == null ? userNameFromRequest() : username; + return getClient().getObjectStore().getS3Secret(actualUsername); + } + + @DELETE public Response revoke() throws IOException { + return revokeInternal(null); + } + + @DELETE + @Path("/{username}") + public Response revoke(@PathParam("username") String username) + throws IOException { + return revokeInternal(username); + } + + private Response revokeInternal(@Nullable String username) + throws IOException { try { - revokeSecret(); + revokeSecret(username); AUDIT.logWriteSuccess(buildAuditMessageForSuccess( S3GAction.REVOKE_SECRET, getAuditParameters())); return Response.ok().build(); @@ -62,8 +105,8 @@ public Response revoke() throws IOException { } } - private void revokeSecret() throws IOException { - getClient().getObjectStore().revokeS3Secret(userNameFromRequest()); + private void revokeSecret(@Nullable String username) throws IOException { + String actualUsername = username == null ? userNameFromRequest() : username; + getClient().getObjectStore().revokeS3Secret(actualUsername); } - }
hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3secret/TestSecretGenerate.java+23 −9 modified@@ -35,10 +35,11 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; /** @@ -47,9 +48,10 @@ @ExtendWith(MockitoExtension.class) public class TestSecretGenerate { private static final String USER_NAME = "test"; + private static final String OTHER_USER_NAME = "test2"; private static final String USER_SECRET = "test_secret"; - private S3SecretGenerateEndpoint endpoint; + private S3SecretManagementEndpoint endpoint; @Mock private ClientProtocol proxy; @@ -62,31 +64,43 @@ public class TestSecretGenerate { @Mock private Principal principal; + private static S3SecretValue getS3SecretValue(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + return new S3SecretValue((String) args[0], USER_SECRET); + } + @BeforeEach void setUp() throws IOException { - S3SecretValue value = new S3SecretValue(USER_NAME, USER_SECRET); - when(proxy.getS3Secret(eq(USER_NAME))).thenReturn(value); + when(proxy.getS3Secret(any())).then(TestSecretGenerate::getS3SecretValue); OzoneConfiguration conf = new OzoneConfiguration(); OzoneClient client = new OzoneClientStub(new ObjectStoreStub(conf, proxy)); - when(principal.getName()).thenReturn(USER_NAME); - when(securityContext.getUserPrincipal()).thenReturn(principal); - when(context.getSecurityContext()).thenReturn(securityContext); - when(uriInfo.getPathParameters()).thenReturn(new MultivaluedHashMap<>()); when(uriInfo.getQueryParameters()).thenReturn(new MultivaluedHashMap<>()); when(context.getUriInfo()).thenReturn(uriInfo); - endpoint = new S3SecretGenerateEndpoint(); + endpoint = new S3SecretManagementEndpoint(); endpoint.setClient(client); endpoint.setContext(context); } @Test void testSecretGenerate() throws IOException { + when(principal.getName()).thenReturn(USER_NAME); + when(securityContext.getUserPrincipal()).thenReturn(principal); + when(context.getSecurityContext()).thenReturn(securityContext); + S3SecretResponse response = (S3SecretResponse) endpoint.generate().getEntity(); assertEquals(USER_SECRET, response.getAwsSecret()); assertEquals(USER_NAME, response.getAwsAccessKey()); } + + @Test + void testSecretGenerateWithUsername() throws IOException { + S3SecretResponse response = + (S3SecretResponse) endpoint.generate(OTHER_USER_NAME).getEntity(); + assertEquals(USER_SECRET, response.getAwsSecret()); + assertEquals(OTHER_USER_NAME, response.getAwsAccessKey()); + } }
hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3secret/TestSecretRevoke.java+19 −6 modified@@ -55,8 +55,9 @@ @ExtendWith(MockitoExtension.class) public class TestSecretRevoke { private static final String USER_NAME = "test"; + private static final String OTHER_USER_NAME = "test2"; - private S3SecretRevokeEndpoint endpoint; + private S3SecretManagementEndpoint endpoint; @Mock private ObjectStoreStub objectStore; @@ -73,27 +74,38 @@ public class TestSecretRevoke { void setUp() { OzoneClient client = new OzoneClientStub(objectStore); - when(principal.getName()).thenReturn(USER_NAME); - when(securityContext.getUserPrincipal()).thenReturn(principal); - when(context.getSecurityContext()).thenReturn(securityContext); - when(uriInfo.getPathParameters()).thenReturn(new MultivaluedHashMap<>()); when(uriInfo.getQueryParameters()).thenReturn(new MultivaluedHashMap<>()); when(context.getUriInfo()).thenReturn(uriInfo); - endpoint = new S3SecretRevokeEndpoint(); + endpoint = new S3SecretManagementEndpoint(); endpoint.setClient(client); endpoint.setContext(context); } + private void mockSecurityContext() { + when(principal.getName()).thenReturn(USER_NAME); + when(securityContext.getUserPrincipal()).thenReturn(principal); + when(context.getSecurityContext()).thenReturn(securityContext); + } + @Test void testSecretRevoke() throws IOException { + mockSecurityContext(); endpoint.revoke(); verify(objectStore, times(1)).revokeS3Secret(eq(USER_NAME)); } + @Test + void testSecretRevokeWithUsername() throws IOException { + endpoint.revoke(OTHER_USER_NAME); + verify(objectStore, times(1)) + .revokeS3Secret(eq(OTHER_USER_NAME)); + } + @Test void testSecretSequentialRevokes() throws IOException { + mockSecurityContext(); Response firstResponse = endpoint.revoke(); assertEquals(OK.getStatusCode(), firstResponse.getStatus()); doThrow(new OMException(S3_SECRET_NOT_FOUND)) @@ -104,6 +116,7 @@ void testSecretSequentialRevokes() throws IOException { @Test void testSecretRevokesHandlesException() throws IOException { + mockSecurityContext(); doThrow(new OMException(ACCESS_DENIED)) .when(objectStore).revokeS3Secret(any()); Response response = endpoint.revoke();
Vulnerability mechanics
Root cause
"Missing authorization check — the S3 secret management endpoint accepts a username path parameter without verifying that the authenticated Kerberos user is authorized to generate or revoke secrets for that user."
Attack vector
An attacker who is a valid Kerberos user of the Ozone cluster can send an HTTP PUT or DELETE request to the S3 Gateway's `/secret/{username}` endpoint (e.g., `curl -X PUT --negotiate -u : https://s3g:9878/secret/targetuser`). The endpoint calls `generateS3Secret(username)` or `revokeSecret(username)` with the attacker-supplied username instead of the authenticated user's own name, thereby generating or revoking the S3 secret of any other user [CWE-287][CWE-863]. This attack is only possible when the configuration `ozone.s3g.secret.http.enabled` is set to `true` (default is `false`) and the Kerberos principal configured in `ozone.s3g.kerberos.principal` is also listed in `ozone.s3.administrators` or `ozone.administrators`.
Affected code
The vulnerable endpoint is the S3 secret management REST API in `S3SecretManagementEndpoint.java` (formerly `S3SecretRevokeEndpoint.java` and `S3SecretGenerateEndpoint.java`). The patch consolidates the `/secret/generate` and `/secret/revoke` endpoints into a single `/secret` endpoint and adds `@PathParam("username")` parameters to the `generate()` and `revoke()` methods, allowing any authenticated Kerberos user to specify another user's username in the URL path.
What the fix does
The patch does not add any authorization check; instead, it explicitly adds `@PathParam("username")` parameters to the `generate()` and `revoke()` methods, making the ability to target other users a designed feature of the new consolidated endpoint. The advisory states that users should upgrade to Apache Ozone 1.4.1, which disables the affected endpoint entirely. No fix that adds an authorization gate is present in the patch — the remediation is to remove the endpoint's availability.
Preconditions
- configozone.s3g.secret.http.enabled must be set to true (default is false)
- configThe Kerberos principal configured in ozone.s3g.kerberos.principal must also be listed in ozone.s3.administrators or ozone.administrators
- authAttacker must be a valid Kerberos-authenticated user of the Ozone cluster
- networkNetwork access to the S3 Gateway HTTP endpoint (typically port 9878)
Generated on May 23, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-rcq8-9q3j-98mwghsaADVISORY
- lists.apache.org/thread/rylnxwttp004kvotpk9j158vb238pfkmghsavendor-advisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2024-45106ghsaADVISORY
- www.openwall.com/lists/oss-security/2024/12/02/1ghsaWEB
- github.com/apache/ozone/pull/5233ghsaWEB
News mentions
0No linked articles in our index yet.