|
| 1 | +--- |
| 2 | +title: CloudFront with S3 |
| 3 | +icon: Cloud |
| 4 | +description: Configure Amazon CloudFront as the CDN layer for LibreChat files stored in S3, including stable media links, signed cookies, and signed download URLs. |
| 5 | +--- |
| 6 | + |
| 7 | +CloudFront lets LibreChat keep files in S3 while serving images, avatars, and downloads through stable CDN URLs. This is the recommended AWS setup when you want S3 durability without exposing users to expiring S3 presigned image URLs. |
| 8 | + |
| 9 | +## When to Use CloudFront |
| 10 | + |
| 11 | +Use CloudFront when you want: |
| 12 | + |
| 13 | +- Stable avatar and image URLs that keep rendering across the UI |
| 14 | +- Global edge caching in front of an S3 bucket |
| 15 | +- Signed cookies for private inline images and avatars |
| 16 | +- Backend-authorized signed URLs for downloads |
| 17 | +- Optional cache invalidation when files are deleted |
| 18 | + |
| 19 | +<Callout type="info" title="S3 is still required"> |
| 20 | + The `cloudfront` file strategy stores objects in S3 and returns CloudFront URLs. Configure the S3 |
| 21 | + environment variables first, then add the `cloudfront` block in `librechat.yaml`. |
| 22 | +</Callout> |
| 23 | + |
| 24 | +## Requirements |
| 25 | + |
| 26 | +- A private S3 bucket |
| 27 | +- A CloudFront distribution with the S3 bucket as an origin |
| 28 | +- An Origin Access Control (OAC) or equivalent origin access policy so CloudFront can read from S3 |
| 29 | +- `AWS_REGION` and `AWS_BUCKET_NAME` |
| 30 | +- `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`, unless your deployment uses an AWS identity provider such as IRSA |
| 31 | +- `CLOUDFRONT_KEY_PAIR_ID` and `CLOUDFRONT_PRIVATE_KEY` when using signed cookies or signed download URLs |
| 32 | + |
| 33 | +## Environment Variables |
| 34 | + |
| 35 | +```bash filename=".env" |
| 36 | +AWS_ACCESS_KEY_ID=your_access_key_id |
| 37 | +AWS_SECRET_ACCESS_KEY=your_secret_access_key |
| 38 | +AWS_REGION=us-east-1 |
| 39 | +AWS_BUCKET_NAME=your_bucket_name |
| 40 | + |
| 41 | +# Required for signed cookies and signed CloudFront download URLs |
| 42 | +CLOUDFRONT_KEY_PAIR_ID=K1234567890ABC |
| 43 | +CLOUDFRONT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" |
| 44 | +``` |
| 45 | + |
| 46 | +`CLOUDFRONT_PRIVATE_KEY` must contain the full PEM private key. In `.env`, quote it and preserve newlines, or inject it from your platform secret manager. |
| 47 | + |
| 48 | +## Basic Configuration |
| 49 | + |
| 50 | +Use `fileStrategies` when you want CloudFront for images and avatars while keeping documents on S3 signed URLs: |
| 51 | + |
| 52 | +```yaml filename="librechat.yaml" |
| 53 | +version: 1.3.11 |
| 54 | + |
| 55 | +fileStrategies: |
| 56 | + avatar: 'cloudfront' |
| 57 | + image: 'cloudfront' |
| 58 | + document: 's3' |
| 59 | + |
| 60 | +cloudfront: |
| 61 | + domain: 'https://cdn.example.com' |
| 62 | + imageSigning: 'none' |
| 63 | + urlExpiry: 3600 |
| 64 | +``` |
| 65 | +
|
| 66 | +Use `fileStrategy` if every file type should use CloudFront: |
| 67 | + |
| 68 | +```yaml filename="librechat.yaml" |
| 69 | +fileStrategy: 'cloudfront' |
| 70 | +
|
| 71 | +cloudfront: |
| 72 | + domain: 'https://cdn.example.com' |
| 73 | +``` |
| 74 | + |
| 75 | +## Signed Cookies |
| 76 | + |
| 77 | +Signed cookies are the secure mode for private inline images and avatars. They let LibreChat keep stable CloudFront URLs in messages and records while authorizing access with short-lived cookies. |
| 78 | + |
| 79 | +```yaml filename="librechat.yaml" |
| 80 | +fileStrategies: |
| 81 | + avatar: 'cloudfront' |
| 82 | + image: 'cloudfront' |
| 83 | + document: 's3' |
| 84 | +
|
| 85 | +cloudfront: |
| 86 | + domain: 'https://cdn.example.com' |
| 87 | + imageSigning: 'cookies' |
| 88 | + cookieDomain: '.example.com' |
| 89 | + cookieExpiry: 1800 |
| 90 | + urlExpiry: 3600 |
| 91 | + requireSignedAccess: true |
| 92 | +``` |
| 93 | + |
| 94 | +### Domain Requirements |
| 95 | + |
| 96 | +For signed cookies, the LibreChat API and CloudFront hostname must share a parent domain: |
| 97 | + |
| 98 | +- API: `https://api.example.com` |
| 99 | +- CloudFront CNAME: `https://cdn.example.com` |
| 100 | +- `cookieDomain: ".example.com"` |
| 101 | +
|
| 102 | +`cookieDomain` must start with a dot. The browser will not send CloudFront cookies to an unrelated domain. |
| 103 | + |
| 104 | +### What Cookies Protect |
| 105 | + |
| 106 | +LibreChat scopes signed cookies to inline media paths: |
| 107 | + |
| 108 | +- `/i/...` private uploaded or generated images, scoped to the user |
| 109 | +- `/a/...` avatar assets, scoped to the tenant when `tenantId` is present |
| 110 | + |
| 111 | +Documents, general uploads, and code outputs stay outside those inline media paths. Downloads are authorized by the backend and returned as signed CloudFront URLs. |
| 112 | + |
| 113 | +### Cookie Refresh |
| 114 | + |
| 115 | +When signed-cookie mode is active, LibreChat advertises a cookie refresh endpoint in startup config: |
| 116 | + |
| 117 | +```text |
| 118 | +POST /api/auth/cloudfront/refresh |
| 119 | +``` |
| 120 | + |
| 121 | +Authenticated sessions refresh cookies during auth flows, token refresh, and CloudFront image retry paths. The refresh response includes the cookie lifetime and the recommended refresh timing. |
| 122 | + |
| 123 | +## Signed Downloads |
| 124 | + |
| 125 | +LibreChat uses signed CloudFront URLs for authorized downloads. The `urlExpiry` setting controls their lifetime in seconds: |
| 126 | + |
| 127 | +```yaml filename="librechat.yaml" |
| 128 | +cloudfront: |
| 129 | + domain: 'https://cdn.example.com' |
| 130 | + imageSigning: 'cookies' |
| 131 | + cookieDomain: '.example.com' |
| 132 | + urlExpiry: 3600 |
| 133 | +``` |
| 134 | + |
| 135 | +For direct-download filename and content-type overrides, configure the CloudFront cache/origin request policy to forward these query strings to S3: |
| 136 | + |
| 137 | +- `response-content-disposition` |
| 138 | +- `response-content-type` |
| 139 | + |
| 140 | +For download paths, attach a response headers policy with: |
| 141 | + |
| 142 | +- `X-Content-Type-Options: nosniff` |
| 143 | +- A restrictive Content Security Policy, such as `default-src 'none'` |
| 144 | + |
| 145 | +## Cache Invalidation |
| 146 | + |
| 147 | +By default, LibreChat deletes the S3 object and does not create a CloudFront invalidation. Enable invalidation when deleted files must disappear from edge cache immediately: |
| 148 | + |
| 149 | +```yaml filename="librechat.yaml" |
| 150 | +cloudfront: |
| 151 | + domain: 'https://cdn.example.com' |
| 152 | + distributionId: 'E1234ABCD' |
| 153 | + invalidateOnDelete: true |
| 154 | +``` |
| 155 | + |
| 156 | +`distributionId` is required when `invalidateOnDelete` is `true`. The AWS identity used by LibreChat also needs `cloudfront:CreateInvalidation`. |
| 157 | + |
| 158 | +## Multi-Region Object Paths |
| 159 | + |
| 160 | +`includeRegionInPath` adds the storage region to newly generated object keys: |
| 161 | + |
| 162 | +```yaml filename="librechat.yaml" |
| 163 | +cloudfront: |
| 164 | + domain: 'https://cdn.example.com' |
| 165 | + storageRegion: 'us-east-2' |
| 166 | + includeRegionInPath: true |
| 167 | +``` |
| 168 | + |
| 169 | +When enabled, new keys include region-aware path segments, for example: |
| 170 | + |
| 171 | +```text |
| 172 | +/i/r/us-east-2/t/tenantId/images/userId/file.png |
| 173 | +/a/r/us-east-2/t/tenantId/avatars/userId/avatar.png |
| 174 | +/r/us-east-2/t/tenantId/images/userId/file.pdf |
| 175 | +``` |
| 176 | + |
| 177 | +This only affects newly generated keys. Existing files are not moved. LibreChat does not configure CloudFront origins, Route 53, or regional routing for you. |
| 178 | + |
| 179 | +## CloudFront Block Reference |
| 180 | + |
| 181 | +<OptionTable |
| 182 | + options={[ |
| 183 | + [ |
| 184 | + 'domain', |
| 185 | + 'string', |
| 186 | + 'CloudFront distribution domain or CNAME. Required.', |
| 187 | + 'domain: "https://cdn.example.com"', |
| 188 | + ], |
| 189 | + [ |
| 190 | + 'distributionId', |
| 191 | + 'string', |
| 192 | + 'Distribution ID used for cache invalidations.', |
| 193 | + 'distributionId: "E1234ABCD"', |
| 194 | + ], |
| 195 | + [ |
| 196 | + 'invalidateOnDelete', |
| 197 | + 'boolean', |
| 198 | + 'Create a CloudFront invalidation when a file is deleted. Default: false.', |
| 199 | + 'invalidateOnDelete: false', |
| 200 | + ], |
| 201 | + [ |
| 202 | + 'imageSigning', |
| 203 | + 'string', |
| 204 | + 'Inline media access mode. Use `"none"` for public CloudFront access or `"cookies"` for signed cookies. `"url"` is reserved and not implemented for images.', |
| 205 | + 'imageSigning: "cookies"', |
| 206 | + ], |
| 207 | + [ |
| 208 | + 'cookieDomain', |
| 209 | + 'string', |
| 210 | + 'Shared parent domain for signed cookies. Required when `imageSigning` is `"cookies"`.', |
| 211 | + 'cookieDomain: ".example.com"', |
| 212 | + ], |
| 213 | + [ |
| 214 | + 'cookieExpiry', |
| 215 | + 'number', |
| 216 | + 'Signed cookie lifetime in seconds. Default: 1800. Maximum: 604800.', |
| 217 | + 'cookieExpiry: 1800', |
| 218 | + ], |
| 219 | + [ |
| 220 | + 'urlExpiry', |
| 221 | + 'number', |
| 222 | + 'Signed download URL lifetime in seconds. Default: 3600.', |
| 223 | + 'urlExpiry: 3600', |
| 224 | + ], |
| 225 | + [ |
| 226 | + 'storageRegion', |
| 227 | + 'string', |
| 228 | + 'Optional region label for region-aware object paths.', |
| 229 | + 'storageRegion: "us-east-2"', |
| 230 | + ], |
| 231 | + [ |
| 232 | + 'includeRegionInPath', |
| 233 | + 'boolean', |
| 234 | + 'Include `storageRegion` in newly generated object keys. Default: false.', |
| 235 | + 'includeRegionInPath: false', |
| 236 | + ], |
| 237 | + [ |
| 238 | + 'requireSignedAccess', |
| 239 | + 'boolean', |
| 240 | + 'Fail startup if signed-cookie CloudFront access cannot initialize. Default: false.', |
| 241 | + 'requireSignedAccess: true', |
| 242 | + ], |
| 243 | + ]} |
| 244 | +/> |
| 245 | + |
| 246 | +## Suggested AWS Permissions |
| 247 | + |
| 248 | +Use least-privilege IAM permissions for the S3 bucket. Add CloudFront invalidation permission only if `invalidateOnDelete` is enabled. |
| 249 | + |
| 250 | +```json filename="iam-policy.json" |
| 251 | +{ |
| 252 | + "Version": "2012-10-17", |
| 253 | + "Statement": [ |
| 254 | + { |
| 255 | + "Effect": "Allow", |
| 256 | + "Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket"], |
| 257 | + "Resource": ["arn:aws:s3:::my-librechat-bucket", "arn:aws:s3:::my-librechat-bucket/*"] |
| 258 | + }, |
| 259 | + { |
| 260 | + "Effect": "Allow", |
| 261 | + "Action": "cloudfront:CreateInvalidation", |
| 262 | + "Resource": "arn:aws:cloudfront::123456789012:distribution/E1234ABCD" |
| 263 | + } |
| 264 | + ] |
| 265 | +} |
| 266 | +``` |
| 267 | + |
| 268 | +## Troubleshooting |
| 269 | + |
| 270 | +- Startup logs `CloudFront domain is required`: add `cloudfront.domain`. |
| 271 | +- Startup logs `S3 must be initialized`: configure S3 environment variables first. |
| 272 | +- Signed cookies are not set: confirm `imageSigning: "cookies"`, `cookieDomain`, `CLOUDFRONT_KEY_PAIR_ID`, and `CLOUDFRONT_PRIVATE_KEY`. |
| 273 | +- Browser still cannot load images: confirm API and CDN hostnames share the configured parent domain and that cookies are allowed with `Secure` and `SameSite=None`. |
| 274 | +- Downloads ignore filename/content type: update the CloudFront cache/origin request policy to forward the response override query strings. |
0 commit comments