From a2893f8343bf1b4fe73203f9b5243859748da41a Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Wed, 20 May 2026 18:21:46 +0200 Subject: [PATCH 1/4] docs(iac/cors): clarify prod tfvars placeholder fails on DNS not Terraform validation (CR nit) --- terraform/environments/aws/github-prod.tfvars | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/terraform/environments/aws/github-prod.tfvars b/terraform/environments/aws/github-prod.tfvars index 3af0e89c..523eb28e 100644 --- a/terraform/environments/aws/github-prod.tfvars +++ b/terraform/environments/aws/github-prod.tfvars @@ -26,7 +26,8 @@ lambda_enable_function_url = true lambda_function_url_auth_type = "NONE" # TODO(env-not-deployed): prod environment is not yet provisioned. Update this # to the actual prod origin (e.g. the customer-facing dashboard domain) when -# the env exists. .invalid placeholder fails fast on accidental apply. +# the env exists. .invalid placeholder ensures any accidental apply fails fast +# on hostname resolution. lambda_allowed_origins = ["https://prod-not-yet-deployed.invalid"] # Fargate Configuration (when compute_platform = "fargate") From 53e3638f13588c7e278b0059adf10acac8799812 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Wed, 20 May 2026 18:38:11 +0200 Subject: [PATCH 2/4] sec(iac): add AWS_IAM Function URL auth + CloudFront OAC scaffolding (#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. --- terraform/environments/aws/compute.tf | 29 +++++++++++++++++++ terraform/environments/aws/frontend.tf | 3 ++ terraform/environments/aws/github-dev.tfvars | 3 ++ terraform/environments/aws/github-prod.tfvars | 3 ++ .../environments/aws/github-staging.tfvars | 3 ++ terraform/modules/compute/aws/lambda/main.tf | 2 +- .../modules/compute/aws/lambda/variables.tf | 18 ++++++++++-- terraform/modules/frontend/aws/main.tf | 21 ++++++++++++++ terraform/modules/frontend/aws/variables.tf | 14 +++++++++ 9 files changed, 93 insertions(+), 3 deletions(-) diff --git a/terraform/environments/aws/compute.tf b/terraform/environments/aws/compute.tf index 51c2683f..374e22b6 100644 --- a/terraform/environments/aws/compute.tf +++ b/terraform/environments/aws/compute.tf @@ -204,3 +204,32 @@ module "compute_fargate" { # Build dependency must be explicit — image_uri is computed before docker push completes depends_on = [module.networking, module.database, module.secrets, module.build] } + +# ============================================== +# Lambda Function URL: CloudFront OAC permission +# ============================================== +# +# This permission lives at the environment layer (not inside the lambda module) +# because both module.compute_lambda and module.frontend must be fully resolved +# before the permission can reference both the Lambda function name and the +# CloudFront distribution ARN. Placing it inside either module would create a +# dependency cycle (Lambda URL is module.frontend's origin; distribution ARN +# would need to feed back into the lambda module). +# +# Created only when: +# - compute platform is lambda (function URL is active) +# - auth_type is AWS_IAM (IAM gate is enforced on the Function URL) +# - CDN module is deployed (distribution ARN is available) +# +# When enable_cdn = false the Function URL remains "NONE" in all current envs so +# this resource is always count = 0 until both conditions are true together. +resource "aws_lambda_permission" "function_url_cloudfront" { + count = var.compute_platform == "lambda" && var.lambda_function_url_auth_type == "AWS_IAM" && var.enable_cdn ? 1 : 0 + + statement_id = "FunctionURLAllowCloudFront" + action = "lambda:InvokeFunctionUrl" + function_name = module.compute_lambda[0].function_name + principal = "cloudfront.amazonaws.com" + source_arn = module.frontend[0].cloudfront_distribution_arn + function_url_auth_type = "AWS_IAM" +} diff --git a/terraform/environments/aws/frontend.tf b/terraform/environments/aws/frontend.tf index fa09f511..9fe28677 100644 --- a/terraform/environments/aws/frontend.tf +++ b/terraform/environments/aws/frontend.tf @@ -30,6 +30,9 @@ module "frontend" { # Don't let module create DNS record if we're managing it in dns_records.tf route53_zone_id = var.subdomain_zone_name != "" ? null : var.frontend_route53_zone_id + # Enable OAC when Lambda is behind CloudFront with AWS_IAM auth + enable_oac = var.compute_platform == "lambda" && var.lambda_function_url_auth_type == "AWS_IAM" + # CloudFront configuration price_class = var.frontend_price_class diff --git a/terraform/environments/aws/github-dev.tfvars b/terraform/environments/aws/github-dev.tfvars index 7a201826..4d829747 100644 --- a/terraform/environments/aws/github-dev.tfvars +++ b/terraform/environments/aws/github-dev.tfvars @@ -23,6 +23,9 @@ lambda_timeout = 300 lambda_reserved_concurrency = -1 lambda_log_retention_days = 7 lambda_enable_function_url = true +# TODO(#424): flip to "AWS_IAM" once enable_cdn = true and CloudFront is deployed for this env. +# Switching to AWS_IAM without a CloudFront OAC in place makes the Function URL unreachable (HTTP 403). +# Steps: set enable_cdn = true, update lambda_allowed_origins to the CloudFront domain, then flip this. lambda_function_url_auth_type = "NONE" # Current deployed dev origin (Lambda Function URL) + local Webpack dev server. # Wildcard is rejected by the module (allow_credentials=true + * = any-origin CSRF). diff --git a/terraform/environments/aws/github-prod.tfvars b/terraform/environments/aws/github-prod.tfvars index 523eb28e..381feb25 100644 --- a/terraform/environments/aws/github-prod.tfvars +++ b/terraform/environments/aws/github-prod.tfvars @@ -23,6 +23,9 @@ lambda_timeout = 300 lambda_reserved_concurrency = -1 lambda_log_retention_days = 30 lambda_enable_function_url = true +# TODO(#424): flip to "AWS_IAM" once enable_cdn = true and CloudFront is deployed for this env. +# Switching to AWS_IAM without a CloudFront OAC in place makes the Function URL unreachable (HTTP 403). +# Steps: set enable_cdn = true, update lambda_allowed_origins to the CloudFront domain, then flip this. lambda_function_url_auth_type = "NONE" # TODO(env-not-deployed): prod environment is not yet provisioned. Update this # to the actual prod origin (e.g. the customer-facing dashboard domain) when diff --git a/terraform/environments/aws/github-staging.tfvars b/terraform/environments/aws/github-staging.tfvars index bb3cabad..9ad260d0 100644 --- a/terraform/environments/aws/github-staging.tfvars +++ b/terraform/environments/aws/github-staging.tfvars @@ -23,6 +23,9 @@ lambda_timeout = 300 lambda_reserved_concurrency = -1 lambda_log_retention_days = 14 lambda_enable_function_url = true +# TODO(#424): flip to "AWS_IAM" once enable_cdn = true and CloudFront is deployed for this env. +# Switching to AWS_IAM without a CloudFront OAC in place makes the Function URL unreachable (HTTP 403). +# Steps: set enable_cdn = true, update lambda_allowed_origins to the CloudFront domain, then flip this. lambda_function_url_auth_type = "NONE" # TODO(env-not-deployed): staging environment is not yet provisioned. Update this # to the actual staging origin when the env exists. Until then the .invalid TLD diff --git a/terraform/modules/compute/aws/lambda/main.tf b/terraform/modules/compute/aws/lambda/main.tf index 6700a924..129ed243 100644 --- a/terraform/modules/compute/aws/lambda/main.tf +++ b/terraform/modules/compute/aws/lambda/main.tf @@ -132,7 +132,7 @@ resource "aws_lambda_function_url" "main" { # Resource-based policy grant for the Function URL. Without this, every request # is rejected at the Lambda edge with HTTP 403 AccessDeniedException, even when # authorization_type = "NONE". The AWS provider used to create this implicitly -# but stopped doing so in recent versions — it must be declared explicitly. +# but stopped doing so in recent versions -- it must be declared explicitly. resource "aws_lambda_permission" "function_url" { count = var.enable_function_url && var.function_url_auth_type == "NONE" ? 1 : 0 diff --git a/terraform/modules/compute/aws/lambda/variables.tf b/terraform/modules/compute/aws/lambda/variables.tf index 131c9c67..216ad7b5 100644 --- a/terraform/modules/compute/aws/lambda/variables.tf +++ b/terraform/modules/compute/aws/lambda/variables.tf @@ -90,9 +90,23 @@ variable "enable_function_url" { } variable "function_url_auth_type" { - description = "Function URL authorization type (NONE or AWS_IAM)" + description = <<-EOT + Function URL authorization type. Use "AWS_IAM" (default, recommended) when a + CloudFront distribution with an OAC is deployed in front of the Function URL — + AWS then enforces SigV4 signing on every request so the Function URL is + unreachable without a valid AWS identity. + Use "NONE" only in environments where direct access without CloudFront is + acceptable (e.g. local dev, or environments not yet provisioned with CloudFront). + Pair with cloudfront_distribution_arn when switching to AWS_IAM so the module can + create the necessary resource-based policy granting CloudFront invocation rights. + EOT type = string - default = "NONE" + default = "AWS_IAM" + + validation { + condition = contains(["NONE", "AWS_IAM"], var.function_url_auth_type) + error_message = "function_url_auth_type must be \"NONE\" or \"AWS_IAM\"." + } } variable "allowed_origins" { diff --git a/terraform/modules/frontend/aws/main.tf b/terraform/modules/frontend/aws/main.tf index 6e93f4fd..14689fd5 100644 --- a/terraform/modules/frontend/aws/main.tf +++ b/terraform/modules/frontend/aws/main.tf @@ -12,6 +12,22 @@ terraform { } } +# Origin Access Control for Lambda Function URL +# Instructs CloudFront to SigV4-sign every forwarded request so the Lambda +# Function URL (authorization_type = "AWS_IAM") only accepts traffic that +# originates from this distribution. The signing happens at the CloudFront edge +# -- the browser never sees an AWS credential. +# Only created when enable_oac = true (Lambda + AWS_IAM deployments). +resource "aws_cloudfront_origin_access_control" "lambda" { + count = var.enable_oac ? 1 : 0 + + name = "${var.project_name}-${var.environment}-lambda-oac" + description = "OAC for ${var.project_name} Lambda Function URL (${var.environment})" + origin_access_control_origin_type = "lambda" + signing_behavior = "always" + signing_protocol = "sigv4" +} + # CloudFront distribution with single compute origin resource "aws_cloudfront_distribution" "frontend" { enabled = true @@ -26,6 +42,11 @@ resource "aws_cloudfront_distribution" "frontend" { domain_name = var.origin_domain_name origin_id = "compute" + # Attach OAC when Lambda Function URL uses authorization_type = "AWS_IAM". + # CloudFront will SigV4-sign every forwarded request. Null when enable_oac = false + # (Fargate ALB or Lambda with auth_type = "NONE"). + origin_access_control_id = var.enable_oac ? aws_cloudfront_origin_access_control.lambda[0].id : null + custom_origin_config { http_port = 80 https_port = 443 diff --git a/terraform/modules/frontend/aws/variables.tf b/terraform/modules/frontend/aws/variables.tf index 093f2df6..5b64391a 100644 --- a/terraform/modules/frontend/aws/variables.tf +++ b/terraform/modules/frontend/aws/variables.tf @@ -69,6 +69,20 @@ variable "alarm_sns_topic_arn" { default = "" } +variable "enable_oac" { + description = <<-EOT + When true, create a CloudFront Origin Access Control (OAC) of type "lambda" + and attach it to the compute origin in the distribution. Required when the + Lambda Function URL uses authorization_type = "AWS_IAM": CloudFront will + SigV4-sign every forwarded request so the Function URL only accepts traffic + from this distribution. + Set to false when the origin is a Fargate ALB or when the Lambda uses + authorization_type = "NONE". + EOT + type = bool + default = false +} + variable "tags" { description = "Additional tags for resources" type = map(string) From dc971f775e176ce1ca65e34874f6d0b8852620f3 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Fri, 22 May 2026 16:26:06 +0200 Subject: [PATCH 3/4] refactor(iac): derive lambda_function_url_auth_type from enable_cdn (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 --- terraform/environments/aws/compute.tf | 37 ++++++++++++++++--- terraform/environments/aws/dev.tfvars.example | 3 +- terraform/environments/aws/frontend.tf | 7 +++- terraform/environments/aws/github-dev.tfvars | 17 ++++----- terraform/environments/aws/github-prod.tfvars | 17 ++++----- .../environments/aws/github-staging.tfvars | 17 ++++----- terraform/environments/aws/variables.tf | 24 +++--------- terraform/profiles/aws/dev.tfvars.example | 2 +- terraform/profiles/aws/prod.tfvars.example | 2 +- 9 files changed, 69 insertions(+), 57 deletions(-) diff --git a/terraform/environments/aws/compute.tf b/terraform/environments/aws/compute.tf index 374e22b6..6e12c862 100644 --- a/terraform/environments/aws/compute.tf +++ b/terraform/environments/aws/compute.tf @@ -2,6 +2,29 @@ # Compute Platform: Lambda (default) # ============================================== +# Lambda Function URL auth type is fully derived from enable_cdn. There is no +# operator-facing variable for it: the two valid combinations are +# +# enable_cdn = false -> auth_type = "NONE" +# The 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 +# (see frontend module's aws_cloudfront_origin_access_control.lambda). +# The Function URL rejects anything not signed by CloudFront's OAC, +# closing the public-internet exposure described in #424. +# +# Tying the two flags together prevents two specific mis-configurations: +# (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. +locals { + lambda_function_url_auth_type = var.enable_cdn ? "AWS_IAM" : "NONE" +} + module "compute_lambda" { source = "../../modules/compute/aws/lambda" count = var.compute_platform == "lambda" ? 1 : 0 @@ -38,7 +61,7 @@ module "compute_lambda" { # Function URL enable_function_url = var.lambda_enable_function_url - function_url_auth_type = var.lambda_function_url_auth_type + function_url_auth_type = local.lambda_function_url_auth_type allowed_origins = var.lambda_allowed_origins # Concurrency @@ -218,13 +241,15 @@ module "compute_fargate" { # # Created only when: # - compute platform is lambda (function URL is active) -# - auth_type is AWS_IAM (IAM gate is enforced on the Function URL) -# - CDN module is deployed (distribution ARN is available) +# - enable_cdn is true (CloudFront distribution is deployed and provides the +# source ARN); when enable_cdn = true the local also flips auth_type to +# AWS_IAM so the IAM gate is enforced on the Function URL. # -# When enable_cdn = false the Function URL remains "NONE" in all current envs so -# this resource is always count = 0 until both conditions are true together. +# When enable_cdn = false the Function URL stays NONE (no IAM gate) and this +# resource is count = 0; the SPA hits the Function URL directly and auth is +# enforced at the application layer. resource "aws_lambda_permission" "function_url_cloudfront" { - count = var.compute_platform == "lambda" && var.lambda_function_url_auth_type == "AWS_IAM" && var.enable_cdn ? 1 : 0 + count = var.compute_platform == "lambda" && var.enable_cdn ? 1 : 0 statement_id = "FunctionURLAllowCloudFront" action = "lambda:InvokeFunctionUrl" diff --git a/terraform/environments/aws/dev.tfvars.example b/terraform/environments/aws/dev.tfvars.example index b7b2e0c6..08f2dec6 100644 --- a/terraform/environments/aws/dev.tfvars.example +++ b/terraform/environments/aws/dev.tfvars.example @@ -30,9 +30,8 @@ lambda_timeout = 60 # Seconds lambda_reserved_concurrency = -1 # -1 = no limit lambda_log_retention_days = 30 -# Lambda Function URL +# Lambda Function URL (auth_type is derived from enable_cdn — see compute.tf) lambda_enable_function_url = true -lambda_function_url_auth_type = "NONE" lambda_allowed_origins = ["*"] # Fargate settings (when compute_platform = "fargate") diff --git a/terraform/environments/aws/frontend.tf b/terraform/environments/aws/frontend.tf index 9fe28677..9a97c974 100644 --- a/terraform/environments/aws/frontend.tf +++ b/terraform/environments/aws/frontend.tf @@ -30,8 +30,11 @@ module "frontend" { # Don't let module create DNS record if we're managing it in dns_records.tf route53_zone_id = var.subdomain_zone_name != "" ? null : var.frontend_route53_zone_id - # Enable OAC when Lambda is behind CloudFront with AWS_IAM auth - enable_oac = var.compute_platform == "lambda" && var.lambda_function_url_auth_type == "AWS_IAM" + # Enable OAC whenever Lambda is fronted by CloudFront (enable_cdn = true, + # which also flips the Function URL auth_type to AWS_IAM via the local in + # compute.tf, so the OAC's SigV4 signing is the auth the Function URL + # expects). + enable_oac = var.compute_platform == "lambda" && var.enable_cdn # CloudFront configuration price_class = var.frontend_price_class diff --git a/terraform/environments/aws/github-dev.tfvars b/terraform/environments/aws/github-dev.tfvars index 4d829747..14f28693 100644 --- a/terraform/environments/aws/github-dev.tfvars +++ b/terraform/environments/aws/github-dev.tfvars @@ -18,15 +18,14 @@ compute_platform = "lambda" enable_docker_build = true # Build and push image via terraform apply on the runner # Lambda Configuration -lambda_memory_size = 2048 -lambda_timeout = 300 -lambda_reserved_concurrency = -1 -lambda_log_retention_days = 7 -lambda_enable_function_url = true -# TODO(#424): flip to "AWS_IAM" once enable_cdn = true and CloudFront is deployed for this env. -# Switching to AWS_IAM without a CloudFront OAC in place makes the Function URL unreachable (HTTP 403). -# Steps: set enable_cdn = true, update lambda_allowed_origins to the CloudFront domain, then flip this. -lambda_function_url_auth_type = "NONE" +lambda_memory_size = 2048 +lambda_timeout = 300 +lambda_reserved_concurrency = -1 +lambda_log_retention_days = 7 +lambda_enable_function_url = true +# Function URL auth_type is derived from enable_cdn (local in compute.tf): +# enable_cdn = false -> NONE (direct browser hits, app-layer auth) +# enable_cdn = true -> AWS_IAM (CloudFront OAC signs every request) # Current deployed dev origin (Lambda Function URL) + local Webpack dev server. # Wildcard is rejected by the module (allow_credentials=true + * = any-origin CSRF). # Update the Lambda Function URL entry when the dev environment is redeployed. diff --git a/terraform/environments/aws/github-prod.tfvars b/terraform/environments/aws/github-prod.tfvars index 381feb25..8d62b68d 100644 --- a/terraform/environments/aws/github-prod.tfvars +++ b/terraform/environments/aws/github-prod.tfvars @@ -18,15 +18,14 @@ compute_platform = "lambda" enable_docker_build = true # Build image via Terraform build module (no separate CI build step) # Lambda Configuration -lambda_memory_size = 1024 -lambda_timeout = 300 -lambda_reserved_concurrency = -1 -lambda_log_retention_days = 30 -lambda_enable_function_url = true -# TODO(#424): flip to "AWS_IAM" once enable_cdn = true and CloudFront is deployed for this env. -# Switching to AWS_IAM without a CloudFront OAC in place makes the Function URL unreachable (HTTP 403). -# Steps: set enable_cdn = true, update lambda_allowed_origins to the CloudFront domain, then flip this. -lambda_function_url_auth_type = "NONE" +lambda_memory_size = 1024 +lambda_timeout = 300 +lambda_reserved_concurrency = -1 +lambda_log_retention_days = 30 +lambda_enable_function_url = true +# Function URL auth_type is derived from enable_cdn (local in compute.tf): +# enable_cdn = false -> NONE (direct browser hits, app-layer auth) +# enable_cdn = true -> AWS_IAM (CloudFront OAC signs every request) # TODO(env-not-deployed): prod environment is not yet provisioned. Update this # to the actual prod origin (e.g. the customer-facing dashboard domain) when # the env exists. .invalid placeholder ensures any accidental apply fails fast diff --git a/terraform/environments/aws/github-staging.tfvars b/terraform/environments/aws/github-staging.tfvars index 9ad260d0..ff26e843 100644 --- a/terraform/environments/aws/github-staging.tfvars +++ b/terraform/environments/aws/github-staging.tfvars @@ -18,15 +18,14 @@ compute_platform = "lambda" enable_docker_build = true # Build image via Terraform build module (no separate CI build step) # Lambda Configuration -lambda_memory_size = 1024 -lambda_timeout = 300 -lambda_reserved_concurrency = -1 -lambda_log_retention_days = 14 -lambda_enable_function_url = true -# TODO(#424): flip to "AWS_IAM" once enable_cdn = true and CloudFront is deployed for this env. -# Switching to AWS_IAM without a CloudFront OAC in place makes the Function URL unreachable (HTTP 403). -# Steps: set enable_cdn = true, update lambda_allowed_origins to the CloudFront domain, then flip this. -lambda_function_url_auth_type = "NONE" +lambda_memory_size = 1024 +lambda_timeout = 300 +lambda_reserved_concurrency = -1 +lambda_log_retention_days = 14 +lambda_enable_function_url = true +# Function URL auth_type is derived from enable_cdn (local in compute.tf): +# enable_cdn = false -> NONE (direct browser hits, app-layer auth) +# enable_cdn = true -> AWS_IAM (CloudFront OAC signs every request) # TODO(env-not-deployed): staging environment is not yet provisioned. Update this # to the actual staging origin when the env exists. Until then the .invalid TLD # ensures any accidental terraform apply fails fast on hostname resolution. diff --git a/terraform/environments/aws/variables.tf b/terraform/environments/aws/variables.tf index 82310274..47c6d551 100644 --- a/terraform/environments/aws/variables.tf +++ b/terraform/environments/aws/variables.tf @@ -240,24 +240,12 @@ variable "lambda_enable_function_url" { default = true } -variable "lambda_function_url_auth_type" { - description = <<-EOT - Lambda Function URL auth type — must be "NONE" or "AWS_IAM". Default is - "NONE" because CUDly is a browser-served SPA that talks directly to the - Function URL; the browser cannot SigV4-sign requests. Auth is enforced at - the application layer (login session, JWT, API keys, CSRF) — see - internal/api/handler_login.go and middleware in router.go. Override to - AWS_IAM only when fronting the URL with a signed-request gateway - (CloudFront with Lambda@Edge, API Gateway, etc.). - EOT - type = string - default = "NONE" - - validation { - condition = contains(["AWS_IAM", "NONE"], var.lambda_function_url_auth_type) - error_message = "lambda_function_url_auth_type must be either \"AWS_IAM\" or \"NONE\"." - } -} +# Lambda Function URL auth type is derived from var.enable_cdn (see +# local.lambda_function_url_auth_type in compute.tf). When enable_cdn = true +# the Function URL is fronted by CloudFront with OAC + SigV4, so auth is +# AWS_IAM; when enable_cdn = false the browser hits the Function URL directly +# and the SPA cannot SigV4-sign, so auth is NONE and the application layer +# (login session, CSRF, API keys) is the gate. variable "lambda_allowed_origins" { description = "CORS allowed origins" diff --git a/terraform/profiles/aws/dev.tfvars.example b/terraform/profiles/aws/dev.tfvars.example index 642ad8e5..99378f23 100644 --- a/terraform/profiles/aws/dev.tfvars.example +++ b/terraform/profiles/aws/dev.tfvars.example @@ -19,7 +19,7 @@ lambda_timeout = 60 lambda_reserved_concurrency = -1 lambda_log_retention_days = 7 lambda_enable_function_url = true -lambda_function_url_auth_type = "NONE" +# Function URL auth_type is derived from enable_cdn — see compute.tf local lambda_allowed_origins = ["*"] # Docker build settings diff --git a/terraform/profiles/aws/prod.tfvars.example b/terraform/profiles/aws/prod.tfvars.example index 9df5cb66..cef1247d 100644 --- a/terraform/profiles/aws/prod.tfvars.example +++ b/terraform/profiles/aws/prod.tfvars.example @@ -19,7 +19,7 @@ lambda_timeout = 60 lambda_reserved_concurrency = -1 lambda_log_retention_days = 30 lambda_enable_function_url = true -lambda_function_url_auth_type = "NONE" +# Function URL auth_type is derived from enable_cdn — see compute.tf local lambda_allowed_origins = ["*"] # TODO: restrict to production domain # Docker build settings From 601318f490190fbca237fec59f3946663e10a25e Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Tue, 26 May 2026 00:42:17 +0200 Subject: [PATCH 4/4] refactor(iac/aws-lambda): drop unused lambda_enable_function_url toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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". --- terraform/environments/aws/compute.tf | 1 - terraform/environments/aws/dev.tfvars.example | 1 - terraform/environments/aws/github-dev.tfvars | 1 - terraform/environments/aws/github-prod.tfvars | 1 - terraform/environments/aws/github-staging.tfvars | 1 - terraform/environments/aws/variables.tf | 5 ----- terraform/modules/compute/aws/lambda/main.tf | 4 +--- terraform/modules/compute/aws/lambda/outputs.tf | 2 +- terraform/modules/compute/aws/lambda/variables.tf | 5 ----- terraform/profiles/aws/dev.tfvars.example | 1 - terraform/profiles/aws/prod.tfvars.example | 1 - 11 files changed, 2 insertions(+), 21 deletions(-) diff --git a/terraform/environments/aws/compute.tf b/terraform/environments/aws/compute.tf index 6e12c862..680517c1 100644 --- a/terraform/environments/aws/compute.tf +++ b/terraform/environments/aws/compute.tf @@ -60,7 +60,6 @@ module "compute_lambda" { } # Function URL - enable_function_url = var.lambda_enable_function_url function_url_auth_type = local.lambda_function_url_auth_type allowed_origins = var.lambda_allowed_origins diff --git a/terraform/environments/aws/dev.tfvars.example b/terraform/environments/aws/dev.tfvars.example index 08f2dec6..6457ebf9 100644 --- a/terraform/environments/aws/dev.tfvars.example +++ b/terraform/environments/aws/dev.tfvars.example @@ -31,7 +31,6 @@ lambda_reserved_concurrency = -1 # -1 = no limit lambda_log_retention_days = 30 # Lambda Function URL (auth_type is derived from enable_cdn — see compute.tf) -lambda_enable_function_url = true lambda_allowed_origins = ["*"] # Fargate settings (when compute_platform = "fargate") diff --git a/terraform/environments/aws/github-dev.tfvars b/terraform/environments/aws/github-dev.tfvars index 14f28693..bef90501 100644 --- a/terraform/environments/aws/github-dev.tfvars +++ b/terraform/environments/aws/github-dev.tfvars @@ -22,7 +22,6 @@ lambda_memory_size = 2048 lambda_timeout = 300 lambda_reserved_concurrency = -1 lambda_log_retention_days = 7 -lambda_enable_function_url = true # Function URL auth_type is derived from enable_cdn (local in compute.tf): # enable_cdn = false -> NONE (direct browser hits, app-layer auth) # enable_cdn = true -> AWS_IAM (CloudFront OAC signs every request) diff --git a/terraform/environments/aws/github-prod.tfvars b/terraform/environments/aws/github-prod.tfvars index 8d62b68d..99470778 100644 --- a/terraform/environments/aws/github-prod.tfvars +++ b/terraform/environments/aws/github-prod.tfvars @@ -22,7 +22,6 @@ lambda_memory_size = 1024 lambda_timeout = 300 lambda_reserved_concurrency = -1 lambda_log_retention_days = 30 -lambda_enable_function_url = true # Function URL auth_type is derived from enable_cdn (local in compute.tf): # enable_cdn = false -> NONE (direct browser hits, app-layer auth) # enable_cdn = true -> AWS_IAM (CloudFront OAC signs every request) diff --git a/terraform/environments/aws/github-staging.tfvars b/terraform/environments/aws/github-staging.tfvars index ff26e843..7e38b86d 100644 --- a/terraform/environments/aws/github-staging.tfvars +++ b/terraform/environments/aws/github-staging.tfvars @@ -22,7 +22,6 @@ lambda_memory_size = 1024 lambda_timeout = 300 lambda_reserved_concurrency = -1 lambda_log_retention_days = 14 -lambda_enable_function_url = true # Function URL auth_type is derived from enable_cdn (local in compute.tf): # enable_cdn = false -> NONE (direct browser hits, app-layer auth) # enable_cdn = true -> AWS_IAM (CloudFront OAC signs every request) diff --git a/terraform/environments/aws/variables.tf b/terraform/environments/aws/variables.tf index 47c6d551..a69171c4 100644 --- a/terraform/environments/aws/variables.tf +++ b/terraform/environments/aws/variables.tf @@ -234,11 +234,6 @@ variable "lambda_timeout" { default = 30 } -variable "lambda_enable_function_url" { - description = "Enable Lambda Function URL" - type = bool - default = true -} # Lambda Function URL auth type is derived from var.enable_cdn (see # local.lambda_function_url_auth_type in compute.tf). When enable_cdn = true diff --git a/terraform/modules/compute/aws/lambda/main.tf b/terraform/modules/compute/aws/lambda/main.tf index 129ed243..21ab8877 100644 --- a/terraform/modules/compute/aws/lambda/main.tf +++ b/terraform/modules/compute/aws/lambda/main.tf @@ -115,8 +115,6 @@ resource "aws_lambda_function" "main" { # ============================================== resource "aws_lambda_function_url" "main" { - count = var.enable_function_url ? 1 : 0 - function_name = aws_lambda_function.main.function_name authorization_type = var.function_url_auth_type @@ -134,7 +132,7 @@ resource "aws_lambda_function_url" "main" { # authorization_type = "NONE". The AWS provider used to create this implicitly # but stopped doing so in recent versions -- it must be declared explicitly. resource "aws_lambda_permission" "function_url" { - count = var.enable_function_url && var.function_url_auth_type == "NONE" ? 1 : 0 + count = var.function_url_auth_type == "NONE" ? 1 : 0 statement_id = "FunctionURLAllowPublicAccess" action = "lambda:InvokeFunctionUrl" diff --git a/terraform/modules/compute/aws/lambda/outputs.tf b/terraform/modules/compute/aws/lambda/outputs.tf index 97b3f7ac..94e7ff63 100644 --- a/terraform/modules/compute/aws/lambda/outputs.tf +++ b/terraform/modules/compute/aws/lambda/outputs.tf @@ -10,7 +10,7 @@ output "function_arn" { output "function_url" { description = "Lambda Function URL" - value = var.enable_function_url ? aws_lambda_function_url.main[0].function_url : null + value = aws_lambda_function_url.main.function_url } output "function_invoke_arn" { diff --git a/terraform/modules/compute/aws/lambda/variables.tf b/terraform/modules/compute/aws/lambda/variables.tf index 216ad7b5..6c062a44 100644 --- a/terraform/modules/compute/aws/lambda/variables.tf +++ b/terraform/modules/compute/aws/lambda/variables.tf @@ -83,11 +83,6 @@ variable "vpc_config" { default = null } -variable "enable_function_url" { - description = "Enable Lambda Function URL" - type = bool - default = true -} variable "function_url_auth_type" { description = <<-EOT diff --git a/terraform/profiles/aws/dev.tfvars.example b/terraform/profiles/aws/dev.tfvars.example index 99378f23..3e90b835 100644 --- a/terraform/profiles/aws/dev.tfvars.example +++ b/terraform/profiles/aws/dev.tfvars.example @@ -18,7 +18,6 @@ lambda_memory_size = 512 lambda_timeout = 60 lambda_reserved_concurrency = -1 lambda_log_retention_days = 7 -lambda_enable_function_url = true # Function URL auth_type is derived from enable_cdn — see compute.tf local lambda_allowed_origins = ["*"] diff --git a/terraform/profiles/aws/prod.tfvars.example b/terraform/profiles/aws/prod.tfvars.example index cef1247d..a61adacf 100644 --- a/terraform/profiles/aws/prod.tfvars.example +++ b/terraform/profiles/aws/prod.tfvars.example @@ -18,7 +18,6 @@ lambda_memory_size = 1024 # More memory for production lambda_timeout = 60 lambda_reserved_concurrency = -1 lambda_log_retention_days = 30 -lambda_enable_function_url = true # Function URL auth_type is derived from enable_cdn — see compute.tf local lambda_allowed_origins = ["*"] # TODO: restrict to production domain