Skip to content

Instantly share code, notes, and snippets.

@simonhaenisch
Last active February 8, 2023 01:20
Show Gist options
  • Save simonhaenisch/f06a06df7a975159a75ced1f9423382e to your computer and use it in GitHub Desktop.
Save simonhaenisch/f06a06df7a975159a75ced1f9423382e to your computer and use it in GitHub Desktop.
Nginx configuration boilerplate with SSL

Nginx Configuration Boilerplate

This is my boilerplate for a Nginx config with SSL. I like the idea of modular includes, so I created a /etc/nginx/includes directory for files that get included into the main config file /etc/nginx/nginx.conf. I moved mime.types and fastcgi.conf to that includes folder. I use a common-location-rules.conf file for location rules that are shared between all sites hosted on the server. Relative paths in include directives are relative to the config prefix path (path to the nginx.conf file, by default /etc/nginx/).

The following files are from h5bp/server-configs-nginx:

  • includes/mime.types: here the correct MIME type for javascript (application/javascript instead of text/javascript) is set among others. Using this file is important because the gzip_types rule is set accordingly in gzip.conf.
  • includes/expires.conf: location rules to set expires rules for certain file types, so the browser doesn't have to reload them. Some CMS's set the "Expires" headers via PHP so this file is not needed in that case.

Here is the list of sources:

Contents:

Directory Structure

/etc/
  ├── nginx/
  │     ├── nginx.conf
  │     │
  │     ├── includes/
  │     │     ├── common-location-rules.conf
  │     │     ├── expires.conf
  │     │     ├── fastcgi.conf
  │     │     ├── file-cache.conf
  │     │     ├── gzip.conf
  │     │     ├── mime.types
  │     │     ├── ssl.conf
  │     │     ├── ssl-dhparam.pem
  │     │
  │     ├── sites-available/
  │     │     ├── domain.tld
  │     │     ├── no-default
  │     │
  │     ├── sites-enabled/
  │           ├── symlink-to-sites-available/domain.tld
  │
  ├── ssl/
        ├── certs/
        │     ├── domain.tld.crt
        │     ├── domain.tld.chain+root.crt
        ├── private/
              ├── domain.tld.key

/var/
  ├── www/
        ├── website
              ├── index.html
              ├── ...

File Permissions

The /var/www directory and all contained files and folders should be owned by $USER:www-data (or whatever you name the nginx user). Then set 755 permissions for the /var/www directory as well as all subdirectories and 644 for all files. If you need a directory that nginx can create files in (e.g. an upload directory or a CMS managed site), set the permissions for that directory to 775. If nginx should be allowed to edit files, set the file permissions to 664. This increases security, because in case of a security issue with nginx it can only modify specified files/folders.

Add $USER to www-data group:

usermod -a -G www-data $USER

Files

nginx.conf

user www-data;
worker_processes auto;
worker_rlimit_nofile 8192; # should be bigger than worker_connectinos
pid /run/nginx.pid;

events {
	use epoll;
	worker_connections 8000;
	multi_accept on;
}

