From 8df1cba71c4984f42a4657c31558090b6a92e2a7 Mon Sep 17 00:00:00 2001
From: Mark Janssen -- Sig-I/O Automatisering <mark@sig-io.nl>
Date: Sun, 14 Jul 2024 21:43:53 +0200
Subject: [PATCH] WIP: generiek nginx role

---
 authorized_keys/foobar.keys             |  4 +-
 group_vars/all.yaml                     | 21 ++++---
 group_vars/monitoring.yaml              |  6 ++
 monitoring.yaml                         |  7 ++-
 roles/acme/tasks/main.yaml              |  2 +-
 roles/common/tasks/main.yaml            |  3 +
 roles/nginx/defaults/main.yaml          | 16 +++++
 roles/nginx/handlers/main.yaml          | 11 ++++
 roles/nginx/tasks/main.yaml             | 80 +++++++++++++++++++++++++
 roles/nginx/templates/default.j2        | 37 ++++++++++++
 roles/nginx/templates/etc-nginx.conf.j2 | 35 +++++++++++
 roles/nginx/templates/site.conf.j2      | 36 +++++++++++
 roles/nginx/templates/tls_params.j2     | 22 +++++++
 snippets/prometheus-nginx.j2            | 13 ++++
 14 files changed, 278 insertions(+), 15 deletions(-)
 create mode 100644 roles/nginx/defaults/main.yaml
 create mode 100644 roles/nginx/handlers/main.yaml
 create mode 100644 roles/nginx/tasks/main.yaml
 create mode 100644 roles/nginx/templates/default.j2
 create mode 100644 roles/nginx/templates/etc-nginx.conf.j2
 create mode 100644 roles/nginx/templates/site.conf.j2
 create mode 100644 roles/nginx/templates/tls_params.j2
 create mode 100644 snippets/prometheus-nginx.j2

diff --git a/authorized_keys/foobar.keys b/authorized_keys/foobar.keys
index 6493dc3..f7fac20 100644
--- a/authorized_keys/foobar.keys
+++ b/authorized_keys/foobar.keys
@@ -1,2 +1,2 @@
-ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUIAkaRsvb6cD1XIGF80JpMH1mYE9XhCgptOkt9AfloZQlO7Ds5XeCwJk5/TsoidTcb/0yFUov8SMwaIVtrFfkNUqqeAsfm3luJ4JwOXeCwrXD6W7c5Wqg/FGNH0eZr0kEnxpNS10L72+oNBQgnlSNjqWS29lEmXApKQ3IKy6aP9cMwEh25fsH/2G7mHsZX2UMPK0tZPC6MPxY5P9PWLIulUpsX96c6OcAvGYIvsCnecsVsTdhK36w4Z/t7XoLFz5X6k3eXT7gG4SMGuBixjroTUhumWzgJJ6T1Nn/eESe7Im8krlzO/0hG/F8uBy3s04TAJuXFmygvtC4YLyq91U5
-ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICyKprIcR81+RFSBxU3iyW4vd0ctr0q1Pqifzxbro+0C
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUIAkaRsvb6cD1XIGF80JpMH1mYE9XhCgptOkt9AfloZQlO7Ds5XeCwJk5/TsoidTcb/0yFUov8SMwaIVtrFfkNUqqeAsfm3luJ4JwOXeCwrXD6W7c5Wqg/FGNH0eZr0kEnxpNS10L72+oNBQgnlSNjqWS29lEmXApKQ3IKy6aP9cMwEh25fsH/2G7mHsZX2UMPK0tZPC6MPxY5P9PWLIulUpsX96c6OcAvGYIvsCnecsVsTdhK36w4Z/t7XoLFz5X6k3eXT7gG4SMGuBixjroTUhumWzgJJ6T1Nn/eESe7Im8krlzO/0hG/F8uBy3s04TAJuXFmygvtC4YLyq91U5 Sig-I/O Beheer key
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICyKprIcR81+RFSBxU3iyW4vd0ctr0q1Pqifzxbro+0C mark@x240-ed25519
diff --git a/group_vars/all.yaml b/group_vars/all.yaml
index bdafa45..fd209d8 100644
--- a/group_vars/all.yaml
+++ b/group_vars/all.yaml
@@ -6,22 +6,25 @@ notify_email: bestuur@bitlair.nl
 acme_bootstrap_certs: no
 trusted_ranges:
     # localhost
