Provision the Upload Archive with Terraform (AWS)
This page is the infrastructure-as-code alternative to Part 2 of the
Configure File Upload Capture
guide. It produces the same AWS resources as the console walkthrough there —
a customer-managed KMS key, an S3 bucket configured for SSE-KMS, and a
cross-account IAM role Anzenna assumes — in a single terraform apply.
Before starting, complete Part 1 in the main guide (generate an age recipient keypair). When you finish the apply below, return to the main guide and continue at Part 3 (Register everything in Anzenna).
The Terraform below provisions everything in one shot: a customer-managed
KMS key, an S3 bucket (versioning + SSE-KMS + Bucket Keys + public access
blocked + TLS-only + SSE-KMS-only PutObject), and the cross-account IAM
role Anzenna assumes. Anzenna's federation role only assumes roles whose
name starts with anzenna-upload-, so the role's name is built
from a customer_label you provide.
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
random = { source = "hashicorp/random", version = "~> 3.0" }
}
}
variable "region" {
type = string
default = "us-west-2"
}
variable "customer_label" {
type = string
description = "Short identifier for this deployment. Becomes part of the IAM role, KMS alias, and bucket names. Lowercase alphanumeric or hyphens, 2-32 chars."
validation {
condition = can(regex("^[a-z0-9][a-z0-9-]{0,30}[a-z0-9]$", var.customer_label))
error_message = "customer_label must be lowercase alphanumeric or hyphens, 2-32 chars, not starting/ending with a hyphen."
}
}
variable "federation_role_arn" {
type = string
description = "ARN of the Anzenna federation role that will assume this upload role."
default = "arn:aws:iam::590183739574:role/anzenna-upload-archival"
}
variable "object_key_prefix" {
type = string
default = "uploads/"
}
provider "aws" {
region = var.region
}
data "aws_caller_identity" "current" {}
resource "random_id" "bucket_suffix" {
byte_length = 4
}
resource "random_uuid" "external_id" {}
locals {
name = "anzenna-upload-${var.customer_label}"
bucket_name = "${local.name}-${random_id.bucket_suffix.hex}"
}
# IAM role Anzenna assumes. Trust policy pins the federation role + External ID.
data "aws_iam_policy_document" "upload_trust" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = [var.federation_role_arn]
}
condition {
test = "StringEquals"
variable = "sts:ExternalId"
values = [random_uuid.external_id.result]
}
}
}
resource "aws_iam_role" "upload" {
name = local.name
assume_role_policy = data.aws_iam_policy_document.upload_trust.json
max_session_duration = 3600
}
# Role can only PutObject under the configured prefix, and only with SSE-KMS
# using this bucket's specific CMK. Both conditions are required.
data "aws_iam_policy_document" "upload_permissions" {
statement {
effect = "Allow"
actions = ["s3:PutObject"]
resources = ["${aws_s3_bucket.upload.arn}/${var.object_key_prefix}*"]
condition {
test = "StringEquals"
variable = "s3:x-amz-server-side-encryption"
values = ["aws:kms"]
}
condition {
test = "StringEquals"
variable = "s3:x-amz-server-side-encryption-aws-kms-key-id"
values = [aws_kms_key.upload.arn]
}
}
}
resource "aws_iam_role_policy" "upload" {
name = "put-objects"
role = aws_iam_role.upload.id
policy = data.aws_iam_policy_document.upload_permissions.json
}
# KMS CMK. The key policy must grant the upload role explicit access, in
# addition to the IAM policy above — KMS requires both.
data "aws_iam_policy_document" "kms_policy" {
statement {
sid = "EnableRootPermissions"
effect = "Allow"
actions = ["kms:*"]
resources = ["*"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"]
}
}
statement {
sid = "AllowUploadRoleEncrypt"
effect = "Allow"
actions = ["kms:Encrypt", "kms:GenerateDataKey", "kms:DescribeKey"]
resources = ["*"]
principals {
type = "AWS"
identifiers = [aws_iam_role.upload.arn]
}
}
}
resource "aws_kms_key" "upload" {
description = "Anzenna upload archive (${var.customer_label})"
deletion_window_in_days = 7
enable_key_rotation = true
policy = data.aws_iam_policy_document.kms_policy.json
}
resource "aws_kms_alias" "upload" {
name = "alias/${local.name}"
target_key_id = aws_kms_key.upload.key_id
}
# S3 bucket: versioned, public access blocked, default SSE-KMS + Bucket Keys.
resource "aws_s3_bucket" "upload" {
bucket = local.bucket_name
}
resource "aws_s3_bucket_versioning" "upload" {
bucket = aws_s3_bucket.upload.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_public_access_block" "upload" {
bucket = aws_s3_bucket.upload.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "upload" {
bucket = aws_s3_bucket.upload.id
rule {
bucket_key_enabled = true
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.upload.arn
}
}
}
# Bucket policy: require TLS and reject any PutObject that isn't SSE-KMS.
data "aws_iam_policy_document" "bucket_policy" {
statement {
sid = "DenyInsecureTransport"
effect = "Deny"
actions = ["s3:*"]
resources = [aws_s3_bucket.upload.arn, "${aws_s3_bucket.upload.arn}/*"]
principals {
type = "*"
identifiers = ["*"]
}
condition {
test = "Bool"
variable = "aws:SecureTransport"
values = ["false"]
}
}
statement {
sid = "DenyWrongSSE"
effect = "Deny"
actions = ["s3:PutObject"]
resources = ["${aws_s3_bucket.upload.arn}/*"]
principals {
type = "*"
identifiers = ["*"]
}
condition {
test = "StringNotEqualsIfExists"
variable = "s3:x-amz-server-side-encryption"
values = ["aws:kms"]
}
}
}
resource "aws_s3_bucket_policy" "upload" {
bucket = aws_s3_bucket.upload.id
policy = data.aws_iam_policy_document.bucket_policy.json
}
output "upload_role_arn" {
value = aws_iam_role.upload.arn
}
output "external_id" {
value = random_uuid.external_id.result
sensitive = true
}
output "bucket_name" {
value = aws_s3_bucket.upload.id
}
output "region" {
value = var.region
}
output "kms_key_arn" {
value = aws_kms_key.upload.arn
}
output "object_key_prefix" {
value = var.object_key_prefix
}
Save the file (e.g.
main.tf) and create aterraform.tfvarswith yourcustomer_label:customer_label = "yourorg"Run
terraform init && terraform apply.Read the outputs you'll paste into Anzenna in Part 3 — the External ID is marked sensitive so it requires an explicit fetch:
terraform output terraform output -raw external_id
You're done with AWS. Return to the
main setup guide
and continue at Part 3 (Register everything in Anzenna).
Need help? Contact Anzenna Support at support@anzenna.ai for assistance.