CORS & ACL Configuration Guide
Complete guide to configuring CORS policies and understanding ACL differences across cloud providers
CORS & ACL Configuration Guide
This guide covers Cross-Origin Resource Sharing (CORS) configuration for file uploads and provides an overview of Access Control Lists (ACLs) across different cloud storage providers.
Table of Contents
- CORS Configuration
- Understanding ACLs
- Provider-Specific Considerations
- Common Issues & Troubleshooting
CORS Configuration
Cross-Origin Resource Sharing (CORS) is essential for allowing your web application to upload files directly to cloud storage from the browser.
Why CORS is Required
When uploading files directly from the browser to cloud storage, you're making requests from your domain (e.g., https://myapp.com
) to a different domain (e.g., https://mybucket.s3.amazonaws.com
). Browsers block these cross-origin requests by default unless the target server explicitly allows them via CORS headers.
Basic CORS Configuration
AWS S3 CORS Configuration
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST",
"DELETE",
"HEAD"
],
"AllowedOrigins": [
"https://yourdomain.com",
"https://www.yourdomain.com"
],
"ExposeHeaders": [
"ETag",
"x-amz-meta-custom-header"
],
"MaxAgeSeconds": 3000
}
]
Setting CORS via AWS CLI
# Save the above JSON to cors-config.json
aws s3api put-bucket-cors \
--bucket your-bucket-name \
--cors-configuration file://cors-config.json
Setting CORS via AWS Console
- Go to S3 Console → Your Bucket → Permissions
- Scroll to "Cross-origin resource sharing (CORS)"
- Click "Edit" and paste your CORS configuration
- Save changes
Development vs Production CORS
Development Configuration (Permissive)
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedOrigins": [
"http://localhost:3000",
"http://localhost:3001",
"http://127.0.0.1:3000",
"https://yourdomain.com"
],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
Production Configuration (Restrictive)
[
{
"AllowedHeaders": [
"Content-Type",
"Content-MD5",
"Authorization",
"x-amz-date",
"x-amz-content-sha256"
],
"AllowedMethods": ["PUT", "POST"],
"AllowedOrigins": [
"https://yourdomain.com",
"https://www.yourdomain.com"
],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 86400
}
]
Provider-Specific CORS Setup
Cloudflare R2
[
{
"AllowedOrigins": ["https://yourdomain.com"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
Set via Cloudflare Dashboard:
- Go to R2 Object Storage → Your Bucket
- Navigate to Settings → CORS policy
- Add your CORS rules
DigitalOcean Spaces
[
{
"AllowedOrigins": ["https://yourdomain.com"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag", "x-amz-meta-*"],
"MaxAgeSeconds": 3600
}
]
Set via DigitalOcean Control Panel:
- Go to Spaces → Your Space → Settings
- Add CORS configuration
MinIO (Self-Hosted)
# Using MinIO Client (mc)
mc admin config set myminio api cors_allow_origin="https://yourdomain.com"
mc admin service restart myminio
Or via MinIO Console:
- Access MinIO Console → Buckets → Your Bucket
- Navigate to Anonymous → Access Rules
- Configure CORS policy
Advanced CORS Configurations
Multiple Environment Setup
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["PUT", "POST"],
"AllowedOrigins": [
"https://yourdomain.com",
"https://staging.yourdomain.com"
],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 86400
},
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET"],
"AllowedOrigins": ["*"],
"MaxAgeSeconds": 3600
}
]
CDN Integration
When using CloudFront or other CDNs:
[
{
"AllowedHeaders": [
"Origin",
"Access-Control-Request-Method",
"Access-Control-Request-Headers"
],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedOrigins": [
"https://yourdomain.com",
"https://d1234567890.cloudfront.net"
],
"ExposeHeaders": ["ETag", "x-amz-version-id"],
"MaxAgeSeconds": 86400
}
]
Testing CORS Configuration
Browser Developer Tools
- Open your web app
- Try uploading a file
- Check Network tab for CORS errors
- Look for preflight OPTIONS requests
Command Line Testing
# Test preflight request
curl -X OPTIONS \
-H "Origin: https://yourdomain.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: Content-Type" \
https://yourbucket.s3.amazonaws.com/
# Should return CORS headers if configured correctly
Programmatic Testing
// Test CORS from browser console
fetch('https://yourbucket.s3.amazonaws.com/', {
method: 'OPTIONS',
headers: {
'Origin': window.location.origin,
'Access-Control-Request-Method': 'PUT'
}
})
.then(response => {
console.log('CORS Headers:', response.headers);
})
.catch(error => {
console.error('CORS Error:', error);
});
Understanding ACLs
Access Control Lists (ACLs) define who can access your uploaded files and what permissions they have. Important: ACL implementation varies significantly across providers.
What are ACLs?
ACLs are permission sets that determine:
- Who can access files (users, groups, public)
- What they can do (read, write, delete)
- How permissions are inherited
Common ACL Types
Public Access Levels
public-read
: Anyone can download the filepublic-read-write
: Anyone can download and uploadprivate
: Only bucket owner has accessauthenticated-read
: Only authenticated users can read
AWS S3 ACL Examples
// Using PushDuck with S3 ACLs
const uploadConfig = createUploadConfig({
provider: s3({
bucket: "my-bucket",
region: "us-east-1",
acl: "public-read" // File will be publicly accessible
}),
// ... other config
});
Provider-Specific ACL Differences
AWS S3
- Full ACL Support: Comprehensive ACL system
- Canned ACLs: Predefined permission sets
- Custom ACLs: Granular user/group permissions
- Bucket Policies: Override ACL settings
// S3 supports traditional ACLs
const s3Config = s3({
bucket: "my-bucket",
acl: "public-read", // ✅ Supported
// Custom ACL with specific permissions
customAcl: {
Owner: { ID: "owner-id", DisplayName: "Owner" },
Grants: [
{
Grantee: { Type: "Group", URI: "http://acs.amazonaws.com/groups/global/AllUsers" },
Permission: "READ"
}
]
}
});
Cloudflare R2
- Limited ACL Support: Basic public/private only
- No Canned ACLs: Doesn't support AWS-style ACL names
- Bucket-Level Permissions: Access controlled at bucket level
// R2 has limited ACL support
const r2Config = cloudflareR2({
bucket: "my-bucket",
// ❌ acl: "public-read" // Not supported
// Access controlled via Cloudflare dashboard or API
});
DigitalOcean Spaces
- S3-Compatible ACLs: Supports most S3 ACL types
- Public/Private Toggle: Simple public access control
- CDN Integration: Automatic CDN for public files
// Spaces supports basic S3 ACLs
const spacesConfig = digitalOceanSpaces({
bucket: "my-space",
acl: "public-read", // ✅ Supported
region: "nyc3"
});
MinIO (Self-Hosted)
- Policy-Based: Uses bucket policies instead of ACLs
- No Traditional ACLs: Custom permission system
- IAM Integration: Role-based access control
// MinIO uses policies, not ACLs
const minioConfig = minio({
endpoint: "https://minio.yourdomain.com",
bucket: "my-bucket",
// ❌ acl: "public-read" // Not supported
// Access controlled via MinIO policies
});
ACL Best Practices
Security Considerations
- Default to Private: Start with
private
ACL - Explicit Public Access: Only make files public when necessary
- Regular Audits: Review public files periodically
- Bucket Policies: Use bucket policies for complex permissions
Implementation Strategy
// Environment-based ACL configuration
const getAcl = () => {
if (process.env.NODE_ENV === 'development') {
return 'public-read'; // Easy testing
}
if (process.env.FILE_TYPE === 'profile-images') {
return 'public-read'; // Profile images need public access
}
return 'private'; // Default to private
};
const uploadConfig = createUploadConfig({
provider: s3({
bucket: process.env.S3_BUCKET!,
acl: getAcl()
})
});
Provider-Specific Considerations
AWS S3
- Bucket Policies Override ACLs: Bucket policies take precedence
- Block Public Access: May prevent ACL-based public access
- IAM Permissions: Required for ACL operations
Cloudflare R2
- Dashboard Configuration: Set public access via Cloudflare dashboard
- Custom Domains: Use custom domains for public files
- Workers Integration: Use Cloudflare Workers for access control
DigitalOcean Spaces
- CDN Integration: Automatic CDN for public files
- Subdomain Access: Public files accessible via subdomain
- CORS + ACL: Both required for browser uploads
MinIO
- Policy-Only: No ACL support, use bucket policies
- Admin Configuration: Set policies via MinIO admin
- Custom Authentication: Integrate with your auth system
Common Issues & Troubleshooting
CORS Issues
Symptom: "CORS policy" errors in browser console
Solution:
- Check CORS configuration includes your domain
- Verify all required methods are allowed
- Ensure preflight requests are handled
# Debug CORS with curl
curl -X OPTIONS \
-H "Origin: https://yourdomain.com" \
-H "Access-Control-Request-Method: PUT" \
-v https://yourbucket.s3.amazonaws.com/
Symptom: Uploads work locally but fail in production
Solution:
- Add production domain to CORS origins
- Check for HTTPS vs HTTP mismatches
- Verify subdomain configurations
ACL Issues
Symptom: Files uploaded but not accessible
Solution:
- Check if bucket has "Block Public Access" enabled
- Verify ACL permissions match requirements
- Review bucket policies for conflicts
Symptom: ACL settings ignored
Solution:
- Provider may not support ACLs (R2, MinIO)
- Bucket policies may override ACL settings
- Check IAM permissions for ACL operations
Mixed Issues
Symptom: Uploads succeed but files have wrong permissions
Solution:
// Ensure ACL is set correctly for your provider
const config = createUploadConfig({
provider: s3({
bucket: "my-bucket",
acl: "public-read", // Only for S3-compatible providers
}),
onUploadComplete: async ({ file, url }) => {
// Verify file accessibility
const response = await fetch(url, { method: 'HEAD' });
if (!response.ok) {
console.error('File not publicly accessible:', response.status);
}
}
});
Debug Checklist
- CORS configuration includes all required origins
- All HTTP methods needed are allowed
- Headers match what your client sends
- ACL settings match provider capabilities
- Bucket policies don't conflict with ACLs
- IAM permissions allow required operations
- Block Public Access settings reviewed
- CDN/proxy configurations considered
Provider Support Matrix
Feature | AWS S3 | Cloudflare R2 | DigitalOcean Spaces | MinIO |
---|---|---|---|---|
CORS | ✅ Full | ✅ Full | ✅ Full | ✅ Full |
Canned ACLs | ✅ Yes | ❌ No | ✅ Limited | ❌ No |
Custom ACLs | ✅ Yes | ❌ No | ❌ No | ❌ No |
Bucket Policies | ✅ Yes | ✅ Yes | ❌ No | ✅ Yes |
Public Access | ✅ Yes | ✅ Dashboard | ✅ Yes | ✅ Policies |
This guide should help you configure CORS properly and understand how ACLs work differently across providers. Remember to test your configuration thoroughly in both development and production environments.