AWS Cloud Operations & Migrations Blog

Restrict Access by member account to a centralized CloudTrail logging bucket

Logging and monitoring are critical components of a governance, risk, and compliance strategy. When you use AWS CloudTrail with AWS Organizations, you get an eagle-eye view of account activity across your AWS infrastructure. However, as your enterprise scales workloads in the cloud and accelerates cloud use, the logs can increase exponentially. Over time, you can save costs by consolidating the CloudTrail logs across all member accounts in your organization. As your cross-team governance process matures, you can anticipate an increase in demand for access to log data for operational analysis.

Balancing these demands can be challenging. How do you reduce redundancy, effectively tighten security controls, and save costs while maintaining appropriate levels of access? In this post, I’ll show you how to construct dynamic, granular AWS Identity and Access Management (IAM) policies to provide each member account least privilege access to the CloudTrail logs for their user activity and API usage:

Logs from the organization trail flow to the target bucket hosted in the management account. Member accounts can only retrieve log files from the bucket’s folder structure indicated by the corresponding account number.

Figure 1: Interaction between AWS Organizations, CloudTrail, and S3

Overview

It’s a best practice to use a dedicated Amazon Simple Storage Service (Amazon S3) bucket as the central repository for your CloudTrail logs. When you create a trail in the CloudTrail console, you can create a bucket or use an existing one. If you are currently transitioning from member accounts trails to an organization trail, see Best practices for moving from member account trails to organization trails in the AWS CloudTrail User Guide. For reduced exposure from compromised credentials, the AWS CloudTrail Best Practices blog post recommends a redundant S3 bucket in a separate security boundary.

When you define your storage location, CloudTrail will create or revise the target S3 bucket policy, granting access to the management account. To view the resulting policy, see Amazon S3 Bucket Policy for CloudTrail. By default, only the management account can access the organization log files.

There are three methods for granting access to member accounts:

  • In the management account, create an IAM role for each member account. This approach requires considerable administrative overhead and is more appropriate for a scenario that involves a small number of member accounts. For more information, see Sharing CloudTrail Log Files Between AWS Accounts.
  • S3 access control lists (ACLs) are a legacy access control mechanism that does not lend itself to scalability and ease of administration. We do not recommend this approach. For more information, see the IAM Policies and Bucket Policies and ACLs! Oh, My! (Controlling Access to S3 Resources) blog post.
  • By adding a few lines to the bucket policy, you can grant access to any number of member accounts without the need for revision each time an account is added or removed. This approach is preferred because it scales from one to many member accounts with low administrative overhead.

Understanding object ownership

Let me provide some conceptual groundwork to explain why some implementation steps are necessary.

First, be aware that S3 adheres to the following default behaviors:

  • S3 objects are owned by the account that uploads the object. Every time an object is created, Amazon S3 associates an ACL subresource with a single grantee referencing the account where the calling entity (user/role) resides. For more information, see Amazon S3 bucket and object ownership in the Amazon S3 User Guide.
  • A bucket policy can only be used as an access control mechanism for objects that are owned by the bucket owner account. If the object is owned by a different account, the bucket policy will not apply.

Keep in mind that these behaviors exist primarily because Amazon S3 predates IAM. The effective permissions of an IAM user or role in the context of S3 are derived from a combination of bucket policies, user policies, and ACLs. For more information, see How Amazon S3 authorizes a request in the Amazon S3 User Guide.

Let’s imagine a cross-account scenario where Account A is granted permission to write to a bucket owned by Account B. By default, Account B doesn’t have access to the files uploaded by Account A. This is why the log delivery bucket policy qualifies the s3:PutObject permission, as shown here. To write the log file, CloudTrail must include the x-amz-acl request header in the PUT Object call and grant FULL_CONTROL to the bucket owner.

