Stop Playing Whac-A-Mole, Start Using Least Privilege IAM Policies

Stop Playing Whac-A-Mole, Start Using Least Privilege IAM Policies

Setting up least privilege access for resources on AWS is… hard. We’ve been writing about least privilege a lot recently because of how important it is. Applying least privilege means that your services and users should operate with the minimum permissions that they need to do their jobs. To the extent that you can implement least privilege, you’ll reduce your attack surface from both bad actors and accidents.

As Adam discussed on our blog, you need to choose how deep to go on implementing least privilege depending on your risk exposure. In this post we’re going to look at some of the challenges of setting up least privilege in AWS IAM. Then we’ll look at how you can start locking down access to S3 resources using a combination of features from Sym and k9 Security.

Why are we starting with S3? For many organizations, S3 buckets contain some of their most sensitive, protected information. This might include audit logs, backups, and other protected customer information. Even with features like block public access, S3 exposures remain a common risk point that, regardless of your risk assessment, makes sense to focus on.

Playing Whac-A-Mole

When it comes to AWS, figuring out what exactly the “minimum permissions” someone needs to do their job can be a bit of an adventure (or as my colleague Sandeep once said, a game of Whac-A-Mole). Achieving a business use case typically requires granting AWS IAM permissions to many different AWS services, and it isn’t always obvious what those permissions are.

Last Spring, Adam covered how you can generate least privilege policies by observing your actions as you run through a use case. AWS IAM Access Analyzer and iamlive plus localstack will record the IAM actions that your use case requires by looking at CloudWatch or the API calls you’re making. Note: IAM Access Analyzer currently doesn’t actually capture all of the actions that your policies require, as observed by Andreas from cloudonaut.

Observability tools can help you limit the IAM actions in your policies, but this is only part of the battle! One of the trickier parts of IAM is setting up proper resource and condition constraints. Resource and condition constraints are what help you scope down policies to only impact certain resources, which is critical for least privilege. Observational tools aren’t good at this, because it isn’t obvious what the right resource and conditions constraints should be by just looking at your activity logs.

Resource and condition constraints vary widely by service. To discover your options, you need to refer to the “Actions, resources and condition keys” page for each service within the “Service Authorization Reference” guide. For some services, you can only specify * or a specific ARN as resource constraints. For other services, you can specify partial ARNs with a wildcard. Some services support global IAM conditions (partially), while some have their own service-specific conditions.


So let’s say you’ve navigated Whac-A-Mole and hammered out a perfect least-privilege identity-based IAM policy for access to an S3 bucket. You followed an AWS tutorial to grant users access to only a subfolder of a target S3 bucket. You’ve got the right resource constraints in place and the right IAM conditions (s3:prefix) added. You’re totally locked down. Mic drop.

When you demo this to your colleague, she’s like “Congratulations.” Then she logs in to the AWS account with administrative privileges and deletes the resources in the folder you were testing with!

What happened? What your amazing least-privilege policy did NOT do, was to make sure that NO ONE ELSE could modify the resources you’re trying to protect. Just because your policy is least-privileged, that doesn’t mean that there aren’t other policies out there that are granting the access you’re working so hard to control!

What About Everyone Else?

How do you make sure that only the principals that you want to have access to a resource have that access? I’ve got some bad news and some good news. The bad news is that it is hard, the good news is that it is possible! How far you go really depends on conducting a risk assessment for your organization.

One top-down option in your arsenal is Service Control Policies (SCPs). With SCPs, you can constrain what actions users can take in any account governed by that SCP. SCPs are powerful, but kind of a heavy hammer. SCPs are best suited for coarse-grained controls rather than for fine-tuning least privilege. It can be very hard for users to understand what is going wrong if they are performing actions in conflict with an SCP.

Besides SCPs, for many AWS Services you need to rely on analysis and monitoring tools to discover potentially overprovisioned policies. AWS IAM Access Analyzer, Ermetic, and k9 Security all provide features that analyze your permissions and notify you of exposure risks.

Resource-based Policies

For about 30 AWS Services, you have another option for implementing least privilege: resource-based policies. A resource-based policy is attached to the resource that it is trying to protect, rather than the user that is trying to access it. Going back to our least privilege scenario above: if you had attached a resource-based least privilege policy to your S3 bucket, the outcome might have been different. Your colleague would not be able to access the bucket unless they were granted access via the resource policy… REGARDLESS of what their user permissions were.

As with most things in IAM, actually getting a least-privileged resource policy right is hard! By default, someone can access a resource if either a resource-based or identity-based policy grants them access to it. So if you really want to lock down access via a resource-based policy, you’ll need it to include Deny rules in addition to Allow rules. Since Deny rules take precedence over Allow rules from any source, you can use Deny rules in your resource-based policy to ensure only the specific principals that you intend can access the resource.

If you want to go deeper on all these details, I highly recommend checking out k9 Security’s “Control Access to any Resource” guide as well as k9 founder Stephen Kuenzli’s book Effective IAM for AWS: Secure AWS with IAM built for continuous delivery!

Using k9 Least Privilege and Sym

To make all this a bit more real, I published an example Sym Flow that integrates with k9 Security’s least privilege approach. The example sets up temporary access to an example S3 bucket. The bucket uses a resource-based policy generated by k9 Security’s k9policy module. The example builds on our core AWS IAM Identity Center Example, so I’m just going to go through the k9-specific bits here.

Setting up the Permission Set

