From ba94509bca09a311d81dbf8007c76e39b0706adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20D=C3=A9siles?= <1536672+cdesiles@users.noreply.github.com> Date: Sat, 20 Dec 2025 23:14:00 +0100 Subject: [PATCH] feat: fix systemd user and add static-web role --- roles/nginx/templates/forwarder.conf.j2 | 16 ++- roles/nginx/templates/nginx.conf.j2 | 5 - roles/ntfy/templates/ntfy.service.j2 | 2 + roles/static-web/README.md | 110 ++++++++++++++++++ roles/static-web/defaults/main.yml | 20 ++++ roles/static-web/handlers/main.yml | 5 + roles/static-web/meta/main.yml | 3 + roles/static-web/tasks/main.yml | 67 +++++++++++ .../static-web/templates/nginx-vhost.conf.j2 | 79 +++++++++++++ roles/unbound/tasks/main.yml | 3 + .../templates/uptime-kuma.service.j2 | 2 + 11 files changed, 301 insertions(+), 11 deletions(-) create mode 100644 roles/static-web/README.md create mode 100644 roles/static-web/defaults/main.yml create mode 100644 roles/static-web/handlers/main.yml create mode 100644 roles/static-web/meta/main.yml create mode 100644 roles/static-web/tasks/main.yml create mode 100644 roles/static-web/templates/nginx-vhost.conf.j2 diff --git a/roles/nginx/templates/forwarder.conf.j2 b/roles/nginx/templates/forwarder.conf.j2 index 6b20efb..b0ebd23 100644 --- a/roles/nginx/templates/forwarder.conf.j2 +++ b/roles/nginx/templates/forwarder.conf.j2 @@ -3,19 +3,23 @@ # Transparent TCP proxy (no protocol inspection) {% if config.http | default(true) %} +upstream {{ domain | replace('.', '_') | replace('-', '_') }}_http { + server {{ config.forward_to }}:80; +} + server { listen 80; - # Using variable forces runtime DNS resolution (avoids startup failures) - set $upstream_http {{ config.forward_to }}; - proxy_pass $upstream_http:80; + proxy_pass {{ domain | replace('.', '_') | replace('-', '_') }}_http; } {% endif %} {% if config.https | default(true) %} +upstream {{ domain | replace('.', '_') | replace('-', '_') }}_https { + server {{ config.forward_to }}:443; +} + server { listen 443; - # Using variable forces runtime DNS resolution (avoids startup failures) - set $upstream_https {{ config.forward_to }}; - proxy_pass $upstream_https:443; + proxy_pass {{ domain | replace('.', '_') | replace('-', '_') }}_https; } {% endif %} diff --git a/roles/nginx/templates/nginx.conf.j2 b/roles/nginx/templates/nginx.conf.j2 index 1bc610b..c982165 100644 --- a/roles/nginx/templates/nginx.conf.j2 +++ b/roles/nginx/templates/nginx.conf.j2 @@ -57,11 +57,6 @@ http { {% if nginx_forwarder and nginx_forwarder | length > 0 %} # Stream block for TCP/UDP proxying stream { - # DNS resolver for runtime hostname resolution - # Using 127.0.0.1 (systemd-resolved) with 30s cache and 5s timeout - resolver 127.0.0.1 valid=30s ipv6=off; - resolver_timeout 5s; - # Load stream configurations include {{ nginx_streams_dir }}/*.conf; } diff --git a/roles/ntfy/templates/ntfy.service.j2 b/roles/ntfy/templates/ntfy.service.j2 index 5372ca8..6072e2e 100644 --- a/roles/ntfy/templates/ntfy.service.j2 +++ b/roles/ntfy/templates/ntfy.service.j2 @@ -6,6 +6,8 @@ After=network-online.target [Service] Type=oneshot RemainAfterExit=true +User={{ ansible_user }} +Group={{ ansible_user }} WorkingDirectory={{ podman_projects_dir }}/ntfy ExecStart=/usr/bin/podman-compose up -d ExecStop=/usr/bin/podman-compose down diff --git a/roles/static-web/README.md b/roles/static-web/README.md new file mode 100644 index 0000000..aa4fdd6 --- /dev/null +++ b/roles/static-web/README.md @@ -0,0 +1,110 @@ +# static-web + +Deploy static websites from Git repositories with Nginx. + +## Features + +- Clone static sites from Git repositories +- Automatic Nginx vhost configuration +- HTTPS enabled by default with Let's Encrypt +- Support for build commands (npm, hugo, jekyll, etc.) +- Subdirectory serving (for built assets) +- Static file caching +- Security headers (including HSTS for HTTPS) + +## Dependencies + +- nginx role (automatically included via meta/main.yml) + +## Variables + +See [defaults/main.yml](defaults/main.yml) + +**Main configuration:** + +```yaml +static_web_sites: + "portfolio.example.fr": + git_repo: "https://github.com/example/portfolio.git" + git_branch: "main" # Optional, defaults to main + git_depth: 1 # Optional, shallow clone + build_command: "npm install && npm run build" # Optional + root_dir: "dist" # Optional, serve subdirectory + ssl_enabled: true # Optional, defaults to true (HTTPS) + + "blog.example.com": + git_repo: "https://github.com/example/blog.git" + # ssl_enabled defaults to true, set to false for HTTP only +``` + +## Usage + +**Inventory (host_vars or group_vars):** + +```yaml +static_web_sites: + "portfolio.example.fr": + git_repo: "https://github.com/username/portfolio.git" + + "docs.example.com": + git_repo: "https://github.com/company/documentation.git" + git_branch: "gh-pages" + root_dir: "_site" +``` + +**Playbook:** + +```yaml +- hosts: webservers + roles: + - static-web +``` + +## File Structure + +Sites are deployed to `/var/www/static//` + +Example: +``` +/var/www/static/ +├── portfolio.example.fr/ +│ └── index.html +└── blog.example.com/ + ├── _site/ # Built assets (if root_dir specified) + └── ... +``` + +## Advanced Examples + +**Hugo site:** +```yaml +static_web_sites: + "blog.example.com": + git_repo: "https://github.com/example/hugo-blog.git" + build_command: "hugo --minify" + root_dir: "public" +``` + +**React app:** +```yaml +static_web_sites: + "app.example.com": + git_repo: "https://github.com/example/react-app.git" + build_command: "npm ci && npm run build" + root_dir: "build" +``` + +## Updating Sites + +Re-run the playbook to pull latest changes: + +```bash +ansible-playbook -i inventory playbook.yml --tags static-web +``` + +## Notes + +- Nginx configuration is deployed to `{{ nginx_conf_dir }}/.conf` +- Sites are owned by nginx user (www-data on Debian, http on Arch) +- Git clones use shallow clone (depth=1) by default for efficiency +- Build commands run as nginx user diff --git a/roles/static-web/defaults/main.yml b/roles/static-web/defaults/main.yml new file mode 100644 index 0000000..93d211a --- /dev/null +++ b/roles/static-web/defaults/main.yml @@ -0,0 +1,20 @@ +--- +# Static web sites configuration +# Define sites as a dictionary with hostname as key +# Example: +# static_web_sites: +# "portfolio.example.fr": +# git_repo: "https://github.com/example/portfolio.git" +# git_branch: "main" # optional, defaults to main +# git_depth: 1 # optional, shallow clone depth +# build_command: "" # optional, command to run after git clone (e.g., npm build) +# root_dir: "" # optional, subdirectory to serve (e.g., "dist" or "build") +# ssl_enabled: true # optional, enable HTTPS with Let's Encrypt (default: true) + +static_web_sites: {} + +# Base directory for static web sites +static_web_base_dir: /var/www/static + +# Nginx user (auto-detected from nginx role) +# static_web_nginx_user: www-data # Set by nginx role vars diff --git a/roles/static-web/handlers/main.yml b/roles/static-web/handlers/main.yml new file mode 100644 index 0000000..7419154 --- /dev/null +++ b/roles/static-web/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Reload nginx + ansible.builtin.systemd: + name: nginx + state: reloaded diff --git a/roles/static-web/meta/main.yml b/roles/static-web/meta/main.yml new file mode 100644 index 0000000..8b662c9 --- /dev/null +++ b/roles/static-web/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: nginx diff --git a/roles/static-web/tasks/main.yml b/roles/static-web/tasks/main.yml new file mode 100644 index 0000000..3612a86 --- /dev/null +++ b/roles/static-web/tasks/main.yml @@ -0,0 +1,67 @@ +--- +- name: Load OS-specific variables for nginx + ansible.builtin.include_vars: "{{ item }}" + with_first_found: + - "../../nginx/vars/{{ ansible_facts['os_family'] }}.yml" + - "../../nginx/vars/debian.yml" + +- name: Install git + ansible.builtin.package: + name: git + state: present + +- name: Ensure static web base directory exists + ansible.builtin.file: + path: "{{ static_web_base_dir }}" + state: directory + owner: root + group: root + mode: "0755" + +- name: Create site directories + ansible.builtin.file: + path: "{{ static_web_base_dir }}/{{ item.key }}" + state: directory + owner: "{{ nginx_user }}" + group: "{{ nginx_user }}" + mode: "0755" + loop: "{{ static_web_sites | dict2items }}" + when: static_web_sites | length > 0 + +- name: Clone or update git repositories + ansible.builtin.git: + repo: "{{ item.value.git_repo }}" + dest: "{{ static_web_base_dir }}/{{ item.key }}" + version: "{{ item.value.git_branch | default('main') }}" + depth: "{{ item.value.git_depth | default(1) }}" + force: true + loop: "{{ static_web_sites | dict2items }}" + when: static_web_sites | length > 0 + become_user: "{{ nginx_user }}" + notify: Reload nginx + +- name: Run build commands if specified + ansible.builtin.shell: "{{ item.value.build_command }}" + args: + chdir: "{{ static_web_base_dir }}/{{ item.key }}" + loop: "{{ static_web_sites | dict2items }}" + when: + - static_web_sites | length > 0 + - item.value.build_command is defined + - item.value.build_command | length > 0 + become_user: "{{ nginx_user }}" + changed_when: true + +- name: Deploy nginx vhost configurations + ansible.builtin.template: + src: nginx-vhost.conf.j2 + dest: "{{ nginx_conf_dir }}/{{ item.key }}.conf" + owner: root + group: root + mode: "0644" + loop: "{{ static_web_sites | dict2items }}" + vars: + hostname: "{{ item.key }}" + site_config: "{{ item.value }}" + when: static_web_sites | length > 0 + notify: Reload nginx diff --git a/roles/static-web/templates/nginx-vhost.conf.j2 b/roles/static-web/templates/nginx-vhost.conf.j2 new file mode 100644 index 0000000..561b8e2 --- /dev/null +++ b/roles/static-web/templates/nginx-vhost.conf.j2 @@ -0,0 +1,79 @@ +# Static web vhost for {{ hostname }} +# Managed by Ansible - DO NOT EDIT MANUALLY + +server { + listen 80; + listen [::]:80; + server_name {{ hostname }}; + +{% if site_config.ssl_enabled | default(true) %} + # Certbot webroot for ACME challenges + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect to HTTPS + location / { + return 301 https://$server_name$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name {{ hostname }}; + + # Let's Encrypt certificates (managed by Certbot) + ssl_certificate /etc/letsencrypt/live/{{ hostname }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ hostname }}/privkey.pem; + + # SSL configuration + ssl_protocols {{ nginx_ssl_protocols }}; + ssl_prefer_server_ciphers {{ 'on' if nginx_ssl_prefer_server_ciphers else 'off' }}; +{% endif %} + + # Document root +{% if site_config.root_dir is defined and site_config.root_dir | length > 0 %} + root {{ static_web_base_dir }}/{{ hostname }}/{{ site_config.root_dir }}; +{% else %} + root {{ static_web_base_dir }}/{{ hostname }}; +{% endif %} + + # Index files + index index.html index.htm; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; +{% if site_config.ssl_enabled | default(true) %} + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; +{% endif %} + + # Logging +{% if nginx_log_backend == 'journald' %} + access_log syslog:server=unix:/dev/log,nohostname,tag=nginx_{{ hostname | replace('.', '_') | replace('-', '_') }}; + error_log syslog:server=unix:/dev/log,nohostname,tag=nginx_{{ hostname | replace('.', '_') | replace('-', '_') }}; +{% else %} + access_log /var/log/nginx/{{ hostname }}-access.log; + error_log /var/log/nginx/{{ hostname }}-error.log; +{% endif %} + + # Main location + location / { + try_files $uri $uri/ =404; + } + + # Deny access to hidden files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + # Static file caching + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/roles/unbound/tasks/main.yml b/roles/unbound/tasks/main.yml index 150881c..e6f9c98 100644 --- a/roles/unbound/tasks/main.yml +++ b/roles/unbound/tasks/main.yml @@ -165,6 +165,9 @@ [Unit] After=wg-quick@wg0.service Requires=wg-quick@wg0.service + # Make Unbound part of network-online.target (provides DNS) + Before=network-online.target + Wants=network-online.target notify: Reload systemd and restart unbound - name: Enables unbound service diff --git a/roles/uptime-kuma/templates/uptime-kuma.service.j2 b/roles/uptime-kuma/templates/uptime-kuma.service.j2 index cd7f41c..46d8fce 100644 --- a/roles/uptime-kuma/templates/uptime-kuma.service.j2 +++ b/roles/uptime-kuma/templates/uptime-kuma.service.j2 @@ -6,6 +6,8 @@ After=network-online.target [Service] Type=oneshot RemainAfterExit=true +User={{ ansible_user }} +Group={{ ansible_user }} WorkingDirectory={{ podman_projects_dir }}/uptime-kuma ExecStart=/usr/bin/podman-compose up -d ExecStop=/usr/bin/podman-compose down