Crafting world-class S3 bucket policies with FsCDK
Precise S3 policies are the backbone of secure data lakes and static sites. This notebook captures the controls recommended by AWS Heroes Ben Kehoe and Yan Cui, plus guidance from the AWS Security Blog series on TLS enforcement and access governance. Use the FsCDK builders below to express guard rails as code and keep your buckets compliant.
Quick start patterns
Each scenario maps to a real-world best practice and references an authoritative resource for deeper study.
#r "../src/bin/Release/net8.0/publish/Amazon.JSII.Runtime.dll"
#r "../src/bin/Release/net8.0/publish/Constructs.dll"
#r "../src/bin/Release/net8.0/publish/Amazon.CDK.Lib.dll"
#r "../src/bin/Release/net8.0/publish/FsCDK.dll"
open FsCDK
open Amazon.CDK
open Amazon.CDK.AWS.S3
open Amazon.CDK.AWS.IAM
Enforce HTTPS-only access
Mirror the AWS Security Blog post “How to enforce TLS for S3” by denying any request that arrives over HTTP. This closes a common compliance gap and is required for SOC2, PCI DSS, and many regional data-protection frameworks.
stack "SecureBucket" {
let! myBucket =
bucket "MyBucket" {
versioned true
encryption BucketEncryption.S3_MANAGED
}
bucketPolicy "SecurePolicy" {
bucket myBucket
denyInsecureTransport
}
}
CloudFront Origin Access Identity
Allow CloudFront to access a private S3 bucket.
open Amazon.CDK.AWS.CloudFront
stack "CloudFrontOrigin" {
let! websiteBucket =
bucket "Website" {
blockPublicAccess BlockPublicAccess.BLOCK_ALL
encryption BucketEncryption.S3_MANAGED
}
// CloudFront Origin Access Identity
let oai = originAccessIdentity "MyOAI" { comment "S3 access for CloudFront" }
bucketPolicy "CloudFrontAccess" {
bucket websiteBucket
allowCloudFrontOAI oai.Identity.Value.OriginAccessIdentityId // CloudFrontOriginAccessIdentityS3CanonicalUserId
denyInsecureTransport
}
}
IP Address Restrictions
Restrict bucket access to specific IP addresses.
stack "IPRestrictedBucket" {
let! privateBucket = bucket "PrivateBucket" { blockPublicAccess BlockPublicAccess.BLOCK_ALL }
bucketPolicy "IPRestriction" {
bucket privateBucket
allowFromIpAddresses [ "203.0.113.0/24"; "198.51.100.0/24" ]
denyInsecureTransport
}
}
Deny Specific IP Addresses
Block access from known malicious IPs.
stack "BlockMaliciousIPs" {
let! publicBucket = bucket "PublicBucket" { () }
bucketPolicy "BlockBadActors" {
bucket publicBucket
denyFromIpAddresses [ "192.0.2.0/24" ]
denyInsecureTransport
}
}
Custom Policy Statements
Add custom policy statements for specific requirements.
stack "CustomPolicy" {
let! dataBucket = bucket "DataBucket" { versioned true }
let readOnlyStatement =
PolicyStatement(
PolicyStatementProps(
Sid = "AllowReadOnly",
Effect = Effect.ALLOW,
Principals = [| AccountPrincipal("123456789012") :> IPrincipal |],
Actions = [| "s3:GetObject"; "s3:ListBucket" |],
Resources = [| dataBucket.BucketArn; dataBucket.BucketArn + "/*" |]
)
)
bucketPolicy "CustomPolicy" {
bucket dataBucket
statement readOnlyStatement
denyInsecureTransport
}
}
Multi-Statement Policy
Combine multiple security controls in one policy.
stack "ComprehensivePolicy" {
let! secureBucket =
bucket "SecureBucket" {
versioned true
encryption BucketEncryption.KMS_MANAGED
}
let adminStatement =
PolicyStatement(
PolicyStatementProps(
Sid = "AdminFullAccess",
Effect = Effect.ALLOW,
Principals = [| ArnPrincipal("arn:aws:iam::123456789012:role/AdminRole") :> IPrincipal |],
Actions = [| "s3:*" |],
Resources = [| secureBucket.BucketArn; secureBucket.BucketArn + "/*" |]
)
)
bucketPolicy "ComprehensivePolicy" {
bucket secureBucket
denyInsecureTransport
allowFromIpAddresses [ "10.0.0.0/8" ]
statement adminStatement
}
}
Best Practices
Security
- ✅ Always deny insecure transport (HTTP)
- ✅ Use principle of least privilege
- ✅ Restrict access by IP when possible
- ✅ Enable bucket versioning with policies
- ✅ Use MFA delete for critical buckets
Operational Excellence
- ✅ Use descriptive Sid values for statements
- ✅ Document policy purpose in comments
- ✅ Test policies before production deployment
- ✅ Version control your policies
Compliance
- ✅ Audit bucket access regularly
- ✅ Enable S3 access logging
- ✅ Use AWS Config to monitor policy changes
- ✅ Implement encryption requirements in policies
Performance
- ✅ Minimize policy complexity
- ✅ Use bucket policies over ACLs
- ✅ Cache policy evaluation results when possible
<summary>Creates an AWS CDK Stack construct.</summary>
<param name="name">The name of the stack.</param>
<code lang="fsharp"> stack "MyStack" { lambda myFunction bucket myBucket } </code>
<summary> Enables or disables versioning for the S3 bucket. **Security Best Practice:** Enable versioning for: - Critical data that requires audit trails - Data subject to compliance requirements (HIPAA, SOC2, etc.) - Production buckets storing business data **Cost Consideration:** Versioning stores all versions of objects, increasing storage costs. Only disable for: - Temporary/cache buckets - Build artifacts with short lifecycle - Development/testing buckets **Default:** false (opt-in for cost optimization) </summary>
<param name="value">True to enable versioning, false to disable.</param>
<param name="config">The current bucket configuration.</param>
<code lang="fsharp"> bucket "production-data" { versioned true // Enable for production } bucket "cache-bucket" { versioned false // Disable for temp data } </code>
<summary>Creates an S3 Bucket Policy with AWS security best practices.</summary>
<param name="name">The policy name.</param>
<code lang="fsharp"> bucketPolicy "MyBucketPolicy" { bucket myBucket denyInsecureTransport allowFromIpAddresses ["203.0.113.0/24"; "198.51.100.0/24"] } </code>
<summary>Sets the bucket for the policy.</summary>
<param name="config">The configuration.</param>
<param name="bucket">The bucket.</param>
<summary>Adds a statement that denies non-HTTPS requests (security best practice).</summary>
type BlockPublicAccess = inherit DeputyBase new: options: IBlockPublicAccessOptions -> unit member BlockPublicAcls: Nullable<bool> member BlockPublicPolicy: Nullable<bool> member IgnorePublicAcls: Nullable<bool> member RestrictPublicBuckets: Nullable<bool> static member BLOCK_ACLS: BlockPublicAccess static member BLOCK_ACLS_ONLY: BlockPublicAccess static member BLOCK_ALL: BlockPublicAccess
--------------------
BlockPublicAccess(options: IBlockPublicAccessOptions) : BlockPublicAccess
<summary>Creates a CloudFront Origin Access Identity.</summary>
<param name="name">The OAI name.</param>
<code lang="fsharp"> originAccessIdentity "MyOAI" { comment "Access to private S3 bucket" } </code>
<summary>Sets a comment for the OAI.</summary>
<summary>Adds a statement that allows CloudFront OAI access.</summary>
<summary>Adds a statement that restricts access to specific IP addresses.</summary>
<summary>Adds a statement that denies access from specific IP addresses.</summary>
type PolicyStatement = inherit DeputyBase new: ?props: IPolicyStatementProps -> unit member AddAccountCondition: accountId: string -> unit member AddAccountRootPrincipal: unit -> unit member AddActions: [<ParamArray>] actions: string array -> unit member AddAllResources: unit -> unit member AddAnyPrincipal: unit -> unit member AddArnPrincipal: arn: string -> unit member AddAwsAccountPrincipal: accountId: string -> unit member AddCanonicalUserPrincipal: canonicalUserId: string -> unit ...
--------------------
PolicyStatement(?props: IPolicyStatementProps) : PolicyStatement
type PolicyStatementProps = interface IPolicyStatementProps new: unit -> unit member Actions: string array member Conditions: IDictionary<string,obj> member Effect: Nullable<Effect> member NotActions: string array member NotPrincipals: IPrincipal array member NotResources: string array member Principals: IPrincipal array member Resources: string array ...
--------------------
PolicyStatementProps() : PolicyStatementProps
type AccountPrincipal = inherit ArnPrincipal new: accountId: obj -> unit member ToString: unit -> string member AccountId: obj member PrincipalAccount: string
--------------------
AccountPrincipal(accountId: obj) : AccountPrincipal
<summary>Adds a policy statement.</summary>
type ArnPrincipal = inherit PrincipalBase new: arn: string -> unit member DedupeString: unit -> string member InOrganization: organizationId: string -> PrincipalBase member ToString: unit -> string member Arn: string member PolicyFragment: PrincipalPolicyFragment
--------------------
ArnPrincipal(arn: string) : ArnPrincipal
FsCDK