Skip to content

fix(iac/aws): grant deploy role KMS key-lifecycle for OIDC signing-key replace - #1496

Merged
cristim merged 2 commits into
mainfrom
fix/cicd-kms-key-lifecycle
Jul 23, 2026
Merged

cristim merged 2 commits into
mainfrom
fix/cicd-kms-key-lifecycle

Conversation

@cristim

@cristim cristim commented Jul 22, 2026 •

Copy link
Copy Markdown
Member

Why

The post-merge deploy of #1480 (OIDC ES256 migration) fails:

AccessDenied: User: .../cudly-terraform-deploy/GitHubActions is not authorized
to perform: kms:ScheduleKeyDeletion on .../key/a8e28eac-...

aws_kms_key.signing changed spec RSA_2048 -> ECC_NIST_P256 (#1480). KMS
cannot change a key spec in place, so Terraform must replace the key. The
deploy role has KMS data-plane + alias actions but no key-lifecycle actions
(the signing key was bootstrap-created), so the replace is denied.

Change

Adds tag-gated KMS key-lifecycle grants to cudly-deploy-compute-b:

  • KMSKeyLifecycleTaggedOnly (ResourceTag Project=CUDly): ScheduleKeyDeletion, CancelKeyDeletion, PutKeyPolicy, EnableKeyRotation/DisableKeyRotation, GetKeyRotationStatus, ListResourceTags. Destructive deletion is scoped so the deploy SA can never schedule deletion of an unrelated account CMK.
  • KMSTagResourceRequestGated (RequestTag Project=CUDly): TagResource/UntagResource (a new key is untagged at create, so gate on the request tag).
  • KMSCreateKey (Resource="*"): CreateKey cannot be resource/tag-scoped; benign (creating a CMK grants no data access; deletion stays tag-gated).

terraform fmt + validate clean.

Operational notes

Summary by CodeRabbit

  • New Features
    • Improved deployment support for managing the OIDC signing key during key replacement.
    • Added safeguards to ensure key lifecycle operations and tagging apply only to appropriately labeled encryption keys.
    • Enabled creation and rotation-related management of deployment encryption keys.

…y replace

The cudly-terraform-deploy GitHub Actions role holds KMS data-plane and alias
actions but no key-lifecycle actions, so the OIDC issuer signing key was
bootstrap-created. PR #1480 migrated aws_kms_key.signing from RSA_2048 to
ECC_NIST_P256 (ES256); KMS cannot change a key spec in place, so Terraform must
replace the key. The deploy then failed on:

  AccessDenied: not authorized to perform kms:ScheduleKeyDeletion on
  arn:aws:kms:us-east-1:...:key/a8e28eac-...

Add a tag-gated key-lifecycle grant so the replace can complete:
- ScheduleKeyDeletion + management reads/mutates gated to Project=CUDly keys
  (deploy SA can never schedule deletion of an unrelated account CMK).
- TagResource/UntagResource gated on aws:RequestTag/Project=CUDly (a new key is
  untagged at create, so the request tag is used).
- CreateKey on "*" (cannot be resource/tag-scoped; benign - creating a CMK
  grants no data access; deletion stays tag-gated).

Requires the ci-cd-permissions bootstrap apply to take effect before the runtime
deploy can complete.
@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/now Drop other things impact/internal Team-internal only effort/xs Trivial / one-liner type/security Security finding labels Jul 22, 2026
@cristim

cristim commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 19 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: de074cb1-3328-4c92-b6c4-debc3838f474

📥 Commits

Reviewing files that changed from the base of the PR and between 58fcc51 and c96e856.

📒 Files selected for processing (1)
  • terraform/environments/aws/ci-cd-permissions/policy_compute.tf
📝 Walkthrough

Walkthrough

The compute_b managed policy adds three KMS permission statements for tagged key lifecycle operations, request-tag-gated tagging, and KMS key creation.

Changes

KMS permissions

Layer / File(s) Summary
KMS lifecycle and creation statements
terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf
Adds tag-gated lifecycle and rotation permissions, request-tag-gated tagging permissions, and unscoped kms:CreateKey access.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding KMS key-lifecycle permissions to the deploy role for OIDC signing-key replacement.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cicd-kms-key-lifecycle

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf`:
- Around line 116-134: Update the KMSTagResourceRequestGated statement to
restrict tagging to CMKs owned by the deployment scope, rather than allowing
Resource = "*" based only on aws:RequestTag/Project. Add an appropriate
resource-side condition or explicit deny that prevents adopting foreign keys
while preserving tagging of newly created, untagged CMKs; do not rely on
aws:TagKeys alone.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 089afc7a-f2b9-4f78-8ae7-7a6d8eb70a12

📥 Commits

Reviewing files that changed from the base of the PR and between 59db0f1 and 58fcc51.

📒 Files selected for processing (1)
  • terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf

Comment thread terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf Outdated
@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 1 file(s) based on 1 unresolved review comment.

Files modified:

  • terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf

Commit: 302c34a1580652642f2a20e8ca04a544987a2e49

The changes have been pushed to the fix/cicd-kms-key-lifecycle branch.

Time taken: 4m 30s

…rants

The prior fix added kms:ScheduleKeyDeletion and other lifecycle actions
gated on aws:ResourceTag/Project=CUDly in policy_compute_b.tf, but those
actions were already granted under an identical condition by
KMSMutateTaggedOnly in policy_compute.tf (IAM unions statements, so the
new grants were no-ops). The real blocker is a tag-value case mismatch:
the signing key's Project tag comes from var.project_name, which
defaults to lowercase "cudly", overriding the provider's default_tags
value of "CUDly" for that resource. StringEquals is case-sensitive, so
the existing tag-gated grant never matched the signing key.

Switch KMSMutateTaggedOnly's condition to StringEqualsIgnoreCase so it
matches regardless of tag casing, add the one genuinely new action
(kms:CancelKeyDeletion) to that statement, and remove the duplicate
KMSKeyLifecycleTaggedOnly/KMSTagResourceRequestGated/KMSCreateKey
statements from policy_compute_b.tf (their actions were already granted
elsewhere; the RequestTag-gated UntagResource was additionally a dead
grant since UntagResource evaluates aws:TagKeys, not aws:RequestTag).
@cristim
cristim force-pushed the fix/cicd-kms-key-lifecycle branch from 302c34a to c96e856 Compare July 23, 2026 14:08
@cristim

cristim commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

Adversarial review found this PR's original fix does not address the reported failure. Filing the finding and the corrected fix here.

Why the original fix is a no-op

The new statements added in policy_compute_b.tf (KMSKeyLifecycleTaggedOnly, KMSTagResourceRequestGated, KMSCreateKey) re-grant actions that already exist under an identical condition on the same cudly-terraform-deploy role:

  • KMSMutateTaggedOnly in policy_compute.tf already has ScheduleKeyDeletion, PutKeyPolicy, TagResource, UntagResource, DisableKey, EnableKey, all gated StringEquals aws:ResourceTag/Project = CUDly.
  • KMSCreateAndRead already has CreateKey on "*".

IAM unions statements across policies attached to the same role, so duplicating an existing grant under the same condition changes nothing. It cannot fix the reported AccessDenied: kms:ScheduleKeyDeletion.

Real cause: tag-value case mismatch

The signing key's Project tag is "cudly" (lowercase), not "CUDly":

  • local.common_tags.Project = var.project_name (terraform/environments/aws/main.tf), and var.project_name defaults to "cudly" (terraform/environments/aws/variables.tf).
  • modules/compute/aws/lambda/signing-key.tf sets tags = merge(var.tags, {...}) on aws_kms_key.signing, i.e. a resource-level tag, which overrides the provider's default_tags value of "CUDly" (uppercase) for that same key.

Every KMS condition in this policy uses StringEquals against the literal "CUDly", which is case-sensitive. So the tag gate never matches the signing key, and every action under it (including ScheduleKeyDeletion) is denied regardless of how many duplicate statements are added.

Fix applied

  • Changed KMSMutateTaggedOnly's condition from StringEquals to StringEqualsIgnoreCase on aws:ResourceTag/Project, so it matches the signing key regardless of tag casing. Added a code comment documenting the cudly/CUDly provider-default-vs-resource-tag override that caused this.
  • Added the one genuinely net-new action, kms:CancelKeyDeletion, to that same statement (lets the deploy role reverse an interrupted key-replace without unlocking deletion of unrelated CMKs).
  • Removed the duplicate statements from policy_compute_b.tf entirely, restoring it to its pre-PR contents. Also dropped EnableKeyRotation/DisableKeyRotation (unnecessary for an ECC_NIST_P256 asymmetric key, which doesn't support automatic rotation) and the RequestTag-gated UntagResource grant (dead: UntagResource's IAM condition key is aws:TagKeys, not aws:RequestTag, so that clause could never apply regardless of casing).

Net effect: same set of actions as before this PR, minus the case-sensitivity bug, on a single statement instead of three redundant ones.

Recommend the maintainer verify the actual tag casing post-apply with:

aws kms list-resource-tags --key-id <signing-key-id>

to confirm the live tag is cudly (or whatever casing is present) before assuming this is fully closed.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

for file in \
  terraform/environments/aws/ci-cd-permissions/policy_compute.tf \
  terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf \
  terraform/environments/aws/main.tf \
  terraform/environments/aws/variables.tf \
  modules/compute/aws/lambda/signing-key.tf
do
  printf '\n===== OUTLINE: %s =====\n' "$file"
  ast-grep outline "$file" --items all || true
done

printf '\n===== RELEVANT POLICY/TAG DECLARATIONS =====\n'
rg -n -C 12 \
  -e 'KMSMutateTaggedOnly' \
  -e 'KMSCreateAndRead' \
  -e 'KMSKeyLifecycleTaggedOnly' \
  -e 'KMSTagResourceRequestGated' \
  -e 'KMSCreateKey' \
  -e 'common_tags' \
  -e 'project_name' \
  -e 'aws_kms_key' \
  -e 'tags\s*=' \
  terraform/environments/aws/ci-cd-permissions/policy_compute.tf \
  terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf \
  terraform/environments/aws/main.tf \
  terraform/environments/aws/variables.tf \
  modules/compute/aws/lambda/signing-key.tf

printf '\n===== CURRENT PR DIFF (relevant files) =====\n'
git diff "$(git merge-base HEAD origin/main)"...HEAD -- \
  terraform/environments/aws/ci-cd-permissions/policy_compute.tf \
  terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf \
  terraform/environments/aws/main.tf \
  terraform/environments/aws/variables.tf \
  modules/compute/aws/lambda/signing-key.tf || true

Length of output: 24118


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n===== SIGNING-KEY RESOURCE / MODULE LOCATIONS =====\n'
fd -HI -t f -e tf . | while IFS= read -r file; do
  if rg -n -q -e 'resource\s+"aws_kms_key"' -e 'signing[_-]?key' -e 'kms_key' "$file"; then
    printf '%s\n' "$file"
    rg -n -C 10 \
      -e 'resource\s+"aws_kms_key"' \
      -e 'signing[_-]?key' \
      -e 'kms_key' \
      -e 'tags\s*=' \
      -e 'policy\s*=' \
      "$file" || true
  fi
done

printf '\n===== MODULE TAG WIRING =====\n'
rg -n -C 8 \
  -e 'module\s+"compute' \
  -e 'tags\s*=\s*local\.common_tags' \
  -e 'tags\s*=\s*var\.tags' \
  terraform/environments/aws modules || true

printf '\n===== COMPLETE CURRENT KMS POLICY BLOCKS =====\n'
sed -n '285,375p' terraform/environments/aws/ci-cd-permissions/policy_compute.tf
sed -n '60,95p' terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf

printf '\n===== PR DIFF =====\n'
base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)"
git diff --unified=40 "$base"...HEAD -- \
  terraform/environments/aws/ci-cd-permissions/policy_compute.tf \
  terraform/environments/aws/ci-cd-permissions/policy_compute_b.tf

printf '\n===== KMS POLICY ACTION REFERENCES =====\n'
rg -n -C 4 \
  -e 'GetKeyPolicy' \
  -e 'PutKeyPolicy' \
  -e 'ScheduleKeyDeletion' \
  -e 'CancelKeyDeletion' \
  terraform modules || true

Length of output: 74002


@cristim Verified. The correction addresses the reported failure:

  • aws_kms_key.signing receives Project = "cudly" through local.common_tags, which overrides the provider default tag value.
  • KMSMutateTaggedOnly now uses StringEqualsIgnoreCase, so kms:ScheduleKeyDeletion and the other mutation actions match that key.
  • kms:CancelKeyDeletion is genuinely net-new.
  • The duplicate lifecycle/tag/create grants were removed from policy_compute_b.tf.

No new blocking finding on the corrected deletion path. One non-blocking consistency note: KMSReadTaggedOnly still uses case-sensitive StringEquals, so kms:GetKeyPolicy remains unavailable for this lowercase-tagged key. The current signing-key resource does not manage a policy, but that condition should be normalized too if Terraform later needs key-policy reads.

Live AWS verification remains appropriate after the bootstrap apply:

aws kms list-resource-tags --key-id <signing-key-id>

✏️ Learnings added
Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/1496

Timestamp: 2026-07-23T14:09:37.283Z
Learning: For the AWS OIDC signing key in `terraform/modules/compute/aws/lambda/signing-key.tf`, `tags = merge(var.tags, ...)` receives `var.tags` from `terraform/environments/aws/compute.tf` as `local.common_tags`. `local.common_tags.Project` derives from `var.project_name`, whose default is lowercase `"cudly"`, and overrides the AWS provider default tag `Project = "CUDly"` for this resource. KMS IAM resource-tag conditions governing this key must account for that casing or standardize the tag value.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim merged commit fc5460d into main Jul 23, 2026
19 checks passed
@cristim
cristim deleted the fix/cicd-kms-key-lifecycle branch July 27, 2026 11:10
cristim added a commit that referenced this pull request Jul 27, 2026
…rant (#1514)

Follow-up to #1496. That PR fixed KMSMutateTaggedOnly in policy_compute.tf
to use StringEqualsIgnoreCase after discovering the OIDC signing key is
tagged Project=cudly (lowercase, from local.common_tags / var.project_name),
which overrides the provider default_tags value CUDly and never matched a
case-sensitive StringEquals "CUDly".

KMSReadTaggedOnly in policy_compute_b.tf gates the deploy role's only grant
for kms:GetKeyPolicy on the same tag with case-sensitive StringEquals, so it
does not match the signing key either. The AWS provider reads the key policy
on every aws_kms_key refresh, so the deploy role hits AccessDenied on
kms:GetKeyPolicy when refreshing or replacing the signing key, blocking the
RSA to ECC replace that #1496/#1512 exist to support.

Switch KMSReadTaggedOnly to StringEqualsIgnoreCase for consistency with
KMSMutateTaggedOnly. terraform validate passes.
cristim added a commit that referenced this pull request Aug 3, 2026
Second slice of the #1698 audit, covering the compute, container, edge and
networking resources. Same three defect classes as the RDS fix: an action
that is missing, an action gated on a condition that cannot hold, and an
action whose grant can never match the request.

Blocking on the deploy path:

- ec2:CreateLaunchTemplateVersion, ec2:ModifyLaunchTemplate and
  ec2:DeleteLaunchTemplateVersions. The fck-nat launch template sources its
  AMI from a most_recent = true lookup, so every upstream AMI republish takes
  the launch template's update path rather than create. Only
  Create/DeleteLaunchTemplate were granted, so the first AMI rotation after
  the ASG exists would have failed the apply.
- EC2RunInstancesFckNAT used StringEquals on ec2:InstanceType. AWS authorizes
  one RunInstances call against every resource type it touches, and
  ec2:InstanceType is only in the request context for the instance leg, so
  the condition evaluated false on the volume, network-interface,
  security-group, subnet, image and launch-template legs and denied the call
  as a whole. Switched to StringEqualsIfExists, which keeps the t4g.nano
  restriction exactly where the key exists. This is the operator the IAM
  condition-operators reference documents for this call.
- iam:CreateServiceLinkedRole for autoscaling.amazonaws.com,
  elasticloadbalancing.amazonaws.com and ecs.amazonaws.com. Service-linked
  roles are created lazily on first use of a service in an account/region, so
  these are invisible where the role already exists and a hard failure in a
  fresh region. Note ecs.application-autoscaling was already granted and is a
  different principal that does not cover plain ecs.

Latent, fixed while the file is open:

- ec2:DeleteTags, the other half of ec2:CreateTags. The provider removes
  dropped keys before adding new ones, so dropping any key from common_tags
  or default_tags on an existing EC2 resource failed the apply.
- ec2:ReplaceRouteTableAssociation, which the association's update path calls
  instead of Disassociate plus Associate.
- ecr:PutImageScanningConfiguration, whose sibling PutImageTagMutability was
  already granted.
- The elasticloadbalancing Set* family plus ModifyListenerAttributes.
  Changing a load balancer's subnets or security groups, which is what
  bumping az_count does, goes through Set* rather than Modify*.

- CloudFrontMutateTaggedOnly was gated on a case-sensitive StringEquals
  against "CUDly" while the distribution's Project tag resolves to the
  lowercase "cudly" via local.common_tags, so all four of its actions were
  silently denied. This is the same defect #1496 fixed on the KMS statements,
  left unfixed here because enable_cdn is false in every tfvars so nothing
  exercised it. It would also have killed the destroy path, since deleting a
  distribution requires UpdateDistribution to disable it first.

The ECR and ELB statements go in policy_compute_b.tf rather than
policy_compute.tf, which is the file's stated purpose: policy_compute is at
5933 of 6144 characters and would not hold them. Sizes after this change:
compute 5933, compute-b 1290, data 4588, networking 2831.

Refs #1698
cristim added a commit that referenced this pull request Aug 3, 2026
…ited grant gaps (#1699)

* fix(iac/aws): unblock deploy on rds:DescribeDBInstances and close audited grant gaps

terraform plan on main fails with AccessDenied on rds:DescribeDBInstances
against arn:aws:rds:us-east-1:*:db:*, even though the action was already
granted on arn:aws:rds:*:*:db:cudly-* and the instance name matches that
prefix.

The Terraform id of aws_db_instance is the DbiResourceId (db-<opaque>), not
the DB identifier. provider v5.100.0's findDBInstanceByID branches on the id
shape and, for a DbiResourceId, sends DescribeDBInstances with
Filters=[dbi-resource-id] and no DBInstanceIdentifier. RDS authorizes an
identifier-less DescribeDBInstances against the wildcard ARN db:*, which no
name-scoped grant can match. It is unconditional on every read: the
provider's retry with the plain identifier only fires on NotFound, and
AccessDenied is not NotFound, so the plan hard-fails.

Rather than add the one action that 403'd, this audits every resource and
data block under terraform/environments/aws/ and its modules against the
union of the four deploy policies, and fixes the gaps that block the deploy
path. It also names a third failure mode the previous four patches did not
account for: a grant can be present and still unusable because the request
carries no identifier, or because the real ARN uses a different segment or a
server-assigned opaque id.

Fixed here:

- Move rds:DescribeDBInstances and the DB proxy reads to a read-only
  Resource = "*" statement; every mutating RDS action stays ARN-scoped.
  Return rds:DescribeDBSubnetGroups to the scoped statement, since that call
  does carry a name and needs no widening.
- Add rds:ModifyDBSubnetGroup, which is required whenever az_count or the
  private subnet set changes.
- Extend the iam:PassedToService allowlist with ec2.amazonaws.com (fck-nat
  instance profile, on the deploy path in every environment),
  vpc-flow-logs.amazonaws.com (staging and prod) and events.amazonaws.com
  (scheduled ECS tasks). Each was a granted-but-unsatisfiable condition of
  the same shape as the kms:TagResource bug.
- Add iam:UpdateAssumeRolePolicy, iam:UpdateRole and iam:UpdateRoleDescription
  for trust-policy and description drift on cudly-* roles.
- Add secretsmanager:UpdateSecretVersionStage and
  secretsmanager:ListSecretVersionIds for secret versions carrying a stage
  other than a lone AWSCURRENT.
- Drop secretsmanager:ListSecrets, which has no resource type and so could
  never be satisfied inside an ARN-scoped statement. Nothing on the deploy
  path calls it.
- Add IAMDenyModifyDeployRoleAndPolicies. iam:UpdateAssumeRolePolicy on
  role/cudly-* also matches cudly-terraform-deploy, so granting it would let
  the deploy role rewrite its own trust policy. The Deny closes that along
  with two pre-existing doors into the same escalation: AttachRolePolicy /
  PutRolePolicy on the deploy role, and CreatePolicyVersion on the
  cudly-deploy-* policies. Denying only the role side would have looked
  complete without being complete.
- Document, for every Resource = "*", which of the three reasons applies (no
  resource type, identifier-less request, or opaque id) so a reviewer can
  tell a justified wildcard from a lazy one, and mark the known-dead RDS
  proxy ARN scopes in place so nothing new is added to them.

Deliberately not fixed here, enumerated in the issue and tracked separately:
the RDS proxy ARN scopes (proxy:/target-group: are unmatchable but the path
is gated off by enable_rds_proxy = false, and scoping an opaque id needs a
tag condition whose satisfiability has to be verified rather than assumed),
and the permissions for the monitoring module, cleanup-lambda and CloudFront,
none of which are instantiated on any deploy path today.

All four managed policies stay under the 6144-character limit; cudly-deploy-data
goes from 3548 to 4219 characters.

This root is bootstrap-only and is applied manually by a privileged human, so
merging does not by itself unblock deploys.

Closes #1698

* fix(iac/aws): close audited deploy-role gaps in compute and networking

Second slice of the #1698 audit, covering the compute, container, edge and
networking resources. Same three defect classes as the RDS fix: an action
that is missing, an action gated on a condition that cannot hold, and an
action whose grant can never match the request.

Blocking on the deploy path:

- ec2:CreateLaunchTemplateVersion, ec2:ModifyLaunchTemplate and
  ec2:DeleteLaunchTemplateVersions. The fck-nat launch template sources its
  AMI from a most_recent = true lookup, so every upstream AMI republish takes
  the launch template's update path rather than create. Only
  Create/DeleteLaunchTemplate were granted, so the first AMI rotation after
  the ASG exists would have failed the apply.
- EC2RunInstancesFckNAT used StringEquals on ec2:InstanceType. AWS authorizes
  one RunInstances call against every resource type it touches, and
  ec2:InstanceType is only in the request context for the instance leg, so
  the condition evaluated false on the volume, network-interface,
  security-group, subnet, image and launch-template legs and denied the call
  as a whole. Switched to StringEqualsIfExists, which keeps the t4g.nano
  restriction exactly where the key exists. This is the operator the IAM
  condition-operators reference documents for this call.
- iam:CreateServiceLinkedRole for autoscaling.amazonaws.com,
  elasticloadbalancing.amazonaws.com and ecs.amazonaws.com. Service-linked
  roles are created lazily on first use of a service in an account/region, so
  these are invisible where the role already exists and a hard failure in a
  fresh region. Note ecs.application-autoscaling was already granted and is a
  different principal that does not cover plain ecs.

Latent, fixed while the file is open:

- ec2:DeleteTags, the other half of ec2:CreateTags. The provider removes
  dropped keys before adding new ones, so dropping any key from common_tags
  or default_tags on an existing EC2 resource failed the apply.
- ec2:ReplaceRouteTableAssociation, which the association's update path calls
  instead of Disassociate plus Associate.
- ecr:PutImageScanningConfiguration, whose sibling PutImageTagMutability was
  already granted.
- The elasticloadbalancing Set* family plus ModifyListenerAttributes.
  Changing a load balancer's subnets or security groups, which is what
  bumping az_count does, goes through Set* rather than Modify*.

- CloudFrontMutateTaggedOnly was gated on a case-sensitive StringEquals
  against "CUDly" while the distribution's Project tag resolves to the
  lowercase "cudly" via local.common_tags, so all four of its actions were
  silently denied. This is the same defect #1496 fixed on the KMS statements,
  left unfixed here because enable_cdn is false in every tfvars so nothing
  exercised it. It would also have killed the destroy path, since deleting a
  distribution requires UpdateDistribution to disable it first.

The ECR and ELB statements go in policy_compute_b.tf rather than
policy_compute.tf, which is the file's stated purpose: policy_compute is at
5933 of 6144 characters and would not hold them. Sizes after this change:
compute 5933, compute-b 1290, data 4588, networking 2831.

Refs #1698

* fix(iac/aws): move rds:DescribeDBSubnetGroups to the account-wide read statement

rds:DescribeDBSubnetGroups was the only rds:Describe* left in the
ARN-scoped RDSResourceScoped statement, and it is in exactly the same
position as the rds:DescribeDBInstances read that has been failing the
deploy: aws_db_subnet_group.main (terraform/modules/database/aws/main.tf:73)
is created in every environment, and a resource-scoped grant on a Describe
authorizes only the form that targets one named resource, while the
enumerate form carries no identifier and is authorized against the wildcard
ARN.

Worth recording why this is not the simpler claim that ARN-scoped Describes
are always dead, because that claim is false and would have justified the
wrong fix elsewhere. AWS's Service Authorization Reference confirms both
actions DO support resource types (db and subgrp), and the real names DO
match the patterns (cudly-<env>-<hex>-postgres against db:cudly-*,
cudly-<env>-<hex>-db-subnet against subgrp:cudly-*). The scoped grants were
syntactically valid; only the request shape differs. The production 403 on
DescribeDBInstances is the empirical proof that the scoped form did not
suffice.

Also records two facts established during review, so they are not
rediscovered:

- enable_rds_proxy is a literal false at
  terraform/environments/aws/database.tf:28, not a variable, so no tfvars
  can override it. All three proxy resources are count = 0 in every
  environment, which makes the proxy Describes here belt-and-braces rather
  than load-bearing.
- The RDSResourceScoped comment now says no rds:Describe* lives there at
  all, rather than naming only DescribeDBInstances.

Policy size is unchanged at 4588 of 6144 characters: the action moved
between statements rather than being added.

Refs #1698
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/xs Trivial / one-liner impact/internal Team-internal only priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/security Security finding urgency/now Drop other things

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant