Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 14 additions & 6 deletions hadoop-hdds/docs/content/design/ozone-sts.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,17 +139,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 +228,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;
Comment thread
ChenSammi marked this conversation as resolved.

@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 @@ -813,12 +813,12 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
}

/**
* Revokes an STS token.
* @param sessionToken The STS sessionToken
* 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 @@ -1648,11 +1648,11 @@ AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName, int du
String awsIamSessionPolicy, String requestId) throws IOException;

/**
* Revokes an STS token.
* @param sessionToken The STS sessionToken
* 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 @@ -3022,8 +3022,8 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
}

@Override
public void revokeSTSToken(String sessionToken) throws IOException {
ozoneManagerClient.revokeSTSToken(sessionToken);
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 @@ -40,6 +40,12 @@ 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 int STS_ACCESS_KEY_ID_LENGTH = STS_TOKEN_PREFIX.length() + STS_ACCESS_KEY_ID_RANDOM_LENGTH;

private S3STSUtils() {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1336,11 +1336,11 @@ default AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName
}

/**
* Revokes an STS token.
* @param sessionToken The STS sessionToken
* 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
*/
default void revokeSTSToken(String sessionToken) throws IOException {
default void revokeSTSToken(String originalAccessKeyId) throws IOException {
throw new UnsupportedOperationException("OzoneManager does not require this to be implemented");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2981,10 +2981,10 @@ public AssumeRoleResponseInfo assumeRole(String roleArn, String roleSessionName,
}

@Override
public void revokeSTSToken(String sessionToken) throws IOException {
public void revokeSTSToken(String originalAccessKeyId) throws IOException {
final OzoneManagerProtocolProtos.RevokeSTSTokenRequest request =
OzoneManagerProtocolProtos.RevokeSTSTokenRequest.newBuilder()
.setSessionToken(sessionToken)
.setOriginalAccessKeyId(originalAccessKeyId)
.build();

final OMRequest omRequest = createOMRequest(Type.RevokeSTSToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ ${ACTION_MATCHES_PUTOBJECT_CREATE_WRITE_ROLE_ARN} arn:aws:iam::123456789012:rol
${ACTION_MATCHES_GETOBJECT_PUTOBJECT_ROLE_ARN} arn:aws:iam::123456789012:role/${ACTION_MATCHES_GETOBJECT_PUTOBJECT_ROLE}
${ACTION_MATCHES_UPLOADPARTCOPY_EXPECTED_OWNER_ROLE_ARN} arn:aws:iam::123456789012:role/${ACTION_MATCHES_UPLOADPARTCOPY_EXPECTED_OWNER_ROLE}
${ACTION_MATCHES_GET_STAR_READ_ROLE_ARN} arn:aws:iam::123456789012:role/${ACTION_MATCHES_GET_STAR_READ_ROLE}
${TEST_USER_ADMIN} testuser
${TEST_USER_NON_ADMIN} testuser2
@{ICEBERG_OBJECT_KEYS} file1.txt file1again.txt folder/pepper.txt folder/salt.txt userA/userA.txt userB/userB.txt userAfile.txt
@{ICEBERG_LISTABLE_OBJECT_KEYS_OBS} file1.txt file1again.txt folder/pepper.txt folder/salt.txt userA/userA.txt userB/userB.txt userAfile.txt zeroByteFile zeroByteFolder/
Expand Down Expand Up @@ -254,6 +255,17 @@ Configure STS Profile With Bogus Credential Part
Configure STS Profile ${STS_ACCESS_KEY_ID} ${STS_SECRET_KEY} bogusSessionToken
END

Verify STS Token Revocation And Post Revocation Assume Role
[Arguments] ${bucket} ${role_arn} ${revoker_user} ${revoker_keytab}
Assume Role And Configure STS Profile perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${role_arn}
Get Object Should Succeed ${bucket} ${ICEBERG_BUCKET_TESTFILE}
Kinit test user ${revoker_user} ${revoker_keytab}
${output} = Execute ozone s3 revokeststoken -o ${PERMANENT_ACCESS_KEY_ID} -y ${OM_HA_PARAM}
Should Contain ${output} STS tokens revoked for originalAccessKeyId
Get Object Should Fail ${bucket} ${ICEBERG_BUCKET_TESTFILE} AccessDenied
Assume Role And Configure STS Profile perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${role_arn}
Get Object Should Succeed ${bucket} ${ICEBERG_BUCKET_TESTFILE}

*** Test Cases ***
Create User in Ranger
${user_json} = Set Variable { "loginId": "${ICEBERG_SVC_CATALOG_USER}", "name": "${ICEBERG_SVC_CATALOG_USER}", "password": "Password123", "firstName": "Iceberg REST", "lastName": "Catalog", "emailAddress": "${ICEBERG_SVC_CATALOG_USER}@example.com", "userRoleList": ["ROLE_USER"], "userPermList": [ { "moduleId": 1, "isAllowed": 1 }, { "moduleId": 3, "isAllowed": 1 }, { "moduleId": 7, "isAllowed": 1 } ] }
Expand Down Expand Up @@ -555,27 +567,32 @@ Verify Token Revocation via CLI
FOR ${bucket} ${role_arn} IN
... ${ICEBERG_BUCKET_OBS} ${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN}
... ${ICEBERG_BUCKET_FSO} ${ICEBERG_ALL_ACCESS_ROLE_FSO_ARN}
Assume Role And Configure STS Profile perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${role_arn}
${output} = Execute ozone s3 revokeststoken -t ${STS_SESSION_TOKEN} -y ${OM_HA_PARAM}
Should Contain ${output} STS token revoked for sessionToken
# Trying to use the token for even get-object should now fail.
Get Object Should Fail ${bucket} ${ICEBERG_BUCKET_TESTFILE} AccessDenied
# Owner of the original access key can revoke the STS token.
Verify STS Token Revocation And Post Revocation Assume Role ${bucket} ${role_arn} ${ICEBERG_SVC_CATALOG_USER} ${ICEBERG_SVC_CATALOG_USER}.keytab
# S3 admin can also revoke an STS token owned by another user.
Verify STS Token Revocation And Post Revocation Assume Role ${bucket} ${role_arn} ${TEST_USER_ADMIN} ${TEST_USER_ADMIN}.keytab
END

Non-Admin Cannot Revoke STS Token
FOR ${role_arn} IN ${ICEBERG_ALL_ACCESS_ROLE_OBS_ARN} ${ICEBERG_ALL_ACCESS_ROLE_FSO_ARN}
# Create a token first.
Assume Role And Get Temporary Credentials perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${role_arn}
${token_to_revoke} = Set Variable ${STS_SESSION_TOKEN}

# Kinit as non-admin user.
Kinit test user ${TEST_USER_NON_ADMIN} ${TEST_USER_NON_ADMIN}.keytab

# Try to revoke - should give USER_MISMATCH error.
${output} = Execute And Ignore Error ozone s3 revokeststoken -t ${token_to_revoke} -y ${OM_HA_PARAM}
${output} = Execute And Ignore Error ozone s3 revokeststoken -o ${PERMANENT_ACCESS_KEY_ID} -y ${OM_HA_PARAM}
Should Contain ${output} USER_MISMATCH
END

Revoke STS Token Should Fail For Unknown Original Access Key Id
# Revoking a bogus originalAccessKeyId must fail before writing to the revocation table.
Kinit test user ${TEST_USER_ADMIN} ${TEST_USER_ADMIN}.keytab
${output} = Execute And Ignore Error ozone s3 revokeststoken -o bogus-original-access-key-id -y ${OM_HA_PARAM}
Should Contain ${output} ACCESS_ID_NOT_FOUND
Should Contain ${output} does not exist

List Objects V1 and V2 IAM Session Policy Matrix for OBS and FSO
Kinit test user ${ICEBERG_SVC_CATALOG_USER} ${ICEBERG_SVC_CATALOG_USER}.keytab

Expand Down Expand Up @@ -610,6 +627,13 @@ Tampered STS Token Service, Policy, or Signature Must Fail
Get Object Should Fail ${ICEBERG_BUCKET_OBS} ${ICEBERG_BUCKET_TESTFILE} AccessDenied
Put Object Should Fail ${ICEBERG_BUCKET_OBS} ${ICEBERG_BUCKET_TESTFILE} AccessDenied

# Exercise malformed token decoding with incorrect token structure. Unlike the earlier
# bogusSessionToken credential-part check, this literal decodes to a negative Writable
# length and covers unchecked decoder failures such as NegativeArraySizeException.
Configure STS Profile ${STS_ACCESS_KEY_ID} ${STS_SECRET_KEY} not-a-valid-token
Get Object Should Fail ${ICEBERG_BUCKET_OBS} ${ICEBERG_BUCKET_TESTFILE} AccessDenied
Put Object Should Fail ${ICEBERG_BUCKET_OBS} ${ICEBERG_BUCKET_TESTFILE} AccessDenied

Assume Role Session Policy With Multiple Buckets Should Access All Buckets
${session_policy} = Set Variable {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_OBS}/*"},{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::${ICEBERG_BUCKET_FSO}/*"}]}
Assume Role And Get Temporary Credentials policy_json=${session_policy} perm_access_key_id=${PERMANENT_ACCESS_KEY_ID} perm_secret_key=${PERMANENT_SECRET_KEY} role_arn=${ICEBERG_MULTI_BUCKET_ROLE_ARN}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2534,18 +2534,20 @@ message UpdateAssumeRoleRequest {
}

message RevokeSTSTokenRequest {
required string sessionToken = 1;
required string originalAccessKeyId = 1;
// Leader-generated revocation cutoff, replicated across OMs in HA mode.
optional uint64 revocationTimeMillis = 2;
}

message RevokeSTSTokenResponse {
}

/**
This will contain a list of revoked STS session tokens whose entries should be removed from
This will contain a list of originalAccessKeyIds whose revocation entries should be removed from
the s3RevokedStsTokenTable.
*/
message DeleteRevokedSTSTokensRequest {
repeated string sessionToken = 1;
repeated string originalAccessKeyId = 1;
}

message DeleteRevokedSTSTokensResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ protected void initializeOmTables(CacheType cacheType,

compactionLogTable = initializer.get(OMDBDefinition.COMPACTION_LOG_TABLE_DEF);

// sessionToken -> insertionTimeMillis
// originalAccessKeyId -> revocationTimeMillis
// FULL_CACHE keeps revocations in memory as there are not expected to be many
s3RevokedStsTokenTable = initializer.get(
OMDBDefinition.S3_REVOKED_STS_TOKEN_TABLE_DEF, cacheType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
* | userTable | /user :- UserVolumeInfo |
* | dTokenTable | OzoneTokenID :- renew_time |
* | s3SecretTable | s3g_access_key_id :- s3Secret |
* | s3RevokedStsTokenTable | sts_session_token :- insertionTimeMillis |
* | s3RevokedStsTokenTable | originalAccessKeyId :- revocationTimeMillis |
* |------------------------------------------------------------------------|
* }
* </pre>
Expand Down Expand Up @@ -169,7 +169,10 @@ public final class OMDBDefinition extends DBDefinition.WithMap {
S3SecretValue.getCodec());

public static final String S3_REVOKED_STS_TOKEN_TABLE = "s3RevokedStsTokenTable";
/** s3RevokedStsTokenTable: sts_session_token :- insertionTimeMillis.*/
/**
* s3RevokedStsTokenTable: originalAccessKeyId :- revocationTimeMillis.
* The value is the revocation cutoff in milliseconds.
*/
public static final DBColumnFamilyDefinition<String, Long> S3_REVOKED_STS_TOKEN_TABLE_DEF
= new DBColumnFamilyDefinition<>(S3_REVOKED_STS_TOKEN_TABLE,
StringCodec.get(),
Expand Down
Loading