Skip to content

refactor(aws): drop the IAM access key fallback from the three WIF backplanes - #287

Closed
JohannesRudolph wants to merge 7 commits into
claude/beautiful-hypatia-883fcafrom
claude/aws-backplane-wif-only
Closed

refactor(aws): drop the IAM access key fallback from the three WIF backplanes#287
JohannesRudolph wants to merge 7 commits into
claude/beautiful-hypatia-883fcafrom
claude/aws-backplane-wif-only

Conversation

@JohannesRudolph

@JohannesRudolph JohannesRudolph commented Aug 24, 2026

Copy link
Copy Markdown
Member

Stacked on #286 — review that one first; this PR's diff is only the three commits above it.

#286 added the AWS Backplane scorecard category and reported three modules as newly flagged without fixing them. This PR fixes them, one commit per module, by dropping the credential path .agents/references/aws-backplane.md already forbids.

Module AWS Backplane Overall
aws/s3_bucket 🔴 43% → 🟢 100% 80% → 93%
aws/route53-dns-record 🟡 71% → 🟢 100% 87% → 93%
aws/route53-dns-alias-record 🟡 71% → 🟢 100% 87% → 93%

All six AWS backplanes are now 🟢 100% on the category.

The shared OIDC provider

Two commits on top of the conversion externalize the OIDC provider, which is the larger change here.

AWS registers one OIDC provider per issuer URL per account, so the meshStack runner's issuer has room for exactly one no matter how many backplanes federate through it. A backplane that creates its own is claiming shared infrastructure: the second backplane in the account fails with EntityAlreadyExists, and destroying whichever owns it breaks every other one. That is unlike Azure (a federated credential is a child of its UAMI) and GCP (a pool is a named per-project resource), where every module can own its own and none can collide.

So modules/aws/oidc-provider now owns it — zero inputs, reading issuer, audience and thumbprint from data.meshstack_integrations — applied once per AWS account. Every WIF backplane takes oidc_provider_arn and creates nothing, losing the provider resource, the mirroring data lookup, the create_oidc_provider toggle and the try() local that picked between them.

Migration is a forget plus an import, not a destroy. Each backplane ships removed { … lifecycle { destroy = false } }, so an account that already applied one drops the provider from that state without deleting it out from under its siblings. For meshcloud-prod that is one forget (dns owns it today) and one import; dns-alias already passed create_oidc_provider = false.

The scorecard follows: aws_wif_oidc_provider (asserted the backplane has a provider) is inverted into aws_wif_external_oidc_provider, aws_wif_create_oidc_provider is deleted, and aws_oidc_provider_notice lints the fixed two-line description on both oidc_provider_arn and the integration's aws_oidc_provider_arn. That notice is the only signpost a first-time platform engineer gets — AWS has no plural OIDC-provider data source, so a missing provider cannot be turned into a friendly precondition, and terraform-docs renders backplane variables but not integration comments.

All six AWS backplanes are 🟢 100% on the category.

The change

Each backplane offered two credential paths: workload identity federation, or an IAM user with an aws_iam_access_key when workload_identity_federation was left null. Nothing was ever on the null branch — every integration builds a non-null workload_identity_federation, and internal-cloudfoundation's meshcloud-prod dns / dns-alias deployments are federated. So the null branch was dead code carrying a count on four to six resources, a policy name that branched on the same condition, and a credentials output that published the literal string "N/A; workload identity federation in use".

workload_identity_federation becomes nullable = false, the count guards and the credentials output go, and each policy keeps its federated-path name (…FederatedPolicy-*) so no live IAM policy is renamed.

s3_bucket needed two things the route53 modules already had: a create_oidc_provider toggle (AWS allows one OIDC provider per issuer URL per account, so a second backplane in the same account was failing its apply with EntityAlreadyExists), and its role ARN output renamed from workload_identity_federation_role_arn to the conventional workload_identity_federation_role, with the BBD input updated to match.

moved blocks — the one thing the reference does not cover

Unlike the GCP equivalent (7c825b9), the federated [0] addresses here exist in production state. Removing the count without moved blocks would destroy and recreate a live IAM role the moment meshsre bumps the hub ref. Each module therefore carries:

moved {
  from = aws_iam_role.assume_federated_role[0]
  to   = aws_iam_role.assume_federated_role
}

.agents/references/aws-backplane.md says nothing about migrating off the fallback shape — that is a genuine gap this exercise surfaced. Each backplane/README.md now documents the migration; whether the reference should carry a general rule is worth a follow-up.

Migration for anyone still on the key path

