Implementing Compliant Secrets with AWS Secrets Manager


I had a conversation with a colleague other day, and he asked who has access to a specific password. We use AWS Secrets Manager to store secret data and AWS Identity and Access Management to control access to it. Seemingly simple question, it was difficult to answer. I started off with describing how an IAM role can have particular permissions on a particular secret, etc. Pretty soon, I realized, that to answer what roles can read a secret, one would need to parse every available IAM policy.

The policy might include actions "secretsmanager:GetSecretValue" like in an example below:

{
    "Statement": [
        {
            "Action": "secretsmanager:GetSecretValue",
            "Effect": "Allow",
            "Resource": [
                "arn:aws:secretsmanager:us-west-1:493370826424:secret:packager-passphrase-focal-gzxQPq",
                "arn:aws:secretsmanager:us-west-1:493370826424:secret:packager-key-focal-XrTWrP"
            ]
        },
    ],
    "Version": "2012-10-17"
}

It also might include wildcards both in the action - "secretsmanager:Get*" - as well as in a resource - "arn:aws:secretsmanager:us-west-1:493370826424:secret:packager-*. My head was already spinning when I thought about cross-account access. There had to be a way to answers that question.

Besides, every security-related certification I had to deal with, required to have a secrets data protection policy with defined access limitations. An auditor then would ask periodically for a proof of enforcing the policy. Who requested access, who approved it and when.

How IAM decides who has permissions

Greatly simplifying Policy evaluation logic, there are two entities.

  • IAM Identity. Can be a user or role
  • Secret

Both of them have a permission policy. The identity policy tells what the identity may do (say, secretsmanager:GetSecretValue of the packager-key-focal-XrTWrP secret). The resource policy (the secret’s permission policy) tells what identities can do what operation on the resource.

Identity-Resource permissions

Identity policyResource policy
{
    "Effect": "Allow",
    "Action": "secretsmanager:GetSecretValue",
    "Resource": [
        "arn:aws:secretsmanager:us-west-1:493370826424:secret:packager-key-focal-XrTWrP"
    ]
}
{
    "Effect" : "Allow",
    "Principal" : {
        "AWS" : [
            "arn:aws:iam::493370826424:role/infrahouse-com-github" ]
        },
    "Action" : [ "secretsmanager:GetSecretValue" ],
    "Resource" : "*"
}

See it? The identity policy can allow all permissions in the world, but the role will be able to read the secret if and only if the resource policy allows secretsmanager:GetSecretValue for the role arn:aws:iam::493370826424:role/infrahouse-com-github.

What Resource Policy Do We Want

We established that to control access to a secret, we need to prepare a resource policy that implements controls we need. Most security standards require access based on a user’s need to know and denying all other access.

Even though there are about 18 various permissions for the secretsmanager:* service, I believe we can categorize them into three access levels for the sake of simplicity:

  • Read permissions:
    • secretsmanager:BatchGetSecretValue
    • secretsmanager:ListSecrets
    • secretsmanager:DescribeSecret
    • secretsmanager:GetSecretValue
    • secretsmanager:GetRandomPassword
    • secretsmanager:ListSecretVersionIds
    • secretsmanager:GetResourcePolicy
  • Write permissions. All above plus:
    • secretsmanager:PutSecretValue
    • secretsmanager:CancelRotateSecret
    • secretsmanager:UpdateSecret
    • secretsmanager:RestoreSecret
    • secretsmanager:RotateSecret
    • secretsmanager:UpdateSecretVersionStage
  • Admin permissions. All above plus:
    • secretsmanager:CreateSecret
    • secretsmanager:DeleteSecret
    • secretsmanager:StopReplicationToReplica
    • secretsmanager:ReplicateSecretToRegions
    • secretsmanager:RemoveRegionsFromReplication
    • secretsmanager:DeleteResourcePolicy
    • secretsmanager:PutResourcePolicy
    • secretsmanager:ValidateResourcePolicy
    • secretsmanager:TagResource
    • secretsmanager:UntagResource

So now we know what inputs we need to build the resource policy. That would be a list of IAM roles with access levels for each.
Or equivalent but more convenient to process - a list of readers, writers, and admins.

There is one more requirement from AWS Secrets Manager and common sense - the role that creates the secret and sets its permissions policy must also be admin.

Any other ARN must have no permissions whatsoever.

Building Resource Policy

Let’s take a sample use-case and create a policy for it. Suppose, we have three roles:

  • arn:aws:iam::493370826424:role/ih-tf-aws-control-493370826424-admin - creates the secret, must be admin.
  • arn:aws:iam::493370826424:role/aws-reserved/sso.amazonaws.com/us-west-1/AWSReservedSSO_AWSAdministratorAccess_a84a03e62f490b50 - writer.
  • arn:aws:iam::493370826424:role/openvpn-portal-20240705183912930900000008 - reader.

Now, when we specify a principal (the IAM role), we need to have a statement for Allow actions and for Deny actions. Keep that in mind.

Rules for Admin

There is only one rule for admin and it’s simple - allow all.

{
    "Effect" : "Allow",
    "Principal" : {
      "AWS" : "arn:aws:iam::493370826424:role/ih-tf-aws-control-493370826424-admin"
    },
    "Action" : "*",
    "Resource" : "*"
}

Rules for Writer

For the writer, we need to specify what actions are allowed and deny the rest.

