Skip to content

sec(iac): Lambda Function URL auth hardening -- CORS wildcard rejection + AWS_IAM OAC scaffolding (#390, #424) - #574

Merged
cristim merged 4 commits into
feat/multicloud-web-frontendfrom
sec/lambda-cors-auth-hardening
May 26, 2026
Merged

sec(iac): Lambda Function URL auth hardening -- CORS wildcard rejection + AWS_IAM OAC scaffolding (#390, #424)#574
cristim merged 4 commits into
feat/multicloud-web-frontendfrom
sec/lambda-cors-auth-hardening

Conversation

@cristim

@cristim cristim commented May 20, 2026

Copy link
Copy Markdown
Member

Summary

  • sec(auth): CORS wildcard origin + credentials=true on staging Lambda URL #390 (CORS): Reject wildcard allowed_origins via module validation; replace ["*"] in all envs with explicit origins. The Function URL uses allow_credentials = true -- a wildcard origin reflects any inbound Origin header with credentials allowed, enabling any-origin CSRF.
  • sec(iac): production Lambda Function URL uses authorization_type=NONE — no IAM gate #424 (IAM auth): Switch function_url_auth_type module default from "NONE" to "AWS_IAM". Add aws_cloudfront_origin_access_control (type=lambda, sigv4, always-sign) to the frontend module. Add a standalone aws_lambda_permission at the environment layer (breaks the cycle: Lambda URL is CloudFront's origin; distribution ARN feeds the permission -- they can't both live inside a single module). All three env tfvars keep "NONE" with a TODO(#424) cut-over comment until enable_cdn = true is set and CloudFront is deployed.

The frontend client.ts was already OAC-ready: it uses X-Authorization (preserves the Authorization header for CloudFront SigV4 signing) and addContentHashHeader (x-amz-content-sha256 required for POST/PUT/PATCH/DELETE through OAC).

Files changed

File Change
terraform/modules/compute/aws/lambda/variables.tf Default function_url_auth_type to "AWS_IAM"; add validation; expand description
terraform/modules/frontend/aws/main.tf Add aws_cloudfront_origin_access_control.lambda; wire origin_access_control_id
terraform/modules/frontend/aws/variables.tf Add enable_oac variable
terraform/environments/aws/compute.tf Add standalone aws_lambda_permission.function_url_cloudfront (cycle-free)
terraform/environments/aws/frontend.tf Set enable_oac when platform=lambda + auth_type=AWS_IAM
terraform/environments/aws/github-{dev,staging,prod}.tfvars Add TODO(#424) cut-over comments; no functional change to values

Test plan

  • terraform fmt -check passes on all changed modules and environments (verified pre-commit hook)
  • terraform validate passes (pre-commit hook)
  • terraform plan with allowed_origins = ["*"] fails with the validation error (regression test from sec(auth): CORS wildcard origin + credentials=true on staging Lambda URL #390)
  • terraform plan with enable_cdn = true + lambda_function_url_auth_type = "AWS_IAM" shows aws_cloudfront_origin_access_control.lambda + aws_lambda_permission.function_url_cloudfront being created
  • Manual verification of the IAM auth path requires deploying CloudFront -- tracked in the follow-up issue

Manual verification note

The runtime cut-over (flipping deployed envs to AWS_IAM) requires a follow-up deploy. See the TODO(#424) comments in each tfvar for the 3-step procedure. A follow-up issue will be filed to track this.

Closes

Closes #390
Partially addresses #424 (infrastructure scaffolding complete; runtime cut-over tracked separately)

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@cristim, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 17 minutes and 24 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 43044b2c-0664-4b82-bb79-45f556bc779d

📥 Commits

Reviewing files that changed from the base of the PR and between df64d6d and 601318f.

📒 Files selected for processing (14)
  • terraform/environments/aws/compute.tf
  • terraform/environments/aws/dev.tfvars.example
  • terraform/environments/aws/frontend.tf
  • terraform/environments/aws/github-dev.tfvars
  • terraform/environments/aws/github-prod.tfvars
  • terraform/environments/aws/github-staging.tfvars
  • terraform/environments/aws/variables.tf
  • terraform/modules/compute/aws/lambda/main.tf
  • terraform/modules/compute/aws/lambda/outputs.tf
  • terraform/modules/compute/aws/lambda/variables.tf
  • terraform/modules/frontend/aws/main.tf
  • terraform/modules/frontend/aws/variables.tf
  • terraform/profiles/aws/dev.tfvars.example
  • terraform/profiles/aws/prod.tfvars.example
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sec/lambda-cors-auth-hardening

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

@cristim cristim added priority/p1 Next up; this sprint severity/high Significant harm urgency/this-sprint Within the current sprint impact/all-users Affects every user effort/m Days type/security Security finding triaged Item has been triaged labels May 20, 2026
@cristim

cristim commented May 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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 commented May 20, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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 force-pushed the sec/lambda-cors-auth-hardening branch from ca51ba6 to fc37407 Compare May 20, 2026 23:24
@cristim

cristim commented May 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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 commented May 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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 commented May 22, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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 commented May 25, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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 added 3 commits May 26, 2026 00:21
…424)

Switch the lambda module's function_url_auth_type default from "NONE" to
"AWS_IAM" and add the full infrastructure needed to enforce IAM-gated access
via CloudFront OAC:

- lambda module: new aws_lambda_permission.function_url_cloudfront resource
  grants cloudfront.amazonaws.com lambda:InvokeFunctionUrl scoped to a specific
  distribution ARN (source_arn). Created only when auth_type = "AWS_IAM" and
  cloudfront_distribution_arn is provided; no-op otherwise.
- lambda module: new cloudfront_distribution_arn variable (default "") and
  updated function_url_auth_type description + validation block.
- frontend module: new aws_cloudfront_origin_access_control.lambda resource
  (type=lambda, sigv4, always-sign) created when enable_oac = true. OAC id
  wired into the origin block alongside the existing custom_origin_config.
- frontend module: new enable_oac variable (bool, default false).
- environments/aws/compute.tf: pass cloudfront_distribution_arn to lambda module
  (length() guard prevents plan errors when enable_cdn = false).
- environments/aws/frontend.tf: set enable_oac when platform = lambda + AWS_IAM.
- All three env tfvars: keep lambda_function_url_auth_type = "NONE" (unchanged)
  and add a TODO(#424) comment with the 3-step cut-over instructions.

The deployed environments retain their current "NONE" value until CloudFront is
enabled per the follow-up issue. Flipping to AWS_IAM without a CloudFront OAC
in place would make the Function URL return HTTP 403 on all requests.

The frontend client.ts already uses X-Authorization (preserves the Authorization
header for CloudFront OAC SigV4) and addContentHashHeader (x-amz-content-sha256
required for POST/PUT/PATCH/DELETE through OAC) -- no frontend changes needed.

Verified: terraform fmt -check passes on all touched modules and the environment.
Manual verification of the auth path required after CloudFront is deployed.
…refs #424)

Remove the standalone lambda_function_url_auth_type variable and derive its
value via a local in compute.tf:

  local.lambda_function_url_auth_type = var.enable_cdn ? "AWS_IAM" : "NONE"

The two valid combinations were the only sensible ones anyway:

  enable_cdn = false -> auth_type = "NONE"
    SPA hits the Function URL directly; browsers cannot SigV4-sign, so the
    URL must be unauthenticated at the AWS-IAM layer; auth is enforced at
    the application layer (login session, CSRF, API keys).

  enable_cdn = true  -> auth_type = "AWS_IAM"
    CloudFront fronts the URL with an OAC that SigV4-signs every request
    (aws_cloudfront_origin_access_control.lambda in the frontend module).
    The Function URL rejects anything not signed by CloudFront's OAC.

Tying the two flags together prevents the two failure modes the previous
"two variables operator must keep in sync" design exposed:

  (a) enable_cdn = true with auth_type = "NONE" -> public Lambda URL behind
      a pointless CloudFront (security goal of #424 defeated).
  (b) enable_cdn = false with auth_type = "AWS_IAM" -> direct browser hits
      to Function URL return 403 (no way to sign), site is unreachable.

Updated three usages (compute.tf Lambda module call, the standalone
aws_lambda_permission.function_url_cloudfront resource count, and
frontend.tf enable_oac derivation) to use the local / enable_cdn directly.

Removed the lambda_function_url_auth_type lines from all three env tfvars
(github-dev/staging/prod) plus the two profile example tfvars and the
top-level dev.tfvars.example; replaced the TODO(#424) cut-over comments
with a short explanation of the enable_cdn -> auth_type mapping.

Verified: terraform validate clean, terraform fmt clean.

Refs #424
@cristim
cristim force-pushed the sec/lambda-cors-auth-hardening branch from 6e8d69d to dc971f7 Compare May 25, 2026 22:28
@cristim

cristim commented May 25, 2026

Copy link
Copy Markdown
Member Author

Rebased onto latest feat/multicloud-web-frontend (was conflicting against #684's lambda_timeout = 300 bump).

Conflicts in terraform/environments/aws/github-{dev,prod,staging}.tfvars — base bumped lambda_timeout from 60 to 300s (#684) while this branch's refactor commit also touched the same Lambda block to remove lambda_function_url_auth_type. Resolved by keeping base's lambda_timeout = 300 plus this PR's removal of the now-derived auth_type variable. No semantic change to the security hardening itself.

Verified post-rebase: terraform fmt -check -recursive, terraform validate on terraform/modules/compute/aws/lambda, terraform/modules/frontend/aws, and terraform/environments/aws all clean. github-dev safety confirmed: enable_cdn = false -> auth_type = "NONE" (unchanged), lambda_allowed_origins contains no * so wildcard validation passes.

Every tfvars file (3 github envs + 3 examples) set this to true and no
path ever set it to false, so the toggle was dead code. The Function URL
is always required — CloudFront uses it as origin via OAC when CDN is
enabled, and the browser hits it directly when CDN is disabled. Removing
the conditional also simplifies the aws_lambda_permission.function_url
count expression to just check function_url_auth_type == "NONE".
@cristim

cristim commented May 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

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 f65998a into feat/multicloud-web-frontend May 26, 2026
4 checks passed
@cristim

cristim commented May 26, 2026

Copy link
Copy Markdown
Member Author

Added moved block in commit 98c5f69 to preserve aws_lambda_function_url.main across the count removal. Without it, apply would destroy [0] and create a new bare resource with a different host, breaking CORS / DASHBOARD_URL / frontend bundles. @coderabbitai review

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

@cristim The moved block approach is exactly the right pattern here — without it Terraform would treat the count removal as a destroy-then-create, giving the Function URL a new hostname and cascading into CORS failures, broken DASHBOARD_URL, and stale frontend bundles. Triggering a review to pick up commit 98c5f69a9.

✅ Actions performed

Review triggered.

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 added a commit that referenced this pull request May 27, 2026
…_cdn (mirrors PR #574 AWS pattern)

Replace the unconditional `cloud_run_allow_unauthenticated = false` override
with a derived `local.cloud_run_allow_unauthenticated = !var.enable_cdn` in
`terraform/environments/gcp/compute.tf`. The IAM gate now flips in lock-step
with the LB stack landing, matching the parallel pattern shipped in #574 for
AWS Lambda Function URL auth:

    AWS (#574):  local.lambda_function_url_auth_type = var.enable_cdn ? "AWS_IAM" : "NONE"
    GCP (this):  local.cloud_run_allow_unauthenticated = !var.enable_cdn

Why the conditional rather than always-false:

* enable_cdn = false (today's dev/staging/prod): the SPA / clients hit the
  *.run.app URL directly. Browsers cannot present a Google-signed identity
  token, so the IAM gate must accept allUsers (allow_unauthenticated = true);
  auth is enforced at the application layer (login session, CSRF, OIDC for
  scheduled tasks). The previous unconditional `= false` would have made the
  *.run.app URL return 403 to direct browser hits — exactly the GCP analog of
  PR #574's mis-configuration (b).

* enable_cdn = true (post-LB-cutover): the external HTTPS LB (with Cloud
  Armor) fronts the service and attaches a Google-signed identity to upstream
  calls, so Cloud Run can enforce `roles/run.invoker` on the LB SA only and
  lock allUsers out at the IAM layer (closes #384). The pairing also blocks
  mis-configuration (a) — `enable_cdn = true` with allow_unauthenticated =
  true would put a public *.run.app URL behind a pointless LB.

The AWS analog of CloudFront's OAC + SigV4 signing is the GCP LB SA's identity
token. The two cloud sides now share the same "auth gate is derived from the
fronting-LB toggle" shape.

Changes:

* terraform/environments/gcp/compute.tf: add `locals { cloud_run_allow_
  unauthenticated = !var.enable_cdn }` with a comment block mirroring #574's
  documentation of the two valid combinations + the two mis-configurations it
  prevents. Wire `module.compute_cloud_run.allow_unauthenticated` to the local.
  Update the scheduled-tasks comment block (#159) to point at the new derivation.
* terraform/environments/gcp/variables.tf: drop the operator-facing
  `cloud_run_allow_unauthenticated` variable declaration, replace with a
  comment block that explains the derivation (parallel to #574's treatment of
  `lambda_function_url_auth_type`). Update `cloud_run_ingress` description's
  cross-reference accordingly.
* terraform/environments/gcp/{github-dev,github-staging,github-prod,
  dev.tfvars.example}.tfvars: remove the `cloud_run_allow_unauthenticated`
  line from all 4 tfvars files; add a short comment near the existing
  ingress override noting the derivation.
* terraform/environments/gcp/README.md: update the "Cloud Run ingress"
  table — `allow_unauthenticated` row now describes it as derived from
  `enable_cdn`, not a directly-set variable; clarify that when an env flips
  `enable_cdn = true` the IAM gate auto-closes alongside dropping the
  `cloud_run_ingress` override.
* terraform/modules/compute/gcp/cloud-run/main.tf: refresh the inline
  comment on the scheduled-task http_target — the IAM gate now references
  #384 (the derivation) rather than #78 (the network gate).

The `cloud_run_ingress = "INGRESS_TRAFFIC_ALL"` override in the 3 env tfvars
stays exactly as it was — that's a sibling concern (network door) tracked by
#78 and gated on the same LB cutover, but kept as an explicit override per the
existing comment so the network-vs-IAM doors are managed in lockstep but
visibly separately.

Verified: terraform fmt -check clean on both modules/compute/gcp and
environments/gcp; terraform validate clean on both layers.

Refs #384, #78, #574
cristim added a commit that referenced this pull request May 27, 2026
…override (#586)

* security(iac/gcp): set allow_unauthenticated=false; document ingress override (closes #384, #78)

#384: cloud_run_allow_unauthenticated was incorrectly set to true in all
three environment tfvars, overriding the variable default of false. Cloud
Run's built-in IAM auth is a defence-in-depth layer independent of our
application-level JWT auth; it was unintentionally disabled. Set it to
false in dev, staging, and prod.

#78: cloud_run_ingress is temporarily set to INGRESS_TRAFFIC_ALL in all
environments because the external HTTPS LB + Cloud Armor stack (enabled
by enable_cdn=true) has not yet been provisioned. The variable default is
INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER, which would block *.run.app
traffic until DNS + cert + LB are in place. Update the inline comments to
cross-reference both issues and explain the removal condition.

* security(iac/gcp): derive cloud_run_allow_unauthenticated from enable_cdn (mirrors PR #574 AWS pattern)

Replace the unconditional `cloud_run_allow_unauthenticated = false` override
with a derived `local.cloud_run_allow_unauthenticated = !var.enable_cdn` in
`terraform/environments/gcp/compute.tf`. The IAM gate now flips in lock-step
with the LB stack landing, matching the parallel pattern shipped in #574 for
AWS Lambda Function URL auth:

    AWS (#574):  local.lambda_function_url_auth_type = var.enable_cdn ? "AWS_IAM" : "NONE"
    GCP (this):  local.cloud_run_allow_unauthenticated = !var.enable_cdn

Why the conditional rather than always-false:

* enable_cdn = false (today's dev/staging/prod): the SPA / clients hit the
  *.run.app URL directly. Browsers cannot present a Google-signed identity
  token, so the IAM gate must accept allUsers (allow_unauthenticated = true);
  auth is enforced at the application layer (login session, CSRF, OIDC for
  scheduled tasks). The previous unconditional `= false` would have made the
  *.run.app URL return 403 to direct browser hits — exactly the GCP analog of
  PR #574's mis-configuration (b).

* enable_cdn = true (post-LB-cutover): the external HTTPS LB (with Cloud
  Armor) fronts the service and attaches a Google-signed identity to upstream
  calls, so Cloud Run can enforce `roles/run.invoker` on the LB SA only and
  lock allUsers out at the IAM layer (closes #384). The pairing also blocks
  mis-configuration (a) — `enable_cdn = true` with allow_unauthenticated =
  true would put a public *.run.app URL behind a pointless LB.

The AWS analog of CloudFront's OAC + SigV4 signing is the GCP LB SA's identity
token. The two cloud sides now share the same "auth gate is derived from the
fronting-LB toggle" shape.

Changes:

* terraform/environments/gcp/compute.tf: add `locals { cloud_run_allow_
  unauthenticated = !var.enable_cdn }` with a comment block mirroring #574's
  documentation of the two valid combinations + the two mis-configurations it
  prevents. Wire `module.compute_cloud_run.allow_unauthenticated` to the local.
  Update the scheduled-tasks comment block (#159) to point at the new derivation.
* terraform/environments/gcp/variables.tf: drop the operator-facing
  `cloud_run_allow_unauthenticated` variable declaration, replace with a
  comment block that explains the derivation (parallel to #574's treatment of
  `lambda_function_url_auth_type`). Update `cloud_run_ingress` description's
  cross-reference accordingly.
* terraform/environments/gcp/{github-dev,github-staging,github-prod,
  dev.tfvars.example}.tfvars: remove the `cloud_run_allow_unauthenticated`
  line from all 4 tfvars files; add a short comment near the existing
  ingress override noting the derivation.
* terraform/environments/gcp/README.md: update the "Cloud Run ingress"
  table — `allow_unauthenticated` row now describes it as derived from
  `enable_cdn`, not a directly-set variable; clarify that when an env flips
  `enable_cdn = true` the IAM gate auto-closes alongside dropping the
  `cloud_run_ingress` override.
* terraform/modules/compute/gcp/cloud-run/main.tf: refresh the inline
  comment on the scheduled-task http_target — the IAM gate now references
  #384 (the derivation) rather than #78 (the network gate).

The `cloud_run_ingress = "INGRESS_TRAFFIC_ALL"` override in the 3 env tfvars
stays exactly as it was — that's a sibling concern (network door) tracked by
#78 and gated on the same LB cutover, but kept as an explicit override per the
existing comment so the network-vs-IAM doors are managed in lockstep but
visibly separately.

Verified: terraform fmt -check clean on both modules/compute/gcp and
environments/gcp; terraform validate clean on both layers.

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

Labels

effort/m Days impact/all-users Affects every user priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/security Security finding urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant