# Production server block for the official image. # # Kept deliberately close to docker/web/nginx.conf — the dev stack's config # — because the security properties below were reviewed as a set. Two # differences, both forced by this # being one container instead of two: # # 1. fastcgi_pass targets 127.0.0.1, not the `app` compose service. # 2. There is no user/uid coordination to do here, because the Dockerfile # has already done it: it points the `user` directive in the package's # own nginx.conf at www-data, so nginx and php-fpm are the same user # and storage/ needs no group juggling. Without that step nginx runs # as the alpine package's `nginx` (uid 100) and cannot read what # php-fpm just wrote — see the comment on that line. server { listen 80 default_server; server_name _; root /var/www/html/public; index index.php; # Uploads arrive in chunks (Uppy resumable), so this caps a single # chunk, not a file. Raising it does not raise the maximum file size. client_max_body_size 100m; # The nginx version number is nobody's business but ours — matches # docker/web/nginx.conf. server_tokens off; # Baseline hardening for every response. `always` so they survive error # responses too. NOTE: nginx does not merge add_header across levels — # a location that declares any add_header of its own inherits none of # these, so /protected-files/ below repeats them deliberately. add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; location / { try_files $uri $uri/ /index.php?$query_string; } # Protected file serving: PHP authorizes, nginx streams. # PHP responds with X-Accel-Redirect: /protected-files/. # # This is the only location that returns bytes someone else uploaded, # so it gets the strictest headers in the file. `sandbox` puts anything # rendered as a document into an opaque origin with scripts disabled — # if a payload ever does reach here with a renderable content type # (FileThumbnailController's allowlist is the primary defence), it # cannot touch this app's origin or the viewer's session. Images loaded # as subresources are unaffected: a CSP on a subresource response never # creates a browsing context, so thumbnails and previews still render. # `^~` so this prefix beats the `\.php$` regex below: without it a # protected path ending in .php would be handed to the PHP handler # instead of streaming under the sandbox headers this block sets. location ^~ /protected-files/ { internal; alias /var/www/html/storage/app/files/; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "sandbox; default-src 'none'" always; } location ~ \.php$ { # Never hand a path to PHP-FPM that isn't a real script on disk: # without this, any URI ending in .php reaches the interpreter and # PATH_INFO resolution decides what actually runs. try_files $uri =404; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; fastcgi_buffer_size 32k; fastcgi_buffers 8 32k; } location ~ /\.(?!well-known) { deny all; } }