sec(iac/aws): reject the all-zeros OIDC thumbprint in the aws-target bundles - #1678
sec(iac/aws): reject the all-zeros OIDC thumbprint in the aws-target bundles#1678cristim wants to merge 3 commits into
Conversation
…dFormation `OIDCThumbprint` defaulted to the all-zeros placeholder and was validated only as 40 hexadecimal characters, which that placeholder satisfies. Current AWS behaviour makes that value wrong in both directions. AWS verifies the JWKS endpoint's TLS certificate against its own library of trusted root CAs and consults the configured thumbprint only when that certificate does not chain to one of them, when AWS cannot retrieve it, or when the endpoint requires TLS 1.3. For both issuers this template documents (login.microsoftonline.com and accounts.google.com) the thumbprint is therefore never read at all. On the fallback path the all-zeros string is not the SHA-1 of any certificate, so it matches nothing and every AssumeRoleWithWebIdentity call fails. It does not bypass chain verification -- it nulls out the only verification that path has, while reading like a deliberate configuration. `ThumbprintList` is optional on AWS::IAM::OIDCProvider: when omitted, IAM retrieves and uses the issuer's real top intermediate CA thumbprint, which is what the IAM console does by default. That is now the template's default. - Default `OIDCThumbprint` to empty and omit `ThumbprintList` via `AWS::NoValue` when it is empty, so IAM retrieves the real thumbprint. - Reject the placeholder in two independent layers: an `AllowedPattern` requiring at least one non-zero hex digit, and an unconditional `Rules` assertion naming the literal. Neither a regex simplification nor a submission path that skips one layer can readmit it alone. - Replace the parameter documentation, which described the placeholder as safe for providers "where AWS auto-validates the chain". - Add regression coverage that reads the `AllowedPattern`, `MaxLength` and `Default` back out of the committed template and evaluates them the way CloudFormation does. Both tests fail against the pre-fix template. `ThumbprintList` updates with no interruption, so existing stacks switch between the two forms without replacing the OIDC provider or changing its ARN. Stacks currently holding the placeholder will fail their next update until the parameter is cleared or set to a real thumbprint; that is deliberate, and such a stack cannot be authenticating on the fallback path today in any case. Closes #1615
…haviour The `thumbprint_list` guard rejected the all-zeros placeholder for issuers other than `login.microsoftonline.com` and `accounts.google.com`, justified in the code as "the all-zeros value bypasses the CA-chain check entirely". That justification does not match how AWS handles thumbprints, and the CloudFormation sibling now encodes the opposite rule, so the two would otherwise contradict each other on the same defect. AWS verifies the JWKS endpoint's TLS certificate against its own library of trusted root CAs and falls back to the configured thumbprints only when that certificate does not chain to one of them, when AWS cannot retrieve it, or when the endpoint requires TLS 1.3. So all-zeros bypasses nothing: on the primary path AWS never reads it, and on the fallback path it is not the SHA-1 of any certificate and matches nothing, failing role assumption outright. The issuer allowlist was beside the point in both directions -- the thumbprint is unread for every publicly-trusted issuer, not only those two, and the placeholder breaks any private-CA issuer regardless of which one it is. - Default `thumbprint_list` to `[]` and pass `null` when empty, so IAM retrieves and uses the issuer's real top intermediate CA thumbprint. - Reject the all-zeros entry unconditionally instead of per-issuer, and drop the non-empty-list validation, since empty is now the correct default. - Record the finding, the corrected reasoning and the upgrade path in `known_issues/13_iac_aws_target.md`, and mark the superseded entry. Verified with `terraform validate` plus `terraform plan` over four inputs: the all-zeros list is rejected even for `accounts.google.com` (previously allowed), a short value is rejected, and both an empty list and a real thumbprint are accepted. Refs #1615
…guard test Review follow-up to the two commits above, no behaviour change. The parameter and variable documentation said the thumbprint is "never consulted" for the two documented issuers. AWS also falls back to thumbprint verification when it cannot retrieve the TLS certificate or when the endpoint requires TLS 1.3, and neither of those is issuer-specific, so the value can be read even for a publicly trusted issuer. Reworded to say it is read only when one of the listed fallback conditions applies. Also assert that the all-zeros placeholder appears exactly once in the template, so it can live in the Rules assertion that rejects it and nowhere else; a `Contains` check would have stayed green if it were reintroduced as a default alongside the assertion. Refs #1615
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 41 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
Comment |
Closes #1615.
What I verified about AWS thumbprint handling
Two current AWS docs, quoted verbatim.
Obtain the thumbprint for an OpenID Connect identity provider:
CreateOpenIDConnectProvider, onThumbprintList:AWS::IAM::OIDCProvidersays the same for the CloudFormation property, and lists it as Update requires: No interruption.aws_iam_openid_connect_providerdocumentsthumbprint_listas(Optional).Note the trusted-CA rule is no longer scoped to a named list of IdPs (the older "Auth0, GitHub, GitLab, Google, S3-hosted JWKS" phrasing that both this repo's Terraform module and much of the community folklore are built on). It now applies to any issuer whose JWKS endpoint presents a publicly trusted certificate.
Corrected reachability
There is no fail-open value for this parameter. The thumbprint is a comparison target for a certificate fingerprint. Unlike
OIDCSubjectClaimin #1602, it is not part of an IAM policy document, so there is no${...}policy-variable expansion, no wildcard, and no value that makes AWS accept an arbitrary certificate. Every non-matching value fails closed.Split by path:
AssumeRoleWithWebIdentitycall fails.So the issue's scenario does not hold. An attacker who MITMs or DNS-hijacks the JWKS endpoint still has to present a certificate signed by a CA in AWS's trusted-root library, and that bar does not move with this parameter — it is identical whether the thumbprint is all-zeros, correct, or absent. The Terraform module's stated rationale, "the all-zeros value bypasses the CA-chain check entirely", describes a mechanism that does not exist.
What is actually wrong is narrower, and is a defect in all three of the ways the parameter was specified:
The severity is therefore availability and misleading documentation on a customer-deployed template, not takeover of the commitment-purchasing role. I have not relabelled the issue; that is the maintainer's call.
The fix
Empty is now the default and means omit
ThumbprintListentirely, so IAM retrieves and uses the issuer's real top intermediate CA thumbprint — what the IAM console does by default. An operator with a private-CA issuer can still pin a real thumbprint. The placeholder is rejected.CloudFormation (
iac/federation/aws-target/cloudformation/template.yaml):OIDCThumbprintdefaults to""; aHasThumbprintcondition resolvesThumbprintListtoAWS::NoValuewhen it is empty.AllowedPattern: "^$|^[0-9a-fA-F]{0,39}[1-9a-fA-F][0-9a-fA-F]{0,39}$"plusMaxLength: 40— empty, or up to 40 hex characters of which at least one is non-zero.Rulesassertion comparing against the literal. Rules are evaluated before any resource is created and abort the operation on failure.Terraform (
iac/federation/aws-target/terraform/): brought to the same rule, because the issue names this module as the reference implementation and its guard rests on the rationale corrected above. Leaving the two paths contradicting each other on the same defect is the drift #1547 tracks.thumbprint_listdefaults to[]and is passed asnullwhen empty; the all-zeros entry is rejected unconditionally rather than for non-allowlisted issuers; the "must contain at least one thumbprint" validation is dropped, since empty is now correct.Why the pattern is not a tautology
The #1602 precedent was an
AllowedPatternthat rejected a harmless value (*, compared literally byStringEquals) while admitting the fail-open one (${...}, expanded by IAM). Enumerating the same way here:The guard is an exact-match rejection over a fully enumerated domain, not a heuristic: within "empty or 40 hex characters",
0* 40 is the only all-zero string, and0has no case variants.The one asymmetry worth flagging: the pattern rejects
0* 40 but accepts0* 39 +1, which is equally bogus. That is deliberate — the goal is to make the shipped placeholder unreachable, not to guess which supplied thumbprints are real, which is not decidable from the string.Upgrade path (deliberate break, same call as #1602)
ThumbprintListis Update requires: No interruption, so switching between the two forms never replaces the OIDC provider or changes its ARN, and no re-onboarding is needed.OIDCThumbprintis cleared or set to a real value.aws cloudformation deployreuses previous parameter values for anything not in--parameter-overrides, so it will resubmit0000…and the change set will be rejected with theConstraintDescription. The remedy is one flag:--parameter-overrides "OIDCThumbprint=".This is deliberate. Such a stack is either on the primary path (where the value was never read, so clearing it changes nothing) or on the fallback path (where it cannot be authenticating today at all). Nothing that currently works regresses.
thumbprint_listis Optional+Computed in the AWS provider, so clearing it does not clear a value already stored on an existing provider — Terraform keeps reading back what is there. Existing providers must be corrected out of band withaws iam update-open-id-connect-provider-thumbprint, or replaced. Documented inmain.tfandknown_issues/13_iac_aws_target.md.aws-wif-cf-params.json.tmpl,aws-cfn-deploy.sh.tmplandaws-wif.tfvars.tmplnever pass a thumbprint, so they pick up the new empty default. Unlike sec(iac/aws): require OIDC subject claim in aws-target CloudFormation #1602, this introduces no newly-required parameter.Verification
internal/api/handler_federation_test.go, following the existing convention in that file of reading../../iac/federation/...from disk). TheAllowedPattern,MaxLengthandDefaultare parsed back out of the committed template rather than retyped, then evaluated the way CloudFormation evaluates them (anchored full match and the length cap), over 8 must-accept and 12 must-reject inputs — including the non-zero digit in first, middle and last position, and as both an upper- and lower-case hex letter. A second test asserts the resource wiring and that the placeholder appears exactly once in the template, in theRulesassertion that rejects it and nowhere else.Both tests fail against the pre-fix template (confirmed by checking out
origin/main's copy and re-running) and pass after. Full package green: 2018 tests.terraform validatesucceeds;terraform fmt -checkclean;terraform planrejects["0000…"]even for theaccounts.google.comissuer that the old guard allowed, rejects a short value, and accepts both[]and a real thumbprint.scripts/check-aws-iam-parity.sh: green. It compares IAM action lists across the CFN/TF/CLI onboarding flavors and has no thumbprint or trust-policy dimension, so there was nothing to extend for this change; the missing parity mechanism is sec(iac): onboarding paths drift - shell/CFN/ARM templates lack the guards their Terraform siblings enforce #1547.cfn-lint: cannot lint this template, onmainand on this branch alike. It bails at parse time withE0000 Unhashable type "{'Fn::Sub': '${OIDCIssuerHost}:aud'}"— the trust policy legitimately uses!Subas a mapping key. Pre-existing and out of scope here; the template is instead parsed and asserted on by the Go tests above.pre-commiton every commit, no--no-verify.Out of scope
internal/iacfiles/templates/aws-wif-cli.sh.tmpl:33hardcodes the same placeholder intoaws iam create-open-id-connect-provider. That is the CLI bundle generator tracked by #1640, so it is left alone here and noted on that issue instead of widening this diff.