-  - { v: ipv4, cidr: 127.0.0.1/8 }
+  - { v: ipv4, cidr: "127.0.0.1/8" }
   - { v: ipv6, cidr: "::1" }
     # rf1928
-  - { v: ipv4, cidr: 10.0.0.0/8 }
-  - { v: ipv4, cidr: 172.16.0.0/12 }
-  - { v: ipv4, cidr: 192.168.0.0/16 }
+  - { v: ipv4, cidr: "10.0.0.0/8" }
+  - { v: ipv4, cidr: "172.16.0.0/12" }
+  - { v: ipv4, cidr: "192.168.0.0/16" }
     # v6 local
   - { v: ipv6, cidr: "fe80::/10" }
     # vihamij
-  - { v: ipv4, cidr: 45.88.49.140 }
+  - { v: ipv4, cidr: "45.88.49.140" }
     # eventinfra
-  - { v: ipv4, cidr: 204.2.64.0/20 }
-
-  - { v: ipv4, cidr: 100.64.0.0/10 }
-  - { v: ipv4, cidr: 185.205.52.194/32 }
+  - { v: ipv4, cidr: "204.2.64.0/20" }
+    # bitlair
+  - { v: ipv4, cidr: "100.64.0.0/10" }
+  - { v: ipv4, cidr: "185.205.52.194/32" }
   - { v: ipv6, cidr: "2a02:166b:92::/48" }
+    # foobar
+  - { v: ipv4, cidr: "31.187.251.213/32" }
+  - { v: ipv6, cidr: "2a0e:5700:4:2::/64" }
 
 root_access:
   - ak
diff --git a/group_vars/monitoring.yaml b/group_vars/monitoring.yaml
index b692290..51d9b97 100644
--- a/group_vars/monitoring.yaml
+++ b/group_vars/monitoring.yaml
@@ -40,3 +40,9 @@ prometheus_scrape_configs:
         target_label: instance
       - target_label: __address__
         replacement: "{{ blackbox_exporter_web_listen_address }}"
+
+nginx_sites:
+  - server_name: "dashboard.bitlair.nl"
+    localproxy: "9000"
+    snippets:
+      - "prometheus-nginx.j2"
diff --git a/monitoring.yaml b/monitoring.yaml
index 9ad8623..9e05df0 100644
--- a/monitoring.yaml
+++ b/monitoring.yaml
@@ -2,6 +2,7 @@
 
 - hosts: monitoring
   roles:
-    - common
-    - acme
-    - monitoring
+    - { role: "common", tags: [ "common" ] }
+    - { role: "acme", tags: [ "acme" ] }
+    - { role: "nginx", tags: [ "nginx" ] }
+    - { role: "monitoring", tags: [ "monitoring" ] }
diff --git a/roles/acme/tasks/main.yaml b/roles/acme/tasks/main.yaml
index 229f566..0be3133 100644
--- a/roles/acme/tasks/main.yaml
+++ b/roles/acme/tasks/main.yaml
@@ -23,7 +23,7 @@
     owner: "{{ item.owner | default('root') }}"
     group: "{{ item.group | default('root') }}"
     mode: "{{ item.mode | default('0640') }}"
-    notify: "{{ item.notify | default([]) }}"
+  notify: "{{ item.notify | default([]) }}"
   with_items:
     - { src: "config.sh",          dest: "/etc/dehydrated/conf.d/ansible.sh", mode: '0755' }
     - { src: "deploy.sh",          dest: "/etc/dehydrated/conf.d/deploy.sh",  mode: '0755' }
