/** * # LambdaAccessKey * * Module to create a lambda function, which assumes to a certain role and * get temporary access credentials. The lambda function url is protected * by cloudfront and origin access control. Credentials are encrypted. Once * resources are deployed, run client.py to send http request and decrypt * the response * * Cloudfront fixed-rate pricing cannot be controlled by terraform or awscli * at time of writing. Change to the free plan on aws console. * * To destroy the cloudfront distribution, you need to cancel the fixed rate plan */ # data sources data "aws_caller_identity" "current" {} # IAM role to assume role to resource "random_uuid" "ExternalId" {} module "TargetIam" { source = "../iam-role-v2" trusted-entity = jsonencode( { "Version" : "2012-10-17", "Statement" : [ { "Effect" : "Allow", "Principal" : { "AWS" : "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" }, "Action" : "sts:AssumeRole", "Condition" : { "StringEquals" : { "sts:ExternalId" : random_uuid.ExternalId.id } } } ] } ) role-name = var.role_name description = "Trusted access role" path = "/" max-session-duration = 14400 # 4 hours attach-managed-policies = [ "arn:aws:iam::aws:policy/IAMFullAccess", "arn:aws:iam::aws:policy/PowerUserAccess" ] } # Lambda execution IAM role resource "aws_iam_policy" "LamdaExecRole" { name_prefix = "${var.role_name}-LambdaExecRole" description = "Lambda execution role policy" policy = jsonencode( { "Version" : "2012-10-17", "Statement" : [ { "Effect" : "Allow", "Action" : "logs:CreateLogGroup", "Resource" : "arn:aws:logs:us-east-1:${data.aws_caller_identity.current.account_id}:*" }, { "Effect" : "Allow", "Action" : [ "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource" : [ "arn:aws:logs:us-east-1:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/${var.role_name}:*" ] }, { "Effect" : "Allow", "Action" : [ "sts:AssumeRole" ], "Resource" : [ module.TargetIam.role-arn ] } ] } ) } module "LambdaExecRole" { source = "../iam-role-v2" trusted-entity = "lambda.amazonaws.com" role-name = "${var.role_name}-LambdaExecRole" description = "Lambda execution role" path = "/" max-session-duration = 3600 # 1 hour attach-managed-policies = [ aws_iam_policy.LamdaExecRole.arn ] } # Lambda function resource "random_password" "this" { length = 50 override_special = "~@#%^&*-_=+:;<,>./" } resource "local_file" "FunctionCode" { content = templatefile("${path.module}/FunctionCode.tpl", { target_role = module.TargetIam.role-arn encryption_pass = random_password.this.result external_id = random_uuid.ExternalId.result }) filename = "${path.module}/FunctionCode.py" } data "archive_file" "LambdaZip" { type = "zip" source_file = local_file.FunctionCode.filename output_path = "${path.module}/FunctionCode.zip" } # Lambda function resource "aws_lambda_function" "this" { filename = data.archive_file.LambdaZip.output_path code_sha256 = data.archive_file.LambdaZip.output_base64sha256 function_name = var.role_name role = module.LambdaExecRole.role-arn handler = "FunctionCode.lambda_handler" runtime = "python3.14" architectures = ["arm64"] reserved_concurrent_executions = 2 } resource "aws_lambda_function_url" "this" { function_name = aws_lambda_function.this.function_name authorization_type = "AWS_IAM" } resource "aws_lambda_permission" "AllowCloudFrontServicePrincipal" { statement_id = "AllowCloudFrontServicePrincipal" action = "lambda:InvokeFunctionUrl" function_name = aws_lambda_function.this.function_name principal = "cloudfront.amazonaws.com" source_arn = "" } resource "aws_lambda_permission" "AllowCloudFrontServicePrincipalInvokeFunction" { statement_id = "AllowCloudFrontServicePrincipalInvokeFunction" action = "lambda:InvokeFunction" function_name = aws_lambda_function.this.function_name principal = "cloudfront.amazonaws.com" source_arn = "" } # CloudFront flatrate plan resource "aws_cloudfront_origin_access_control" "CloudfrontOac" { name = "lambda-function-url-oac" description = "OAC for secure Lambda Function URL backend connection" origin_access_control_origin_type = "lambda" signing_behavior = "always" signing_protocol = "sigv4" } resource "aws_cloudfront_distribution" "this" { enabled = true is_ipv6_enabled = true price_class = "PriceClass_All" origin { domain_name = split("/", aws_lambda_function_url.this.function_url)[2] origin_id = "LambdaBackendOrigin" origin_access_control_id = aws_cloudfront_origin_access_control.CloudfrontOac.id custom_origin_config { http_port = 80 https_port = 443 origin_protocol_policy = "https-only" origin_ssl_protocols = ["TLSv1.2"] } } default_cache_behavior { allowed_methods = ["GET", "HEAD"] cached_methods = ["GET", "HEAD"] target_origin_id = "LambdaBackendOrigin" viewer_protocol_policy = "redirect-to-https" # Use default AWS-managed Cache/Origin Request policies to stay within flat-rate free bounds cache_policy_id = data.aws_cloudfront_cache_policy.NoCache.id origin_request_policy_id = data.aws_cloudfront_origin_request_policy.AllButHost.id # host header must be dropped for OAC } # Flat-rate plans include basic managed WAF rule support automatically restrictions { geo_restriction { restriction_type = "none" } } viewer_certificate { cloudfront_default_certificate = true } lifecycle { ignore_changes = [ web_acl_id # after converting to flat-rate plan, a waf acl will be attached and we want to allow it ] } } data "aws_cloudfront_cache_policy" "NoCache" { name = "Managed-CachingDisabled" } data "aws_cloudfront_origin_request_policy" "AllButHost" { name = "Managed-AllViewerExceptHostHeader" } # Decryption file / client resource "local_file" "client" { content = templatefile("${path.module}/client.tpl", { encryption_pass = random_password.this.result cloudfront_url = "https://${aws_cloudfront_distribution.this.domain_name}" }) filename = "${path.module}/client.py" }