From 70a4684f51e1585d4e287f95024f8a3b5a8403c4 Mon Sep 17 00:00:00 2001 From: uumas Date: Wed, 19 Apr 2023 00:24:44 +0300 Subject: [PATCH] add nginx role --- roles/nginx/defaults/main.yml | 13 ++ roles/nginx/handlers/main.yml | 6 + roles/nginx/tasks/certbot.yml | 46 ++++++ roles/nginx/tasks/main.yml | 38 +++++ .../nginx/templates/conf/ssl-headers.conf.j2 | 12 ++ roles/nginx/templates/letsencrypt.conf.j2 | 5 + roles/nginx/templates/site.j2 | 156 ++++++++++++++++++ 7 files changed, 276 insertions(+) create mode 100644 roles/nginx/defaults/main.yml create mode 100644 roles/nginx/handlers/main.yml create mode 100644 roles/nginx/tasks/certbot.yml create mode 100644 roles/nginx/tasks/main.yml create mode 100644 roles/nginx/templates/conf/ssl-headers.conf.j2 create mode 100644 roles/nginx/templates/letsencrypt.conf.j2 create mode 100644 roles/nginx/templates/site.j2 diff --git a/roles/nginx/defaults/main.yml b/roles/nginx/defaults/main.yml new file mode 100644 index 0000000..d619955 --- /dev/null +++ b/roles/nginx/defaults/main.yml @@ -0,0 +1,13 @@ +--- + +nginx_confs: [] + +nginx_certbot: true +certbot_regenerate: false + +nginx_proxy_headers: + X-Forwarded-For: '$remote_addr' + X-Forwarded-Proto: '$scheme' + Host: '$host' + Upgrade: '$http_upgrade' + Connection: '$connection_upgrade' diff --git a/roles/nginx/handlers/main.yml b/roles/nginx/handlers/main.yml new file mode 100644 index 0000000..581a0d6 --- /dev/null +++ b/roles/nginx/handlers/main.yml @@ -0,0 +1,6 @@ +--- + +- name: Reload nginx + ansible.builtin.systemd: + name: nginx + state: reloaded diff --git a/roles/nginx/tasks/certbot.yml b/roles/nginx/tasks/certbot.yml new file mode 100644 index 0000000..5fe16e5 --- /dev/null +++ b/roles/nginx/tasks/certbot.yml @@ -0,0 +1,46 @@ +--- + +- name: Esnure certbot installed + ansible.builtin.apt: + name: + - certbot + - python3-certbot-nginx + state: present + +- name: Check if certificate exists + ansible.builtin.stat: + path: /etc/letsencrypt/live/{{ ansible_fqdn }}/cert.pem + register: cert + +- name: Get current certificate info + community.crypto.x509_certificate_info: + path: /etc/letsencrypt/live/{{ ansible_fqdn }}/cert.pem + register: certinfo + +- name: Set fact to regenerate certificates if new domains are added + ansible.builtin.set_fact: + certbot_regenerate: true + when: item.name is defined and 'DNS:' + item.name not in certinfo.subject_alt_name + loop: "{{ nginx_servers }}" + +- name: Generate new certificate if one doesn't exist. + ansible.builtin.command: > + certbot --nginx certonly + --non-interactive + --email {{ certbot_admin_email }} + --agree-tos + --expand + --domains {{ ansible_fqdn }}{% for server in nginx_servers %}{% if server.name is defined %},{{ server.name }}{% endif %}{% endfor %} + when: not cert.stat.exists or certbot_regenerate + notify: Reload nginx + +- name: Ensure certificate configured for nginx + ansible.builtin.template: + src: letsencrypt.conf.j2 + dest: /etc/nginx/conf.d/letsencrypt.conf + mode: 0644 + notify: Reload nginx + +- name: Add ssl header config to the list of configs + ansible.builtin.set_fact: + nginx_confs: "{{ nginx_confs + ['ssl-headers'] }}" diff --git a/roles/nginx/tasks/main.yml b/roles/nginx/tasks/main.yml new file mode 100644 index 0000000..17c508e --- /dev/null +++ b/roles/nginx/tasks/main.yml @@ -0,0 +1,38 @@ +--- + +- name: Ensure nginx installed + ansible.builtin.apt: + name: nginx + state: latest + +- name: Ensure nginx default config disabled + ansible.builtin.file: + path: /etc/nginx/sites-enabled/default + state: absent + notify: Reload nginx + +- name: Set up certbot + ansible.builtin.include_tasks: certbot.yml + when: nginx_certbot + +- name: Ensure nginx configs in place + ansible.builtin.template: + src: "conf/{{ item }}.conf.j2" + dest: "/etc/nginx/conf.d/{{ item }}.conf" + mode: 0644 + loop: "{{ nginx_confs }}" + notify: Reload nginx + +- name: Ensure nginx main site configured + ansible.builtin.template: + src: "site.j2" + dest: "/etc/nginx/sites-available/main" + mode: 0644 + notify: Reload nginx + +- name: Ensure nginx main site enabled + ansible.builtin.file: + src: "../sites-available/main" + dest: "/etc/nginx/sites-enabled/main" + state: link + notify: Reload nginx diff --git a/roles/nginx/templates/conf/ssl-headers.conf.j2 b/roles/nginx/templates/conf/ssl-headers.conf.j2 new file mode 100644 index 0000000..03328bf --- /dev/null +++ b/roles/nginx/templates/conf/ssl-headers.conf.j2 @@ -0,0 +1,12 @@ +# {{ ansible_managed }} + +# Strict Transport Security (HSTS), Tell browsers to use only https (adding includeSubDomains would also force subdomains to use HSTS) +add_header Strict-Transport-Security "max-age=15552000; preload" always; + +# Expect Certificate Transparency and -Stapling, Security measures for checking HTTPS-certificate validity/revocation +add_header Expect-CT 'enforce; max-age=86400;' always; +add_header Expect-Staple 'max-age=86400; preload' always; +add_header Referrer-Policy "no-referrer-when-downgrade" always; +add_header X-Content-Type-Options "nosniff" always; +add_header X-Xss-Protection "1; mode=block" always; +add_header X-Frame-Options "SAMEORIGIN" always; diff --git a/roles/nginx/templates/letsencrypt.conf.j2 b/roles/nginx/templates/letsencrypt.conf.j2 new file mode 100644 index 0000000..67e0ef3 --- /dev/null +++ b/roles/nginx/templates/letsencrypt.conf.j2 @@ -0,0 +1,5 @@ +# {{ ansible_managed }} + +ssl_certificate /etc/letsencrypt/live/{{ ansible_fqdn }}/fullchain.pem; +ssl_certificate_key /etc/letsencrypt/live/{{ ansible_fqdn }}/privkey.pem; +ssl_trusted_certificate /etc/letsencrypt/live/{{ ansible_fqdn }}/fullchain.pem; diff --git a/roles/nginx/templates/site.j2 b/roles/nginx/templates/site.j2 new file mode 100644 index 0000000..7c7ffd0 --- /dev/null +++ b/roles/nginx/templates/site.j2 @@ -0,0 +1,156 @@ +# {{ ansible_managed }} + +{% if nginx_upstreams is defined %} +# Upstreams +{% for upstream in nginx_upstreams | dict2items %} +{% if upstream.value.servers | length != 0 %} +upstream {{ upstream.key }} { +{% if upstream.value.method is defined %} + {{ upstream.value.method }}; +{% endif %} +{% for server in upstream.value.servers %} +{% if server | int != 0 %} + server 127.0.0.1:{{ server }}; +{% else %} + server {{ server }}; +{% endif %} +{% endfor %} +} + +{% endif %} +{% endfor %} +{% endif %} + +{% if nginx_maps is defined %} +{% for map in nginx_maps | dict2items %} +{% if map.value.var | length != 0 %} +map ${{ map.value.var }} ${{ map.key }} { +{% for rule in map.value.rules | dict2items %} + {{ rule.key }} {{ rule.value }}; +{% endfor %} +} + +{% endif %} +{% endfor %} +{% endif %} + +{% if nginx_certbot %} +# HTTP -> HTTPS redirect +server { + listen 0.0.0.0:80 default_server; +{% if ansible_default_ipv6.address is defined %} + listen [::]:80 default_server; +{% endif %} + server_name {{ ansible_fqdn }}; + location / { + return 301 https://$host$request_uri; + } +} +{% endif %} + +# Websocket map +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +{% for server in nginx_servers %} +server { +{% for listener in server.listen | default([{}]) %} +{% if listener.ip | default('all') == 'all' %} + listen 0.0.0.0:{{ listener.port | default('443' if nginx_certbot else '80') }} {{ 'ssl http2 ' if nginx_certbot else '' }}{{ 'default_server' if server.name is not defined or server.name == ansible_fqdn }}; +{% if ansible_default_ipv6.address is defined %} + listen [::]:{{ listener.port | default('443' if nginx_certbot else '80') }} {{ 'ssl http2 ' if nginx_certbot else '' }}{{ 'default_server' if server.name is not defined or server.name == ansible_fqdn }}; +{% endif %} + server_name {{ server.name | default(ansible_fqdn) }}; + +{% elif listener.ip == 'localhost' %} + listen 127.0.0.1:{{ listener.port }} default_server; +{% if ansible_default_ipv6.address is defined %} + listen [::1]:{{ listener.port }} default_server; +{% endif %} + server_name localhost; +{% endif %} +{% endfor %} + +{% if server.return is defined %} +# Sipmle returns +{% for return in server.return %} + location {{ return.location }} { + return {{ return.type | default('200') }} '{{ return.content | to_json if return.content_type | default('plain') == 'json' else return.content }}'; +{% if return.headers is mapping %} +{% for header in return.headers | dict2items %} + add_header {{ header.key }} {{ header.value }}; +{% endfor %} +{% endif %} + } +{% endfor %} +{% endif %} + +{% if server.reverse_proxy is defined %} +# Reverse proxy +{% for upstream in server.reverse_proxy %} +{% if nginx_upstreams[upstream].servers | length != 0 %} + +# {{ upstream }} +{% for location in nginx_upstreams[upstream].locations | default([{'name': '/'}]) %} + +{% if location.name | length != 0 %} + location {{ location.name }} { + proxy_pass http://{{ upstream }}; +{% if location.proxy_headers is mapping %} +{% for header in location.proxy_headers | dict2items %} + proxy_set_header {{ header.key }} {{ header.value }}; +{% endfor %} +{% endif %} +{% if nginx_proxy_headers is mapping %} +{% for header in nginx_proxy_headers | dict2items %} +{% if location.proxy_headers[header.key] is not defined %} + proxy_set_header {{ header.key }} {{ header.value }}; +{% endif %} +{% endfor %} +{% endif %} +{% if location.additional_options is defined %} +{% for item in location.additional_options %} + {{ item }}; +{% endfor %} +{% endif %} + } +{% endif %} + +{% endfor %} +{% endif %} + +{% endfor %} +{% endif %} + +{% if server.reverse_proxy_map is defined %} +# Mapping reverse proxy +{% for map in server.reverse_proxy_map %} +{% if nginx_maps[map].var | length != 0 %} + +# {{ map }} +{% for location in nginx_maps[map].locations %} + +{% if location.name | length != 0 %} + location {{ location.name }} { + proxy_pass http://${{ map }}; +{% if nginx_proxy_headers is mapping %} +{% for header in nginx_proxy_headers | dict2items %} + proxy_set_header {{ header.key }} {{ header.value }}; +{% endfor %} +{% endif %} +{% if location.additional_options is defined %} +{% for item in location.additional_options %} + {{ item }}; +{% endfor %} +{% endif %} + } +{% endif %} +{% endfor %} +{% endif %} +{% endfor %} +{% endif %} +} + +{% endfor %}