Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to delete S3 objects/versions on MFA protected bucket using boto3?

I have a versioned S3 bucket named protected-bucket and I want to programmatically delete objects or versions (sometimes just some versions). Bucket has the following policy attached that enforces the MFA to be present when Delete* actions are about to be executed:

{
    "Sid": "RequireMFAForDelete",
    "Effect": "Deny",
    "Principal": {
        "AWS": "*"
    },
    "Action": "s3:Delete*",
    "Resource": "arn:aws:s3:::protected-bucket/*",
    "Condition": {
        "Bool": {
            "aws:MultiFactorAuthPresent": "false"
        }
    }
}

I also tried to use the "Condition": { "Null": { "aws:MultiFactorAuthAge": true }} in bucket policy, as suggested on the https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-bucket-policies.html#example-bucket-policies-use-case-7 page. Got the same problem from below...

Here is a minimal Python3 code that is supposed to delete object version in the bucket I mentioned above:

#!/usr/bin/env python3
import boto3
from datetime import datetime


mfa_totp = input("Enter the MFA code: ")

session_name='my-test-session-' + str(int(datetime.utcnow().timestamp()))
client=boto3.client('sts', 'us-east-1')
ar_res = client.assume_role(
    RoleArn='arn:aws:iam::123456789102:role/test-role',
    RoleSessionName=session_name,
    DurationSeconds=900,
    SerialNumber='arn:aws:iam::987654321098:mfa/my_user_name',
    TokenCode=mfa_totp,
)
print(ar_res)
tmp_creds = ar_res["Credentials"]

s3_client = boto3.client("s3", "us-east-1",
                         aws_access_key_id=tmp_creds["AccessKeyId"],
                         aws_secret_access_key=tmp_creds["SecretAccessKey"],
                         aws_session_token=tmp_creds["SessionToken"])

s3_bucket = "protected-bucket"
s3_key = "test/test4.txt"
s3_version = "XYZXbHbi3lpCNlOM8peIim6gi.IZQJqM"

# If I put code here that lists objects in 
if s3_version:
    response = s3_client.delete_object(Bucket=s3_bucket,
                                       Key=s3_key,
                                       VersionId=s3_version)
else:
    response = s3_client.delete_object(Bucket=s3_bucket,Key=s3_key)

print(response)

The error I am getting follows:

Traceback (most recent call last):
  File "./del_test.py", line 37, in <module>
    response = s3_client.delete_object(Bucket=s3_bucket,
  File "/home/dejan/py/myproj/lib64/python3.8/site-packages/botocore/client.py", line 386, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/home/dejan/py/myproj/lib64/python3.8/site-packages/botocore/client.py", line 705, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.exceptions.ClientError: An error occurred (AccessDenied) when calling the DeleteObject operation: Access Denied

Few things to note:

  • Role I am assuming is a different account (some people may have noticed different account numbers in the Python code.
  • That Role has Delete* actions allowed in a policy attached to the role. When I remove the MFA protection in the bucket policy, the Python 3 code above works - it can delete objects and versions.
like image 235
DejanLekic Avatar asked Dec 10 '25 18:12

DejanLekic


1 Answers

It turned out I missed the key piece of information given in the https://docs.amazonaws.cn/en_us/IAM/latest/UserGuide/id_credentials_mfa_configure-api-require.html document:

The temporary credentials returned by AssumeRole do not include MFA information in the context, so you cannot check individual API operations for MFA. This is why you must use GetSessionToken to restrict access to resources protected by resource-based policies.

In short, if I just assume_role(), with MFA, like I did in the Python code presented in the question, the MFA data will not be passed down, so get_session_token() is a must... Following refactored code (made with help of my colleague @Chadwick) works as expected:

#!/usr/bin/env python3
import boto3
from datetime import datetime


mfa_serial = "arn:aws:iam::987654321098:mfa/my_user_name"
role_to_assume = "arn:aws:iam::123456789102:role/test-role"

mfa_totp = input("Enter the MFA code: ")
mfa_sts_client = boto3.client("sts", "us-east-1")
mfa_credentials = mfa_sts_client.get_session_token(
    SerialNumber=mfa_serial,
    TokenCode=mfa_totp,
)["Credentials"]

session_name='my-test-session-' + str(int(datetime.utcnow().timestamp()))
# We now create a client with credentials from the MFA enabled session we created above:
ar_sts_client=boto3.client("sts", "us-east-1",
                           aws_access_key_id=mfa_credentials["AccessKeyId"],
                           aws_secret_access_key=mfa_credentials["SecretAccessKey"],
                           aws_session_token=mfa_credentials["SessionToken"])
ar_res = ar_sts_client.assume_role(
    RoleArn=role_to_assume,
    RoleSessionName=session_name,
    DurationSeconds=900
)
print(ar_res)
tmp_creds = ar_res["Credentials"]

s3_client = boto3.client("s3", "us-east-1",
                         aws_access_key_id=tmp_creds["AccessKeyId"],
                         aws_secret_access_key=tmp_creds["SecretAccessKey"],
                         aws_session_token=tmp_creds["SessionToken"])


s3_bucket = "protected-bucket"
s3_key = "test/test4.txt"
s3_version = "YYFMqnLaVEosoZ1Zk3Xy8dVbNGQVEF35"
# s3_version = None

if s3_version:
    response = s3_client.delete_object(Bucket=s3_bucket,
                                       Key=s3_key,
                                       VersionId=s3_version)
else:
    response = s3_client.delete_object(Bucket=s3_bucket,Key=s3_key)

print(response)
like image 159
DejanLekic Avatar answered Dec 12 '25 14:12

DejanLekic