internal-cloudfoundation's meshcloud-dev s3_bucket deployment (pinned to an old SHA) is the only known one. It must pass workload_identity_federation; the next apply destroys its IAM user and revokes the key. That is the intended migration, same as the GCP key-path removal.

Not changed

s3_bucket's integration still takes a hand-supplied variable "workload_identity" instead of reading data.meshstack_integrations the way the route53 modules and the reference's wiring example do. That is an input-contract change for its consumers and no scorecard check covers it — left for whoever is next in the module.

Verification — and what could not be verified

Green:

  • Scorecard — all six AWS backplanes 100% on the category.
  • tofu validate — all three backplane/ modules.
  • tofu validate on each integration root wired against its local backplane (module source temporarily repointed to ./backplane), which is what actually proves the s3_bucket output rename and the new create_oidc_provider pass-through are wired correctly.
  • Full pre-commit suite (terraform_docs, terraform_fmt, trailing-whitespace, validate_modules.sh) — this is exactly what CI runs.

A fourth commit adds modules/aws/s3_bucket/e2e — the first e2e test for any AWS hub module. It provisions an ephemeral backplane, builds the BBD from this branch, orders a workspace-level building block and asserts on its outputs, the bucket ARN in particular (that is what proves the bucket was created by the federated backplane role rather than the name being echoed back). It validates with its whole module chain resolved — ../ → the integration → the backplane fetched from GitHub at this branch — which is what proves the renamed output and the new create_oidc_provider line up end to end.

Still not verified against a live AWS account. The test is discovered by CI but cannot pass yet: the smoke-test harness has no AWS fixture — no account, no credentials — so there is nothing for it to run against. A fixture PR is up for review separately; until it is applied, this remains:

Not verified — no e2e test was run, and none could be:

  • No AWS module has an e2e/ directory. All seven sit at 🔴 33% Testing.
  • The smoke-test repo has no AWS fixture at all — no AWS account, no credentials, not a single .tf or workflow file mentioning AWS. Hub-e2e (build-from-source) mode is therefore impossible for AWS today; AWS onboarding is a later wave of the coverage initiative.
  • Foundation mode would need the changed backplane and BBD applied into the real meshcloud-dev/prod AWS accounts first, which is a manual apply, not something this PR can self-verify.
  • The s3_bucket buildingblock's existing tofu test creates a real S3 bucket and needs AWS credentials, which this environment does not have.

So: the conversion is verified as far as static analysis and the repo's own CI go, and unverified against a live AWS account. Worth applying the backplane by hand in meshcloud-dev before merging, or landing AWS smoke-test onboarding first.

🤖 Generated with Claude Code

JohannesRudolph and others added 3 commits August 24, 2026 21:49
The backplane offered two credential paths: workload identity federation, or an IAM user with an
`aws_iam_access_key` when `workload_identity_federation` was left null. Supporting both cost a
`count` on six resources, a policy name that branched on the same condition, and a `credentials`
output that published the string "N/A; workload identity federation in use" on the federated path.

`.agents/references/aws-backplane.md` already forbids this: "Long-lived IAM access keys for
single-account building blocks — use WIF (Pattern A) instead". The new `aws_wif_no_access_key` and
`aws_wif_nonnullable` scorecard checks now enforce it.

Nothing was on the null branch. The integration always passes a non-null
`workload_identity_federation` built from `data.meshstack_integrations`, so the IAM user, its policy
attachment and its access key were dead code — and `internal-cloudfoundation`'s meshcloud-prod
deployment is on the federated path.

`workload_identity_federation` becomes `nullable = false`, the `count` guards go, the policy keeps
its federated-path name (`Route53RecordBuildingBlockFederatedPolicy-*`) so no live policy is renamed,
and the `credentials` output is deleted.

`moved` blocks carry `aws_iam_role.assume_federated_role` and its policy attachment across the
removed `count`. Without them a deployment already on the federated path would destroy and recreate
its live IAM role on the next apply — unlike the GCP equivalent, these `[0]` addresses exist in
production state.

Module scores 100% on the AWS Backplane category. `tofu validate` passes on the backplane and on the
integration root wired against it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… WIF only

The backplane offered two credential paths: workload identity federation, or an IAM user with an
`aws_iam_access_key` when `workload_identity_federation` was left null. Supporting both cost a
`count` on six resources, a policy name that branched on the same condition, and a `credentials`
output that published the string "N/A; workload identity federation in use" on the federated path.

`.agents/references/aws-backplane.md` already forbids this: "Long-lived IAM access keys for
single-account building blocks — use WIF (Pattern A) instead". The new `aws_wif_no_access_key` and
`aws_wif_nonnullable` scorecard checks now enforce it.

