sh-openswe-traces/template.yaml

245 lines
9.3 KiB
YAML
Raw Normal View History

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
sh-openswe-traces — LangSmith Bulk Export destination. Storage-only:
KMS-encrypted S3 bucket, a least-privilege IAM writer for LangSmith's
export job, and a Secrets Manager holder for that writer's access key.
No compute — the export schedule is configured on the LangSmith side.
Parameters:
ExportPrefix:
Type: String
Default: langsmith/
Description: S3 key prefix LangSmith writes exports under (also the lifecycle scope).
Resources:
TracesKey:
Type: AWS::KMS::Key
Properties:
Description: SSE-KMS CMK for sh-openswe-traces (LangSmith export archive).
EnableKeyRotation: true
KeyPolicy:
Version: "2012-10-17"
Statement:
# Root-enable so IAM identity policies (below) govern access.
- Sid: EnableIAMPolicies
Effect: Allow
Principal:
AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root"
Action: "kms:*"
Resource: "*"
TracesKeyAlias:
Type: AWS::KMS::Alias
Properties:
AliasName: alias/sh-openswe-traces
TargetKeyId: !Ref TracesKey
TracesBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
# Logging target policy must exist before S3 will accept LoggingConfiguration.
DependsOn: TracesLogBucketPolicy
Properties:
BucketName: sh-openswe-traces
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: aws:kms
KMSMasterKeyID: !Ref TracesKey
BucketKeyEnabled: true
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
# Tamper recovery: the writer key is write-only (no DeleteObject / no
# DeleteObjectVersion), so a malicious or buggy overwrite creates a noncurrent
# version the prior bytes are recoverable from. Exports write new partitioned
# keys, so noncurrent versions are rare — expire them after 90 days.
VersioningConfiguration:
Status: Enabled
LifecycleConfiguration:
Rules:
- Id: archive-exports-to-deep-archive
Status: Enabled
Prefix: !Ref ExportPrefix
Transitions:
- StorageClass: DEEP_ARCHIVE
TransitionInDays: 90
- Id: expire-noncurrent-versions
Status: Enabled
NoncurrentVersionExpiration:
NoncurrentDays: 90
- Id: abort-incomplete-multipart
Status: Enabled
AbortIncompleteMultipartUpload:
DaysAfterInitiation: 7
LoggingConfiguration:
DestinationBucketName: !Ref TracesLogBucket
LogFilePrefix: s3-access/
TracesBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref TracesBucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: DenyInsecureTransport
Effect: Deny
Principal: "*"
Action: "s3:*"
Resource:
- !GetAtt TracesBucket.Arn
- !Sub "${TracesBucket.Arn}/*"
Condition:
Bool:
"aws:SecureTransport": "false"
# 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 (absent key
# evaluates true). Encryption is instead guaranteed by the bucket DEFAULT
# (SSE-KMS with our CMK, applied to every header-less put) plus S3's baseline
# (no object is ever stored unencrypted). If LangSmith explicitly requests
# AES256, that object lands as SSE-S3 rather than CMK — verify post-write
# (head-object) and decide CMK-vs-SSE-S3 if so.
# Server access logging target for TracesBucket — read attribution for the
# secret-bearing archive (the org trail logs no S3 data events). SSE-S3 only:
# S3 log delivery cannot write to an SSE-KMS bucket.
TracesLogBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
BucketName: sh-openswe-traces-logs
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
LifecycleConfiguration:
Rules:
- Id: expire-access-logs
Status: Enabled
ExpirationInDays: 365
Tags:
- Key: project
Value: sh-openswe-traces
- Key: role
Value: s3-access-logs
TracesLogBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref TracesLogBucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: S3ServerAccessLogsWrite
Effect: Allow
Principal:
Service: logging.s3.amazonaws.com
Action: "s3:PutObject"
Resource: !Sub "${TracesLogBucket.Arn}/s3-access/*"
Condition:
ArnLike:
# literal (not !GetAtt) to avoid a cycle with TracesBucket's DependsOn
"aws:SourceArn": "arn:aws:s3:::sh-openswe-traces"
StringEquals:
"aws:SourceAccount": !Ref "AWS::AccountId"
- Sid: DenyInsecureTransport
Effect: Deny
Principal: "*"
Action: "s3:*"
Resource:
- !GetAtt TracesLogBucket.Arn
- !Sub "${TracesLogBucket.Arn}/*"
Condition:
Bool:
"aws:SecureTransport": "false"
# No explicit UserName: an IAM name would require CAPABILITY_NAMED_IAM, but the
# standard cd-sam.yaml reusable workflow deploys with CAPABILITY_IAM only. The
# principal is referenced by ARN (in the secret + LangSmith config), not by name;
# the tag carries the human-facing identifier.
LangSmithExportUser:
Type: AWS::IAM::User
Properties:
Tags:
- Key: Name
Value: sh-openswe-langsmith-export
- Key: purpose
Value: langsmith-bulk-export-writer
Policies:
- PolicyName: langsmith-export-put
PolicyDocument:
Version: "2012-10-17"
Statement:
# Bucket-wide (not prefix-scoped): LangSmith's destination-creation
# validation writes a test object whose key is NOT guaranteed to be
# under ExportPrefix (docs reference a /tmp path), and its documented
# policy scopes PutObject to the whole bucket. This bucket is
# single-purpose, so bucket-wide write is still tightly bounded.
# Deliberately NO s3:GetObject / s3:DeleteObject (both optional per
# LangSmith): omitting them keeps the writer write-only (no exfil,
# no delete). Trade-off: LangSmith skips post-write size verification
# and leaves its small test object behind (harmless).
- Sid: PutExportObjects
Effect: Allow
Action:
- "s3:PutObject"
- "s3:AbortMultipartUpload"
Resource: !Sub "${TracesBucket.Arn}/*"
- Sid: EncryptWithBucketKey
Effect: Allow
Action:
- "kms:GenerateDataKey"
- "kms:Encrypt"
# Required by S3 at CompleteMultipartUpload for SSE-KMS. Safe: the
# writer has no s3:GetObject, so there is no object to decrypt/exfil.
- "kms:Decrypt"
Resource: !GetAtt TracesKey.Arn
# Usable only through S3 — blocks a leaked key from calling kms:Decrypt
# directly against arbitrary ciphertext under this CMK.
Condition:
StringEquals:
"kms:ViaService": !Sub "s3.${AWS::Region}.amazonaws.com"
# Holder only — the real access key is minted post-deploy and written in
# with `aws secretsmanager put-secret-value` (see README). Never in the template.
ExportKeySecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: sh-openswe/langsmith-export-s3
Description: >
Access key for the sh-openswe-langsmith-export IAM user, consumed by
LangSmith Bulk Export. Populated out-of-band post-deploy; rotate quarterly.
# Encrypted with the aws/secretsmanager managed key — deliberately NOT the
# trace CMK, so the credential and the data it protects never share a key the
# writer principal holds any KMS grant on.
Outputs:
BucketName:
Value: !Ref TracesBucket
BucketArn:
Value: !GetAtt TracesBucket.Arn
KmsKeyArn:
Value: !GetAtt TracesKey.Arn
ExportUserName:
Value: !Ref LangSmithExportUser
ExportKeySecretName:
Value: sh-openswe/langsmith-export-s3