Server Default Settings
The source material uses a server-conf.d/ directory that each virtual server includes. This series uses nginx’s snippets/ directory, which is the standard Ubuntu nginx location for reusable configuration fragments.
Each virtual server includes the relevant snippets. Some snippets apply universally: security headers and file access restrictions. Others are applied selectively: browser cache control rules are included by static sites but not by reverse proxy configurations where the upstream backend controls caching.
Creating the snippets directory
sudo mkdir -p /etc/nginx/snippets
File access restrictions
Create /etc/nginx/snippets/deny-sensitive-files.conf:
#
# Deny access to sensitive file types and hidden files
# Include in every virtual server
# /etc/nginx/snippets/deny-sensitive-files.conf
#
# Deny access to hidden files (starting with a dot)
# Allow .well-known/ for ACME challenges and other standards
location ~* /\.(?!well-known\/) {
deny all;
return 404;
}
# Deny access to backup and configuration files
location ~* \.(?:bak|config|sql|fla|psd|ini|log|sh|inc|swp|dist|orig|save)$ {
deny all;
return 404;
}
# Deny access to source control directories
location ~* /(?:\.git|\.svn|\.hg)/ {
deny all;
return 404;
}
# Deny access to composer and npm files
location ~* (?:composer\.(?:json|lock)|package\.(?:json|lock\.json)|yarn\.lock)$ {
deny all;
return 404;
}
Security headers
Create /etc/nginx/snippets/security-headers.conf:
#
# Security response headers
# Include in every HTTPS virtual server
# /etc/nginx/snippets/security-headers.conf
#
# Prevent MIME type sniffing
add_header X-Content-Type-Options 'nosniff' always;
# Prevent clickjacking: deny framing from other origins
# Override per site if the application needs framing (e.g. within itself)
add_header X-Frame-Options 'SAMEORIGIN' always;
# Control referer information sent with requests
add_header Referrer-Policy 'strict-origin-when-cross-origin' always;
# HTTP Strict Transport Security
# Tells browsers to only connect via HTTPS for the specified duration
# max-age=31536000 = 1 year
# includeSubDomains: applies to all subdomains of this host
# DO NOT enable preload unless you are certain; it is very difficult to remove
add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains' always;
# Permissions Policy: restrict access to browser features
# Adjust per site if features like camera or microphone are needed
add_header Permissions-Policy 'camera=(), microphone=(), geolocation=(), payment=()' always;
# Content Security Policy is set per virtual server since it varies by application
# See the CSP page for per-site policy configuration
# Note: X-XSS-Protection is deprecated and removed from the spec.
# Modern browsers use CSP instead. Do not set X-XSS-Protection.
# Note: X-UA-Compatible for IE is irrelevant in 2026.
# Do not set X-UA-Compatible.
HSTS considerations
HSTS (Strict-Transport-Security) tells browsers to always connect to this domain via HTTPS, even if the user types http://. Once set, it is cached by the browser for the max-age duration and cannot be easily undone. Before enabling it:
- Verify HTTPS is working correctly for all resources on the site
- Verify redirects from HTTP to HTTPS are working
- Verify all subdomains included by
includeSubDomainshave valid HTTPS
The preload directive submits the domain to browser preload lists (browsers ship with these lists built in). This is effectively permanent: removing the domain from the preload list takes months to propagate. Do not add preload unless you are certain HTTPS on this domain is permanent.
Error pages
Create a directory for error page HTML files:
sudo mkdir -p /var/www/errors
Create simple error pages. A minimal example for /var/www/errors/50x.html:
<!DOCTYPE html>
<html>
<head><title>Server Error</title></head>
<body>
<h1>Server Error</h1>
<p>Something went wrong on our end. Please try again later.</p>
</body>
</html>
Create /etc/nginx/snippets/error-pages.conf:
#
# Custom error pages
# /etc/nginx/snippets/error-pages.conf
#
# Client errors
error_page 400 /errors/400.html;
error_page 401 /errors/401.html;
error_page 403 /errors/403.html;
error_page 404 /errors/404.html;
error_page 429 /errors/429.html;
# Server errors
error_page 500 502 503 504 /errors/50x.html;
# Serve error pages from a shared location
location ^~ /errors/ {
root /var/www;
internal;
}
# Rate limit error response headers
# Applied when 429 is returned by rate limiting
location = /errors/429.html {
root /var/www;
internal;
add_header Retry-After 60 always;
add_header X-RateLimit-Limit '10r/s' always;
}
The internal directive prevents clients from directly accessing /errors/ URLs. Error pages are served internally by nginx when the corresponding error code is triggered.
Browser cache control
Create /etc/nginx/snippets/browser-cache.conf:
#
# Browser cache control for static assets
# Include in virtual servers serving static files
# NOT recommended for reverse proxy configurations (let the backend control caching)
# /etc/nginx/snippets/browser-cache.conf
#
# HTML, JSON, XML: never cache (always fresh)
location ~* \.(?:html?|xml|json|manifest|appcache)$ {
expires -1;
add_header Cache-Control 'no-store, no-cache, must-revalidate' always;
}
# RSS and Atom feeds: short cache
location ~* \.(?:rss|atom)$ {
expires 1h;
add_header Cache-Control 'public' always;
}
# Images, video, audio: long cache
# Disable gzip for already-compressed formats
location ~* \.(?:jpg|jpeg|gif|png|webp|avif|svg|mp3|mp4|ogg|webm)$ {
expires 1M;
add_header Cache-Control 'public' always;
gzip off;
}
# Icons and favicons: long cache
location ~* \.(?:ico|cur)$ {
expires 1M;
add_header Cache-Control 'public' always;
}
# CSS and JavaScript: long cache
# Use cache-busting in filenames (e.g. style.v2.css) when updating
location ~* \.(?:css|js)$ {
expires 1y;
add_header Cache-Control 'public' always;
}
# Web fonts: long cache with CORS for cross-origin font loading
location ~* \.(?:ttf|ttc|otf|eot|woff|woff2)$ {
expires 1y;
add_header Cache-Control 'public' always;
add_header Access-Control-Allow-Origin '*' always;
}
# Archives and documents: medium cache
location ~* \.(?:zip|tgz|gz|rar|bz2|pdf|doc|docx|xls|xlsx|ppt|pptx)$ {
expires 1M;
add_header Cache-Control 'public' always;
gzip off;
}
Note: Cache-Control: no-transform from the source material is a PageSpeed directive. Since this series does not use PageSpeed, this header is not needed.
Proxy headers snippet
For reverse proxy virtual servers, create /etc/nginx/snippets/proxy-headers.conf:
#
# Standard proxy headers
# Include in reverse proxy location blocks
# /etc/nginx/snippets/proxy-headers.conf
#
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Pass upgrade headers for WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Timeout settings for proxied connections
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffer settings
proxy_buffering on;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
The $connection_upgrade variable needs a map block. Add this to /etc/nginx/conf.d/websocket.conf:
#
# WebSocket connection upgrade map
# /etc/nginx/conf.d/websocket.conf
#
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
This map handles the Connection header correctly for both WebSocket and regular HTTP connections, which is required by Nextcloud, Jellyfin, and other services that use WebSockets.
Using snippets in virtual servers
Virtual server configurations include snippets with nginx’s include directive:
server {
listen 443 ssl;
http2 on;
server_name example.yourdomain.net;
ssl_certificate /etc/letsencrypt/live/example.yourdomain.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.yourdomain.net/privkey.pem;
# Security: deny access to sensitive files
include snippets/deny-sensitive-files.conf;
# Security: add security response headers
include snippets/security-headers.conf;
# Custom error pages
include snippets/error-pages.conf;
# For static sites: browser cache control
# include snippets/browser-cache.conf;
# For reverse proxy: proxy headers in location blocks
location / {
proxy_pass http://127.0.0.1:8080;
include snippets/proxy-headers.conf;
}
access_log /var/log/nginx/example.access.log main;
error_log /var/log/nginx/example.error.log;
}
Snippet inventory
A quick reference of all snippets and when to use them:
| Snippet | Use in |
|---|---|
deny-sensitive-files.conf | Every virtual server |
security-headers.conf | Every HTTPS virtual server |
error-pages.conf | Every virtual server |
browser-cache.conf | Static file servers only |
proxy-headers.conf | Reverse proxy location blocks |
X-Frame-Options: DENYfrom the source material prevents the page from being displayed in any frame, including from the same origin.SAMEORIGINis usually the correct value: it prevents framing from external origins while allowing the application to use frames internally. Some applications (Nextcloud, SnappyMail) set their ownX-Frame-Options; check that your global setting does not conflict with theirs.