Nothing was on the null branch. The integration always passes a non-null
`workload_identity_federation` built from `data.meshstack_integrations`, so the IAM user, its policy
attachment and its access key were dead code — and `internal-cloudfoundation`'s meshcloud-prod
deployment is on the federated path.

`workload_identity_federation` becomes `nullable = false`, the `count` guards go, the policy keeps
its federated-path name (`Route53AliasRecordBuildingBlockFederatedPolicy-*`) so no live policy is renamed,
and the `credentials` output is deleted.

`moved` blocks carry `aws_iam_role.assume_federated_role` and its policy attachment across the
removed `count`. Without them a deployment already on the federated path would destroy and recreate
its live IAM role on the next apply — unlike the GCP equivalent, these `[0]` addresses exist in
production state.

Module scores 100% on the AWS Backplane category. `tofu validate` passes on the backplane and on the
integration root wired against it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same conversion as the two route53 backplanes, plus two conventions this module never had.

The backplane offered two credential paths: workload identity federation, or an IAM user with an
`aws_iam_access_key` when `workload_identity_federation` was left null — a `count` on four resources,
a policy name branching on the same condition, and a `credentials` output. The integration always
passes a non-null `workload_identity_federation`, so the key path was dead code.
`.agents/references/aws-backplane.md` forbids it for a single-account building block, and
`aws_wif_no_access_key` / `aws_wif_nonnullable` now enforce that.

Two more scorecard checks were failing here but not on the route53 modules:

- **`create_oidc_provider`** was missing, so the backplane always created its own OIDC provider. AWS
  allows one provider per issuer URL per account, so a second backplane in the same account failed
  its apply with `EntityAlreadyExists`. The variable and the `data` lookup are now present, matching
  the route53 modules, and the integration passes it through.
- **The role ARN output** was named `workload_identity_federation_role_arn`; the convention is
  `workload_identity_federation_role`. Renamed, with the BBD's `AWS_ROLE_ARN` input updated.

`workload_identity_federation` becomes `nullable = false`, the policy keeps its federated-path name
(`S3BuildingBlockFederatedPolicy-*`), and `moved` blocks carry the role and its policy attachment
across the removed `count` so a federated deployment does not recreate its live IAM role.

Not changed: this integration still takes a hand-supplied `variable "workload_identity"` instead of
reading `data.meshstack_integrations` the way the route53 modules and the reference's wiring example
do. That is a separate input-contract change for whoever is next in the module — no scorecard check
covers it.

Module scores 100% on the AWS Backplane category. `tofu validate` passes on the backplane and on the
integration root wired against it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Scorecard Check

Scorecard run on commit 015ff432a4fdbb801d4001a5ae430db63da46ca0 relative to origin/claude/beautiful-hypatia-883fca

Warning: module "aws/oidc-provider" not found — skipping.

📊 meshstack-hub Module Scorecard

Generated: 2026-08-25 | Modules scanned: 3 | Categories: 7

📋 Per-Module Category Summary

Score per category per building block. n/a = category does not apply to this module.

Module Overall Core Structure Integration AWS Backplane Azure Backplane GCP Backplane STACKIT Backplane Testing
aws/route53-dns-alias-record 🟢 93% 🟢 100% 🟢 100% 🟢 100% n/a n/a n/a 🔴 33%
aws/route53-dns-record 🟢 93% 🟢 100% 🟢 100% 🟢 100% n/a n/a n/a 🔴 33%
aws/s3_bucket 🟢 100% 🟢 100% 🟢 100% 🟢 100% n/a n/a n/a 🟢 100%

⚠️ 2 modules have failing checks — failing categories are expanded below.

Core Structure — ✅ all passing

Basic module file structure and documentation — applies to 3 modules

Module Score 📦 🔗 📋 📝 🖼️ 📌 🔒
aws/route53-dns-alias-record 🟢 100%
aws/route53-dns-record 🟢 100%
aws/s3_bucket 🟢 100%

Core Structure — Summary

Emoji Criterion Coverage Status
📦 buildingblock/ directory exists 3/3 🟢 100%
🔗 meshstack_integration.tf present 3/3 🟢 100%
📋 buildingblock/APP_TEAM_README.md present (no-integration fallback) n/a
📝 buildingblock/README.md with YAML front-matter 3/3 🟢 100%
🖼️ buildingblock/logo.png included 3/3 🟢 100%
📌 buildingblock/versions.tf present 3/3 🟢 100%
🔒 Provider versions use minimum constraint (>=) 3/3 🟢 100%
Integration — ✅ all passing

