1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
|
---
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!
|