Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ public final class OzoneConsts {
public static final String S3_SETSECRET_USER = "S3SetSecretUser";
public static final String S3_REVOKESECRET_USER = "S3RevokeSecretUser";
public static final String S3_REVOKESTSTOKEN_USER = "S3RevokeSTSTokenUser";
public static final String S3_STS_TEMP_ACCESS_KEY_ID = "tempAccessKeyId";
public static final String RENAMED_KEYS_MAP = "renamedKeysMap";
public static final String UNRENAMED_KEYS_MAP = "unRenamedKeysMap";
public static final String MULTIPART_UPLOAD_PART_NUMBER = "partNumber";
Expand Down
7 changes: 4 additions & 3 deletions hadoop-hdds/common/src/main/resources/ozone-default.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5254,9 +5254,10 @@
<value>3h</value>
<tag>OZONE, OM, PERFORMANCE, SECURITY</tag>
<description>
A background job that periodically checks revoked STS token entries and
deletes ones that have existed for 12 hours. This entry controls the interval of this
cleanup check. Unit could be defined with postfix (ns,ms,s,m,h,d).
A background service that periodically scans the s3RevokedStsTokenTable and deletes
revocation entries whose cutoff is older than the maximum STS token lifetime (12 hours).
This property controls how often the cleanup service runs. Unit could be defined with
postfix (ns,ms,s,m,h,d).
</description>
</property>
<property>
Expand Down
30 changes: 22 additions & 8 deletions hadoop-hdds/docs/content/design/ozone-sts.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ solutions that want to aggregate data across multiple cloud providers.

# 3. How Ozone STS Works

The initial implementation of Ozone STS supports only the [AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html)
API from the AWS specification. A new STS endpoint on port `9880` (port `9881` for https) will be created to service STS requests in the S3 Gateway at the root path (`/`).
The initial implementation of Ozone STS supports the [AssumeRole](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html)
and [GetCallerIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html)
APIs from the AWS specification. A new STS endpoint on port `9880` (port `9881` for https) will be created to service STS requests in the S3 Gateway at the root path (`/`).
We use a separate port for STS to align with AWS so we don't have conflicts at a later time. This means we have:
- Admin port for Ozone specific S3 admin operations
- STS port for STS APIs, analogous to AWS' separate STS endpoint
Expand All @@ -66,6 +67,11 @@ return value of the AssumeRole call will be temporary credentials consisting of
an IAM policy is specified, the temporary credential will have the permissions comprising the intersection of the role permissions
and the IAM policy permissions. **Note:** If the IAM policy is specified and does not grant any permissions, then
the generated temporary credentials won't have any permissions and will essentially be useless.
- [GetCallerIdentity](https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html) returns the account,
ARN, and user ID for the caller credentials used to sign the request. Ozone uses a static account ID of `123456789012`.
For permanent S3 credentials, `UserId` is the resolved Kerberos principal and `Arn` is `arn:aws:iam::123456789012:user/<kerberosShortName>`
where `<kerberosShortName>` is the short username of the Kerberos principal. For STS temporary credentials, `UserId` is
the `AssumedRoleId` and `Arn` is the assumed-role user ARN from the session token.

## 3.2 Limitations in AssumeRole API Support

Expand Down Expand Up @@ -139,17 +145,24 @@ was included with the AssumeRole request, the String return value will also incl
would further limit the scope of the permissions, resources and actions granted by the role in Ranger, such that the temporary
credential will have the permissions and actions comprising the intersection of the role permissions and actions and the sessionPolicy permissions and actions.
- HMAC-SHA256 signature - used to ensure the sessionToken was created by Ozone and was not altered since it was created.
- creation time of the token (via `OMTokenProto#issueDate`, exposed as `STSTokenIdentifier#getCreationTime()`)
- expiration time of the token (via `ShortLivedTokenIdentifier#getExpiry()`)
- UUID of the OzoneManager secret key used to sign the sessionToken and encrypt the secretAccessKey (via `ShortLivedTokenIdentifier#getSecretKeyId()`)

## 3.5 STS Token Revocation

In the rare event temporary credentials need to be revoked (ex. for security reasons), a table in the OzoneManager RocksDB will be created
to store revoked tokens, and a command-line utility will be created to add tokens to the table. A background cleaner service
will be created to run every 3 hours to delete revoked tokens that have been in the table for more than 12 hours. The
input parameter for the command-line utility will be the sessionToken - this value is returned in plain text as a result
of the AssumeRole call (mentioned above). In this way, specific STS tokens can be revoked as opposed to all tokens. Furthermore,
AWS doesn't have a standard API to revoke tokens therefore we are creating our own system.
to store revocation cutoffs per originalAccessKeyId, and a command-line utility will be created to add entries to the table.
A background cleaner service will be created to run every 3 hours to delete revocation entries whose cutoff is more than 12 hours old.

The command-line utility accepts only `originalAccessKeyId`. The OM stores revocations by keying the table on
`originalAccessKeyId` and storing the revocation cutoff time in milliseconds as the value. When the command is issued,
all STS tokens created by that `originalAccessKeyId` whose signed `creationTime` is strictly before the cutoff are
revoked. Tokens created at or after the cutoff remain valid.

Before writing a revocation entry, the OM verifies that `originalAccessKeyId` corresponds to a real Kerberos identity by
checking that an S3 secret exists for it. This prevents bogus entries from filling the table. Non-admins may only
revoke their own `originalAccessKeyId`; S3 and tenant admins may revoke other principals.

Additionally, if the Kerberos identity of the user that created the STS token is revoked via the `ozone s3 revokesecret`
command, then all the existing and unexpired STS tokens that user created will be revoked.
Expand Down Expand Up @@ -221,7 +234,8 @@ created in Ranger as per the Prerequisites above.
originalAccessKeyId in the session token and perform the following checks:
- Ensure that if the accessKeyId starts with "ASIA", that a sessionToken was included in the `x-amz-security-token` header
- Ensure the sessionToken is not expired
- Ensure the sessionToken is not revoked via a `keyMayExist` check in OzoneManager RocksDB
- Ensure the STS credentials are not revoked by looking up the revocation cutoff for the token's originalAccessKeyId
and comparing it against the token's signed creationTime
- Validate the HMAC-SHA256 signature in the sessionToken
- Decrypt the secretAccessKey from the sessionToken and validate the AWS signature
- Authorize the call with either RangerOzoneAuthorizer or OzoneNativeAuthorizer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@
/**
* Executes revocation of STS tokens.
*
* <p>This command marks the specified STS token as revoked by adding it to the OM's revoked STS token table.
* Subsequent S3 requests using the same session token will be rejected once the revocation
* state has propagated.</p>
* <p>This command records a revocation cutoff for the given original access key ID in the OM's
* revoked STS token table. Subsequent S3 requests using STS tokens created before that cutoff
* will be rejected once the revocation state has propagated.</p>
*/
@Command(name = "revokeststoken",
description = "Revoke S3 STS token for the given session token")
description = "Revoke S3 STS tokens for the given original access key ID")
public class RevokeSTSTokenHandler extends S3Handler {

@Option(names = "-t",
@Option(names = {"-o", "--original-access-key-id"},
required = true,
description = "STS session token")
private String sessionToken;
description = "Original long-lived access key ID whose STS tokens should be revoked")
private String originalAccessKeyId;

@Option(names = "-y",
description = "Continue without interactive user confirmation")
Expand All @@ -56,8 +56,8 @@ protected void execute(OzoneClient client, OzoneAddress address)
throws IOException {

if (!yes) {
out().print("Enter 'y' to confirm STS token revocation for sessionToken '" +
sessionToken + "': ");
out().print(
"Enter 'y' to confirm STS token revocation for originalAccessKeyId '" + originalAccessKeyId + "': ");
out().flush();
final Scanner scanner = new Scanner(new InputStreamReader(System.in, StandardCharsets.UTF_8));
final String confirmation = scanner.next().trim().toLowerCase();
Expand All @@ -67,7 +67,7 @@ protected void execute(OzoneClient client, OzoneAddress address)
}
}

client.getObjectStore().revokeSTSToken(sessionToken);
out().println("STS token revoked for sessionToken '" + sessionToken + "'.");
client.getObjectStore().revokeSTSToken(originalAccessKeyId);
out().println("STS tokens revoked for originalAccessKeyId '" + originalAccessKeyId + "'.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
import org.apache.hadoop.ozone.om.helpers.OmVolumeArgs;
Expand Down Expand Up @@ -813,12 +814,21 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
}

/**
* Revokes an STS token.
* @param sessionToken The STS sessionToken
* Returns the caller identity for the current S3-authenticated request.
* @return CallerIdentityInfo containing account, arn, and userId
* @throws IOException if an error occurs during the GetCallerIdentity operation
*/
public CallerIdentityInfo getCallerIdentity() throws IOException {
return proxy.getCallerIdentity();
}

/**
* Revokes STS tokens for the given original access key ID.
* @param originalAccessKeyId The original long-lived access key ID whose STS tokens to revoke
* @throws IOException if an error occurs while revoking the STS token
*/
public void revokeSTSToken(String sessionToken) throws IOException {
proxy.revokeSTSToken(sessionToken);
public void revokeSTSToken(String originalAccessKeyId) throws IOException {
proxy.revokeSTSToken(originalAccessKeyId);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import org.apache.hadoop.ozone.om.OMConfigKeys;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
import org.apache.hadoop.ozone.om.helpers.ErrorInfo;
import org.apache.hadoop.ozone.om.helpers.LeaseKeyInfo;
Expand Down Expand Up @@ -1648,11 +1649,18 @@ AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName, int du
String awsIamSessionPolicy, String requestId) throws IOException;

/**
* Revokes an STS token.
* @param sessionToken The STS sessionToken
* Returns the caller identity for the current S3-authenticated request.
* @return CallerIdentityInfo containing account, arn, and userId
* @throws IOException if an error occurs during the GetCallerIdentity operation
*/
CallerIdentityInfo getCallerIdentity() throws IOException;

/**
* Revokes STS tokens for the given original access key ID.
* @param originalAccessKeyId The original long-lived access key ID whose STS tokens to revoke
* @throws IOException if an error occurs while revoking the STS token
*/
void revokeSTSToken(String sessionToken) throws IOException;
void revokeSTSToken(String originalAccessKeyId) throws IOException;

/**
* Gets the lifecycle configuration information.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
import org.apache.hadoop.ozone.om.OmConfig;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
import org.apache.hadoop.ozone.om.helpers.BasicOmKeyInfo;
import org.apache.hadoop.ozone.om.helpers.BucketEncryptionKeyInfo;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
Expand Down Expand Up @@ -3022,8 +3023,13 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
}

@Override
public void revokeSTSToken(String sessionToken) throws IOException {
ozoneManagerClient.revokeSTSToken(sessionToken);
public CallerIdentityInfo getCallerIdentity() throws IOException {
return ozoneManagerClient.getCallerIdentity();
}

@Override
public void revokeSTSToken(String originalAccessKeyId) throws IOException {
ozoneManagerClient.revokeSTSToken(originalAccessKeyId);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ public static boolean isReadOnly(OMRequest omRequest) {
case FinalizeUpgradeProgress:
case PrepareStatus:
case GetS3VolumeContext:
case GetCallerIdentity:
case ListTenant:
case TenantGetUserInfo:
case TenantListUser:
Expand Down Expand Up @@ -383,6 +384,7 @@ public static boolean shouldSendToFollower(OMRequest omRequest) {
case FinalizeUpgradeProgress:
case PrepareStatus:
case GetS3VolumeContext:
case GetCallerIdentity:
case ListTenant:
case TenantGetUserInfo:
case TenantListUser:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.om.helpers;

import java.util.Objects;
import net.jcip.annotations.Immutable;
import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetCallerIdentityResponse;

/**
* Utility class to handle GetCallerIdentityResponse protobuf message.
*/
@Immutable
public class CallerIdentityInfo {

private final String account;
private final String arn;
private final String userId;

public CallerIdentityInfo(String account, String arn, String userId) {
this.account = account;
this.arn = arn;
this.userId = userId;
}

public String getAccount() {
return account;
}

public String getArn() {
return arn;
}

public String getUserId() {
return userId;
}

public static CallerIdentityInfo fromProtobuf(GetCallerIdentityResponse response) {
return new CallerIdentityInfo(response.getAccount(), response.getArn(), response.getUserId());
}

public GetCallerIdentityResponse getProtobuf() {
return GetCallerIdentityResponse.newBuilder()
.setAccount(account)
.setArn(arn)
.setUserId(userId)
.build();
}

@Override
public String toString() {
return "CallerIdentityInfo{" + "account='" + account + "', arn='" + arn + "', userId='" + userId + "'}";
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}

if (o == null || getClass() != o.getClass()) {
return false;
}

final CallerIdentityInfo that = (CallerIdentityInfo) o;
return Objects.equals(account, that.account) && Objects.equals(arn, that.arn) &&
Objects.equals(userId, that.userId);
}

@Override
public int hashCode() {
return Objects.hash(account, arn, userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,47 @@ public final class S3STSUtils {
// AWS limit for session policy is 2048 characters
public static final int MAX_SESSION_POLICY_LENGTH = 2048;

public static final String STS_TOKEN_PREFIX = "ASIA";
public static final String STS_ACCESS_KEY_ID_ALLOWED_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static final int STS_ACCESS_KEY_ID_ALLOWED_CHARS_LENGTH = STS_ACCESS_KEY_ID_ALLOWED_CHARS.length();
public static final int STS_ACCESS_KEY_ID_RANDOM_LENGTH = 20;

public static final String OZONE_STATIC_ACCOUNT_ID = "123456789012";

private S3STSUtils() {
}

/**
* Builds an IAM user ARN for the given Kerberos short name.
*/
public static String toIamUserArn(String kerberosShortName) {
return "arn:aws:iam::" + OZONE_STATIC_ACCOUNT_ID + ":user/" + kerberosShortName;
}

/**
* Resolves the caller identity for GetCallerIdentity with permanent S3 credentials.
*
* @param resolvedPrincipal full Kerberos principal of the caller
* @param kerberosShortName short username
* @return caller identity with account, arn, and userId
*/
public static CallerIdentityInfo resolveCallerIdentityForPermanentCredentials(String resolvedPrincipal,
String kerberosShortName) {
return new CallerIdentityInfo(OZONE_STATIC_ACCOUNT_ID, toIamUserArn(kerberosShortName), resolvedPrincipal);
}

/**
* Resolves the caller identity for GetCallerIdentity with temporary STS credentials.
*
* @param assumedRoleId assumed role ID from the STS token
* @param assumedRoleUserArn assumed role user ARN from the STS token
* @return caller identity with account, arn, and userId
*/
public static CallerIdentityInfo resolveCallerIdentityForStsCredentials(String assumedRoleId,
String assumedRoleUserArn) {
return new CallerIdentityInfo(OZONE_STATIC_ACCOUNT_ID, assumedRoleUserArn, assumedRoleId);
}

/**
* Adds standard AssumeRole audit params.
*/
Expand Down
Loading