diff --git a/group_vars/nginx.yml b/group_vars/nginx.yml new file mode 100644 index 0000000..eef80da --- /dev/null +++ b/group_vars/nginx.yml @@ -0,0 +1,24 @@ +--- +glob_nginx: + contact: tech.aurore@lists.crans.org + who: "L'équipe technique d'Aurore" + service_name: service + ssl: + cert: /etc/letsencrypt/live/auro.re/fullchain.pem + cert_key: /etc/letsencrypt/live/auro.re/privkey.pem + trusted_cert: /etc/letsencrypt/live/auro.re/chain.pem + servers: + - ssl: false + server_name: + - "default" + - "_" + root: "/var/www/html" + locations: + - filter: "/" + params: [] + upstreams: [] + + auth_passwd: [] + default_server: + default_ssl_server: + deploy_robots_file: false diff --git a/roles/nginx/handlers/main.yml b/roles/nginx/handlers/main.yml new file mode 100644 index 0000000..6dfcdd7 --- /dev/null +++ b/roles/nginx/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Reload nginx + systemd: + name: nginx + state: reloaded diff --git a/roles/nginx/tasks/main.yml b/roles/nginx/tasks/main.yml new file mode 100644 index 0000000..4d4179c --- /dev/null +++ b/roles/nginx/tasks/main.yml @@ -0,0 +1,121 @@ +--- +- name: Install NGINX + apt: + update_cache: true + name: nginx + register: apt_result + retries: 3 + until: apt_result is succeeded + +- name: Copy snippets + template: + src: "nginx/snippets/{{ item }}.j2" + dest: "/etc/nginx/snippets/{{ item }}" + owner: root + group: root + mode: 0644 + loop: + - options-ssl.conf + - options-proxypass.conf + +- name: Copy dhparam + template: + src: letsencrypt/dhparam.j2 + dest: /etc/letsencrypt/dhparam + owner: root + group: root + mode: 0644 + +- name: Disable default site + file: + dest: "/etc/nginx/sites-enabled/default" + state: absent + +- name: Copy reverse proxy sites + when: nginx.reverseproxy_sites is defined or nginx.redirect_sites is defined + template: + src: "nginx/sites-available/{{ item }}.j2" + dest: "/etc/nginx/sites-available/{{ item }}" + owner: root + group: root + mode: 0644 + loop: + - reverseproxy + - reverseproxy_redirect_dname + - redirect + notify: Reload nginx + +- name: Activate reverse proxy sites + when: nginx.reverseproxy_sites is defined or nginx.redirect_sites is defined + file: + src: "/etc/nginx/sites-available/{{ item }}" + dest: "/etc/nginx/sites-enabled/{{ item }}" + owner: root + group: root + state: link + loop: + - reverseproxy + - reverseproxy_redirect_dname + - redirect + notify: Reload nginx + ignore_errors: "{{ ansible_check_mode }}" + +- name: Copy service nginx configuration + when: nginx.servers is defined and nginx.servers|length > 0 + template: + src: "nginx/sites-available/service.j2" + dest: "/etc/nginx/sites-available/{{ nginx.service_name }}" + owner: root + group: root + mode: 0644 + notify: Reload nginx + +- name: Activate local nginx service site + when: nginx.servers is defined and nginx.servers|length > 0 + file: + src: "/etc/nginx/sites-available/{{ nginx.service_name }}" + dest: "/etc/nginx/sites-enabled/{{ nginx.service_name }}" + owner: root + group: root + state: link + notify: Reload nginx + ignore_errors: "{{ ansible_check_mode }}" + +- name: Copy 50x error page + template: + src: www/html/50x.html.j2 + dest: /var/www/html/50x.html + owner: www-data + group: www-data + mode: 0644 + +- name: Copy robots.txt file + when: nginx.deploy_robots_file + template: + src: www/html/robots.txt.j2 + dest: /var/www/html/robots.txt + owner: www-data + group: www-data + mode: 0644 + +- name: Indicate role in motd + template: + src: update-motd.d/05-service.j2 + dest: /etc/update-motd.d/05-nginx + mode: 0755 + +- name: Install passwords + when: nginx.auth_passwd|length > 0 + template: + src: nginx/passwd.j2 + dest: /etc/nginx/passwd + mode: 0644 + +- name: Copy 401 error page + when: nginx.auth_passwd|length > 0 + template: + src: www/html/401.html.j2 + dest: /var/www/html/401.html + owner: www-data + group: www-data + mode: 0644 diff --git a/roles/nginx/templates/letsencrypt/dhparam.j2 b/roles/nginx/templates/letsencrypt/dhparam.j2 new file mode 100644 index 0000000..9b182b7 --- /dev/null +++ b/roles/nginx/templates/letsencrypt/dhparam.j2 @@ -0,0 +1,8 @@ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz ++8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a +87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7 +YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi +7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD +ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg== +-----END DH PARAMETERS----- diff --git a/roles/nginx/templates/nginx/passwd.j2 b/roles/nginx/templates/nginx/passwd.j2 new file mode 100644 index 0000000..6e61ce2 --- /dev/null +++ b/roles/nginx/templates/nginx/passwd.j2 @@ -0,0 +1,4 @@ +# {{ ansible_managed }} +{% for user, hash in nginx.auth_passwd.items() -%} +{{ user }}: {{ hash }} +{% endfor -%} diff --git a/roles/nginx/templates/nginx/sites-available/redirect.j2 b/roles/nginx/templates/nginx/sites-available/redirect.j2 new file mode 100644 index 0000000..28e9b7d --- /dev/null +++ b/roles/nginx/templates/nginx/sites-available/redirect.j2 @@ -0,0 +1,67 @@ +# {{ ansible_managed }} + +{% for site in nginx.redirect_sites %} +# Redirect http://{{ site.from }} to http://{{ site.to }} +server { + listen 80; + listen [::]:80; + + server_name {{ site.from }}; + + location / { + return 302 http://{{ site.to }}$request_uri; + } +} + +# Redirect https://{{ site.from }} to https://{{ site.to }} +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name {{ site.from }}; + + # SSL common conf + include "/etc/nginx/snippets/options-ssl.conf"; + + location / { + return 302 https://{{ site.to }}$request_uri; + } +} + +{% endfor %} + +{# Also redirect for DNAMEs #} +{% for dname in nginx.redirect_dnames %} +{% for site in nginx.redirect_sites %} +{% set from = site.from | regex_replace('crans.org', dname) %} +{% if from != site.from %} +# Redirect http://{{ from }} to http://{{ site.to }} +server { + listen 80; + listen [::]:80; + + server_name {{ from }}; + + location / { + return 302 http://{{ site.to }}$request_uri; + } +} + +# Redirect https://{{ from }} to https://{{ site.to }} +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name {{ from }}; + + # SSL common conf + include "/etc/nginx/snippets/options-ssl.conf"; + + location / { + return 302 https://{{ site.to }}$request_uri; + } +} + +{% endif %} +{% endfor %} +{% endfor %} diff --git a/roles/nginx/templates/nginx/sites-available/reverseproxy.j2 b/roles/nginx/templates/nginx/sites-available/reverseproxy.j2 new file mode 100644 index 0000000..d29d13c --- /dev/null +++ b/roles/nginx/templates/nginx/sites-available/reverseproxy.j2 @@ -0,0 +1,56 @@ +# {{ ansible_managed }} + +# Automatic Connection header for WebSocket support +# See http://nginx.org/en/docs/http/websocket.html +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +{% for site in nginx.reverseproxy_sites %} +# Redirect http://{{ site.from }} to https://{{ site.from }} +server { + listen 80; + listen [::]:80; + + server_name {{ site.from }}; + + location / { + return 302 https://$host$request_uri; + } +} + +# Reverse proxify https://{{ site.from }} to http://{{ site.to }} +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name {{ site.from }}; + + # SSL common conf + include "/etc/nginx/snippets/options-ssl.conf"; + + # Log into separate log files + access_log /var/log/nginx/{{ site.from }}.log; + error_log /var/log/nginx/{{ site.from }}_error.log; + + # Keep the TCP connection open a bit for faster browsing + keepalive_timeout 70; + + # Custom error page + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /var/www/html; + } + + set_real_ip_from 10.231.136.0/24; + set_real_ip_from 2a0c:700:0:2::/64; + real_ip_header P-Real-Ip; + + location / { + proxy_pass http://{{ site.to }}; + include "/etc/nginx/snippets/options-proxypass.conf"; + } +} + +{% endfor %} diff --git a/roles/nginx/templates/nginx/sites-available/reverseproxy_redirect_dname.j2 b/roles/nginx/templates/nginx/sites-available/reverseproxy_redirect_dname.j2 new file mode 100644 index 0000000..4edda25 --- /dev/null +++ b/roles/nginx/templates/nginx/sites-available/reverseproxy_redirect_dname.j2 @@ -0,0 +1,37 @@ +# {{ ansible_managed }} + +{% for dname in nginx.redirect_dnames %} +{% for site in nginx.reverseproxy_sites %} +{% set from = site.from | regex_replace('crans.org', dname) %} +{% set to = site.from %} +{% if from != site.from %} +# Redirect http://{{ from }} to http://{{ to }} +server { + listen 80; + listen [::]:80; + + server_name {{ from }}; + + location / { + return 302 http://{{ to }}$request_uri; + } +} + +# Redirect https://{{ from }} to https://{{ to }} +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name {{ from }}; + + # SSL common conf + include "/etc/nginx/snippets/options-ssl.conf"; + + location / { + return 302 https://{{ to }}$request_uri; + } +} + +{% endif %} +{% endfor %} +{% endfor %} diff --git a/roles/nginx/templates/nginx/sites-available/service.j2 b/roles/nginx/templates/nginx/sites-available/service.j2 new file mode 100644 index 0000000..3d9db5d --- /dev/null +++ b/roles/nginx/templates/nginx/sites-available/service.j2 @@ -0,0 +1,114 @@ +# {{ ansible_managed }} + +# Automatic Connection header for WebSocket support +# See http://nginx.org/en/docs/http/websocket.html +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +{% for upstream in nginx.upstreams -%} +upstream {{ upstream.name }} { + # Path of the server + server {{ upstream.server }}; +} +{% endfor -%} + +{% if nginx.default_ssl_server -%} +# Redirect all services to the main site +server { + listen 443 default_server ssl; + listen [::]:443 default_server ssl; + include "/etc/nginx/snippets/options-ssl.conf"; + + server_name _; + charset utf-8; + + # Hide Nginx version + server_tokens off; + + location / { + return 302 https://{{ nginx.default_ssl_server }}$request_uri; + } +} +{% endif -%} + +{% if nginx.default_server -%} +# Redirect all services to the main site +server { + listen 80 default_server; + listen [::]:80 default_server; + + server_name _; + charset utf-8; + + # Hide Nginx version + server_tokens off; + + location / { + return 302 http://{{ nginx.default_server }}$request_uri; + } +} +{% endif -%} + +{% for server in nginx.servers %} +{% if server.ssl is defined and server.ssl -%} +# Redirect HTTP to HTTPS +server { + listen 80; + listen [::]:80; + + server_name {{ server.server_name|join(" ") }}; + charset utf-8; + + # Hide Nginx version + server_tokens off; + + location / { + return 302 https://$host$request_uri; + } +} +{% endif -%} + +server { + {% if server.ssl is defined and server.ssl -%} + listen 443 ssl; + listen [::]:443 ssl; + include "/etc/nginx/snippets/options-ssl.conf"; + {% else -%} + listen 80; + listen [::]:80; + {% endif -%} + + server_name {{ server.server_name|join(" ") }}; + charset utf-8; + + # Hide Nginx version + server_tokens off; + + {% if server.root is defined -%} + root {{ server.root }}; + {% endif -%} + {% if server.index is defined -%} + index {{ server.index|join(" ") }}; + {% endif -%} + + {% if server.access_log is defined -%} + access_log {{ server.access_log }}; + {% endif -%} + {% if server.error_log is defined -%} + error_log {{ server.error_log }}; + {% endif -%} + + {% if server.locations is defined -%} + + {% for location in server.locations -%} + location {{ location.filter }} { + {% for param in location.params -%} + {{ param }}; + {% endfor -%} + } + {% endfor -%} +{% endif -%} +} +{% endfor %} diff --git a/roles/nginx/templates/nginx/snippets/fastcgi.conf.j2 b/roles/nginx/templates/nginx/snippets/fastcgi.conf.j2 new file mode 100644 index 0000000..0b21030 --- /dev/null +++ b/roles/nginx/templates/nginx/snippets/fastcgi.conf.j2 @@ -0,0 +1,18 @@ +# {{ ansible_managed }} + +# regex to split $uri to $fastcgi_script_name and $fastcgi_path +fastcgi_split_path_info (^/[^/]*)(.*)$; + +# check that the PHP script exists before passing it +try_files $fastcgi_script_name =404; + +# Bypass the fact that try_files resets $fastcgi_path_info +# see: http://trac.nginx.org/nginx/ticket/321 +set $path_info $fastcgi_path_info; +fastcgi_param PATH_INFO $path_info; + +# Let NGINX handle errors +fastcgi_intercept_errors on; + +include /etc/nginx/fastcgi.conf; +fastcgi_pass unix:/var/run/fcgiwrap.socket; diff --git a/roles/nginx/templates/nginx/snippets/options-proxypass.conf.j2 b/roles/nginx/templates/nginx/snippets/options-proxypass.conf.j2 new file mode 100644 index 0000000..9515d81 --- /dev/null +++ b/roles/nginx/templates/nginx/snippets/options-proxypass.conf.j2 @@ -0,0 +1,19 @@ +# {{ ansible_managed }} + +proxy_redirect off; +proxy_set_header Host $host; + +# Pass the real client IP +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + +# Tell proxified server that we are HTTPS, fix Wordpress +proxy_set_header X-Forwarded-Proto https; + +# WebSocket support +proxy_http_version 1.1; +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection $connection_upgrade; + +# For Owncloud WebDav +client_max_body_size 10G; diff --git a/roles/nginx/templates/nginx/snippets/options-ssl.conf.j2 b/roles/nginx/templates/nginx/snippets/options-ssl.conf.j2 new file mode 100644 index 0000000..fee51c6 --- /dev/null +++ b/roles/nginx/templates/nginx/snippets/options-ssl.conf.j2 @@ -0,0 +1,17 @@ +# {{ ansible_managed }} + +ssl_certificate {{ nginx.ssl.cert }}; +ssl_certificate_key {{ nginx.ssl.cert_key }}; +ssl_session_timeout 1d; +ssl_session_cache shared:MozSSL:10m; +ssl_session_tickets off; +ssl_dhparam /etc/letsencrypt/dhparam; +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 off; + +# Enable OCSP Stapling, point to certificate chain +ssl_stapling on; +ssl_stapling_verify on; +ssl_trusted_certificate {{ nginx.ssl.trusted_cert }}; + diff --git a/roles/nginx/templates/update-motd.d/05-service.j2 b/roles/nginx/templates/update-motd.d/05-service.j2 new file mode 100755 index 0000000..fdff0b8 --- /dev/null +++ b/roles/nginx/templates/update-motd.d/05-service.j2 @@ -0,0 +1,3 @@ +#!/usr/bin/tail +14 +# {{ ansible_managed }} +[0m> [38;5;82mNGINX[0m a été déployé sur cette machine. Voir [38;5;6m/etc/nginx/[0m. diff --git a/roles/nginx/templates/www/html/401.html.j2 b/roles/nginx/templates/www/html/401.html.j2 new file mode 100644 index 0000000..93fc38a --- /dev/null +++ b/roles/nginx/templates/www/html/401.html.j2 @@ -0,0 +1,18 @@ +{{ ansible_header | comment('xml') }} + + +
++ Pour éviter le scan des adresses de diffusions par un robot, cette page demande un identifiant et mot de passe. +
+Whoops, le service prend trop de temps à répondre…
+Essayez de rafraîchir la page. Si le problème persiste, pensez + à contacter {{ nginx.who }}.
+ + + diff --git a/roles/nginx/templates/www/html/robots.txt.j2 b/roles/nginx/templates/www/html/robots.txt.j2 new file mode 100644 index 0000000..3fbaed7 --- /dev/null +++ b/roles/nginx/templates/www/html/robots.txt.j2 @@ -0,0 +1,4 @@ +{{ ansible_header | comment }} + +User-agent: * +Disallow: /