1
0

feat: LambdaAccessKey module

This commit is contained in:
xpk
2026-06-14 16:05:47 +08:00
parent 2ef2ad1571
commit 5611195a0d
8 changed files with 419 additions and 1 deletions
@@ -0,0 +1,69 @@
import json
import boto3
from botocore.exceptions import ClientError
import base64
import hashlib
# from cryptography.fernet import Fernet
def decrypt_data(encrypted_data: str, secret_key: str) -> str:
key_hash = hashlib.sha256(secret_key.encode()).digest()
encrypted_bytes = base64.b64decode(encrypted_data.encode())
decrypted = bytes(b ^ key_hash[i % len(key_hash)] for i, b in enumerate(encrypted_bytes))
return decrypted.decode()
def lambda_handler(event, context):
# 1. Extract parameters from the incoming Lambda event payload
role_arn = "${target_role}"
session_name = "AssumedRole"
# Validation: Ensure the Role ARN was provided
if not role_arn:
return {
"statusCode": 400,
"body": json.dumps(
{"error": "Missing required parameter: 'role_arn'"}
),
}
# 2. Initialize the STS client
# Note: Lambda uses its own Execution Role to make this call.
# Ensure the Lambda role has the 'sts:AssumeRole' permission for the target ARN.
sts_client = boto3.client("sts")
try:
# 3. Assume the target role
response = sts_client.assume_role(
RoleArn=role_arn,
RoleSessionName=session_name,
ExternalId='${external_id}'
)
# Extract the credentials block
credentials = response["Credentials"]
plainText = f"export AWS_ACCESS_KEY_ID={credentials["AccessKeyId"]} AWS_SECRET_ACCESS_KEY={credentials["SecretAccessKey"]} AWS_SESSION_TOKEN={credentials["SessionToken"]}"
# Encrypt the credentials
key_hash = hashlib.sha256('${encryption_pass}'.encode()).digest()
encrypted = bytes(b ^ key_hash[i % len(key_hash)] for i, b in enumerate(plainText.encode()))
# 4. Return the standard Lambda proxy response containing the JSON payload
return {
"statusCode": 200,
"body": json.dumps(
{
"result" : base64.b64encode(encrypted).decode()
}
)
}
except ClientError as e:
return {
"statusCode": 500,
"body": json.dumps(
{
"error": "Failed to assume role",
"details": e.response["Error"]["Message"],
}
),
}
@@ -0,0 +1,80 @@
<!-- This readme file is generated with terraform-docs -->
## Example
```hcl
module "TrustedAccess" {
source = "../"
role_name = "TrustedAccess"
}
```
# 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
## Requirements
No requirements.
## Providers
| Name | Version |
| ---- | ------- |
| archive | n/a |
| aws | n/a |
| local | n/a |
| random | n/a |
## Modules
| Name | Source | Version |
| ---- | ------ | ------- |
| LambdaExecRole | ../iam-role-v2 | n/a |
| TargetIam | ../iam-role-v2 | n/a |
## Resources
| Name | Type |
| ---- | ---- |
| [aws_cloudfront_distribution.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distribution) | resource |
| [aws_cloudfront_origin_access_control.CloudfrontOac](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_access_control) | resource |
| [aws_iam_policy.LamdaExecRole](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
| [aws_lambda_function.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource |
| [aws_lambda_function_url.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function_url) | resource |
| [aws_lambda_permission.AllowCloudFrontServicePrincipal](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource |
| [aws_lambda_permission.AllowCloudFrontServicePrincipalInvokeFunction](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource |
| [local_file.FunctionCode](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file) | resource |
| [local_file.client](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file) | resource |
| [random_password.this](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource |
| [random_uuid.ExternalId](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/uuid) | resource |
| [archive_file.LambdaZip](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source |
| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
| [aws_cloudfront_cache_policy.NoCache](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudfront_cache_policy) | data source |
| [aws_cloudfront_origin_request_policy.AllButHost](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudfront_origin_request_policy) | data source |
## Inputs
| Name | Description | Type | Default | Required |
| ---- | ----------- | ---- | ------- | :------: |
| role\_name | Name of target role | `string` | n/a | yes |
## Outputs
| Name | Description |
| ---- | ----------- |
| CloudFrontDist | n/a |
| LambdaFunctionArn | n/a |
| TargetRole | n/a |
---
## Authorship
This module was developed by UPDATE_THIS.
@@ -0,0 +1,23 @@
#!/usr/bin/python3
import base64
import hashlib
import requests
def decrypt_data(encrypted_data: str) -> str:
key_hash = hashlib.sha256('${encryption_pass}'.encode()).digest()
encrypted_bytes = base64.b64decode(encrypted_data.encode())
decrypted = bytes(b ^ key_hash[i % len(key_hash)] for i, b in enumerate(encrypted_bytes))
return decrypted.decode()
url = "${cloudfront_url}"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
print(decrypt_data(data['result']))
except requests.exceptions.HTTPError as http_err:
print(f"HTTP error occurred: {http_err}")
except Exception as err:
print(f"An error occurred: {err}")
@@ -0,0 +1,4 @@
module "TrustedAccess" {
source = "../"
role_name = "TrustedAccess"
}
@@ -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"
}
@@ -0,0 +1,11 @@
output "TargetRole" {
value = module.TargetIam.role-arn
}
output "LambdaFunctionArn" {
value = aws_lambda_function.this.arn
}
output "CloudFrontDist" {
value = aws_cloudfront_distribution.this.arn
}
@@ -0,0 +1,4 @@
variable "role_name" {
type = string
description = "Name of target role"
}