resource "aws_s3_bucket" "logs" { bucket = local.logs_bucket_name tags = { role = "s3-access-logs" } lifecycle { prevent_destroy = true } } resource "aws_s3_bucket_public_access_block" "logs" { bucket = aws_s3_bucket.logs.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } resource "aws_s3_bucket_ownership_controls" "logs" { bucket = aws_s3_bucket.logs.id rule { object_ownership = "BucketOwnerEnforced" } } resource "aws_s3_bucket_server_side_encryption_configuration" "logs" { bucket = aws_s3_bucket.logs.id rule { apply_server_side_encryption_by_default { # SSE-S3 only: S3 log delivery cannot write to an SSE-KMS bucket. sse_algorithm = "AES256" } } } resource "aws_s3_bucket_lifecycle_configuration" "logs" { bucket = aws_s3_bucket.logs.id rule { id = "expire-access-logs" status = "Enabled" filter {} expiration { days = 365 } } } resource "aws_s3_bucket_policy" "logs" { bucket = aws_s3_bucket.logs.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "S3ServerAccessLogsWrite" Effect = "Allow" Principal = { Service = "logging.s3.amazonaws.com" } Action = "s3:PutObject" Resource = "${aws_s3_bucket.logs.arn}/s3-access/*" Condition = { ArnLike = { "aws:SourceArn" = aws_s3_bucket.traces.arn } StringEquals = { "aws:SourceAccount" = local.account_id } } }, { Sid = "DenyInsecureTransport" Effect = "Deny" Principal = "*" Action = "s3:*" Resource = [ aws_s3_bucket.logs.arn, "${aws_s3_bucket.logs.arn}/*", ] Condition = { Bool = { "aws:SecureTransport" = "false" } } } ] }) } resource "aws_s3_bucket" "traces" { bucket = local.traces_bucket_name lifecycle { prevent_destroy = true } } resource "aws_s3_bucket_public_access_block" "traces" { bucket = aws_s3_bucket.traces.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } resource "aws_s3_bucket_ownership_controls" "traces" { bucket = aws_s3_bucket.traces.id rule { object_ownership = "BucketOwnerEnforced" } } resource "aws_s3_bucket_versioning" "traces" { bucket = aws_s3_bucket.traces.id versioning_configuration { status = "Enabled" } } resource "aws_s3_bucket_server_side_encryption_configuration" "traces" { bucket = aws_s3_bucket.traces.id rule { apply_server_side_encryption_by_default { sse_algorithm = "aws:kms" kms_master_key_id = aws_kms_key.traces.arn } bucket_key_enabled = true } } resource "aws_s3_bucket_lifecycle_configuration" "traces" { bucket = aws_s3_bucket.traces.id rule { id = "archive-exports-to-deep-archive" status = "Enabled" filter { prefix = var.export_prefix } transition { days = 90 storage_class = "DEEP_ARCHIVE" } } rule { id = "expire-noncurrent-versions" status = "Enabled" filter {} noncurrent_version_expiration { noncurrent_days = 90 } } rule { id = "abort-incomplete-multipart" status = "Enabled" filter {} abort_incomplete_multipart_upload { days_after_initiation = 7 } } } resource "aws_s3_bucket_logging" "traces" { bucket = aws_s3_bucket.traces.id target_bucket = aws_s3_bucket.logs.id target_prefix = "s3-access/" depends_on = [aws_s3_bucket_policy.logs] } resource "aws_s3_bucket_policy" "traces" { bucket = aws_s3_bucket.traces.id # No SSE-header enforcement Deny. LangSmith's exporter does not send an # "aws:kms" SSE header, so a "must be aws:kms" Deny blocks its writes — and # StringNotEqualsIfExists on a Deny also blocks header-less puts. Encryption # is guaranteed by the bucket DEFAULT (SSE-KMS with our CMK). policy = jsonencode({ Version = "2012-10-17" Statement = [ { Sid = "DenyInsecureTransport" Effect = "Deny" Principal = "*" Action = "s3:*" Resource = [ aws_s3_bucket.traces.arn, "${aws_s3_bucket.traces.arn}/*", ] Condition = { Bool = { "aws:SecureTransport" = "false" } } } ] }) }