http {
	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;
	
	keepalive_timeout 30; # longer values are better for each ssl client, but take up a worker connection longer
	types_hash_max_size 2048;
	server_tokens off;
	
	# maximum file upload size
	# update 'upload_max_filesize' & 'post_max_size' in /etc/php5/fpm/php.ini accordingly
	client_max_body_size 32m;
	# client_body_timeout 60s;
	
	# set default index file (can be overwritten for each site individually)
	index index.html;
	
	# load MIME types
	include includes/mime.types;
	default_type application/octet-stream; # set default MIME type
	
	# create a php upstream handler that will be used to pass php scripts to php-fpm via a unix socket
	# see http://nginx.org/en/docs/http/ngx_http_upstream_module.html
	# and http://serverfault.com/a/546363
	upstream php {
		server unix:/var/run/php5-fpm.sock;
		# or a <server>:<port>, e.g. localhost:9000
	}
	
	# logging
	access_log /var/log/nginx/access.log;
	error_log /var/log/nginx/error.log;
	
	# turn on gzip compression by including config
	include includes/gzip.conf;
	
	# enable caching of frequently used files
	include includes/file-cache.conf;
	
	# disable content type sniffing for more security
	add_header "X-Content-Type-Options" "nosniff";
	
	# force the latest IE version
	add_header "X-UA-Compatible" "IE=Edge";
	
	# enable anti-cross-site scripting filter built into IE 8+
	add_header "X-XSS-Protection" "1; mode=block";
	
	# include virtual host configs
	include sites-enabled/*;
}

includes/common-location-rules.conf

# deny all hidden files (starting with dot, e.g. .htpasswd) except .well-known/
# see https://www.mnot.net/blog/2010/04/07/well-known
location ~* /\.(?!well-known\/) {
	deny all;
}

# protect system files
location ~* (?:\.(?:bak|conf|dist|fla|in[ci]|log|psd|sh|sql|sw[op])|~)$ {
	deny all;
}

# pass php scripts to php-fpm via fastcgi
location ~ \.php$ {
	fastcgi_split_path_info ^(.+\.php)(/.+)$;
	include includes/fastcgi.conf;
	fastcgi_index index.php;
	fastcgi_pass php; # pass to php upstream
}

includes/fastcgi.conf

# use fastcgi.conf instead of fastcgi_params
# see https://blog.martinfjordvald.com/2013/04/nginx-config-history-fastcgi_params-versus-fastcgi-conf/
# this is the default config + a httpoxy mitigation (see https://httpoxy.org/)

fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;
fastcgi_param  HTTPS              $https if_not_empty;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

# secure against httpoxy
fastcgi_param  HTTP_PROXY         "";

includes/file-cache.conf

open_file_cache max=1000 inactive=30s; # cache max 1000 files; release from cache after 30 seconds
open_file_cache_valid 60s; # revalidate files every 60 seconds
open_file_cache_min_uses 2; # only cache files that have 2 been accessed times or more
open_file_cache_errors off; # en-/disables caching of file lookup errors

includes/gzip.conf

gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_buffers 16 8k; # see http://stackoverflow.com/a/5132440
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types
	application/atom+xml
	application/javascript
	application/json
	application/ld+json
	application/manifest+json
	application/rss+xml
	application/vnd.geo+json
	application/vnd.ms-fontobject
	application/x-font-ttf
	application/x-web-app-manifest+json
	application/xhtml+xml
	application/xml
	font/opentype
	image/bmp
	image/svg+xml
	image/x-icon
	text/cache-manifest
	text/css
	text/plain
	text/vcard
	text/vnd.rim.location.xloc
	text/vtt
	text/x-component
	text/x-cross-domain-policy;

includes/ssl.conf

# set the paths to your cert and key files here
ssl_certificate /etc/ssl/certs/domain.tld.crt;
ssl_certificate_key /etc/ssl/private/domain.tld.key;

# diffie-hellman parameter, create with: openssl dhparam -out /etc/nginx/includes/ssl-dhparam.pem 2048
# see https://weakdh.org/
ssl_dhparam /etc/nginx/includes/ssl-dhparam.pem;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA;
ssl_prefer_server_ciphers on;

ssl_session_cache shared:SSL:10m; # a 1mb cache can hold about 4000 sessions, so we can hold 40000 sessions
ssl_session_timeout 24h;

# Use a higher keepalive timeout to reduce the need for repeated handshakes
keepalive_timeout 300s; # up from 75 secs default

# submit domain for preloading in browsers at: https://hstspreload.appspot.com
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload;";

# OCSP stapling
# nginx will poll the CA for signed OCSP responses, and send them to clients so clients don't make their own OCSP calls.
# see https://sslmate.com/blog/post/ocsp_stapling_in_apache_and_nginx on how to create the chain+root
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/ssl/certs/domain.tld.chain+root.crt;
resolver 8.8.8.8 8.8.4.4 216.146.35.35 216.146.36.36 valid=60s;
resolver_timeout 2s;

sites-available/domain.tld

# redirect http to non-www https
server {
	listen [::]:80;
	listen 80;
	server_name domain.tld www.domain.tld;

	return 301 https://domain.tld$request_uri;
}

# redirect www https to non-www https
server {
	listen [::]:443 ssl;
	listen 443 ssl;
	server_name www.domain.tld;
	
	# add ssl cert w/ options
	include includes/ssl.conf;
	
	return 301 https://domain.tld$request_uri;
}

# serve website
server {
	listen [::]:443 ssl;
	listen 443 ssl;
	server_name domain.tld;

	# add ssl cert w/ options
	include includes/ssl.conf;
	
	root /var/www/domain.tld;
	
	index index.html index.php;
	
	# return 404 if uri doesn't point to file or directory
	location / {
		try_files $uri $uri/ =404;
	}
	
	# set expires rules
	include includes/expires.conf;

	include includes/common-location-rules.conf;
}

sites-available/no-default

# drop requests for unknown hosts to prevent host header attacks

server {
	listen [::]:80 default_server;
	listen 80 default_server;
	return 444;
}

server {
	listen [::]:443 ssl default_server;
	listen 443 ssl default_server;
	include includes/ssl.conf;
	return 444;
}
@simonhaenisch
Copy link
Author

just saw your comment... thanks for the hint 🤓 cheers

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment