aboutsummaryrefslogtreecommitdiff
path: root/content/blog/ansible/nginx-ansible-role.md
blob: 0c465a9834309200947e7387b5df113f670198fe (plain)
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!