Minimal AWS S3 deployment policy permissions

I am trying to figure out the minimal set of permission I need to add to an AWS IAM user in order to deploy to an S3 bucket. My current policy looks like this:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::deploy-bucket/*"
        },
        {
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::deploy-bucket"
        }
    ]
}

However, I am seeing some strange behaviour from hugo deploy, all of the upload operations are successful but all of the delete operations fail:

$> AWS_PROFILE=minimal hugo deploy
...
INFO 2022/07/21 18:04:21 Uploading js/scripts.min.3b48523909c7813fe45ef70c4af1afc6d46acad9fa34c0a738449eb8613c4412.js (182 B, Cache-Control: "max-age=31536000, no-transform, public", Content-Encoding: "gzip", Content-Type: "application/javascript"): not found at target...
INFO 2022/07/21 18:04:21 Deleting js/scripts.min.02154cf8c9c2a5dc1741cae86d72768d12ac400b03a67c591a7c1f9ecb9b11ca.js...
INFO 2022/07/21 18:04:21 Deleting js/scripts.min.98ee06cc35517b5800b382aecb0fc59893e95b9c11dd21842d0d57e4f68043e3.js...
Encountered 2 errors.
Error: blob (key "js/scripts.min.02154cf8c9c2a5dc1741cae86d72768d12ac400b03a67c591a7c1f9ecb9b11ca.js") (code=Unknown): Forbidden: Forbidden
	status code: 403, request id: [REDACTED], host id: [REDACTED]

This is extra confusing because calling s3:DeleteObject directly from the aws cli works just fine, and I can confirm that the operation is successful in the AWS console e.g.

$> aws --profile minimal s3api delete-object --bucket deploy-bucket --key js/scripts.min.02154cf8c9c2a5dc1741cae86d72768d12ac400b03a67c591a7c1f9ecb9b11ca.js
{
    "DeleteMarker": true,
    "VersionId": "[REDACTED]"
}

I am clearly missing a necessary permission somewhere but am at a complete loss as to what permission is missing. Especially since I’ve tried the following 3 test policies with mixed results.

  • FAILS: Grant s3:* on objects but only s3:ListBucket on the bucket
  • FAILS: Grant s3:PutObject and s3:DeleteObject on objects and s3:* on the bucket
  • SUCCESS: Grant s3:* on objects and s3:* on the bucket

The bucket in question has BucketOwnerEnforced configured, and I have checked that all of the objects in the bucket are owned by the account and not another IAM user. The bucket does have versioning enabled but adding s3:DeleteObject* does not resolve the issue.

Going through the list of S3 permission I can’t find any set of permissions that would cause this behaviour if they were not present. I’ve also dug through the hugo source for the delete operations and the gocloud.dev/blob/s3blob Delete operation source and can’t find anything that would require permissions beyond s3:DeleteObject.

I’ve also tried the AWS IAM Access Advisor to try and figure out what permission I am missing but it only shows bucket-level permissions and none of them have been used in the last 24 hours. Unfortunately, I am not in a position to enable CloudTrail at the moment to see all of the object-level operations attempted by the user.

If anyone has any insight on this your thoughts would be greatly appreciated.

Pulling out something that “worked for me” at some point (with my tool s3deploy, that is, but they work very similiary):

{
   "Version": "2012-10-17",
   "Statement":[
      {
         "Effect":"Allow",
         "Action":[
            "s3:ListBucket",
            "s3:GetBucketLocation"
         ],
         "Resource":"arn:aws:s3:::<bucketname>"
      },
      {
         "Effect":"Allow",
         "Action":[
            "s3:PutObject",
            "s3:PutObjectAcl",
            "s3:DeleteObject"
         ],
         "Resource":"arn:aws:s3:::<bucketname>/*"
      }
   ]
}

Looking at the above, I’m guessing you’re missing the s3:PutObjectAcl.

Unfortunately, I am still receiving the same error after trying all three combinations of those permissions:

  • FAILS: Just s3:PutObjectAcl
  • FAILS: Just s3:GetBucketLocation
  • FAILS: s3:PutObjectAcl and s3:GetBucketLocation

If you don’t get a working answer in here within a reasonable time I would suggest you open up an issue on GitHub. I can /cc in some (or, one) people there who I expect would know; and we should have an example of this in the documentation.

1 Like

I ended up getting CloudTrail enabled and made some more progress. It looks like DeleteObject is preceded by a HeadObject call which is actually the request that is failing. I am stumped here because according to the HeadObject documentation that should be allowed by the s3:ListBucket permission.

The bucket I am deploying to is unencrypted so it’s not an issue with a missing kms:Decrypt permission.

I stumbled on this issue as well. It seems like you need to give it s3:GetObject, otherwise it won’t be able to delete any objects. I don’t understand why, and I didn’t want to spend time reading the source code to figure out what they’re doing.

CloudTrail won’t be very helpful, since object-level actions aren’t logged, so you won’t really see what it is doing.

{
    "Statement": [
        {
            "Action": "s3:ListBucket",
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::foobar"
        },
        {
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::foobar/*"
        },
        {
            "Action": "cloudfront:CreateInvalidation",
            "Effect": "Allow",
            "Resource": "arn:aws:cloudfront::123412341234:distribution/foobar"
        }
    ],
    "Version": "2012-10-17"
}
1 Like

Hugo is checking for remote file equivalence via checksums and md5’s etc, which explains the need for GetObject permissions. Makes sense in hindsight after @vegardx’s post above.