In, we set up an example Permission Set that our Sym Flow can grant requesters access to. The interesting thing here is that the Permission Set does not need to actually grant users S3 access! Users will get S3 access from the resource policy that we’ll associate with the S3 bucket. We add the standard ViewOnlyAccess managed policy to the permission set so that users can navigate around the console. (Note: Beware of the similarly named ReadOnlyAccess managed policy! This grants users read-only access to every resource in your account, typically not what you want as a starter “harmless” permission set).

# Create an AWS SSO PermissionSet that allows access to Sensitive S3 Buckets
resource "aws_ssoadmin_permission_set" "s3_access" {
  name             = "SymS3Access"
  description      = "Access to Sensitive S3 Buckets"
  instance_arn     = tolist(data.aws_ssoadmin_instances.this.arns)[0]
  session_duration = "PT2H"

  provider = aws.sso

  tags = var.tags

 * The Permission Set doesn't actually need to grant access to S3, because we're
 * going to use K9 Security to generate a bucket policy that only lets this
 * permission set ARN read and write to the bucket.
 * We're going to provide the managed ViewOnlyAccess policy which lets the
 * user poke around in the console.
resource "aws_ssoadmin_managed_policy_attachment" "s3_access" {
  instance_arn       = tolist(data.aws_ssoadmin_instances.this.arns){:target="_blank"}[0]
  managed_policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"
  permission_set_arn = aws_ssoadmin_permission_set.s3_access.arn

  provider = aws.sso

Create the Target Bucket

Switching over to, we create a bucket with KMS encryption enabled (the k9 bucket policy requires that you use kms to read/write objects):

 * Create an example S3 bucket that we will manage access to with Sym and K9!
module "s3_bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "~> 3.5.0"

  bucket_prefix = "sym-target-"
  acl           = "private"

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true

  server_side_encryption_configuration = {
    rule = {
      apply_server_side_encryption_by_default = {
        sse_algorithm = "aws:kms"

  tags = var.tags

Find the IAM Roles for our Permission Sets

Now we grab the IAM Roles that will have access to our bucket. Since we’re using IAM Identity Center, the IAM Role names are generated based on the Permission Set name. Luckily Terraform already has a data resource to help you find these IAM roles, so we can use that here to look up the IAM Role for the standard AdministratorAccess permission set along with the S3Access permission set we just created:

 * Look up the IAM Roles for our permission sets that are generated by IAM
 * Identity Center
data "aws_iam_roles" "admin_role" {
  name_regex  = "AWSReservedSSO_AWSAdministratorAccess_.*"
  path_prefix = "/aws-reserved/"

data "aws_iam_roles" "s3_role" {
  name_regex  = "AWSReservedSSO_${}_.*"
  path_prefix = "/aws-reserved/"

  depends_on = [
    # Depend on our account assignment to ensure the role exists before we try to find it

Generate and Attach a Least-Privileged Bucket Policy

We configure k9 Security’s bucket policy module with the ARNs we want to be able to administer, read, and write to the bucket, and then attach the generated policy to our example bucket:

# Set up the principal ARNs that will be able to work with our target S3 bucket
locals {
  administrator_arns = [

  read_config_arns = local.administrator_arns

  read_data_arns = [

  write_data_arns = local.read_data_arns

 * Use K9's bucket policy module to provision a least-privilege bucket policy that only
 * grants the administrator and the s3_access permission sets access to the bucket.
module "k9_bucket_policy" {
  source  = "k9securityio/s3-bucket/aws//k9policy"
  version = "0.7.3"

  s3_bucket_arn = module.s3_bucket.s3_bucket_arn

  allow_administer_resource_arns = local.administrator_arns
  allow_read_config_arns         = local.read_config_arns
  allow_read_data_arns           = local.read_data_arns
  allow_write_data_arns          = local.write_data_arns

resource "aws_s3_bucket_policy" "this" {
  bucket = module.s3_bucket.s3_bucket_id
  policy = module.k9_bucket_policy.policy_json

Testing It Out

First let’s get our example flow provisioned:

% terraform apply


Apply complete! Resources: 26 added, 0 changed, 0 destroyed.


bucket_arn = "arn:aws:s3:::sym-target-20221106111111111111111111"
permission_set_arn = "arn:aws:sso:::permissionSet/ssoins-123456789abcdefg/ps-123456789abcdefg"

Now, we need to configure the S3 Access Permission Set in our local AWS config. Once we request access with Sym, we’ll be able to use this:

[profile s3_access]
region = us-east-1

Make a Sym request to use the S3 Access Permission Set:

Once approved, we can read and write objects to the bucket using the AWS CLI:

% AWS_PROFILE=s3 aws s3 cp --sse aws:kms HelloWorld.txt s3://sym-target-20221106111111111111111111/
upload: ./HelloWorld.txt to s3://sym-target-20221106111111111111111111/HelloWorld.txt

Recap & Next Steps

Many teams could use an effective way to safeguard access to S3, if only they had the time to implement one. It usually takes a lot of work to get least privilege set up right! Our example Sym Flow, combined with a least-privilege policy from k9 Security, lets you rapidly configure controls for S3 that you can feel confident about.

Thanks to Stephen Kuenzli and k9 Security for publishing these great resources on IAM. Following the lead from k9, we’ll follow up on this discussion of S3 access to go deeper on KMS access. KMS is another service that supports resource-based policies, and can help you implement security controls around sensitive data in both S3 and many other AWS services.

We’re also looking forward to seeing what new capabilities get announced at AWS re:invent in just a few weeks! Every year AWS releases new features to make working with IAM easier, so we will likely need to revisit this blog post in December with a tour of what’s new.

Related Posts