{
    "Effect" : "Allow",
    "Principal" : {
      "AWS" : "arn:aws:iam::493370826424:role/aws-reserved/sso.amazonaws.com/us-west-1/AWSReservedSSO_AWSAdministratorAccess_a84a03e62f490b50"
    },
    "Action" : [
      "secretsmanager:UpdateSecretVersionStage", 
      "secretsmanager:UpdateSecret", 
      "secretsmanager:RotateSecret", 
      "secretsmanager:RestoreSecret", 
      "secretsmanager:PutSecretValue", 
      "secretsmanager:ListSecrets", 
      "secretsmanager:ListSecretVersionIds", 
      "secretsmanager:GetSecretValue", 
      "secretsmanager:GetResourcePolicy", 
      "secretsmanager:GetRandomPassword", 
      "secretsmanager:DescribeSecret", 
      "secretsmanager:CancelRotateSecret", 
      "secretsmanager:BatchGetSecretValue" 
    ],
    "Resource" : "*"
}, 
{
    "Effect" : "Deny",
    "Principal" : {
      "AWS" : "arn:aws:iam::493370826424:role/aws-reserved/sso.amazonaws.com/us-west-1/AWSReservedSSO_AWSAdministratorAccess_a84a03e62f490b50"
    },
    "Action" : [ 
      "secretsmanager:ValidateResourcePolicy", 
      "secretsmanager:UntagResource", 
      "secretsmanager:TagResource", 
      "secretsmanager:StopReplicationToReplica", 
      "secretsmanager:ReplicateSecretToRegions", 
      "secretsmanager:RemoveRegionsFromReplication", 
      "secretsmanager:PutResourcePolicy", 
      "secretsmanager:DeleteSecret", 
      "secretsmanager:DeleteResourcePolicy", 
      "secretsmanager:CreateSecret" 
    ],
    "Resource" : "*"
}

Rules for Reader

Next, we need to create the rules for the reader. There are also two - one Allow effect and one - Deny.

{
    "Effect" : "Allow",
    "Principal" : {
      "AWS" : "arn:aws:iam::493370826424:role/openvpn-portal-20240705183912930900000008"
    },
    "Action" : [ 
      "secretsmanager:ListSecretVersionIds", 
      "secretsmanager:GetSecretValue", 
      "secretsmanager:GetResourcePolicy", 
      "secretsmanager:GetRandomPassword", 
      "secretsmanager:DescribeSecret" 
    ],
    "Resource" : "*"
}, 
{
    "Effect" : "Deny",
    "Principal" : {
      "AWS" : "arn:aws:iam::493370826424:role/openvpn-portal-20240705183912930900000008"
    },
    "Action" : [ 
      "secretsmanager:ValidateResourcePolicy", 
      "secretsmanager:UpdateSecretVersionStage", 
      "secretsmanager:UpdateSecret", 
      "secretsmanager:UntagResource", 
      "secretsmanager:TagResource", 
      "secretsmanager:StopReplicationToReplica", 
      "secretsmanager:RotateSecret", 
      "secretsmanager:RestoreSecret", 
      "secretsmanager:ReplicateSecretToRegions", 
      "secretsmanager:RemoveRegionsFromReplication", 
      "secretsmanager:PutSecretValue", 
      "secretsmanager:PutResourcePolicy", 
      "secretsmanager:ListSecrets", 
      "secretsmanager:DeleteSecret", 
      "secretsmanager:DeleteResourcePolicy", 
      "secretsmanager:CreateSecret", 
      "secretsmanager:CancelRotateSecret", 
      "secretsmanager:BatchGetSecretValue" 
    ],
    "Resource" : "*"
}

Rules for Every Other Roles

Finally, we need a rule that would deny any access for any other role. The statement reads, that if you ain’t either of these three roles, all actions are denied.

{
    "Effect" : "Deny",
    "Principal" : {
      "AWS" : "*"
    },
    "Action" : "*",
    "Resource" : "*",
    "Condition" : {
      "StringNotLike" : {
        "aws:PrincipalArn" : [ 
          "arn:aws:iam::493370826424:role/ih-tf-aws-control-493370826424-admin", 
          "arn:aws:iam::493370826424:role/aws-reserved/sso.amazonaws.com/us-west-1/AWSReservedSSO_AWSAdministratorAccess_a84a03e62f490b50", 
          "arn:aws:iam::493370826424:role/openvpn-portal-20240705183912930900000008" 
        ]
      }
    }
  }

Here we go. If we concatenate all policy statements above, we’ll have a policy that grants different access levels to three IAM roles. Any other role won’t have any access to the secret.

A tad verbose, isn’t it?

Introducing terraform-aws-secret

Maintaining the resource permission policy statements is error-prone if done directly. The policy logic would get deployed without tests. Besides, we can’t really answer the original question - what roles have access? We still need to parse the policy.

The terraform-aws-secret module puts together creating the secret, setting its value and permissions policy. It’s user and security-compliance friendly. You can easily tell what role has what access to the secret. Together with GitOps, you can tell who requested the change, whe approved it, and when it happened.

module "api_key" {
  source             = "registry.infrahouse.com/infrahouse/secret/aws"
  version            = "~> 0.6"
  secret_description = "API token to some service."
  secret_name        = "API_KEY"
  secret_value       = random_id.api_key.hex
  readers = [
    data.aws_iam_role.sso["Developers"].arn,
    aws_iam_role.ecs_task.arn,
  ]
  writers = [
    data.aws_iam_role.sso["AWSAdministratorAccess"].arn,
  ]
}

The module generates dynamically the secret’s permission policy from the given roles in the readers, writers, or admins lists. The generation logic is covered with Terraform unit tests, but that’s a story for another time.

Meanwhile, give it a star :) on GitHub and let’s chat if you have questions.