Skip to content

Roles and trust

The module creates two IAM roles and grants each exactly one sensitive action. The split is load-bearing.

The counter-intuitive part

The obvious reading is that the reader role is the sensitive one — it decrypts the mail. That is half right, and the less important half.

Role Sensitive grant What it can do
reader kms:DeriveSharedSecret Read everything ever sent to this identity
certifier kms:Sign on the primary Change which key future mail is encrypted to

Signing with the certification primary can mint a new subkey binding signature, attaching a different encryption key to the same identity. Republish that certificate and every future message is encrypted to a key someone else holds — and the original encryption key is untouched, so nothing in KMS looks wrong.

That is a strictly larger capability than reading today's mail, and it does not belong to whoever is on the reading rota this week. Hence certifier_principal_arns should be a strict subset of reader_principal_arns, or a dedicated break-glass principal.

Reader exposure is retroactive

Worth stating separately because it changes how you think about granting it.

A signing key compromise lets an attacker forge things going forward. An encryption key compromise exposes the entire history — every message ever encrypted to that key, including anything already archived. Rotating afterwards limits future exposure and does nothing about the past.

So reader access is not "can read new mail from today". It is "can read everything, ever". Grant it accordingly, and remove it promptly.

Why both roles can read public keys

Each key policy grants both roles kms:GetPublicKey and kms:DescribeKey, while keeping the sensitive action to one role each.

This costs nothing — a public key is public by definition, and this one is published as part of the certificate — and it buys two things:

  • The certifier needs it. Assembling a certificate means emitting a subkey packet for the encryption key, which is impossible without its public half. An earlier revision of this module granted each role read access only to its own key, which produced a certifier that held kms:Sign, looked correct, and could not perform the one operation it existed for.
  • The reader can verify the published certificate still matches the keys in KMS. That is how a rebind would be caught, and it should not require assuming the higher-privileged role to check.

No OIDC trust, anywhere

The sibling signing module grants its capability to a CI pipeline, because signing a release is something CI legitimately does. Receiving confidential mail is not.

A CI-assumable decrypt role would place the entire message history behind the security of a build runner — and of every dependency that executes on one. The module exposes no oidc_provider_arn input at all. Adding one should mean re-opening the decision, not adding a variable.

MFA and federated sessions

The MFA condition lives in the roles' trust policies, never in a key policy. AWS documents the reason:

This condition key is not present for federated identities or requests made using access keys to sign AWS CLI, AWS API, or AWS SDK requests.

An IAM Identity Center session is a federated identity, so aws:MultiFactorAuthPresent is absent rather than false. An Allow carrying Bool: ... = true can never match it. In a key policy that would lock out an SSO operator holding a perfectly good MFA'd session — at the exact moment they are trying to read something urgent.

Setting reader_require_mfa = false for a federated principal is not a weakening. Identity Center enforces MFA at sign-in; it simply is not expressible as an IAM condition. The control moved upstream, it did not disappear. (AWS suggests passing the authentication method as a session tag instead — which needs an external IdP that supplies one. Identity Center's own directory does not.)

Why the flags are per-role

The intended shape for an SSO estate is asymmetric:

Role Principal Flag
reader Identity Center permission set reader_require_mfa = false
certifier dedicated IAM role certifier_require_mfa = true

Routine decryption arrives through SSO, while the higher-privileged certification path keeps a hard IAM-level MFA gate. A single module-wide flag would have dragged the certifier down to whatever trust model the reader needed — exactly the wrong direction for the more dangerous capability.

No attached identity policies

Neither role has an attached IAM policy. The key policies are sufficient, and a second grant surface would only create the risk of role-policy / key-policy drift. Each role is single-purpose and needs exactly one KMS action, so a separate identity policy would be pure ceremony.

See also