Server Default Settings

Posted on 19 2026

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 includeSubDomains have 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:

SnippetUse in
deny-sensitive-files.confEvery virtual server
security-headers.confEvery HTTPS virtual server
error-pages.confEvery virtual server
browser-cache.confStatic file servers only
proxy-headers.confReverse proxy location blocks

X-Frame-Options: DENY from the source material prevents the page from being displayed in any frame, including from the same origin. SAMEORIGIN is usually the correct value: it prevents framing from external origins while allowing the application to use frames internally. Some applications (Nextcloud, SnappyMail) set their own X-Frame-Options; check that your global setting does not conflict with theirs.