feat: LambdaAccessKey module
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* # 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"
|
||||
}
|
||||
Reference in New Issue
Block a user