{
            "Sid": "AWSCloudTrailWrite20150319",
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "cloudtrail.amazonaws.com"
                ]
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::my-organization-bucket/AWSLogs/o-exampleorgid/*",
            "Condition": {
                "StringEquals": {
                    "s3:x-amz-acl": "bucket-owner-full-control"
                }
            }
        }

FULL_CONTROL maps to a combination of corresponding IAM permissions, including s3:GetObject, s3:PutObject, and s3:DeleteObject. However, it does not encompass or transfer object ownership. Returning to our cross-account scenario, what if Account B needs to share the files written by Account A with Account C? Mapping these hypothetical entities to our real-life logging scenario, Account B is the management account, Account A is the CloudTrail service, and Account C is an organization member account.

To resolve this you will enable S3 Object Ownership, a bucket setting that gives you the option to automatically take ownership of objects uploaded to your bucket from a different account. Before its release, a recommended workaround used the to publish an event to AWS Lambda on log delivery. The Lambda function runs custom code to check object ownership and perform an overwrite copy to reset the owner ID. The S3 Object Ownership feature requires fewer moving parts and can be enabled with a few simple steps.

  1. In the Amazon S3 console, choose your log delivery bucket.
  2. On the Permissions tab, scroll down to Object ownership and choose Edit.
  3. Choose Bucket owner preferred and then choose Save changes.

If the user or role you used to sign in has permission to modify this setting (see PutBucketOwnershipControls), you will see confirmation that ownership has been successfully updated.

The Object ownership section displays two options: Object writer and Bucket owner preferred.

Figure 2: Object ownership

Bucket policy overview

You have enabled the bucket policy to function as an access control mechanism to share CloudTrail logs with member accounts. However, here are some requirements for the solution:

  • S3 bucket policies are limited to 20 KB in size. The solution must operate within this size constraint.
  • The solution must scale and reduce administrative overhead.

Let’s imagine you need to grant access to member account 222222222222. From the management account, you could manually edit the bucket policy and add the following statement:

{
            "Sid": "MemberAccountAccessObjectLevel",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::222222222222:root"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::my-organization-bucket/AWSLogs/o-exampleorgid/222222222222/*"
      }

However, you might be managing access for dozens or hundreds of member accounts. This method doesn’t scale and you would quickly exceed the policy size limit. You need a mechanism to dynamically represent the account that owns the calling entity and test whether that account belongs to the organization.

This is where IAM policy variables come in. Policy variables function as placeholders in a policy. When the policy is evaluated, the policy variables are replaced with values that come from the context of the request. Policy variables can be used in the Resource and Condition elements of an IAM policy.

For this use case, you will use the aws:PrincipalAccount as a policy variable and principalOrgId as a condition. As its name implies, aws:PrincipalAccount represents the account where the requesting principal resides. aws:PrincipalOrgId compares the identifier of the organization where the requesting principal resides with the identifier specified in the policy.

The variable syntax is composed of a $ prefix followed by a pair of curly brackets that enclose the key (for example, ${aws:PrincipalAccount}.

In the following policy statements, I make the required revisions to the previous example.

        {
            "Sid": "MemberAccountAccessObjectLevel",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::my-organization-bucket/AWSLogs/o-exampleorgid/${aws:PrincipalAccount}/*",
            "Condition": {
                "StringEquals": {
                    "aws:PrincipalOrgID": "o-exampleorgid"
                }
            }
        },
        {
            "Sid": "MemberAccountAccessBucketLevel",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::my-organization-bucket",
            "Condition": {
                "StringEquals": {
                    "aws:PrincipalOrgID": "o-exampleorgid"
                },
                "StringLike": {
                    "s3:prefix": "AWSLogs/o-exampleorgid/${aws:PrincipalAccount}/*"
                }
            }
              }

Note: S3 actions have different syntax requirements, depending on the corresponding resource type. For example, s3:ListBucket corresponds to the bucket resource type and supports an ARN in the format arn:${Partition}:s3:::${BucketName}. This necessitates a separate statement and the s3:prefix condition key to restrict the action.

Putting it all together with the original default CloudTrail bucket policy, here is the final result:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AWSCloudTrailAclCheck20150319",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudtrail.amazonaws.com"
            },
            "Action": "s3:GetBucketAcl",
            "Resource": "arn:aws:s3:::my-organization-bucket"
        },
        {
            "Sid": "AWSCloudTrailWriteAccount20150319",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudtrail.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::my-organization-bucket/AWSLogs/111111111111/*",
            "Condition": {
                "StringEquals": {
                    "s3:x-amz-acl": "bucket-owner-full-control"
                }
            }
        },
        {
            "Sid": "AWSCloudTrailWriteOrg20150319",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudtrail.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::my-organization-bucket/AWSLogs/o-exampleorgid/*",
            "Condition": {
                "StringEquals": {
                    "s3:x-amz-acl": "bucket-owner-full-control"
                }
            }
        },
        {
            "Sid": "MemberAccountAccessObjectLevel",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::my-organization-bucket/AWSLogs/o-exampleorgid/${aws:PrincipalAccount}/*",
            "Condition": {
                "StringEquals": {
                    "aws:PrincipalOrgID": "o-exampleorgid"
                }
            }
        },
        {
            "Sid": "MemberAccountAccessBucketLevel",
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::my-organization-bucket",
            "Condition": {
                "StringEquals": {
                    "aws:PrincipalOrgID": "o-exampleorgid"
                },
                "StringLike": {
                    "s3:prefix": "AWSLogs/o-exampleorgid/${aws:PrincipalAccount}/*"
                }
            }
        }
    ]
}

In this example, I’m using “Principal”: {“AWS”: ”*”}, which means this policy will allow any authenticated request as long as it originates from a member account in your organization. If you prefer a more restrictive approach to enforce stricter security controls and segregation of duties, here are two suggestions:

  1. In each member account, designate a role with a consistent naming convention as the allowed Principal.

Note: This isn’t as simple as adding a role ARN in the Principal element with a wildcard (*) in place of an account number. This will result in the API response Invalid principal in policy. The Principal element only supports wildcards to represent public access (anonymous users) “Principal”: {”*”} or all authenticated users “Principal”: {“AWS”: ”*”}.

You can do this with the aws:PrincipalArn global condition key. Here is an example Condition element expression:

      "Condition": {"ArnLike": {"aws:PrincipalArn":"arn:aws:iam::*:role/LogAccessRole"}}

  1. If you choose a tagging strategy, the aws:PrincipalTag condition key will serve a similar function. In the following example, only IAM users, groups, or roles in each member account with the LogAccess=true tag will be allowed access.

"Condition": {"StringEquals": {"aws:PrincipalTag/LogAccess": "true"}}

Note: aws:PrincipalTag and aws:PrincipalOrgID typically use the StringEquals condition operator, but will work with any string condition operator. The following condition element expression demonstrates the correct JSON syntax:

"Condition": {
                "StringEquals": {
                    "aws:PrincipalTag/s3test": "true",
                    "aws:PrincipalOrgID": "o-exampleorgid"
                }
            }

S3 bucket encryption considerations

Encryption is another essential security control to include in your strategy for protecting sensitive data. When you create a trail, the option to encrypt your log files with SSE-KMS encryption using a customer-managed CMK is enabled by default. See Figure 2.

Under Trail log bucket and folder, Log file SSE-KMS encryption is selected. Under AWS KMS customer managed CMK, New is selected.

Figure 3: Log file encryption settings

It is possible that the logging bucket has default bucket encryption enabled with the same option or the bucket might be configured to use a Bucket Key for SSE-KMS. If either scenario applies to the CloudTrail logs in your logging bucket, consider the following:

  • To access their CloudTrail logs, users in the member accounts will need kms:Decrypt permission for the KMS CMK used for encryption.
  • If the default AWS-managed CMK (aws/s3) is used, you cannot share the CloudTrail logs with member accounts. You can view the key policy for an AWS-managed CMK, but it cannot be revised, which means it can’t be used to grant cross-account access. If this is the case, switch to a customer-managed CMK or configure CloudTrail log file encryption.

To enable access, append the following statement to your key policy:

{
            "Sid": "Allow Member Account Access",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "kms:Decrypt",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "aws:PrincipalOrgID": "o-exampleorgid"
                }
            }
        }

Following the same instructions for the bucket policy, you can use aws:PrincipalTag and aws:PrincipalOrgID here to tighten member account access.

Conclusion

In this post, I walked you through the steps required to facilitate least privilege access to a centralized CloudTrail log bucket for your organization member accounts. I shared important aspects of S3 security features, KMS encryption, and advanced IAM topics and features such as conditions and policy variables. I hope you can add these tips and tricks to your toolbelt, and that you now feel better equipped to implement and maintain a centrally managed, secure, multi-account AWS environment.

About the author

Sean Mclaughlin

Sean Mclaughlin

Sean McLaughlin is a Technical Account Manager based in Dallas. He is passionate about helping media and entertainment customers achieve their security and compliance objectives in the AWS Cloud. Outside of work, Sean enjoys movie theaters, walking his chihuahua mix, and travel adventures with his family.