Nginx ansible role
2024-10-28 - The ansible role I use to manage my nginx web servers
Tags: ansible nginx
Introduction
Before succumbing to nixos, I had been using an ansible role to manage my nginx web servers. Now that I am in need of it again I refined it a bit: here is the result.
The role
Vars
The role has OS specific vars in files named after the operating system. For example in vars/Debian.yaml
I have:
---
nginx:
etc_dir: '/etc/nginx'
pid_file: '/run/nginx.pid'
www_user: 'www-data'
While in vars/FreeBSD.yaml
I have:
---
nginx:
etc_dir: '/usr/local/etc/nginx'
pid_file: '/var/run/nginx.pid'
www_user: 'www'
Tasks
The main tasks file setups nginx and the global configuration common to all virtual hosts:
---
- include_vars: '{{ ansible_distribution }}.yaml'
- name: 'Install nginx'
package:
name:
- 'nginx'
- name: 'Make nginx vhost directory'
file:
path: '{{ nginx.etc_dir }}/vhost.d'
mode: '0755'
owner: 'root'
state: 'directory'
- name: 'Deploy nginx configuration files'
copy:
src: '{{ item }}'
dest: '{{ nginx.etc_dir }}/{{ item }}'
notify: 'reload nginx'
loop:
- 'headers_base.conf'
- 'headers_secure.conf'
- 'headers_static.conf'
- 'headers_unsafe_inline_csp.conf'
- name: 'Deploy nginx configuration template'
template:
src: 'nginx.conf'
dest: '{{ nginx.etc_dir }}/'
notify: 'reload nginx'
- name: 'Deploy nginx certificates'
copy:
src: '{{ item }}'
dest: '{{ nginx.etc_dir }}/'
notify: 'reload nginx'
loop:
- 'adyxax.org.fullchain'
- 'adyxax.org.key'
- 'dh4096.pem'
- name: 'Start nginx and activate it on boot'
service:
name: 'nginx'
enabled: true
state: 'started'
I have a vhost.yaml
task file which currently simply deploys a file and reload nginx:
- name: 'Deploy {{ vhost.name }} vhost {{ vhost.path }}'
template:
src: '{{ vhost.path }}'
dest: '{{ nginx.etc_dir }}/vhost.d/{{ vhost.name }}.conf'
notify: 'reload nginx'
Handlers
There is a single main.yaml
handler:
---
- name: 'reload nginx'
service:
name: 'nginx'
state: 'reloaded'
Files
I deploy four configuration files in this role. These are all variants of the same theme and their purpose is just to prevent duplicating statements in the virtual hosts configuration files.
headers_base.conf
:
###############################################################################
# \_o< WARNING : This file is being managed by ansible! >o_/ #
# ~~~~ ~~~~ #
###############################################################################
add_header X-Frame-Options deny;
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options nosniff;
add_header Referrer-Policy strict-origin;
add_header Cache-Control no-transform;
add_header Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()";
# 6 months HSTS pinning
add_header Strict-Transport-Security max-age=16000000;
headers_secure.conf
:
###############################################################################
# \_o< WARNING : This file is being managed by ansible! >o_/ #
# ~~~~ ~~~~ #
###############################################################################
include headers_base.conf;
add_header Content-Security-Policy "script-src 'self'";
headers_static.conf
:
###############################################################################
# \_o< WARNING : This file is being managed by ansible! >o_/ #
# ~~~~ ~~~~ #
###############################################################################
include headers_secure.conf;
# Infinite caching
add_header Cache-Control "public, max-age=31536000, immutable";
headers_unsafe_inline_csp.conf
:
###############################################################################
# \_o< WARNING : This file is being managed by ansible! >o_/ #
# ~~~~ ~~~~ #
###############################################################################
include headers_base.conf;
add_header Content-Security-Policy "script-src 'self' 'unsafe-inline'";
Templates
I have a single template for nginx.conf
:
###############################################################################
# \_o< WARNING : This file is being managed by ansible! >o_/ #
# ~~~~ ~~~~ #
###############################################################################
user {{ nginx.www_user }};
worker_processes auto;
pid {{ nginx.pid_file }};
error_log /var/log/nginx/error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
types_hash_max_size 4096;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
gzip on;
gzip_static on;
gzip_vary on;
gzip_comp_level 5;
gzip_min_length 256;
gzip_proxied expired no-cache no-store private auth;
gzip_types application/atom+xml application/geo+json application/javascript application/json application/ld+json application/manifest+json application/rdf+xml application/vnd.ms-fontobject application/wasm application/x-rss+xml application/x-web-app-manifest+json application/xhtml+xml application/xliff+xml application/xml font/collection font/otf font/ttf image/bmp image/svg+xml image/vnd.microsoft.icon text/cache-manifest text/calendar text/css text/csv text/javascript text/markdown text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/xml;
proxy_redirect off;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_http_version 1.1;
proxy_set_header "Connection" "";
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-Server $host;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
client_max_body_size 40M;
server_tokens off;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
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 REQUEST_SCHEME $scheme;
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 REMOTE_USER $remote_user;
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;
uwsgi_param QUERY_STRING $query_string;
uwsgi_param REQUEST_METHOD $request_method;
uwsgi_param CONTENT_TYPE $content_type;
uwsgi_param CONTENT_LENGTH $content_length;
uwsgi_param REQUEST_URI $request_uri;
uwsgi_param PATH_INFO $document_uri;
uwsgi_param DOCUMENT_ROOT $document_root;
uwsgi_param SERVER_PROTOCOL $server_protocol;
uwsgi_param REQUEST_SCHEME $scheme;
uwsgi_param HTTPS $https if_not_empty;
uwsgi_param REMOTE_ADDR $remote_addr;
uwsgi_param REMOTE_PORT $remote_port;
uwsgi_param SERVER_PORT $server_port;
uwsgi_param SERVER_NAME $server_name;
ssl_dhparam dh4096.pem;
ssl_session_cache shared:SSL:2m;
ssl_session_timeout 1h;
ssl_session_tickets off;
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
access_log off;
server_name_in_redirect off;
return 444;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name _;
access_log off;
server_name_in_redirect off;
return 444;
ssl_certificate adyxax.org.fullchain;
ssl_certificate_key adyxax.org.key;
}
include vhost.d/*.conf;
}
Usage example
I do not call the role from a playbook, I prefer running the setup from an application’s role that relies on nginx using a meta/main.yaml
containing something like:
---
dependencies:
- role: 'borg'
- role: 'nginx'
- role: 'postgresql'
Then from a tasks file:
- include_role:
name: 'nginx'
tasks_from: 'vhost'
vars:
vhost:
name: 'www'
path: 'roles/www.adyxax.org/files/nginx-vhost.conf'
I did not find an elegant way to pass a file path local to one role to another. Because of that, here I just specify the full vhost file path complete with the roles/
prefix.
Conclusion
I you have an elegant idea for passing the local file path from one role to another do not hesitate to ping me!