Skip to content

Key policies

Both KMS keys carry a policy that is the single source of permission grants. Neither IAM role has an attached identity policy, so there is exactly one place to read to know who can do what.

Four principal classes

Each policy grants the same four classes, mirroring the sibling signing module:

Class Gets Explicitly does not get
account root kms:*
key_administrator_arns key lifecycle: describe, enable/disable, tag, aliases kms:Sign, kms:DeriveSharedSecret, kms:PutKeyPolicy
automation_role_arn manage the key as a Terraform resource kms:Sign, kms:DeriveSharedSecret
the reader / certifier role exactly one crypto action each the other one

The separation that matters most is the third row. The apply pipeline can create, tag and re-policy these keys, and cannot use them. A full compromise of the runner yields no ability to decrypt a message or rebind the certificate.

Why account root keeps kms:*

This looks like an over-broad grant and is deliberate. It is AWS's recommended break-glass provision, and it is what guarantees the key can never become unmanageable — a key whose policy has locked out every administrator is unrecoverable in a way that no amount of care prevents.

It is scoped to the root principal specifically, rather than the whole account, which is a narrower grant than the AWS default "Enable IAM policies" statement.

Why there is no Deny

A tempting hardening is an explicit Deny with NotPrincipal, fencing kms:DeriveSharedSecret to the reader role so that no future policy edit can widen it.

It was considered and rejected. NotPrincipal combined with Deny on a KMS key policy is a well-known way to brick a key irrecoverably — the statement denies everyone not listed, including principals you did not think about, and the fix requires a principal the policy now denies. The estate convention, and AWS's guidance, is to retain account-root kms:* and keep policies allow-only.

Defence in depth is not worth a key that can lock out its own owner. Especially this key, where the failure mode is an unreadable archive.

Why resources = "*"

Every statement uses resources = ["*"], which security scanners flag. In a key policy that wildcard resolves to the single key the policy is attached to — key policies are resource-based, not identity-based, so there is no "arbitrary resources" reading available.

The module carries checkov:skip annotations with that justification inline rather than suppressing the checks globally, so a reviewer sees the reasoning at the statement rather than in a config file somewhere else.

Administrator deletion rights

kms:ScheduleKeyDeletion is withheld from administrators unless allow_administrator_key_deletion is set — AWS's named control for accidental key deletion is the key policy, and deleting an asymmetric encryption key is the one mistake here that cannot be undone.

kms:DeleteAlias is granted regardless, because aliases are cheap to recreate. kms:CancelKeyDeletion is always granted because cancelling is protective rather than destructive.

[!NOTE] Administrator actions are enumerated explicitly rather than by wildcard, and kms:PutKeyPolicy is not among them — so an administrator cannot rewrite the policy to grant themselves deletion. The sibling signing module's kms:Put* would have permitted exactly that; a wildcard granting the power to rewrite the grant is not a guardrail.

The control is still not absolute: the account root holds kms:* for break-glass, and the automation role can change the policy through a reviewed plan/apply.

A caveat about testing

These policies are reviewed but, at the time of writing, unexercised against real AWS. Nothing in the module's gate can catch a policy that is too narrow: tofu validate checks shape, and checkov and trivy check for grants that are too broad. A role that cannot perform the operation it exists for passes all three.

One such defect was found by reading rather than by testing — see roles and trust. Expect the first real use to be the first genuine exercise of these policies, and treat a permissions error as a likely module bug rather than a KMS limitation.

See also