Implement reversible PII de-identification round trip (T1–T7)
Build the AWS side of the HNCB demo end to end (region ap-southeast-1): - T1 /tokenize + T2 /restore: Lambdas behind a public API Gateway (shared-secret auth), Presidio detection, random per-request tokens, DynamoDB vault; overlap resolution so a ROC ID stays TW_ROC_ID. - T3: Presidio made private (SG-locked to the tokenize Lambda in-VPC; DynamoDB gateway endpoint); only /tokenize + /restore are public. - T4: RAG Lambda registered as an MCP tool on an AgentCore Gateway (AWS_IAM/SigV4); agentcore_setup.sh + a SigV4 MCP invoke test. - T5: Strands agent deployed to AgentCore Runtime; SigV4 gateway auth, apac inference profile, pinned deps. - T6: advisor UI on S3+CloudFront with a Fusion-less demo orchestrator (/demo) chaining tokenize -> runtime -> restore. - T7: README runbook + trace check; teardown deletes gateway/runtime/memory/ECR. Verified live: the cloud AgentCore/Bedrock trace shows only tokens, never the real name. Secrets stay in gitignored local.auto.tfvars. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
296
terraform/gateway.tf
Normal file
296
terraform/gateway.tf
Normal file
@@ -0,0 +1,296 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public HTTPS endpoints that Fusion SaaS calls: POST /tokenize (T1) and
|
||||
# POST /restore (T2), both on one API Gateway HTTP API. Fusion is shared SaaS, so
|
||||
# it can only reach PUBLIC HTTPS with auth -- each handler checks a shared secret
|
||||
# (tokenize_api_key, sent as x-api-key / bearer). These live in the "on-prem" zone
|
||||
# (tags): they own the reversible token<->PII map, which must never leave it.
|
||||
# (API Gateway, not a Lambda Function URL: this org's SCP blocks unauthenticated
|
||||
# Function URLs.)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
data "archive_file" "tokenize" {
|
||||
type = "zip"
|
||||
source_dir = "${path.module}/../gateway_api/tokenize"
|
||||
output_path = "${path.module}/tokenize_lambda.zip"
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "tokenize" {
|
||||
name = "${local.name}-tokenize-role"
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Principal = { Service = "lambda.amazonaws.com" }
|
||||
Action = "sts:AssumeRole"
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "tokenize" {
|
||||
name = "${local.name}-tokenize-policy"
|
||||
role = aws_iam_role.tokenize.id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
# Mint into the vault; scan customers for best-effort PERSON -> customer_id.
|
||||
Effect = "Allow"
|
||||
Action = ["dynamodb:PutItem"]
|
||||
Resource = [aws_dynamodb_table.vault.arn]
|
||||
},
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = ["dynamodb:Scan"]
|
||||
Resource = [aws_dynamodb_table.customers.arn]
|
||||
},
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
|
||||
Resource = "arn:aws:logs:*:*:*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_lambda_function" "tokenize" {
|
||||
function_name = "${local.name}-tokenize"
|
||||
role = aws_iam_role.tokenize.arn
|
||||
runtime = "python3.12"
|
||||
handler = "handler.lambda_handler"
|
||||
filename = data.archive_file.tokenize.output_path
|
||||
source_code_hash = data.archive_file.tokenize.output_base64sha256
|
||||
timeout = 15
|
||||
environment {
|
||||
variables = {
|
||||
VAULT_TABLE = aws_dynamodb_table.vault.name
|
||||
CUSTOMERS_TABLE = aws_dynamodb_table.customers.name
|
||||
PRESIDIO_URL = var.presidio_url
|
||||
TOKENIZE_API_KEY = var.tokenize_api_key
|
||||
VAULT_TTL_SECONDS = "3600"
|
||||
}
|
||||
}
|
||||
# In-VPC so it can reach the now-private Presidio over 5001. DynamoDB is reached
|
||||
# via the gateway endpoint (see ecs.tf); no NAT needed.
|
||||
vpc_config {
|
||||
subnet_ids = data.aws_subnets.default.ids
|
||||
security_group_ids = [aws_security_group.tokenize_lambda.id]
|
||||
}
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
# VPC-attached Lambdas need ENI management permissions.
|
||||
resource "aws_iam_role_policy_attachment" "tokenize_vpc" {
|
||||
role = aws_iam_role.tokenize.name
|
||||
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
|
||||
}
|
||||
|
||||
# Public HTTPS front door. We use an API Gateway HTTP API rather than a Lambda
|
||||
# Function URL because this org's SCP blocks unauthenticated (auth_type=NONE)
|
||||
# Function URLs. The API is public; auth is the shared secret checked in-handler
|
||||
# (Fusion sends it as x-api-key / bearer). T2 adds a POST /restore route here.
|
||||
resource "aws_apigatewayv2_api" "gateway" {
|
||||
name = "${local.name}-gateway"
|
||||
protocol_type = "HTTP"
|
||||
# The static UI (CloudFront origin) calls /demo cross-origin -> allow CORS.
|
||||
cors_configuration {
|
||||
allow_origins = ["*"]
|
||||
allow_methods = ["POST", "OPTIONS"]
|
||||
allow_headers = ["content-type", "x-api-key"]
|
||||
}
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_integration" "tokenize" {
|
||||
api_id = aws_apigatewayv2_api.gateway.id
|
||||
integration_type = "AWS_PROXY"
|
||||
integration_uri = aws_lambda_function.tokenize.invoke_arn
|
||||
payload_format_version = "2.0"
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_route" "tokenize" {
|
||||
api_id = aws_apigatewayv2_api.gateway.id
|
||||
route_key = "POST /tokenize"
|
||||
target = "integrations/${aws_apigatewayv2_integration.tokenize.id}"
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_stage" "default" {
|
||||
api_id = aws_apigatewayv2_api.gateway.id
|
||||
name = "$default"
|
||||
auto_deploy = true
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
resource "aws_lambda_permission" "tokenize_apigw" {
|
||||
statement_id = "AllowApiGatewayInvoke"
|
||||
action = "lambda:InvokeFunction"
|
||||
function_name = aws_lambda_function.tokenize.function_name
|
||||
principal = "apigateway.amazonaws.com"
|
||||
source_arn = "${aws_apigatewayv2_api.gateway.execution_arn}/*/*"
|
||||
}
|
||||
|
||||
########################################
|
||||
# T2: /restore -- egress re-identification (vault lookup by session_id)
|
||||
########################################
|
||||
data "archive_file" "restore" {
|
||||
type = "zip"
|
||||
source_dir = "${path.module}/../gateway_api/restore"
|
||||
output_path = "${path.module}/restore_lambda.zip"
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "restore" {
|
||||
name = "${local.name}-restore-role"
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Principal = { Service = "lambda.amazonaws.com" }
|
||||
Action = "sts:AssumeRole"
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "restore" {
|
||||
name = "${local.name}-restore-policy"
|
||||
role = aws_iam_role.restore.id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
# Read-only: look up this session's tokens to re-attach identity.
|
||||
Effect = "Allow"
|
||||
Action = ["dynamodb:Scan"]
|
||||
Resource = [aws_dynamodb_table.vault.arn]
|
||||
},
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
|
||||
Resource = "arn:aws:logs:*:*:*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_lambda_function" "restore" {
|
||||
function_name = "${local.name}-restore"
|
||||
role = aws_iam_role.restore.arn
|
||||
runtime = "python3.12"
|
||||
handler = "handler.lambda_handler"
|
||||
filename = data.archive_file.restore.output_path
|
||||
source_code_hash = data.archive_file.restore.output_base64sha256
|
||||
timeout = 15
|
||||
environment {
|
||||
variables = {
|
||||
VAULT_TABLE = aws_dynamodb_table.vault.name
|
||||
TOKENIZE_API_KEY = var.tokenize_api_key
|
||||
}
|
||||
}
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_integration" "restore" {
|
||||
api_id = aws_apigatewayv2_api.gateway.id
|
||||
integration_type = "AWS_PROXY"
|
||||
integration_uri = aws_lambda_function.restore.invoke_arn
|
||||
payload_format_version = "2.0"
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_route" "restore" {
|
||||
api_id = aws_apigatewayv2_api.gateway.id
|
||||
route_key = "POST /restore"
|
||||
target = "integrations/${aws_apigatewayv2_integration.restore.id}"
|
||||
}
|
||||
|
||||
resource "aws_lambda_permission" "restore_apigw" {
|
||||
statement_id = "AllowApiGatewayInvoke"
|
||||
action = "lambda:InvokeFunction"
|
||||
function_name = aws_lambda_function.restore.function_name
|
||||
principal = "apigateway.amazonaws.com"
|
||||
source_arn = "${aws_apigatewayv2_api.gateway.execution_arn}/*/*"
|
||||
}
|
||||
|
||||
########################################
|
||||
# T6: /demo -- Fusion-less orchestrator the static UI calls (tokenize->agent->restore)
|
||||
########################################
|
||||
data "archive_file" "orchestrator" {
|
||||
type = "zip"
|
||||
source_dir = "${path.module}/../gateway_api/orchestrator"
|
||||
output_path = "${path.module}/orchestrator_lambda.zip"
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "orchestrator" {
|
||||
name = "${local.name}-orchestrator-role"
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Principal = { Service = "lambda.amazonaws.com" }
|
||||
Action = "sts:AssumeRole"
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "orchestrator" {
|
||||
name = "${local.name}-orchestrator-policy"
|
||||
role = aws_iam_role.orchestrator.id
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
# Invoke the AgentCore Runtime. Scoped to the configured runtime ARN when set.
|
||||
Effect = "Allow"
|
||||
Action = ["bedrock-agentcore:InvokeAgentRuntime"]
|
||||
Resource = var.agent_runtime_arn != "" ? [var.agent_runtime_arn, "${var.agent_runtime_arn}/*"] : ["*"]
|
||||
},
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
|
||||
Resource = "arn:aws:logs:*:*:*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_lambda_function" "orchestrator" {
|
||||
function_name = "${local.name}-orchestrator"
|
||||
role = aws_iam_role.orchestrator.arn
|
||||
runtime = "python3.12"
|
||||
handler = "handler.lambda_handler"
|
||||
filename = data.archive_file.orchestrator.output_path
|
||||
source_code_hash = data.archive_file.orchestrator.output_base64sha256
|
||||
timeout = 120
|
||||
environment {
|
||||
variables = {
|
||||
REGION = var.region
|
||||
TOKENIZE_URL = "${trimsuffix(aws_apigatewayv2_stage.default.invoke_url, "/")}/tokenize"
|
||||
RESTORE_URL = "${trimsuffix(aws_apigatewayv2_stage.default.invoke_url, "/")}/restore"
|
||||
TOKENIZE_API_KEY = var.tokenize_api_key
|
||||
AGENT_RUNTIME_ARN = var.agent_runtime_arn
|
||||
}
|
||||
}
|
||||
tags = local.onprem_tag
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_integration" "orchestrator" {
|
||||
api_id = aws_apigatewayv2_api.gateway.id
|
||||
integration_type = "AWS_PROXY"
|
||||
integration_uri = aws_lambda_function.orchestrator.invoke_arn
|
||||
payload_format_version = "2.0"
|
||||
}
|
||||
|
||||
resource "aws_apigatewayv2_route" "orchestrator" {
|
||||
api_id = aws_apigatewayv2_api.gateway.id
|
||||
route_key = "POST /demo"
|
||||
target = "integrations/${aws_apigatewayv2_integration.orchestrator.id}"
|
||||
}
|
||||
|
||||
resource "aws_lambda_permission" "orchestrator_apigw" {
|
||||
statement_id = "AllowApiGatewayInvoke"
|
||||
action = "lambda:InvokeFunction"
|
||||
function_name = aws_lambda_function.orchestrator.function_name
|
||||
principal = "apigateway.amazonaws.com"
|
||||
source_arn = "${aws_apigatewayv2_api.gateway.execution_arn}/*/*"
|
||||
}
|
||||
|
||||
output "tokenize_url" { value = "${trimsuffix(aws_apigatewayv2_stage.default.invoke_url, "/")}/tokenize" }
|
||||
output "restore_url" { value = "${trimsuffix(aws_apigatewayv2_stage.default.invoke_url, "/")}/restore" }
|
||||
output "demo_url" { value = "${trimsuffix(aws_apigatewayv2_stage.default.invoke_url, "/")}/demo" }
|
||||
Reference in New Issue
Block a user