VYPR
High severityNVD Advisory· Published Dec 3, 2024· Updated Dec 3, 2024

Apache Ozone: Improper authentication when generating S3 secrets

CVE-2024-45106

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.

PackageAffected versionsPatched versions
org.apache.ozone:ozoneMaven
>= 1.4.0, < 1.4.11.4.1

Affected products

3

Patches

1
4ee6c07acfdf

HDDS-9203. Allow generating/revoking S3 secret for other users via REST (#5233)

https://github.com/apache/ozoneIvan ZlenkoSep 6, 2023via ghsa-ref
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

News mentions

0

No linked articles in our index yet.