Header menu logo FsCDK

S3 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

Operational Excellence

Compliance

Performance

namespace FsCDK
namespace Amazon
namespace Amazon.CDK
namespace Amazon.CDK.AWS
namespace Amazon.CDK.AWS.S3
namespace Amazon.CDK.AWS.IAM
val stack: name: string -> StackBuilder
<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>
val myBucket: IBucket
val bucket: name: string -> BucketBuilder
custom operation: versioned (bool) Calls BucketBuilder.Versioned
<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>
custom operation: encryption (BucketEncryption) Calls BucketBuilder.Encryption
[<Struct>] type BucketEncryption = | UNENCRYPTED = 0 | KMS_MANAGED = 1 | S3_MANAGED = 2 | KMS = 3 | DSSE_MANAGED = 4 | DSSE = 5
field BucketEncryption.S3_MANAGED: BucketEncryption = 2
val bucketPolicy: name: string -> BucketPolicyBuilder
<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>
custom operation: bucket (IBucket) Calls BucketPolicyBuilder.Bucket
<summary>Sets the bucket for the policy.</summary>
<param name="config">The configuration.</param>
<param name="bucket">The bucket.</param>
custom operation: denyInsecureTransport Calls BucketPolicyBuilder.DenyInsecureTransport
<summary>Adds a statement that denies non-HTTPS requests (security best practice).</summary>
namespace Amazon.CDK.AWS.CloudFront
val websiteBucket: IBucket
custom operation: blockPublicAccess (BlockPublicAccess) Calls BucketBuilder.BlockPublicAccess
Multiple items
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
property BlockPublicAccess.BLOCK_ALL: BlockPublicAccess with get
val oai: OriginAccessIdentitySpec
val originAccessIdentity: name: string -> OriginAccessIdentityBuilder
<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>
custom operation: comment (string) Calls OriginAccessIdentityBuilder.Comment
<summary>Sets a comment for the OAI.</summary>
custom operation: allowCloudFrontOAI (string) Calls BucketPolicyBuilder.AllowCloudFrontOAI
<summary>Adds a statement that allows CloudFront OAI access.</summary>
OriginAccessIdentitySpec.Identity: IOriginAccessIdentity option
property Option.Value: IOriginAccessIdentity with get
property IOriginAccessIdentity.OriginAccessIdentityId: string with get
val privateBucket: IBucket
custom operation: allowFromIpAddresses (string list) Calls BucketPolicyBuilder.AllowFromIpAddresses
<summary>Adds a statement that restricts access to specific IP addresses.</summary>
val publicBucket: IBucket
custom operation: denyFromIpAddresses (string list) Calls BucketPolicyBuilder.DenyFromIpAddresses
<summary>Adds a statement that denies access from specific IP addresses.</summary>
val dataBucket: IBucket
val readOnlyStatement: PolicyStatement
Multiple items
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
Multiple items
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
[<Struct>] type Effect = | ALLOW = 0 | DENY = 1
field Effect.ALLOW: Effect = 0
Multiple items
type AccountPrincipal = inherit ArnPrincipal new: accountId: obj -> unit member ToString: unit -> string member AccountId: obj member PrincipalAccount: string

--------------------
AccountPrincipal(accountId: obj) : AccountPrincipal
type IPrincipal = inherit IGrantable override AddToPrincipalPolicy: statement: PolicyStatement -> IAddToPrincipalPolicyResult member AssumeRoleAction: string member PolicyFragment: PrincipalPolicyFragment member PrincipalAccount: string
property IBucket.BucketArn: string with get
custom operation: statement (PolicyStatement) Calls BucketPolicyBuilder.Statement
<summary>Adds a policy statement.</summary>
val secureBucket: IBucket
field BucketEncryption.KMS_MANAGED: BucketEncryption = 1
val adminStatement: PolicyStatement
Multiple items
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

Type something to start searching.