meshstack_integration.tf conventions — applies to 3 modules

Module Score 🏷️ 🏢 📤 🔌 📎 🔀 🌱 📋 🏷️ 🧱 📖 📝 📊 🚫 🔄
aws/route53-dns-alias-record 🟢 100%
aws/route53-dns-record 🟢 100%
aws/s3_bucket 🟢 100%

Integration — Summary

Emoji Criterion Coverage Status
🏷️ variable "hub" in integration 3/3 🟢 100%
🏢 variable "meshstack" in integration 3/3 🟢 100%
📤 building_block_definition output exposed 3/3 🟢 100%
🔌 meshcloud/meshstack in required_providers 3/3 🟢 100%
📎 backplane source uses var.hub.git_ref 3/3 🟢 100%
🔀 ref_name uses var.hub.git_ref 3/3 🟢 100%
🌱 BBD terraform_version >= 1.12.0 3/3 🟢 100%
📋 version_spec.draft uses var.hub.bbd_draft 3/3 🟢 100%
🏷️ BBD metadata.tags forwards var.meshstack.tags 3/3 🟢 100%
🧱 BBD input argument vars with optional() have explicit defaults 3/3 🟢 100%
📖 BBD readme field present 3/3 🟢 100%
📝 BBD readme starts with plain-text description (no heading) 3/3 🟢 100%
📊 BBD readme has shared responsibility table (✅/❌) 3/3 🟢 100%
🚫 No documentation_md output in backplane 3/3 🟢 100%
🔄 meshstack_platform has lifecycle ignore_changes = [availability] n/a
AWS Backplane — ✅ all passing

AWS automation principal conventions (WIF or cross-account StackSet) — applies to 3 modules

Module Score 🔐 📌 🚫 🛂 📤 🌐 🧭 📚 🔑
aws/route53-dns-alias-record 🟢 100%
aws/route53-dns-record 🟢 100%
aws/s3_bucket 🟢 100%

AWS Backplane — Summary

Emoji Criterion Coverage Status
🔐 Takes oidc_provider_arn instead of creating a provider 3/3 🟢 100%
📌 oidc_provider_arn carries the shared-provider notice 3/3 🟢 100%
🚫 No aws_iam_access_key on the federation path 3/3 🟢 100%
workload_identity_federation is non-nullable 3/3 🟢 100%
🛂 Trust policy scopes :sub to the BBD's WIF subjects 3/3 🟢 100%
📤 Outputs workload_identity_federation_role as a constructed ARN 3/3 🟢 100%
🌐 Integration wires AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE 3/3 🟢 100%
🧭 Declares aws.management and aws.backplane aliases n/a
📚 StackSet is SERVICE_MANAGED, auto-deploying, retaining nothing n/a
🔑 Outputs the access key, a sensitive secret, and the target role name n/a
Azure Backplane — not applicable

Azure UAMI-based automation principal conventions — applies to 0 modules

No applicable modules.

GCP Backplane — not applicable

GCP workload-identity-federation automation principal conventions — applies to 0 modules

No applicable modules.

STACKIT Backplane — not applicable

STACKIT WIF-based automation principal conventions — applies to 0 modules

No applicable modules.

Testing — some checks failing

End-to-end test coverage — applies to 3 modules

Module Score ⚙️ 🧪
aws/route53-dns-alias-record 🔴 33%
aws/route53-dns-record 🔴 33%
aws/s3_bucket 🟢 100%

Testing — Summary

Emoji Criterion Coverage Status
⚙️ backplane/ directory (optional tier) 3/3 🟢 100%
🧪 e2e/ test directory exists 1/3 🔴 33%
e2e/ contains .tftest.hcl files 1/3 🔴 33%

📈 Overall Summary

Overall Average Score: 95%

Score Distribution

  • 🟢 High maturity (≥80%): 3 modules
  • 🟡 Medium maturity (50–79%): 0 modules
  • 🔴 Low maturity (<50%): 0 modules

JohannesRudolph and others added 4 commits August 24, 2026 22:09
First e2e test for any AWS hub module. Build-from-source mode provisions an ephemeral backplane in
the smoke-test AWS account, builds the BBD from this branch, orders a workspace-level building
block, and asserts on its outputs — the bucket ARN in particular, since that is what proves the
bucket was created by the federated backplane role rather than the name being echoed back.

Modelled on `gcp/storage-bucket/e2e`, with two AWS-specific details:

