← Back to blog
Cloud / AWSMay 22, 2026· 10 min

KMS Key Policies, Grants, and Secure Cross-Account Encryption

A deep, practical guide to AWS KMS authorization: how key policies, IAM, and grants actually interact, and how to build least-privilege cross-account encryption that holds up under review.

KMS Key Policies, Grants, and Secure Cross-Account Encryption

AWS KMS is deceptively simple to call and genuinely hard to secure. The cryptographic operations are a thin surface; almost every real-world KMS incident is an authorization failure, not a cryptographic one. An over-broad key policy, a forgotten grant, or a cross-account trust that never narrowed past the account root is what turns an encrypted data store into a compliance finding. This article walks through how KMS authorization actually resolves, why the default key policy is a trap, and how to build cross-account encryption patterns that survive a hostile review.

The default key policy trap

When you create a customer managed key (CMK) in the console or with default settings, KMS attaches a policy whose first statement grants the AWS account root principal full access via kms:* with Resource set to *. AWS documents this as enabling IAM policies to control the key, and that framing is accurate, but it is widely misread. The statement does not merely "allow IAM to work" — it delegates all authorization decisions for that key to IAM in the account. Any principal with a sufficiently broad IAM policy (think a wildcard kms:* or an attached AdministratorAccess) can now administer and use the key, even though they were never named on the key. The blast radius of an over-permissioned IAM role silently extends to every key that carries this default statement.

Treat the root-account statement as a deliberate decision, not a default. If you keep it, your key's security is only as tight as the loosest IAM policy in the account. If you remove or scope it, you must enumerate key administrators explicitly in the key policy — otherwise you can lock yourself out, because KMS will not let you save a policy that has no path to administer the key.

Key policy vs IAM vs grants: how access actually resolves

Three mechanisms can authorize a KMS request, and engineers routinely get their precedence wrong. There is no priority ordering where one "wins" over another — KMS evaluates the union. A request is allowed if ANY of these grants it and NOTHING explicitly denies it:

  • Key policy: the resource policy attached to the key. It is the root of trust. For a principal in the SAME account, the key policy alone can authorize a request. For a principal in a DIFFERENT account, the key policy is necessary but never sufficient — see the cross-account section.
  • IAM policy: an identity policy in the key's account. It only grants KMS access if the key policy enables IAM to do so (the root-account statement, or a statement naming the account/principal). IAM cannot grant access to a key whose policy does not delegate to IAM.
  • Grants: a programmatic, temporary delegation created by kms:CreateGrant. Grants allow a principal (the grantee) a specific subset of operations, optionally constrained by encryption context and resource conditions. AWS services like EBS, RDS, and Lambda create grants on your behalf so they can decrypt on your resources without you widening the key policy.

The mental model: key policy and IAM are evaluated like any other resource-policy/identity-policy pair, and an explicit Deny anywhere overrides every Allow. Grants are additive on top — they can broaden but never override a Deny. Because grants are created via API and not visible in the key policy document, they are the most commonly overlooked path to a key. If you audit only the key policy and IAM, you have not audited the key.

A least-privilege key policy

The following policy separates key administrators (who can manage the key but cannot use it cryptographically) from key users (who can encrypt/decrypt but cannot change the policy or schedule deletion). It scopes service usage with kms:ViaService, lets AWS resource integrations create grants while blocking arbitrary grant creation via kms:GrantIsForAWSResource, and pins an encryption context so a stolen ciphertext cannot be decrypted out of context.

{
  "Version": "2012-10-17",
  "Id": "key-least-privilege",
  "Statement": [
    {
      "Sid": "KeyAdministrators",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:role/kms-key-admins"
      },
      "Action": [
        "kms:Create*",
        "kms:Describe*",
        "kms:Enable*",
        "kms:List*",
        "kms:Put*",
        "kms:Update*",
        "kms:Revoke*",
        "kms:Disable*",
        "kms:Get*",
        "kms:Delete*",
        "kms:TagResource",
        "kms:UntagResource",
        "kms:ScheduleKeyDeletion",
        "kms:CancelKeyDeletion"
      ],
      "Resource": "*"
    },
    {
      "Sid": "KeyUsersViaS3Only",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:role/app-data-plane"
      },
      "Action": [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:ReEncrypt*",
        "kms:GenerateDataKey",
        "kms:GenerateDataKey*",
        "kms:DescribeKey"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "s3.ap-south-1.amazonaws.com",
          "kms:EncryptionContext:project": "shieldsync-prod"
        }
      }
    },
    {
      "Sid": "AllowAWSResourceGrantsOnly",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::111122223333:role/app-data-plane"
      },
      "Action": [
        "kms:CreateGrant",
        "kms:ListGrants",
        "kms:RevokeGrant"
      ],
      "Resource": "*",
      "Condition": {
        "Bool": {
          "kms:GrantIsForAWSResource": "true"
        }
      }
    }
  ]
}

Note what is absent: there is no kms:* for the users, no Decrypt for the administrators, and no unconditional CreateGrant. The kms:ViaService condition means the app role can only invoke the key through S3 — a leaked credential cannot call kms:Decrypt directly against the KMS API to dump ciphertext. The kms:EncryptionContext:project condition binds every operation to a named context, and the GrantIsForAWSResource condition lets EBS, RDS, or S3 set up the grants they need while preventing the role from minting an arbitrary grant to an attacker-controlled principal.

The data-plane operations that matter

Most workloads never call Encrypt with raw plaintext. They use envelope encryption: kms:GenerateDataKey returns a plaintext data key plus an encrypted copy; the application encrypts the payload locally with the plaintext key, discards it, and stores the ciphertext blob alongside the encrypted data key. On read, kms:Decrypt unwraps the data key. This is why GenerateDataKey and Decrypt are the operations to guard most tightly — Decrypt is the one that turns ciphertext back into plaintext, and a principal with Decrypt on a key can read everything that key protects. Grant or deny these per the principle that read access to plaintext is the crown jewel, not the ability to write new ciphertext.

Encryption context as an authorization control

Encryption context is a set of non-secret key/value pairs bound into the AEAD additional authenticated data of every ciphertext. It is not encrypted, but it is integrity-protected: a ciphertext encrypted with context {tenant: A} cannot be decrypted by a request that supplies {tenant: B}, and KMS logs the context in CloudTrail. Used in a key policy condition (kms:EncryptionContext:key or kms:EncryptionContextKeys), it becomes a real authorization boundary — for example, forcing a multi-tenant service to prove which tenant a decrypt is for, so a confused-deputy bug cannot cross tenants on a shared key. Treat encryption context as mandatory metadata for any shared key, and assert it in both your application code and your key policy.

Cross-account encryption done right

Cross-account KMS access has a rule with no exceptions: the key policy in the key's owner account must explicitly allow the external principal, AND that principal must have an IAM policy in its own account allowing the same KMS actions. Both halves are required. The key policy alone never grants cross-account access, and IAM in the caller's account obviously cannot grant access to a key it does not own. A frequent mistake is to name only the external account root (Principal: {AWS: arn:aws:iam::444455556666:root}) in the key policy and stop there — that delegates the decision to the OTHER account's IAM, meaning any sufficiently-privileged role over there can use your key. Pin the specific role ARN when you can, and pair it with conditions:

  • In the key owner account, add a key policy statement naming arn:aws:iam::444455556666:role/cross-account-reader with only kms:Decrypt and kms:DescribeKey, plus a kms:ViaService or encryption-context condition.
  • In the consumer account (444455556666), attach an IAM policy to that role allowing kms:Decrypt on the specific key ARN — not Resource: *.
  • For S3 cross-account reads, remember the request reaches KMS via the S3 service principal context; verify the kms:ViaService value matches the bucket's region, and that bucket policy plus key policy agree on the same external principal.
  • Never use Resource: * in the consumer's IAM policy for KMS unless you intend that role to use every key it is ever granted on; scope to explicit key ARNs so a future cross-account grant does not silently widen reach.

The danger of broad kms:* and detecting risky grants

A single kms:* on a key or in IAM collapses the administrator/user separation entirely: the same principal can read plaintext (Decrypt), rewrite the policy (PutKeyPolicy), and schedule the key for deletion. That last one is a denial-of-data risk — anyone with ScheduleKeyDeletion can render every object encrypted under the key permanently unrecoverable after the waiting period. Always enumerate actions; reserve deletion and policy-write actions for a small admin role; and consider an explicit Deny on kms:ScheduleKeyDeletion for everyone except a break-glass identity. For grants, audit continuously: list grants on every key with kms:ListGrants, flag any whose GranteePrincipal is outside your expected service principals or accounts, watch CloudTrail for CreateGrant events that lack the GrantIsForAWSResource constraint, and revoke orphaned grants left behind by deleted resources. Because grants live outside the key policy, a key that looks locked down in its policy can still be wide open through a stale grant — make grant inventory a first-class part of your KMS review, not an afterthought.

Pull these together and the pattern is consistent: name principals explicitly, split administration from use, constrain every statement with kms:ViaService and encryption context, require both sides of any cross-account trust, and treat grants as a tracked, expiring delegation rather than fire-and-forget. KMS gives you the controls; least privilege is the discipline of actually using all of them.

Learn it by doing

Spin up a real AWS security lab, or explore our training tracks.

24 people viewing now