During most of my career, I almost never setup a server from scratch. I'm usually building on top of others' Ansible playbooks. I've always felt a gaping hole in knowledge in terms of sysadmin and ops tasks.
So two years ago, I picked up Servers for Hackers which took a terrific bottom up approach on how to setup and run a server. I've been using it as a reference to setup servers for my side projects, I highly recommend it.
Here's what I used to get Nginx up and running, with SSL, for both static sites and applications needing a reverse proxy. I'm using Ubuntu 16 LTS.
Site Configuration
I keep these tasks under a webserver
role.
For the site.yml
file, I keep a list of sites for each host group under the sites
and
sites_nginx
vars. The latter maps Nginx templates to their destination.
- hosts: example
roles:
- webserver
vars:
sites:
- example.com
sites_nginx:
- { src: "nginx.conf", dest: "nginx.conf" }
- { src: "nginx-example.conf", dest: "sites-available/example.com" }
Let's Encrypt SSL Tasks
Let's Encrypt is amazing! I can get a free SSL certificate, automate its installation, and even automate renewal.
In roles/webserver/tasks/ssl.yml
:
- name: install apt-transport-https
apt: pkg=apt-transport-https state=present update_cache=true
- name: generate dhparams file
command: openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096
args:
creates: /etc/ssl/certs/dhparam.pem
- name: install certbot
apt: pkg=certbot state=present update_cache=true
- name: install python3-certbot-nginx
apt: pkg=python3-certbot-nginx state=present update_cache=true
- name: check if certificate exists
stat:
path: /etc/letsencrypt/live//cert.pem
register: letsencrypt_cert
with_items: ""
- name: stop nginx for certbot
service: name=nginx state=stopped
with_items: ""
when: item.stat.exists == False
- name: generate new certificate
shell: "certbot certonly --standalone --noninteractive --agree-tos --rsa-key-size 4096 --email YOUR_EMAIL_ADDRESS@EXAMPLE.COM -d "
with_items: ""
when: item.stat.exists == False
- name: start nginx for certbot
service: name=nginx state=started
with_items: ""
when: item.stat.exists == False
- name: add crontab to renew certificates
cron: name="renew ssl" minute="30" hour="8" weekday="0" job="certbot renew --pre-hook \"service nginx stop\" --post-hook \"service nginx start\" >> /var/log/le-renew.log"
This installs certbot
and its prerequisites, generates the private key and SSL cert, and adds
a cronjob to renew the certificate. Thank you, Let's Encrypt!
Using a 4096 key size will take longer to generate, but is a good trade-off for security.
Nginx Handler and Tasks
Make sure Nginx is started with roles/webserver/handlers/nginx.yml
:
- name: start nginx
service: name=nginx state=started
Then for roles/webserver/tasks/nginx.yml
:
- name: install nginx
apt: pkg=nginx state=present update_cache=true
notify:
- start nginx
- name: remove default sites-available and sites-enabled
file: path=/etc/nginx//default state=absent
with_items:
- { directory: 'sites-available' }
- { directory: 'sites-enabled' }
- name: setup nginx configurations
copy: src=../files/ dest=/etc/nginx/ owner=root group=root mode=644
with_items: ""
- name: setup sites-enabled
file: src=/etc/nginx/sites-available/
dest=/etc/nginx/sites-enabled/
state=link
owner=root
group=root
force=yes
with_items: ""
This will install Nginx and copy over the configuration file for each site. It follows the standard
pattern of keeping configurations in sites-available
directory and linking them from sites-enabled
.
I keep one main nginx.conf
along with a different configuration for each site.
Nginx Conf
For the standard roles/webserver/files/nginx.conf
:
user www-data;
worker_processes 1; # usually one per core
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
# Defaults
sendfile on;
tcp_nopush on;
tcp_nodelay on;
client_body_timeout 12;
client_header_timeout 12;
keepalive_timeout 15;
send_timeout 10;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Compression
gzip on;
gzip_comp_level 2;
gzip_min_length 1000;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain application/x-javascript text/xml text/css application/xml;
# Virtual Hosts
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
For individual sites, the configuration should live at roles/webserver/files/nginx-example.conf
.
A server block should do a permanent 301 redirect to SSL (handling non-SSL connections):
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name example.com;
root /usr/share/nginx/html;
index index.html;
location ~ /.well-known {
allow all;
}
location / {
return 301 https://$host$request_uri;
}
}
The next server block should handle SSL connections. I've added some lines to handle trailing slashes, caching asset files (I usually fingerprint these), and handling 405/500 errors:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/ssl/certs/dhparam.pem;
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: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_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_stapling on;
ssl_stapling_verify on;
# to remove trailing slash
rewrite ^(/.*)\.html(\?.*)?$ $1$2 permanent;
rewrite ^/([^.]*)/$ /$1 permanent;
root /home/deploy/example;
index index.html;
location ~* \.(jpg|jpeg|png|gif|ico|css|js|eot|ttf|woff|woff2)$ {
expires max;
try_files $uri =404;
}
location ~ ^/([^.]*)/?$ {
expires 1h;
try_files $uri $uri.html $uri/index.html =404;
}
error_page 404 =404 /404;
error_page 500 502 503 504 =500 /500;
}
The actual directory holding your HTML files and web assets is at /home/deploy/example
here.
Enabling these SSL Ciphers should get you 100% at SSL Labs.
For reverse proxies, use the proxy_pass
directive. In the case below, I'm mapping routes that
start with /api
to port 8000
:
location ~ ^/api/.*$ {
proxy_pass http://localhost:8000;
}
Conclusion
I hope that's helpful! I keep a single repository with my deploy playbooks. When I need to add a
new site, I simply add a new nginx-*.conf
file and update the sites
and sites_nginx
vars.
Follow me via Newsletter, RSS feed, or Twitter.
You may also enjoy:
Half Dome
·
Raise and Rescue
·
A History of Computing
·
All Articles →