- `create_oidc_provider` comes from the fixture and is false. AWS permits one OIDC provider per
  issuer URL per account, so the meshStack issuer's provider is a long-lived fixture in the
  smoke-test account rather than something each backplane creates and destroys — otherwise the
  second AWS case in the parallel matrix fails with EntityAlreadyExists and the first to finish
  deletes the provider the other is still using.
- The integration takes `workload_identity` as a hand-supplied object rather than reading
  `data.meshstack_integrations` itself (unlike the route53 modules), so the e2e module reads the
  data source and derives the subject namespace prefix from the replicator's own subject.

`provider "aws"` pins `allowed_account_ids` to the fixture account, so a wrong local session errors
instead of creating IAM roles in someone else's account.

Requires the AWS fixture in the smoke-test harness, which does not exist yet — the test is
discovered by CI but cannot pass until that lands and is applied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AWS registers one OIDC provider per issuer URL per account, so the meshStack runner's issuer can be
registered only once in an account regardless of how many building block backplanes federate through
it. Today each AWS backplane creates its own and offers a `create_oidc_provider` toggle to opt out,
which makes the first backplane deployed the de-facto owner of shared infrastructure: the second one
fails with EntityAlreadyExists unless someone remembers the toggle, and destroying the owner breaks
every other backplane in the account.

This module is that provider, on its own, applied once per AWS account. It takes no inputs — issuer,
audience and thumbprint come from `data.meshstack_integrations` — so the only thing a platform team
supplies is the two providers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…provider

Replaces the two checks that assumed a backplane owns its OIDC provider:

- `aws_wif_oidc_provider` asserted the backplane *creates or reads* an
  `aws_iam_openid_connect_provider`. Now `aws_wif_external_oidc_provider` asserts the opposite —
  no provider resource, and a required non-nullable `oidc_provider_arn` input instead.
- `aws_wif_create_oidc_provider` required the opt-out toggle. The toggle is gone with the resource,
  so a leftover `create_oidc_provider` variable is now itself a failure.

Plus `aws_oidc_provider_notice`, which lints the fixed two-line description on both
`oidc_provider_arn` (backplane) and `aws_oidc_provider_arn` (integration). That notice is the only
signpost a first-time platform engineer gets: AWS has no plural OIDC-provider data source, so a
missing provider cannot be turned into a friendly precondition, and terraform-docs renders backplane
variables but not integration comments. Linting it keeps the copy-paste honest.

`.agents/references/aws-backplane.md` gains 'The shared OIDC provider' as the canonical section both
checks point at: why an account-level singleton cannot belong to a building block, the one-line
module that owns it, why Azure and GCP genuinely differ, and the `removed`-block migration for an
account whose provider still sits in a backplane's state.

The three WIF modules now fail both checks — the next commit converts them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All three WIF backplanes stop creating an `aws_iam_openid_connect_provider` and take
`oidc_provider_arn` instead, supplied by `modules/aws/oidc-provider` once per AWS account.

What each backplane loses: the provider resource, the `data` lookup that mirrored it, the
`create_oidc_provider` toggle, and the `try()` local that picked between the two. What it gains: one
required non-nullable variable. The trust policy is unchanged — it still scopes `:sub` to this
building block definition's subjects.

`s3_bucket` gains the input rather than trading a toggle for it: the previous commit added
`create_oidc_provider` to bring it level with the route53 modules, and this removes it from all
three. Reviewing that pair as one change is the honest reading — the toggle was never the
destination.

Each backplane carries a `removed` block with `destroy = false`. An account that already applied one
of these owns the provider in that backplane's state; without the block, the next apply would delete
it out from under every other backplane federating through the same issuer. With it, the apply just
forgets it and the provider can be imported into the root that owns
`modules/aws/oidc-provider`. Concretely for `internal-cloudfoundation`'s meshcloud-prod: `dns` owns
the provider today and `dns-alias` was already passing `create_oidc_provider = false`, so the
migration is one forget plus one import, with no downtime for either.

The integrations expose it as `aws_oidc_provider_arn` — flat and provider-prefixed per the repo
convention — carrying the same fixed notice as the backplane variable, which the scorecard's
`aws_oidc_provider_notice` check enforces on both.

`aws/s3_bucket/e2e` reads the ARN from `fixtures.aws.oidc_provider_arn` instead of passing a
create-or-not flag, matching the harness that owns the provider for the whole account.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@JohannesRudolph

Copy link
Copy Markdown
Member Author

Superseded by #293, which squashes both onto current main with the intermediate states dropped — the create_oidc_provider toggle these two added and then removed never appears there. Same final tree, and the AWS content is byte-identical to the commit whose e2e run passed.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant