--- title: 'Nginx ansible role' description: 'The ansible role I use to manage my nginx web servers' date: '2024-10-28' 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: ``` yaml --- nginx: etc_dir: '/etc/nginx' pid_file: '/run/nginx.pid' www_user: 'www-data' ``` While in `vars/FreeBSD.yaml` I have: ``` yaml --- 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: ``` yaml --- - 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: ``` yaml - 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: ``` yaml --- - 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`: ``` nginx ############################################################################### # \_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`: ``` nginx ############################################################################### # \_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`: ``` nginx ############################################################################### # \_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`: ``` nginx ############################################################################### # \_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`: ``` nginx ############################################################################### # \_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: ``` yaml --- dependencies: - role: 'borg' - role: 'nginx' - role: 'postgresql' ``` Then from a tasks file: ``` yaml - 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!