diff --git a/.github/workflows/lambda.yml b/.github/workflows/lambda.yml
new file mode 100644
index 00000000..d254d5ec
--- /dev/null
+++ b/.github/workflows/lambda.yml
@@ -0,0 +1,49 @@
+name: Build Lambda Package
+
+on: workflow_dispatch
+
+jobs:
+ build:
+ runs-on: ubuntu-24.04-arm
+
+ container:
+ image: public.ecr.aws/codebuild/amazonlinux2-aarch64-standard:3.0
+
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@v4
+
+ - name: Install development packages
+ run: sudo yum install -y krb5-devel openldap-devel
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Install Cargo Lambda
+ uses: jaxxstorm/action-install-gh-release@v1.9.0
+ with:
+ repo: cargo-lambda/cargo-lambda
+ platform: linux
+ arch: aarch64
+
+ - name: Setup rust Cache
+ uses: Swatinem/rust-cache@v2
+
+ - name: Build with Cargo
+ run: cargo lambda build --verbose
+
+ - name: Copy libpq and its dependencies
+ run: cp /lib64/{libcrypt.so.2,liblber-2.4.so.2,libldap_r-2.4.so.2,libpq.so.5,libsasl2.so.3} target/lambda/vaultwarden/
+
+ # This ensures passes the startup checks for the web-vault, which is
+ # instead served statically from an S3 Bucket
+ - name: Create placeholder web-vault/index.html
+ run: |-
+ mkdir target/lambda/vaultwarden/web-vault
+ echo "
Web Vault Placeholder
" > target/lambda/vaultwarden/web-vault/index.html
+
+ - name: Archive function package
+ uses: actions/upload-artifact@v4
+ with:
+ name: vaultwarden-lambda
+ path: target/lambda/vaultwarden/*
\ No newline at end of file
diff --git a/CargoLambda.toml b/CargoLambda.toml
new file mode 100644
index 00000000..c7ce819e
--- /dev/null
+++ b/CargoLambda.toml
@@ -0,0 +1,7 @@
+[build]
+features = ["aws"]
+release = true
+arm64 = true
+
+[build.compiler]
+type = "cargo"
\ No newline at end of file
diff --git a/aws/.gitignore b/aws/.gitignore
new file mode 100644
index 00000000..a0b96b53
--- /dev/null
+++ b/aws/.gitignore
@@ -0,0 +1,3 @@
+.aws-sam
+vaultwarden-lambda.zip
+web-vault
diff --git a/aws/README.md b/aws/README.md
new file mode 100644
index 00000000..b123641d
--- /dev/null
+++ b/aws/README.md
@@ -0,0 +1,53 @@
+# AWS Serverless Deployment Instructions
+
+## Architecture
+```
+CloudFront CDN
+├─ API Lambda Function
+│ ├─ Data S3 Bucket
+│ ├─ Aurora DSQL Database
+│ └─ Amazon Simple Email Service (SES)
+└─ Web-vault static assets S3 Bucket
+```
+
+## A Note On AWS Accounts and Security
+It is common to have one AWS account host multiple services. But it's easy, and doesn't cost any additional amount, to separate workloads into their own accounts. Doing so makes it easier to control for security concerns and monitor costs. AWS Identity and Access Management (IAM) enforces additional controls for cross-account access than for within-account access, for example, making it harder for security attacks to hop from workload to workload when they are in separate accounts.
+
+Given the confidential nature of data stored in Vaultwarden, it is *highly* recommended that you create a new, separate AWS account just for Vaultwarden. If you only have one account, investigate creating an [AWS Organization](https://aws.amazon.com/organizations/) to make it easy to create a second account tied to the same billing and account management mechanism, and investigate creating an [AWS IAM Identity Center](https://aws.amazon.com/iam/identity-center/) instance for easy SSO access across your accounts.
+
+## Initial Deployment
+1. Create an AWS account
+1. Install the AWS CLI
+1. Install AWS SAM CLI
+1. Download the vaultwarden-lambda.zip Lambda Function code package (e.g. from POC GHA artifact from run https://github.com/txase/vaultwarden/actions/runs/13315966383) to this directory
+1. Pick a region that supports DSQL to deploy the Vaultwarden application into (must be one of us-east-1 or us-east-2 during DSQL Preview)
+1. Create an Aurora DSQL Cluster in the region using the AWS Console (this will be automated when CloudFormation ships DSQL support at GA)
+1. Setup local AWS configuration to access account and region from CLI
+1. Copy DSQL Cluster ID
+1. Run `./deploy.sh` in this directory
+ * Most parameters can be skipped at first, but you must provide the `DSQLClusterID` parameter value.
+1. Note the "Output" values from the deploy command
+ * These can also be retrieved later by running `sam list stack-outputs`
+1. Download the latest [web-vault build](https://github.com/dani-garcia/bw_web_builds/releases) and extract it
+1. Sync the web-vault build contents into the WebVaultAssetsBucket:
+ * Inside the web-vault build folder run `aws s3 sync . s3://`, where `WebVaultAssetsBucket` is a stack output value
+1. You can now navigate to your instance at the location of your `CDNDomain` stack output value
+
+## Custom Domain
+1. Create an AWS Certificate Manager (ACM) Certificate for your domain **in the us-east-1 region**
+ * There are many tutorials and/or automated ways to do this, including following the official docs [here](https://docs.aws.amazon.com/acm/latest/userguide/acm-public-certificates.html)
+ * It must be in the us-east-1 region because CloudFront only supports certificates from us-east-1
+ * Use key algorithm RSA 2048
+ * Continue to the next step once the certificate is in the *Issued* state
+ * Note the certificate's ARN
+1. Run `./deploy.sh` again and add the following parameter values:
+ * **Domain**: `https://`
+ * **ACMCertificateArn**: The ARN of the certificate you created for the domain
+1. Create a CNAME record for the custom domain set to the value of the CDNDomain stack output
+
+## Email via AWS Simple Email Service (SES)
+Email is complicated. These instructions will not attempt to walk you through setting up SES identities for sending email. You may find docs and guides online for how to do this.
+
+In order for Vaultwarden to send emails using SES you must have an SES Email Address Identity that **does not have a default configuration set**. An identity with a default configuration set breaks the IAM permission model set up for the Vaultwarden API Function.
+
+Once you have an SES Identity for the sending email address, run `./deploy.sh` again and provide the email address in the `SMTP_FROM` parameter.
\ No newline at end of file
diff --git a/aws/deploy.sh b/aws/deploy.sh
new file mode 100755
index 00000000..b5c86178
--- /dev/null
+++ b/aws/deploy.sh
@@ -0,0 +1,9 @@
+#!/bin/sh -e
+
+echo 'Building template...'
+
+sam build
+
+echo ''
+
+sam deploy --guided
diff --git a/aws/samconfig.toml b/aws/samconfig.toml
new file mode 100644
index 00000000..e4804022
--- /dev/null
+++ b/aws/samconfig.toml
@@ -0,0 +1,12 @@
+version = 0.1
+
+[default.global.parameters]
+stack_name = "vaultwarden"
+
+[default.deploy.parameters]
+resolve_s3 = true
+s3_prefix = "vaultwarden"
+confirm_changeset = true
+capabilities = "CAPABILITY_IAM"
+image_repositories = []
+disable_rollback = true
\ No newline at end of file
diff --git a/aws/template.yaml b/aws/template.yaml
new file mode 100644
index 00000000..376e30f4
--- /dev/null
+++ b/aws/template.yaml
@@ -0,0 +1,582 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Description: AWS CloudFormation template for running VaultWarden on AWS serverless services.
+
+Parameters:
+ Domain:
+ Type: String
+ Description: >-
+ The domain name for the Vaultwarden instance (e.g. https://example.com). If this parameter or the ACMCertificateArn
+ parameter are left empty, the Vaultwarden instance can still be reached at the output CDN domain
+ (e.g. https://xxxxxxxx.cloudfront.net).
+ AllowedPattern: (https://[a-z0-9.-]+|)
+ Default: ''
+ ACMCertificateArn:
+ Type: String
+ Description: The ARN of a us-east-1 ACM certificate to use for the domain. Required if the `Domain` parameter is set.
+ AllowedPattern: (arn:aws:acm:us-east-1:[0-9]+:certificate/[0-9a-f-]+|)
+ Default: ''
+ DSQLClusterId:
+ Type: String
+ Description: The endpoint of the DSQL database.
+ AllowedPattern: '[a-z0-9]+'
+ APILogRetention:
+ Type: Number
+ Description: The number of days to retain the API logs. -1 means to never expire.
+ Default: -1
+ AllowedValues: [-1, 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1096, 1827, 2192, 2557, 2922, 3288, 3653]
+ SignupsAllowed:
+ Type: String
+ Description: Controls if new users can register
+ Default: 'true'
+ AllowedValues: ['true', 'false']
+ IconService:
+ Type: String
+ Description: Allowed icon service sources.
+ Default: bitwarden
+ AdminToken:
+ Type: String
+ Description: Token for the admin interface, preferably an Argon2 PCH string. If empty, the admin interface will be disabled.
+ Default: ''
+ SMTPFrom:
+ Type: String
+ Description: The email address to send emails from. Email service is disabled if this value is empty.
+ Default: ''
+ SMTPFromName:
+ Type: String
+ Description: The name to send emails from.
+ Default: Vaultwarden
+
+Mappings:
+ IconSource:
+ internal:
+ CSP: ''
+ bitwarden:
+ CSP: https://icons.bitwarden.net/
+ duckduckgo:
+ CSP: https://icons.duckduckgo.com/ip3/
+ google:
+ CSP: https://www.google.com/s2/favicons https://*.gstatic.com/favicon
+
+Conditions:
+ IsDomainAndCertificateSet: !And
+ - !Not [!Equals [!Ref Domain, '']]
+ - !Not [!Equals [!Ref ACMCertificateArn, '']]
+ IsApiLogRetentionNeverExpire: !Equals
+ - !Ref APILogRetention
+ - -1
+ IconSourceIsPredefined: !Or
+ - !Equals [!Ref IconService, internal]
+ - !Equals [!Ref IconService, bitwarden]
+ - !Equals [!Ref IconService, duckduckgo]
+ - !Equals [!Ref IconService, google]
+ IsAdminTokenEmpty: !Equals
+ - !Ref AdminToken
+ - ''
+ IsEmailEnabled: !Not
+ - !Equals
+ - !Ref SMTPFrom
+ - ''
+
+Resources:
+ DataBucket:
+ Type: AWS::S3::Bucket
+ Properties:
+ BucketEncryption:
+ ServerSideEncryptionConfiguration:
+ - BucketKeyEnabled: true
+ ServerSideEncryptionByDefault:
+ SSEAlgorithm: aws:kms
+ BucketName: !Sub ${AWS::StackName}-${AWS::AccountId}-${AWS::Region}-data
+ CorsConfiguration:
+ CorsRules:
+ - AllowedMethods:
+ - GET
+ - HEAD
+ AllowedOrigins:
+ - '*'
+ LifecycleConfiguration:
+ Rules:
+ - AbortIncompleteMultipartUpload:
+ DaysAfterInitiation: 2
+ ExpiredObjectDeleteMarker: true
+ NoncurrentVersionExpiration:
+ NoncurrentDays: 30
+ Status: Enabled
+ PublicAccessBlockConfiguration:
+ BlockPublicAcls: true
+ BlockPublicPolicy: true
+ IgnorePublicAcls: true
+ RestrictPublicBuckets: true
+ VersioningConfiguration:
+ Status: Enabled
+
+ DataBucketEnforceEncryptionAndStorageTier:
+ Type: AWS::S3::BucketPolicy
+ Properties:
+ Bucket: !Ref DataBucket
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Sid: DenyUnencryptedObjectUploads
+ Effect: Deny
+ Principal: '*'
+ Action: s3:PutObject
+ Resource: !Sub arn:${AWS::Partition}:s3:::${DataBucket}/*
+ Condition:
+ 'Null':
+ s3:x-amz-server-side-encryption-aws-kms-key-id: true
+ - Sid: DenyUnencryptedTransit
+ Effect: Deny
+ Principal: '*'
+ Action: s3:*
+ Resource:
+ - !Sub arn:${AWS::Partition}:s3:::${DataBucket}
+ - !Sub arn:${AWS::Partition}:s3:::${DataBucket}/*
+ Condition:
+ Bool:
+ aws:SecureTransport: false
+ - Sid: DenyNonIntelligentTieringStorageClass
+ Effect: Deny
+ Principal: '*'
+ Action: s3:PutObject
+ Resource: !Sub arn:aws:s3:::${DataBucket}/*
+ Condition:
+ StringNotEquals:
+ s3:x-amz-storage-class: INTELLIGENT_TIERING
+
+ ApiFunctionRole:
+ Type: AWS::IAM::Role
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Action: sts:AssumeRole
+ Effect: Allow
+ Principal:
+ Service: lambda.amazonaws.com
+ ManagedPolicyArns:
+ - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
+ Policies:
+ - PolicyName: AccessAWSServices
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Effect: Allow
+ Action:
+ - s3:GetObject
+ - s3:ListBucket
+ - s3:PutObject
+ - s3:DeleteObject
+ Resource:
+ - !Sub arn:${AWS::Partition}:s3:::${DataBucket}
+ - !Sub arn:${AWS::Partition}:s3:::${DataBucket}/*
+ - Effect: Allow
+ Action: dsql:DbConnectAdmin
+ Resource: !Sub arn:${AWS::Partition}:dsql:${AWS::Region}:${AWS::AccountId}:cluster/${DSQLClusterId}
+ - !If
+ - IsEmailEnabled
+ - Effect: Allow
+ Action: ses:SendRawEmail
+ Resource: '*'
+ Condition:
+ StringEquals:
+ ses:FromAddress: !Ref SMTPFrom
+ ses:FromDisplayName: !Ref SMTPFromName
+ - !Ref AWS::NoValue
+
+ ApiFunction:
+ Type: AWS::Lambda::Function
+ Properties:
+ Architectures:
+ - arm64
+ Code: ./vaultwarden-lambda.zip
+ Environment:
+ Variables:
+ AWS_LWA_PORT: 8000
+ AWS_LWA_READINESS_CHECK_PATH: /alive
+ AWS_LWA_ASYNC_INIT: true
+ AWS_LWA_ENABLE_COMPRESSION: true
+ AWS_LWA_INVOKE_MODE: RESPONSE_STREAM
+ DATA_FOLDER: !Sub s3://${DataBucket}
+ TMP_FOLDER: /tmp
+ DATABASE_URL: !Sub dsql://${DSQLClusterId}.dsql.${AWS::Region}.on.aws
+ ENABLE_WEBSOCKET: false
+ DOMAIN: !If
+ - IsDomainAndCertificateSet
+ - !Ref Domain
+ - !Ref AWS::NoValue
+ SIGNUPS_ALLOWED: !Ref SignupsAllowed
+ IP_HEADER: X-Forwarded-For
+ ICON_SERVICE: !Ref IconService
+ ICON_REDIRECT_CODE: 301
+ ADMIN_TOKEN: !If
+ - IsAdminTokenEmpty
+ - !Ref AWS::NoValue
+ - !Ref AdminToken
+ SMTP_FROM: !If
+ - IsEmailEnabled
+ - !Ref SMTPFrom
+ - !Ref AWS::NoValue
+ SMTP_FROM_NAME: !Ref SMTPFromName
+ USE_AWS_SES: true
+ FunctionName: !Sub ${AWS::StackName}-api
+ Handler: bootstrap
+ Layers:
+ - !Sub arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerArm64:24
+ MemorySize: 3008 # Maximum value allowed for new accounts, higher value reduces cold start times, should still fit under free tier usage for personal use
+ Role: !GetAtt ApiFunctionRole.Arn
+ Runtime: provided.al2023
+ Timeout: 300
+
+ ApiFunctionLogs:
+ Type: AWS::Logs::LogGroup
+ DeletionPolicy: RetainExceptOnCreate
+ Properties:
+ LogGroupName: !Sub /aws/lambda/${ApiFunction}
+ RetentionInDays: !If
+ - IsApiLogRetentionNeverExpire
+ - !Ref AWS::NoValue
+ - !Ref APILogRetention
+
+ ApiFunctionUrl:
+ Type: AWS::Lambda::Url
+ Properties:
+ TargetFunctionArn: !Ref ApiFunction
+ AuthType: NONE
+ InvokeMode: RESPONSE_STREAM
+
+ ApiFunctionUrlPublicPermissions:
+ Type: AWS::Lambda::Permission
+ Properties:
+ Action: lambda:InvokeFunctionUrl
+ FunctionName: !Ref ApiFunction
+ Principal: '*'
+ FunctionUrlAuthType: NONE
+
+ WebVaultAssetsBucket:
+ Type: AWS::S3::Bucket
+ Properties:
+ BucketName: !Sub ${AWS::StackName}-${AWS::AccountId}-${AWS::Region}-web-vault
+ PublicAccessBlockConfiguration:
+ BlockPublicAcls: true
+ BlockPublicPolicy: true
+ IgnorePublicAcls: true
+ RestrictPublicBuckets: true
+
+ WebVaultAssetsBucketEnforceEncryptionInTransitAndStorageTier:
+ Type: AWS::S3::BucketPolicy
+ Properties:
+ Bucket: !Ref DataBucket
+ PolicyDocument:
+ Version: '2012-10-17'
+ Statement:
+ - Sid: DenyUnencryptedTransit
+ Effect: Deny
+ Principal: '*'
+ Action: s3:*
+ Resource:
+ - !Sub arn:${AWS::Partition}:s3:::${DataBucket}
+ - !Sub arn:${AWS::Partition}:s3:::${DataBucket}/*
+ Condition:
+ Bool:
+ aws:SecureTransport: false
+ - Sid: DenyNonIntelligentTieringStorageClass
+ Effect: Deny
+ Principal: '*'
+ Action: s3:PutObject
+ Resource: !Sub arn:aws:s3:::${DataBucket}/*
+ Condition:
+ StringNotEquals:
+ s3:x-amz-storage-class: INTELLIGENT_TIERING
+
+ WebVaultAssetsBucketOriginAccessControl:
+ Type: AWS::CloudFront::OriginAccessControl
+ Properties:
+ OriginAccessControlConfig:
+ Name: !Sub ${AWS::StackName}-${AWS::Region}-web-vault-access-control
+ OriginAccessControlOriginType: s3
+ SigningBehavior: always
+ SigningProtocol: sigv4
+
+ # The following mirrors the header values in util.rs
+ ResponseHeaderPolicy:
+ Type: AWS::CloudFront::ResponseHeadersPolicy
+ Properties:
+ ResponseHeadersPolicyConfig:
+ Name: !Sub ${AWS::StackName}-${AWS::Region}
+ CustomHeadersConfig:
+ Items:
+ - Header: Cache-Control
+ Override: false
+ Value: no-cache, no-store, max-age=0
+ - Header: X-Robots-Tag
+ Override: true
+ Value: noindex, nofollow
+ - Header: Permissions-Policy
+ Override: true
+ Value: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()
+ SecurityHeadersConfig:
+ ContentSecurityPolicy:
+ ContentSecurityPolicy: !Sub
+ - >-
+ default-src 'self';
+ base-uri 'self';
+ form-action 'self';
+ object-src 'self' blob:;
+ script-src 'self' 'wasm-unsafe-eval';
+ style-src 'self' 'unsafe-inline';
+ child-src 'self' https://*.duosecurity.com https://*.duofederal.com;
+ frame-src 'self' https://*.duosecurity.com https://*.duofederal.com;
+ frame-ancestors 'self'
+ chrome-extension://nngceckbapebfimnlniiiahkandclblb
+ chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh
+ moz-extension://*;
+ img-src 'self' data:
+ https://haveibeenpwned.com
+ ${IconServiceCSP};
+ connect-src 'self'
+ https://api.pwnedpasswords.com
+ https://api.2fa.directory
+ https://app.simplelogin.io/api/
+ https://app.addy.io/api/
+ https://api.fastmail.com/
+ https://api.forwardemail.net
+ https://${DataBucket.RegionalDomainName};
+ - IconServiceCSP: !If
+ - IconSourceIsPredefined
+ - !FindInMap [IconSource, !Ref IconService, CSP]
+ - !Select
+ - 0
+ - !Split ['{', !Ref IconService]
+ Override: true
+ ContentTypeOptions:
+ Override: true
+ FrameOptions:
+ FrameOption: SAMEORIGIN
+ Override: true
+ ReferrerPolicy:
+ Override: true
+ ReferrerPolicy: same-origin
+ StrictTransportSecurity:
+ AccessControlMaxAgeSec: 63072000
+ IncludeSubdomains: true
+ Override: true
+ Preload: true
+ XSSProtection:
+ Override: true
+ Protection: false
+
+ # The following mirrors the header values in util.rs
+ ConnectorHtmlResponseHeaderPolicy:
+ Type: AWS::CloudFront::ResponseHeadersPolicy
+ Properties:
+ ResponseHeadersPolicyConfig:
+ Name: !Sub ${AWS::StackName}-${AWS::Region}-connector-html
+ CustomHeadersConfig:
+ Items:
+ - Header: Cache-Control
+ Override: true
+ Value: no-cache, no-store, max-age=0
+ - Header: X-Robots-Tag
+ Override: true
+ Value: noindex, nofollow
+ - Header: Permissions-Policy
+ Override: true
+ Value: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()
+ SecurityHeadersConfig:
+ ContentTypeOptions:
+ Override: true
+ ReferrerPolicy:
+ Override: true
+ ReferrerPolicy: same-origin
+ StrictTransportSecurity:
+ AccessControlMaxAgeSec: 63072000
+ IncludeSubdomains: true
+ Override: true
+ Preload: true
+ XSSProtection:
+ Override: true
+ Protection: false
+
+ CDN:
+ Type: AWS::CloudFront::Distribution
+ Properties:
+ DistributionConfig:
+ Aliases: !If
+ - IsDomainAndCertificateSet
+ - - !Select
+ - 2
+ - !Split
+ - /
+ - !Ref Domain
+ - !Ref AWS::NoValue
+ CacheBehaviors:
+ - AllowedMethods:
+ - DELETE
+ - HEAD
+ - GET
+ - OPTIONS
+ - PATCH
+ - POST
+ - PUT
+ CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
+ Compress: true
+ OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac # AllViewerExceptHostHeader
+ PathPattern: /api/*
+ ResponseHeadersPolicyId: !Ref ResponseHeaderPolicy
+ TargetOriginId: Api
+ ViewerProtocolPolicy: redirect-to-https
+ - AllowedMethods:
+ - DELETE
+ - HEAD
+ - GET
+ - OPTIONS
+ - PATCH
+ - POST
+ - PUT
+ CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
+ Compress: true
+ OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac # AllViewerExceptHostHeader
+ PathPattern: /admin
+ ResponseHeadersPolicyId: !Ref ResponseHeaderPolicy
+ TargetOriginId: Api
+ ViewerProtocolPolicy: redirect-to-https
+ - AllowedMethods:
+ - DELETE
+ - HEAD
+ - GET
+ - OPTIONS
+ - PATCH
+ - POST
+ - PUT
+ CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
+ Compress: true
+ OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac # AllViewerExceptHostHeader
+ PathPattern: /admin/*
+ ResponseHeadersPolicyId: !Ref ResponseHeaderPolicy
+ TargetOriginId: Api
+ ViewerProtocolPolicy: redirect-to-https
+ - AllowedMethods:
+ - DELETE
+ - HEAD
+ - GET
+ - OPTIONS
+ - PATCH
+ - POST
+ - PUT
+ CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
+ Compress: true
+ OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac # AllViewerExceptHostHeader
+ PathPattern: /events/*
+ ResponseHeadersPolicyId: !Ref ResponseHeaderPolicy
+ TargetOriginId: Api
+ ViewerProtocolPolicy: redirect-to-https
+ - AllowedMethods:
+ - DELETE
+ - HEAD
+ - GET
+ - OPTIONS
+ - PATCH
+ - POST
+ - PUT
+ CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
+ Compress: true
+ OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac # AllViewerExceptHostHeader
+ PathPattern: /identity/*
+ ResponseHeadersPolicyId: !Ref ResponseHeaderPolicy
+ TargetOriginId: Api
+ ViewerProtocolPolicy: redirect-to-https
+ - CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
+ Compress: true
+ OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac # AllViewerExceptHostHeader
+ PathPattern: /css/*
+ ResponseHeadersPolicyId: !Ref ResponseHeaderPolicy
+ TargetOriginId: Api
+ ViewerProtocolPolicy: redirect-to-https
+ - CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
+ Compress: true
+ OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac # AllViewerExceptHostHeader
+ PathPattern: /vw_static/*
+ ResponseHeadersPolicyId: !Ref ResponseHeaderPolicy
+ TargetOriginId: Api
+ ViewerProtocolPolicy: redirect-to-https
+ - CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
+ Compress: true
+ OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac # AllViewerExceptHostHeader
+ PathPattern: /icons/*
+ ResponseHeadersPolicyId: !Ref ResponseHeaderPolicy
+ TargetOriginId: Api
+ ViewerProtocolPolicy: redirect-to-https
+ - CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
+ Compress: true
+ PathPattern: '*.html'
+ ResponseHeadersPolicyId: !Ref ResponseHeaderPolicy
+ TargetOriginId: WebVaultAssetsBucket
+ ViewerProtocolPolicy: redirect-to-https
+ - CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
+ Compress: true
+ PathPattern: '*connector.html'
+ ResponseHeadersPolicyId: !Ref ConnectorHtmlResponseHeaderPolicy
+ TargetOriginId: WebVaultAssetsBucket
+ ViewerProtocolPolicy: redirect-to-https
+ Comment: Vaultwarden CDN
+ CustomErrorResponses:
+ - ErrorCode: 403
+ ResponseCode: 200
+ ResponsePagePath: /404.html
+ DefaultCacheBehavior:
+ CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
+ Compress: true
+ ResponseHeadersPolicyId: !Ref ResponseHeaderPolicy
+ TargetOriginId: WebVaultAssetsBucket
+ ViewerProtocolPolicy: redirect-to-https
+ DefaultRootObject: index.html
+ Enabled: true
+ HttpVersion: http2and3
+ IPV6Enabled: true
+ Origins:
+ - Id: WebVaultAssetsBucket
+ DomainName: !GetAtt WebVaultAssetsBucket.RegionalDomainName
+ OriginAccessControlId: !GetAtt WebVaultAssetsBucketOriginAccessControl.Id
+ S3OriginConfig:
+ OriginAccessIdentity: ''
+ - Id: Api
+ CustomOriginConfig:
+ OriginProtocolPolicy: https-only
+ OriginSSLProtocols:
+ - TLSv1.2
+ DomainName: !Select
+ - 2
+ - !Split
+ - /
+ - !GetAtt ApiFunctionUrl.FunctionUrl
+ PriceClass: PriceClass_All
+ ViewerCertificate: !If
+ - IsDomainAndCertificateSet
+ - AcmCertificateArn: !Ref ACMCertificateArn
+ MinimumProtocolVersion: TLSv1.2_2021
+ SslSupportMethod: sni-only
+ - !Ref AWS::NoValue
+
+ WebVaultAssetsBucketPolicyCloudFrontAccess:
+ Type: AWS::S3::BucketPolicy
+ Properties:
+ Bucket: !Ref WebVaultAssetsBucket
+ PolicyDocument:
+ Id: CloudFrontAccess
+ Version: '2012-10-17'
+ Statement:
+ - Principal:
+ Service: !Sub cloudfront.${AWS::URLSuffix}
+ Action: s3:GetObject
+ Effect: Allow
+ Resource: !Sub ${WebVaultAssetsBucket.Arn}/*
+ Condition:
+ StringEquals:
+ AWS:SourceArn: !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${CDN.Id}
+
+Outputs:
+ WebVaultAssetsBucket:
+ Value: !Ref WebVaultAssetsBucket
+ CDNDomain:
+ Value: !GetAtt CDN.DomainName