diff --git a/roles/common/tasks/main.yaml b/roles/common/tasks/main.yaml
index 10ce3a1..d20da44 100644
--- a/roles/common/tasks/main.yaml
+++ b/roles/common/tasks/main.yaml
@@ -18,6 +18,7 @@
     - { src: "sources.list.j2", dest: "/etc/apt/sources.list" }
     - { src: "apt-auto-upgrades.j2", dest: "/etc/apt/apt.conf.d/20auto-upgrades" }
     - { src: "apt-unattended-upgrades.j2", dest: "/etc/apt/apt.conf.d/50unattended-upgrades" }
+  register: aptconfig
   when:
     - ansible_os_family == "Debian"
   tags:
@@ -56,6 +57,8 @@
 
 - name: Install standard packages
   ansible.builtin.apt:
+    cache_valid_time: 3600
+    update_cache: "{{ aptconfig.changed | bool | default(false) }}"
     pkg:
       - curl
       - fzf
diff --git a/roles/nginx/defaults/main.yaml b/roles/nginx/defaults/main.yaml
new file mode 100644
index 0000000..b9e4710
--- /dev/null
+++ b/roles/nginx/defaults/main.yaml
@@ -0,0 +1,16 @@
+---
+
+nginx_package:              "nginx-light"
+nginx_user:                 "www-data"
+nginx_modules_dir:          "/etc/nginx/modules-enabled"
+
+
+nginx_tls_version:          "TLSv1.2 TLSv1.3"
+nginx_tls_cipherlist:       "EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:!SHA:!RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS"
+nginx_tls_curve:            "prime256v1:secp384r1"
+nginx_tls_cache_size:       "10m"
+nginx_tls_session_timeout:  "1h"
+nginx_ssl_stapling:         "on"
+nginx_ssl_stapling_verify:  "on"
+nginx_wk_acme:              "/var/lib/dehydrated/acme-challenges"
+
diff --git a/roles/nginx/handlers/main.yaml b/roles/nginx/handlers/main.yaml
new file mode 100644
index 0000000..e9738d0
--- /dev/null
+++ b/roles/nginx/handlers/main.yaml
@@ -0,0 +1,11 @@
+---
+
+- name: Reload nginx
+  ansible.builtin.systemd:
+    name: nginx
+    state: reloaded
+    enabled: true
+  listen: "Reload app-services"
+  when:
+    - nginx_sites is defined
+
diff --git a/roles/nginx/tasks/main.yaml b/roles/nginx/tasks/main.yaml
new file mode 100644
index 0000000..78f6f9b
--- /dev/null
+++ b/roles/nginx/tasks/main.yaml
@@ -0,0 +1,80 @@
+---
+
+- name: Install nginx base package
+  ansible.builtin.apt:
+    name: "{{ nginx_package }}"
+    state: present
+  when:
+    - nginx_sites is defined
+
+- name: Create sites-available / sites-enabled directories
+  ansible.builtin.file:
+    state: directory
+    path: "{{ item.path }}"
+    owner: "{{ item.owner | default('root') }}"
+    group: "{{ item.group | default('root') }}"
+    mode: "{{ item.mode | default('0755') }}"
+  with_items:
+    - { path: "/etc/nginx/sites-available" }
+    - { path: "/etc/nginx/sites-enabled" }
+  notify: Reload nginx
+  when:
+    - nginx_sites is defined
+
+- name: Template default nginx config files
+  ansible.builtin.template:
+    src: "{{ item.src }}"
+    dest: "{{ item.dest }}"
+    owner: "{{ item.owner | default('root') }}"
+    group: "{{ item.group | default('root') }}"
+    mode: "{{ item.mode | default('0644') }}"
+    force: "{{ item.force | default('yes') }}"
+    backup: true
+  loop_control:
+    label: "{{ item.dest }}"
+  with_items:
+    - { src: "etc-nginx.conf.j2",   dest: "/etc/nginx/nginx.conf", notify: "Reload nginx" }
+    - { src: "tls_params.j2",       dest: "/etc/nginx/tls_params", notify: "Reload nginx" }
+    - { src: "default.j2",          dest: "/etc/nginx/sites-available/default", notify: "Reload nginx" }
+#    - { src: "dhparam.pem.j2", dest: "{{ nginx_dhparams_file }}", notify: "Reload nginx" }
+#    - { src: "check_nginx.j2", dest: "{{ nagios_plugin_location }}/check_nginx", mode: '755' }
+#    - { src: "nrpe-check_nginx.j2", dest: "/etc/nagios/nrpe.d/10-nginx.cfg", notify: "Restart nrpe" }
+  notify: "{{ item.notify | default(omit) }}"
+  when:
+    - nginx_sites is defined
+
+- name: Template site-specific configs
+  ansible.builtin.template:
+    src: "site.conf.j2"
+    dest: "/etc/nginx/sites-available/{{ site.server_name }}.conf"
+    owner: "{{ site.owner | default('root') }}"
+    group: "{{ site.group | default('root') }}"
+    mode:  "{{ site.mode  | default('0644') }}"
+    force: "{{ site.force | default('yes') }}"
+    backup: true
+  loop: "{{ nginx_sites }}"
+  loop_control:
+    loop_var: site
+    label: "{{ site.server_name }}"
+  notify: Reload nginx
+  when:
+    - nginx_sites is defined
+  tags:
+    - nginxextra
+    - nginx_site
+
+- name: Enable nginx sites
+  ansible.builtin.file:
+    src: "/etc/nginx/sites-available/{{ site.server_name }}.conf"
+    path: "/etc/nginx/sites-enabled/{{ site.server_name }}.conf"
+    state: "{% if site.disabled | default(false) %}absent{% else %}link{% endif %}"
+    mode: "0644"
+  loop: "{{ nginx_sites }}"
+  loop_control:
+    loop_var: site
+    label: "{{ site.server_name }}"
+  notify: Reload nginx
+  when:
+    - nginx_sites is defined
+  ignore_errors: "{{ ansible_check_mode }}"
+
diff --git a/roles/nginx/templates/default.j2 b/roles/nginx/templates/default.j2
new file mode 100644
index 0000000..b417134
--- /dev/null
+++ b/roles/nginx/templates/default.j2
@@ -0,0 +1,37 @@
+# {{ ansible_managed }}
+
+server {
+    listen 80 default_server;
+    listen [::]:80
+
+    server_name {{ inventory_hostname }};
+
+    # Accept ACME-Challenges over http
+    location ^~ /.well-known/acme-challenge/ {
+        alias {{ nginx_wk_acme }}/;
+    }
+
+    # Block .ht files
+    location ~ /\.ht {
+        deny all;
+    }
+
+    # Redirect everything to https by default
+    location / {
+        return 301 https://$host$request_uri;
+    }
+
+    location /server_status {
+        # Enable Nginx stats
+        stub_status on;
+        # Only allow access from localhost
+        allow 127.0.0.1;
+        # Other request should be denied
+        deny all;
+    }
+}
+
+{% for line in nginx_default_extra | default([]) %}
+{{ line }}
+{% endfor %}
+
diff --git a/roles/nginx/templates/etc-nginx.conf.j2 b/roles/nginx/templates/etc-nginx.conf.j2
new file mode 100644
index 0000000..b4d4d7a
--- /dev/null
+++ b/roles/nginx/templates/etc-nginx.conf.j2
@@ -0,0 +1,35 @@
+# {{ ansible_managed }}
+
+user                {{ nginx_user }};
+worker_processes    auto;
+pid                 /run/nginx.pid;
+worker_rlimit_nofile 16384;
+include             {{ nginx_modules_dir }}/*.conf;
+
+http {
+    sendfile on;
+    tcp_nopush on;
+    tcp_nodelay on;
+    keepalive_timeout 65;
+    types_hash_max_size 2048;
+    server_tokens off;
+
+    include /etc/nginx/mime.types;
+    default_type application/octet-stream;
+
+    # Default nginx log format with $request time added
+    log_format bitlair '$remote_addr - $remote_user [$time_local] '
+                    '"$request" $status $body_bytes_sent '
+                    '"$http_referer" "$http_user_agent" $request_time';
+    access_log /var/log/nginx/access.log bitlair;
+
+    gzip on;
+    gzip_disable "msie6";
+
+{% for line in nginx_http_extra | default([]) %}
+    {{ line }}
+{% endfor %}
+
+    include /etc/nginx/conf.d/*.conf;
+    include /etc/nginx/sites-enabled/*;
+}
diff --git a/roles/nginx/templates/site.conf.j2 b/roles/nginx/templates/site.conf.j2
new file mode 100644
index 0000000..09e4e0c
--- /dev/null
+++ b/roles/nginx/templates/site.conf.j2
@@ -0,0 +1,36 @@
+# {{ ansible_managed }}
+
+server {
+    listen 443 ssl http2;
+    listen [::]:443 ssl http2;
+
+    server_name {{ site.server_name|default(inventory_hostname) }}{% if site.server_alias is defined %} {{ site.server_alias }}{% endif %};
+
+    include /etc/nginx/tls_params;
+    ssl_certificate        /var/lib/dehydrated/certs/{{ site.server_name }}/fullchain.pem;
+    ssl_certificate_key    /var/lib/dehydrated/certs/{{ site.server_name }}/fullkey.pem;
+
+    location ~ /\.ht {
+        deny all;
+    }
+
+    access_log  /var/log/nginx/{{ site.server_name }}.access.log bitlair;
+    error_log   /var/log/nginx/{{ site.server_name }}.error.log;
+
+{% if site.localproxy is defined %}
+    location / {
+        proxy_pass http://localhost:{{ site.localproxy }}/;
+        include proxy_params;
+    }
+{% endif %}
+
+    # Include snippets
+{% for file in site.snippets | default([]) %}
+{% include "../../../snippets/" . file %}
+{% endif %}
+
+    # Per site configuration
+{% for line in site.config | default([]) %}
+    {{ line }}
+{% endfor %}
+}
diff --git a/roles/nginx/templates/tls_params.j2 b/roles/nginx/templates/tls_params.j2
new file mode 100644
index 0000000..7abe3b6
--- /dev/null
+++ b/roles/nginx/templates/tls_params.j2
@@ -0,0 +1,22 @@
+# {{ ansible_managed }}
+
+ssl_session_timeout         {{ nginx_tls_session_timeout }};
+ssl_session_tickets         off;
+
+ssl_prefer_server_ciphers   on;
+ssl_session_cache           shared:SSL:{{ nginx_tls_cache_size }};
+
+ssl_protocols               {{ nginx_tls_version }};
+ssl_ciphers                 {{ nginx_tls_cipherlist }};
+ssl_ecdh_curve              {{ nginx_tls_curve }};
+
+# HSTS (ngx_http_headers_module is required) (63072000 seconds)
+add_header                  Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
+add_header                  X-Frame-Options "sameorigin";
+add_header                  X-Content-Type-Options "nosniff";
+add_header                  X-Robots-Tag noindex;
+
+# OCSP stapling
+ssl_stapling                {{ nginx_ssl_stapling }};
+ssl_stapling_verify         {{ nginx_ssl_stapling_verify }};
+
diff --git a/snippets/prometheus-nginx.j2 b/snippets/prometheus-nginx.j2
new file mode 100644
index 0000000..a38e527
--- /dev/null
+++ b/snippets/prometheus-nginx.j2
@@ -0,0 +1,13 @@
+# dashboard nginx config snippet
+
+location /prometheus/ {
+    proxy_pass http://localhost:9090/prometheus/;
+    include proxy_params;
+
+{% for host in bitlair_ip_whitelist %}
+    allow {{ host }};
+{% endif %}
+    allow "127.0.0.0/8"
+    allow "::1";
+    deny all;
+}