Mastering Mixed Public & Private Files in Drupal 10 with S3fs (The "Private Bucket" Method)
Implementing a file system in Drupal 10 that securely handles sensitive documents while serving public images — all from a single, private AWS S3 bucket.
Summary
Implementing a file system in Drupal 10 that securely handles sensitive documents while serving public images — all from a single, Private AWS S3 bucket.
The Core Problem: The "View Media" Paradox
Drupal controls file access based on the Parent Entity, not the Field. Granting "View media" to Anonymous users automatically grants download access to ALL attached files — including private ones.
Technical Obstacles
- s3:// Protocol Gap — Private buckets return 403.
- Query Parameter Failures — triggers WAF LFI alerts.
- Routing Slash Problem — breaks Drupal's router.
- S3FS Reserved Keywords —
public/orprivate/throws CrossSchemeAccessException. - Presigned URL Caching — unique signatures break CDN caching.
Solution: Base64-encode the S3 key: /s3-proxy/{base64_key}
Architecture
open/folder → public access,Cache-Control: publicfile-private/folder → authenticated only,Cache-Control: private
Routing
[MODULE].s3_proxy:
path: '/s3-proxy/{key}'
requirements:
_access: 'TRUE'
key: .+
URL Rewriter
function mymodule_file_url_alter(&$uri) {
$scheme = \Drupal::service('stream_wrapper_manager')->getScheme($uri);
if ($scheme === 's3') {
$target = \Drupal::service('stream_wrapper_manager')->getTarget($uri);
$uri = Url::fromRoute('mymodule.s3_proxy', ['key' => base64_encode($target)])->toString();
}
}