Spec: signing-kms v0.1¶
Scope¶
A small, focused module that produces exactly two AWS resources plus their policies:
- An asymmetric KMS key (
SIGN_VERIFY, defaultRSA_4096) with a stable alias. - An IAM role with an OIDC trust policy scoped to caller-supplied
StringLikesubpatterns, no attached IAM policy.
Plus the key policy that grants exactly four principal classes (account root, administrators, automation, signer) the minimum permissions each needs.
The consumer of v0.1 is phpboyscout/infra src/main.signing-kms.tf,
which feeds the role + key into phpboyscout/go-tool-base's Phase 2
signed-update pipeline.
Decisions¶
D1 — Key spec defaults to RSA_4096¶
AWS KMS does not expose Ed25519 for asymmetric SIGN_VERIFY keys.
RSA-4096 is the strongest RSA option and matches the consuming repo's
prep-doc requirement. ECC variants are accepted via the
key_spec input but the default and recommended choice is RSA-4096
because the consumer's OpenPGP packet encoder is RSA-oriented.
D2 — Signing algorithm chosen at sign time, not key creation¶
AWS KMS keys with key_usage = SIGN_VERIFY and key_spec = RSA_4096
support both RSASSA_PSS_SHA_256 and RSASSA_PKCS1_V1_5_SHA_256 at
sign time. The module does NOT add a kms:SigningAlgorithm condition
to the signer's key-policy statement, so the caller's signing shim can
pick whichever shape its envelope format expects:
- OpenPGP packet encoding traditionally uses PKCS#1 v1.5.
- Modern non-OpenPGP envelopes prefer PSS.
A future major version MAY add an allowed_signing_algorithms input
that materialises as a StringEquals condition; v0.1 leaves it open
for flexibility.
D3 — Trust policy: caller passes the full sub patterns¶
The module accepts ci_subject_filters as a list of StringLike
patterns matched against <issuer>:sub. Validation enforces that:
- the list is non-empty (an empty list would trust every token from the issuer);
- each entry matches GitLab's OIDC
subshapeproject_path:<group/repo>:ref_type:(branch|tag):ref:<pattern>.
The validation is GitLab-specific because (a) GitLab is the only
issuer this module is built to support today, and (b) catching a
malformed pattern at plan time is much cheaper than catching it via a
failed sts:AssumeRoleWithWebIdentity at sign time.
The recommended scope is the tightest one: a single
project_path:<consumer>:ref_type:tag:ref:v* pattern. That trusts
exactly tag-pipelines whose tag matches the v* glob and rejects MR
+ branch pipelines (their sub contains ref_type:branch).
D4 — Permission classes in the key policy¶
Four statements, four principal classes:
| Principal | Sid | Permitted |
|---|---|---|
| Account root | EnableAccountRoot |
kms:* — break-glass |
var.key_administrator_arns |
KeyAdministrators |
Lifecycle verbs (Create/Describe/Enable/List/Put/Update/Revoke/Disable/Get/Delete + Tag/Untag + Schedule/Cancel deletion). Not Sign, not PutKeyPolicy. |
var.automation_role_arn |
AutomationManageKey |
DescribeKey, GetKeyPolicy, GetKeyRotationStatus, ListResourceTags, Tag/Untag, UpdateKeyDescription, PutKeyPolicy. Manages the key as a Terraform resource. Not Sign, not GetPublicKey. |
| Signer role | SignerSign |
Sign, GetPublicKey, DescribeKey. Nothing else. |
The administrator class does NOT include PutKeyPolicy because policy
changes flow through Terraform — the automation role applies them. An
operator with the InfraAdmin role can change the policy by editing the
spec/module and going through the apply pipeline; direct console
edits are not part of the supported lifecycle.
D5 — No attached IAM policy on the signer role¶
The role has no managed policy attached and no inline policy. The key policy is sufficient for same-account access and is the single source of permission grants. This:
- removes the audit risk of role-policy / key-policy drift;
- makes the role's effective permissions trivially auditable from a single document;
- prevents an attacker with
iam:PutRolePolicyon the signer role (improbable but possible if IAM Access Analyzer findings are ignored) from quietly broadening access — the key policy still has the final say.
D6 — Asymmetric enable_key_rotation = false¶
AWS KMS does not support managed rotation for asymmetric keys —
attempts return UnsupportedOperationException. Rotation is handled
out-of-band: mint a v2 instance of this module
(name = "<consumer>-signing-v2"), embed both v1 and v2 public keys
in the consumer's trust set during the support window, sign with both
(GoReleaser supports multiple signs: entries), then delete the v1
instance once the support window closes.
The key_spec and name inputs are intentionally immutable in
practice (any change forces destroy + recreate; never apply that on
a live signing key).
D7 — deletion_window_in_days = 30 default¶
The KMS-imposed maximum. For a signing key, the longest recovery window is the safest default — accidental schedule-deletion gives 30 days of headroom to cancel before the key is unrecoverable. Callers can shrink it for testing scenarios but the default favours safety.
D8 — Tagging: Component = "signing-kms" + caller var.tags¶
Every taggable resource carries merge(var.tags, { Component =
"signing-kms" }). Cross-cutting tags (Project, Environment,
ManagedBy, Repository) come from the consuming stack's provider
default_tags block and must not be repeated here. This matches the
terraform-aws-bootstrap and terraform-aws-security-baseline
convention.
Public interface¶
Inputs¶
name, description, key_spec, deletion_window_in_days,
oidc_provider_arn, oidc_audience, oidc_issuer_host,
ci_subject_filters, key_administrator_arns, automation_role_arn,
tags. Every input has a description and a type; sensitive inputs
would be marked but none here are sensitive. Validations on name,
key_spec, deletion_window_in_days, oidc_provider_arn,
oidc_issuer_host, ci_subject_filters, key_administrator_arns,
automation_role_arn keep malformed input from reaching the apply
phase.
Outputs¶
key_id, key_arn, key_alias_name, key_alias_arn,
signer_role_arn, signer_role_name. None are sensitive — the key
ARN and role ARN are not secrets (the secret is the assume-role
condition that scopes who can use them).
Out of scope¶
- The OIDC IDP itself —
terraform-aws-bootstrapowns it. - The WKD endpoint — Cloudflare Pages, administered independently from AWS.
- Key rotation orchestration — out-of-band, by minting v2.
- The signing shim script — consumer-owned (
scripts/sign-release.shin the consumer's repo). - Sigstore / Rekor integration — deferred to a future Phase per the consumer's spec.
Open questions¶
| # | Question | Default if not answered |
|---|---|---|
| 1 | Should the module accept an allowed_signing_algorithms input that adds a kms:SigningAlgorithm condition to the signer statement? |
Defer to v0.2 — keeping v0.1 minimal until a consumer needs the narrowing. |
| 2 | Should the role's max_session_duration be configurable? |
Defer to v0.2 — 1h (3600) covers every release-job duration we have evidence for. |
| 3 | Should the module also create a corresponding aws_iam_role_policy for parity with conventional patterns, even though the key policy makes it redundant? |
No — see D5. The redundancy is an explicit anti-goal. |
| 4 | Should the module emit a CloudWatchLogs log group for KMS use? |
No — CloudTrail data events on the key are configured at the trail level (separate concern, in terraform-aws-security-baseline). |