Skip to content

chore(iac): derive the CFN issuer host, create the GCP signing key first, read the dev Function URL from state - #1312

Draft
IAmJSD wants to merge 8 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-324
Draft

IAmJSD wants to merge 8 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-324

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes two of the three low IaC findings from the 2026-09-02 audit that #324 groups together (A13c-024 and A13c-017), and makes the parameter change for the third (A13b-014). A13b-014 is not finished here: the AWS-target CloudFormation template it lands in can't be loaded by CloudFormation, on main as well, and that is now tracked in #1489. So this PR is Refs #324 and Refs #1489, not Closes. I re-checked each finding against current main (a14dc8f2) before changing anything.

Finding Still live on main? Fix Which deploy applies it
A13b-014: CFN OIDCIssuerHost parameter Yes. template.yaml still declared it. handler_federation.go and aws-cfn-deploy.sh.tmpl still passed it Parameter removed. The trust policy derives the host from OIDCIssuerURL, and the server stops passing it. Covered by #1489, not closed here: the template still can't be loaded (cfn-lint E0000, same as on main), and the loadable rewrite is #1489 No infra deploy. It ships in the CUDly server binary that serves GET /api/federation/iac, so the AWS Lambda app deploy picks it up. CUDly instances hosted on GCP or Azure only pick it up once the deploy pause (in place since 2026-10-20) lifts. Customer stacks change only when a customer re-downloads the bundle and runs it
A13c-024: GCP OIDC signing key with no create-first Yes. signing-key.tf still had prevent_destroy = false, a hard-coded 86400s and no create_before_destroy create_before_destroy = true. destroy_scheduled_duration becomes an input whose default leaves deployed keys unchanged deploy-gcp.yml (Cloud Run). That workflow is paused since 2026-10-20, so this only takes effect after the pause lifts. Expected plan diff: none (a lifecycle change, and the default equals today's value)
A13c-017: dev Function URL host hand-pasted in github-dev.tfvars Yes. 33pz…lambda-url.us-east-1.on.aws was still there. #16 (the CloudFront cut-over) is parked: the maintainer deferred it and the issue is unclaimed, so the issue's "fix here only if #16 stays parked" condition holds The host is read back from the previous apply's lambda_function_url output and passed as TF_VAR_lambda_deployed_function_url deploy-aws-lambda.yml, which runs on its own when this merges (it touches terraform/environments/aws/** and the workflow). rollback.yml does the same for Lambda rollbacks. Expected CORS diff on dev: none, as long as the URL in state is still the one that was pasted

Root cause and fix

A13b-014: the CloudFormation issuer host (covered by #1489)

Status: the change below is kept, but it does not close A13b-014. cfn-lint 1.58.0 rejects the template at this head (E0000 Unhashable type "{'Fn::Sub': ['${Host}:aud', ...]}", line 268) and on main (E0000 Unhashable type "{'Fn::Sub': '${OIDCIssuerHost}:aud'}", line 272): the trust policy uses intrinsic functions as map keys, which CloudFormation does not accept. So the aws-target bundle can't create a stack before or after this PR. Rebuilding AssumeRolePolicyDocument as one !Sub JSON string, the stricter AllowedPatterns that needs, and cfn-lint in CI are #1489. A13b-014 is complete once #1489 lands. iac/aws_target_cfn_test.go checks the derivation with the repo's own evaluator, which accepts the mapping keys CloudFormation rejects, so it is not evidence that the template loads.

iac/federation/aws-target/cloudformation/template.yaml took the trust policy's condition-key host as a second free-text parameter. Its own description said the operator had to keep it equal to OIDCIssuerURL minus https://. If the two differ (a trailing slash, or a typo in the tenant ID), IAM never fills in <host>:aud/<host>:sub. The stack still reports success, and every AssumeRoleWithWebIdentity is then denied.

  • The parameter is gone. Both condition keys are now !Sub ["${Host}:aud|sub", {Host: !Select [1, !Split ["https://", !Ref OIDCIssuerURL]]}]. OIDCIssuerURL's existing AllowedPattern (^https://[^/]+(/[^/]+)*$) guarantees exactly one leading https:// and no trailing slash. So element 1 is the whole host and path, the same value as local.oidc_condition_host in the Terraform sibling.
  • CloudFormation rejects a parameter the template doesn't declare. So in the same change I removed OIDCIssuerHost from buildCFParamsJSON, from aws-cfn-deploy.sh.tmpl's --parameter-overrides, and from federationIaCData, where nothing else used it.

A13c-024: the GCP OIDC signing key

terraform/modules/compute/gcp/cloud-run/signing-key.tf had neither create-first ordering nor a working protection.

  • create_before_destroy = true, matching modules/compute/aws/lambda/signing-key.tf (cli#1480). Destroying a Cloud KMS key schedules all of its versions for destruction, which stops AsymmetricSign at once. So a change that forces a new key used to break client-assertion minting immediately. With create-first, one of two things happens:

    • A region move builds the new key ring and key before the old key goes.
    • Any other replacement fails on the key-name collision before the old key is touched, because Cloud KMS never frees a key name.

    Either way the live key keeps signing.

  • destroy_scheduled_duration is now an input (signing_key_destroy_scheduled_duration, passed through from terraform/environments/gcp). It is validated to the 1–120 day range Cloud KMS accepts. I checked the provider source: in google provider v5.45.2 (the version in the lock files) this attribute is ForceNew. A new default would therefore plan a replacement of every deployed key, so the default stays 86400s. A longer window only applies to a project whose key doesn't exist yet. I did not change any tfvars.

  • prevent_destroy cannot be an input. Terraform only accepts a literal for lifecycle meta-arguments. Setting it to true would break cleanup-staging.yml, which runs terraform destroy on staging. The line that restated Terraform's default (prevent_destroy = false) is replaced with a comment explaining this. The deletion_policy = "PREVENT" field that would allow per-environment protection only exists from provider v7. This deviates from the issue's suggested fix, which asked for both values as module inputs.

A13c-017: the dev Function URL CORS origin

The Function URL ID is assigned by AWS. So lambda_allowed_origins in github-dev.tfvars carried a pasted copy that went stale whenever the URL was reissued, with no Terraform diff.

  • I added a new variable, lambda_deployed_function_url, with validation ^https://[a-z0-9]+\.lambda-url\.[a-z0-9-]+\.on\.aws/?$. compute.tf appends its origin (trailing slash removed, de-duplicated) to lambda_allowed_origins for the Lambda module only. It is a separate variable rather than TF_VAR_lambda_allowed_origins as the issue suggested, because a -var-file value overrides a TF_VAR_*. Since github-dev.tfvars keeps its static localhost:3000 entry, the issue's version would have been silently ignored.
  • scripts/read-lambda-function-url.sh reads terraform output -raw lambda_function_url from the already-initialized directory:
    • Prints nothing when the output is missing (first deploy) or null.
    • Removes the trailing slash.
    • Exits 1 on anything that isn't a single Function URL, so a bad value never reaches CORS or $GITHUB_OUTPUT.
  • deploy-aws-lambda.yml runs the script before the plan and passes the value to terraform plan. A new step after the apply emits a ::warning:: when the apply created or reissued the URL: the allowlist only gains the new origin on the next deploy.
  • rollback.yml's Lambda job exports the same variable before its direct terraform apply. Without that, a rollback would drop the origin.
  • github-dev.tfvars now lists only http://localhost:3000. Staging and prod keep their *.invalid placeholders. Once deployed they will also get their own Function URL origin, which is the origin the SPA is served from.

Refs #324: this PR finishes A13c-024 and A13c-017. #324 also lists A13b-014, which needs #1489 to be complete, so #324 should be closed when #1489 lands (or closed by the owner pointing at #1489), not by this merge.
Refs #1489

Verification

All of the evidence below is unit, mock or fixture evidence. Nothing ran against a real cloud account: no plan, apply or deploy, and no real credentials. Go runs used AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true, and Terraform used TF_CLI_CONFIG_FILE=/dev/null.

Fail-before / pass-after. I put main's versions of the implementation files back into the tree, kept the new tests, ran everything below, then restored the fix.

Test On main's implementation On this branch
go test ./iac/ -run 'TestAWSTargetCFN_|TestCFNEval_' FAIL. OIDCIssuerHost is still a parameter, and the condition keys can't be evaluated from OIDCIssuerURL alone ok
go test ./internal/api/ -run TestCFNBundle_ FAIL. The gcp and azure bundles still declare and pass OIDCIssuerHost ok
go test ./terraform/modules/compute/gcp/ -run SigningKey FAIL. No create_before_destroy, a restated prevent_destroy = false, a hard-coded duration ok
go test ./terraform/environments/aws/ -run 'FunctionURL|CORS' FAIL. github-dev.tfvars pins 33pz…lambda-url.us-east-1.on.aws, nothing is wired into the module, deploy or rollback ok
terraform test in terraform/modules/compute/gcp/cloud-run (mocked google) 2 passed, 2 failed, 2 skipped 6 passed
terraform test in terraform/environments/aws (all providers mocked, target module.compute_lambda) 0 passed, 1 failed, 4 skipped 5 passed
bash scripts/test-read-lambda-function-url.sh (stubbed terraform) 0 passed, 10 failed (script missing) 10 passed

What the new tests cover:

  • iac/aws_target_cfn_test.go parses the shipped template with yaml.v3. It evaluates each StringEquals key with a small !Ref/!Select/!Split/!Sub evaluator for a Google and an Azure issuer URL, and compares the result with Terraform's trimsuffix(trimprefix(url, "https://"), "/"). TestCFNEval_Positive is the control, so the test cannot pass by evaluating nothing.
  • TestCFNBundle_OverridesMatchTemplateParameters checks every key in the downloaded bundle's params JSON and deploy script against the template the same bundle ships. Every passed key must be declared, and every parameter without a default must be passed.

Other checks, all on this branch:

  • go build ./..., go vet ./..., go vet -tags integration ./...: clean.
  • go test -count=1 with no integration tag, one package at a time: ./iac/, ./internal/api/, ./internal/iacfiles/, ./scripts/, ./terraform/environments/aws/, ./terraform/environments/gcp/, ./terraform/modules/compute/gcp/, ./terraform/modules/compute/aws/, ./internal/mocks/. All ok.
  • golangci-lint v2.10.1 run --allow-parallel-runners on the touched Go packages: 0 issues. gocyclo -over 10 on internal/api/handler_federation.go: clean (that file only has removals).
  • shellcheck on both new scripts and actionlint on ci.yml, deploy-aws-lambda.yml and rollback.yml: clean.
  • terraform fmt -check -recursive terraform/: clean.
    • terraform/modules/compute/gcp/cloud-run: init -backend=false -lockfile=readonly and validate.
    • terraform/environments/aws and terraform/environments/gcp: init -backend=false and validate. Their lock files were restored afterwards, unchanged.
  • CloudFormation: bash scripts/check-aws-iam-parity.sh (ok) and bash scripts/test-aws-iam-parity.sh (16 passed). cfn-lint 1.58.0 (venv) on iac/federation/aws-target/cloudformation/template.yaml: E0000 at this head and on main, as described under A13b-014 and tracked in fix(iac/federation): AWS-target CloudFormation template can't be loaded (intrinsic functions used as map keys) #1489. bash scripts/test-aws-tfstate-platform-key.sh: 26 passed.

CI wiring added:

  • A "Test GCP compute modules" step (matrix gcp) that runs terraform/modules/compute/gcp/*/tests. For it to work with -lockfile=readonly on Ubuntu runners, cloud-run/.terraform.lock.hcl gains the linux_amd64 h1: hash for the same google 5.45.2 package; without it, readonly init fails the checksum check. That hash was added with terraform providers lock -platform=linux_amd64.
  • A "Run Function URL reader self-tests" step in the aws-tfstate-platform-key job.
  • terraform/environments/aws/tests/ has no CI runner on main yet. The open fix(iac): require PostgreSQL 15+ in every environment's version input #1233 branch adds "Test environment inputs", which runs terraform test in terraform/environments/<cloud> whenever a tests dir exists, and that picks this file up. The Go tests in terraform/environments/aws run under go test ./... in the meantime.

Combined-tree verification (historical). The table below is from an earlier head; the branch has since been merged with main, and the latest PR comment lists the current head and the checks run on it. This branch was tested together with the other queued branches on a local, never-pushed tree: queue/platform @ b56afb2b = origin/main a14dc8f2 plus these heads, merged in this order:

Branch Tested head
fix/issue-204 76f85a4
fix/issue-438 78fe011
fix/issue-838 120b07f
fix/issue-143 aef1256
fix/issue-349 cfbb551
fix/issue-80 63405f9
fix/issue-199 36b9f8f
fix/issue-41 96fc237
fix/issue-299 6af14ed
fix/issue-324 3ab6729

fix/issue-324 and fix/issue-204 both edit .github/workflows/ci.yml next to the azure-kv-access-policy job. The tree kept #324's new "Run Function URL reader self-tests" step and #204's updated job comment; whichever PR merges second takes that same resolution.

Linux, Go 1.26.9, AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers. The full suite ran on ed0c7174, which differs from b56afb2b only by one .gitallowed line on fix/issue-324:

  • Passed: go build ./..., go vet ./..., go vet -tags integration ./... and the full go test -race -short -count=1 ./....
  • Static checks: golangci-lint v2.10.1 found 0 issues on every changed package, with and without --build-tags integration. gocyclo -over 10 is silent on the changed non-test files.
  • Integration tests, every changed package that has them, one package at a time (-tags integration -count=1, 60-minute local timeout): internal/api, internal/purchase, internal/scheduler and internal/server all pass.
  • Secrets: the first run caught a bare placeholder account ID in fix/issue-324's new tftest, now allowlisted with an anchored .gitallowed line. Re-run at b56afb2b: git-secrets, using the repo's scripts/setup-git-secrets.sh patterns in a fresh repo with an isolated HOME, found nothing in the changed files, and a planted fake key was flagged. scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.
  • Nothing was applied to any cloud.

Out of scope / for the owner

Maintainer: please mirror the issue's triage labels on this PR: impact/internal, type/chore, triaged, priority/p3, severity/low, urgency/eventually, effort/s.

🤖 Generated with Claude Code

IAmJSD and others added 2 commits October 23, 2026 08:45
…rst, read the dev Function URL from state

Three low findings from the 2026-09-02 audit.

A13b-014: the aws-target CloudFormation template took the trust policy's
condition-key host as a second parameter, OIDCIssuerHost, that the operator
had to keep equal to OIDCIssuerURL minus https://. The host is now derived
with !Select [1, !Split ["https://", !Ref OIDCIssuerURL]], the same value as
the Terraform module's local.oidc_condition_host, and the server no longer
passes the parameter in the bundle's params JSON or deploy script (CFN would
reject an undeclared parameter).

A13c-024: the Cloud Run module's OIDC signing key now has
create_before_destroy, matching its AWS twin, so a change that forces a new
key cannot destroy the live one first. destroy_scheduled_duration becomes an
input whose default keeps every deployed key unchanged (the provider marks it
ForceNew). prevent_destroy cannot be an input (Terraform requires a literal)
and staging cleanup runs terraform destroy, so the redundant
`prevent_destroy = false` line is replaced by a comment saying so.

A13c-017: github-dev.tfvars no longer pins the dev Lambda Function URL host.
The deploy and rollback workflows read the previous apply's
lambda_function_url output (scripts/read-lambda-function-url.sh) and pass it
as TF_VAR_lambda_deployed_function_url; compute.tf appends its origin to the
CORS allowlist. A post-apply step warns when the apply changed the URL.

Closes LeanerCloud#324

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…S tftest

The combined-tree git-secrets run flagged
terraform/environments/aws/tests/lambda_cors_origins.tftest.hcl:10, a bare
account_id = "123456789012" in the mocked aws_caller_identity. Add an
anchored whole-line .gitallowed entry for that one line (bracketed digits so
the entry does not match itself), placed before the other terraform/ entries
so it does not collide with LeanerCloud#1255's entry for its own tftest.

Refs LeanerCloud#324

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added impact/internal Team-internal only type/chore Maintenance / non-user-visible triaged Item has been triaged priority/p3 Polish / idea / may never ship severity/low Minor harm urgency/eventually No deadline effort/s Hours labels Oct 23, 2026
@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

Bring the branch up to date with main.

Conflicted files:
- .github/workflows/ci.yml: this branch appended a "Run Function URL
  reader self-tests" step to the aws-tfstate-platform-key job; main
  (LeanerCloud#1112) added a new rollback-ecr-registry job right after that job.
  Kept both: the step stays at the end of aws-tfstate-platform-key and
  main's job follows it unchanged.

rollback.yml merged cleanly: main's rewritten "Verify image exists" steps
and this branch's TF_VAR_lambda_deployed_function_url export touch
different steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Merged origin/main (2374b4e) into this branch. New head: cdf6a69. It still merges cleanly with the current main (bfb4625).

Conflict resolution:

  • .github/workflows/ci.yml: this branch appended a "Run Function URL reader self-tests" step to the aws-tfstate-platform-key job. Main (fix(ci): verify the rollback image in AWS_ACCOUNT_ID's registry #1112) added a new rollback-ecr-registry job right after that job. I kept both: the step stays at the end of aws-tfstate-platform-key, and main's job follows unchanged.

rollback.yml merged without conflicts. Main's rewritten "Verify image exists" steps and this branch's TF_VAR_lambda_deployed_function_url export are in different steps.

Checks on the merged head:

  • build, vet (also with -tags integration) and e2e vet pass.
  • golangci-lint found 0 issues with and without the integration tag on iac, internal/api, internal/iacfiles, terraform/environments/aws and terraform/modules/compute/gcp, and gocyclo is silent.
  • go test -race -short passes on those packages, and go test -race -tags integration ./internal/api passes.
  • test-read-lambda-function-url.sh passes 10 of 10, test-rollback-ecr-registry.sh passes 52 and fails 0, test-aws-tfstate-platform-key.sh passes 26 and fails 0, and actionlint is clean.
  • The secrets scan is clean and the planted key was flagged. The allowlist test passes.
  • I did not run terraform test again.

@IAmJSD

IAmJSD commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: REQUEST_CHANGES cdf6a6915dcaf936bc3b59660217741b5ea22b0b

Blocking:

  • iac/federation/aws-target/cloudformation/template.yaml:268-275: the A13b-014 fix lands in a template CloudFormation can't load. The body's "possible existing CloudFormation problem" is real: cfn-lint 1.58.0 reports E0000 Unhashable type "{'Fn::Sub': ['${Host}:aud', ...]}" (line 268) at this head, and E0000 Unhashable type "{'Fn::Sub': '${OIDCIssuerHost}:aud'}" (line 272) on main. A YAML complex key (? !Sub [...]) becomes a mapping key, and CloudFormation only accepts string keys. So aws cloudformation deploy of the aws-target bundle fails at template validation, before and after this PR. The failure: an operator downloads the AWS-target CFN bundle and runs deploy-cfn.sh, and the stack is never created. chore(iac): low findings from the 2026-09-02 audit: CFN host param, GCP KMS key, dev CORS #324 is then closed with A13b-014 marked fixed, and iac/aws_target_cfn_test.go passes because its evaluator accepts mapping keys CloudFormation rejects. The test certifies a property the real parser never gets to check. Either fix the shape here or take A13b-014 out:
    • Fix here: build AssumeRolePolicyDocument (a Json-typed property, so a string is accepted) as one !Sub JSON string with Host derived as now. Constrain OIDCAudience, OIDCSubjectClaim and OIDCIssuerURL with AllowedPattern so " and \ can't break out of the JSON. Add a test guard (no non-scalar mapping keys, or cfn-lint in CI).
    • Or take A13b-014 out of this PR and file it as its own issue. The body would then say Refs #324 for that finding.
  • The PR also conflicts with main (GitHub: CONFLICTING). This resolve is mechanical. In .github/workflows/ci.yml, keep both the "Run Function URL reader self-tests" step and main's new init-backend/tfstate-concurrency jobs. In terraform/modules/compute/gcp/cloud-run/.terraform.lock.hcl, keep the union of the h1: hashes under main's ~> 5.45 constraint. The new head needs re-review either way.
    Non-blocking:
  • scripts/read-lambda-function-url.sh:33: 2>/dev/null ... || true treats any terraform output failure, including a transient state read error, as "no URL". In that case the next apply drops the dev Function URL origin. The post-apply "Check CORS covers the Function URL" warning catches it (live != ""), so this is acceptable, but the reason is easy to miss in a warning.
  • signing-key.tf: create_before_destroy on the key implicitly applies to google_kms_key_ring.signing as well. That's the intended collision-first behaviour and worth one line in the comment.
    Attacked: Workflow injection: steps.deployed-url.outputs.url is regex-validated (single line, Function URL shape) before it reaches $GITHUB_OUTPUT, and it reaches run: only through env:. I checked hashicorp/setup-terraform@dfe3c3f8's wrapper source: it writes raw stdout, and its ::error:: line on a failed output is filtered by grep -v '::'. Rollback checks out the default branch, so the script is always present. allow_origins is a set, so reordering distinct(concat(...)) causes no dev diff. Ran: go build ./..., go vet (plain, integration) on the changed packages, go test -race -short -count=1 on iac, internal/api, internal/iacfiles, scripts, terraform/environments/{aws,gcp} and terraform/modules/compute/gcp (all ok), bash scripts/test-read-lambda-function-url.sh (10 passed), terraform test in cloud-run (6 passed) and environments/aws (5 passed, lock file restored, TF_CLI_CONFIG_FILE=/dev/null, no credentials), shellcheck and actionlint on the touched scripts and workflows (clean), and cfn-lint on the template at head and main (both E0000). Fail-before: with compute.tf and github-dev.tfvars at the merge-base, TestTfvarsCarryNoFunctionURLHost and TestLambdaModuleGetsMergedCORSOrigins fail.
    Body-stale:
  • "Possible existing CloudFormation problem, not verified" is now verified by cfn-lint, as above.
  • "Existing customer stacks ... fails with Parameters: [OIDCIssuerHost] do not exist" is moot, because no stack can have been created from this template.
  • The combined-tree table lists fix/issue-324 at 3ab67298, but the head is now cdf6a691.

Reviewer: rE3. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths.

IAmJSD and others added 5 commits October 26, 2026 12:27
Two textual conflicts, both resolved by keeping both sides:
- ci.yml: main added an init-backend.sh self-test step to the same job
  this branch appends the Function URL reader self-tests to, plus a new
  tfstate-concurrency-group job after it. Both steps stay in the job,
  and main's job follows.
- cloud-run/.terraform.lock.hcl: main's "~> 5.45" constraint with the
  union of both sides' h1: hashes for google 5.45.2.

LeanerCloud#1256 (merged on main) added OIDCIssuerHost to the standalone
generator; the next commit drops it again to match this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LeanerCloud#1256 added OIDCIssuerHost to iacData because aws-cfn-deploy.sh.tmpl
passed it at the time. This branch removes the parameter from the
CloudFormation template and the deploy script (the host is derived from
OIDCIssuerURL), so the field is unused again and the drift test that
compares iacData with federationIaCData fails until it goes.

The bundle test now checks that the rendered deploy script passes
OIDCIssuerURL and never OIDCIssuerHost, since CloudFormation rejects an
override for an undeclared parameter. The field walker's control uses
OIDCIssuerURL instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-ups on LeanerCloud#1312. create_before_destroy on the signing key
also applies to the key ring it depends on, and the Function URL reader
treats any terraform output failure as "no URL". Both are intended, but
neither was written down where the code is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LeanerCloud#1223 on main made federationHandler take *testing.T so it can register
AssertExpectations on its mock. This branch's CFN bundle parameter test
still called it with no arguments, so internal/api stopped compiling
after the merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picks up LeanerCloud#1467, which also edits scripts/generate-federation-iac.go, so
the generator tests are re-run on the merged tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD

IAmJSD commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator Author

Pushed 2d2705b (head 2d2705b), answering rE3's review of cdf6a69:

Blocking: A13b-014 lands in a template CloudFormation can't load

Blocking: conflict with main

Non-blocking (2b39b249, comments only)

  • read-lambda-function-url.sh: the comment now says that || true also turns a transient state read error into "no URL", and that the post-apply CORS check then warns.
  • signing-key.tf: the comment now says create_before_destroy also applies to google_kms_key_ring.signing, so a region move creates the new ring first, as intended.

Body-stale: the "possible CloudFormation problem" bullet is replaced by the verified result and #1489. The customer-stacks note is marked moot until #1489. The #153 note now describes #1256 and 277dbb5f. The combined-tree table is marked historical.

Checks on 2d2705b

  • go build ./..., go vet ./..., go vet -tags integration ./... and the e2e vet pass.
  • golangci-lint v2.10.1 found 0 issues on iac, internal/api, internal/iacfiles, scripts, terraform/environments/aws and terraform/modules/compute/gcp, with and without --build-tags integration. gocyclo -over 10 is clean.
  • go test -race -short -count=1 passes on those packages, and go test -race -tags integration -count=1 ./internal/api/ passes.
  • Script tests: test-read-lambda-function-url.sh 10/10, test-aws-tfstate-platform-key.sh 26/26, test-init-backend.sh 24/24, test-tfstate-concurrency-group.sh 41/41.
  • shellcheck, actionlint (ci, deploy-aws-lambda, rollback) and terraform fmt -check -recursive are clean.
  • terraform test in cloud-run gives 6 passed with -lockfile=readonly and the merged lock file. That run was on 2b39b24, and nothing in that module changed after it.
  • The secrets scan is clean and the planted key was flagged. test-git-secrets-allowlist.sh gives 183 PASS, 0 FAIL.
  • Nothing ran against a cloud.

The new head needs a fresh review.

@IAmJSD

IAmJSD commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE 2d2705b2a6e46b6c672229cf7300ed9a8ddf40dd

Blocking:

  • none

Non-blocking:

  • scripts/read-lambda-function-url.sh:36: || true makes a real state-read failure look the same as "no URL yet". On dev, a transient S3/backend error before the plan would make the apply drop the Function URL origin from CORS, which breaks the dev SPA until the next deploy. The post-apply step only warns. terraform output -json plus a key check would tell "output missing or null" apart from "terraform failed", and the script could fail on the second. This fails closed (no origin is widened), so it's not blocking.
  • deploy-aws-lambda.yml, "Check CORS covers the Function URL": if state somehow holds a malformed URL after a successful apply, this step exits 1. The run then reports a failed deploy after the apply has already gone through. Running it with continue-on-error or || echo ::warning:: would keep that state honest.
  • known_issues/13_iac_aws_target.md:23 still lists OIDCIssuerHost among the CFN parameter overrides.
  • The CFN template still can't be loaded, before and after this PR (intrinsic functions as mapping keys). The PR body discloses this and moves it to fix(iac/federation): AWS-target CloudFormation template can't be loaded (intrinsic functions used as map keys) #1489, and iac/aws_target_cfn_test.go's evaluator accepts keys that CloudFormation rejects. The PR uses Refs rather than Closes, which is correct. chore(iac): low findings from the 2026-09-02 audit: CFN host param, GCP KMS key, dev CORS #324 should stay open until fix(iac/federation): AWS-target CloudFormation template can't be loaded (intrinsic functions used as map keys) #1489 lands.
  • A13c-024 departs from the issue's suggested fix: no prevent_destroy input, because Terraform requires a literal there and cleanup-staging.yml runs a destroy. I agree with the reasoning.

Attacked:

  • Workflow injection: the new ${{ }} values only reach env:, and the script validates the URL against an anchored regex before it reaches $GITHUB_OUTPUT (newline rejected).
  • Rollback: rollback.yml checks out the dispatch ref, so the script exists there, and its relative path from terraform/environments/aws resolves. A non-zero exit fails the step under set -e.
  • GCP: create_before_destroy on the key, which by implication also applies to the key ring. The default 86400s equals the old literal, so there's no ForceNew diff.
  • Staging and prod: both are undeployed (.invalid placeholders, enable_cdn=false). Adding their own Function URL origin later is a same-origin addition.
  • CFN bundle consistency: no non-doc reference to OIDCIssuerHost is left in Go, the templates or the scripts.
  • Ran: go build ./.... go vet with and without -tags integration on iac/api/iacfiles/scripts/terraform. go test -race -short -count=1 on iac, internal/api, internal/iacfiles, scripts, terraform/{environments/aws,environments/gcp,modules/compute/gcp,modules/compute/aws}: all ok. bash scripts/test-read-lambda-function-url.sh: 10 passed. Mocked terraform test in cloud-run (6 passed) and in environments/aws with -filter=tests/lambda_cors_origins.tftest.hcl (5 passed). shellcheck, actionlint, terraform fmt -check -recursive, golangci-lint v2.10.1 (0 issues), gocyclo (silent). PR CI is green, including "Validate Terraform (gcp)", which runs the new step.
  • Fail-before (scratch, restored): with main's signing-key.tf, compute.tf and template.yaml back in place, TestGCPSigningKeyCreatesBeforeDestroy, TestLambdaModuleGetsMergedCORSOrigins and TestAWSTargetCFN_{HasNoIssuerHostParameter,ConditionKeysDeriveFromIssuerURL} FAIL, and the CORS tftest gives 0 passed, 1 failed.

Body-stale:

Reviewer: rE6. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths.

This branch has not been deployed

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

Labels

effort/s Hours impact/internal Team-internal only priority/p3 Polish / idea / may never ship severity/low Minor harm triaged Item has been triaged type/chore Maintenance / non-user-visible urgency/eventually No deadline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant