Files
Clément Désiles 314fa715fd fix(nginx): prevent cert leak on IPv6 / unknown SNI
Two issues caused TLS to break on photos.carabosse.cloud over IPv6
(GrapheneOS + Immich app via Orange 5G NAT64):

1. Per-service vhosts only listened on IPv4 (listen 443 ssl). On IPv6,
   nginx fell back to the first vhost loaded alphabetically and served
   its certificate, breaking hostname verification on every other vhost.

2. /etc/letsencrypt/{live,archive} were 0700 root:root after certbot
   created them, so the nginx worker (user http on Arch) could not read
   the chained intermediates and served the leaf-only chain.

Changes:
- Add catch-all 00-default.conf default_server on :80 and :443 (v4+v6)
  with a self-signed cert and 'return 444'. ACME challenges still
  answered on :80.
- Add IPv6 listeners ([::]:80 and [::]:443 ssl) to immich, gitea, ntfy,
  uptime_kuma vhosts and to the temporary ACME provisioning vhost.
- Apply 0755 on /etc/letsencrypt/live and /etc/letsencrypt/archive on
  every run, not only at initial cert provisioning.
2026-05-30 17:06:10 +02:00
..
2025-11-14 00:23:03 +01:00
2025-12-20 20:52:24 +01:00

Nginx Role

Installs and configures Nginx as a reverse proxy for web applications with modular vhost management.

Features

  • Modular vhost configuration via /etc/nginx/conf.d/
  • Zero-downtime reloads
  • Configurable logging backend (journald or traditional files)
  • Automatic logrotate for file-based logging
  • SSL/TLS configuration
  • Native ACME/Let's Encrypt support (Nginx 1.25.0+)
  • Transparent proxy forwarding (HTTP/HTTPS to other hosts)
  • Catch-all default_server that rejects unknown SNI/Host with 444

Catch-all default_server

A 00-default.conf vhost is deployed and marked default_server on both ports 80 and 443. It uses a self-signed cert (/etc/nginx/ssl/default.crt) and returns 444 (close connection) for any request whose SNI/Host does not match an explicit vhost. ACME HTTP-01 challenges (/.well-known/acme-challenge/) are still answered on port 80 so Certbot keeps working for new hostnames.

Without this, clients hitting the server IP directly (or doing HTTP/2 connection coalescing across vhosts sharing the same IP) would receive the certificate of the first vhost loaded alphabetically, leaking that hostname and breaking TLS verification on other vhosts.

Disable with nginx_default_server_enabled: false.

Service Integration Pattern

Each service role should deploy its own vhost config:

In service role tasks:

- name: Deploy nginx vhost
  ansible.builtin.template:
      src: nginx-vhost.conf.j2
      dest: /etc/nginx/conf.d/myservice.conf
      validate: nginx -t
  when: myservice_nginx_enabled
  notify: Reload nginx

- name: Remove nginx vhost when disabled
  ansible.builtin.file:
      path: /etc/nginx/conf.d/myservice.conf
      state: absent
  when: not myservice_nginx_enabled
  notify: Reload nginx

Transparent Proxy Forwarding

Forward TCP traffic from this Nginx instance to services on other hosts using the stream module (layer 4 proxy).

Configuration:

nginx_forwarder:
    "blog.hello.com":
        forward_to: "my.host.lan"
        http: true # Forward port 80 (default: true)
        https: true # Forward port 443 (default: true)

How it works:

  • Stream-based TCP proxy (layer 4, not HTTP layer 7)
  • No protocol inspection - just forwards raw TCP packets
  • HTTPS passes through encrypted - backend host handles TLS termination
  • HTTP also uses stream (simpler, but no HTTP features like headers/logging)

Use case: Omega (gateway) forwards all traffic to Andromeda (internal server) that handles its own TLS certificates.

Important notes:

  • Stream configs deployed to /etc/nginx/streams.d/
  • No HTTP logging (stream doesn't understand HTTP protocol)
  • No X-Forwarded-For headers (transparent TCP forwarding)
  • Only ONE domain can use port 443 forwarding (TCP port limitation)

Logging Backends

journald (default):

  • Logs sent to systemd journal via syslog
  • View: journalctl -u nginx -f

file:

  • Traditional /var/log/nginx/*.log files
  • Automatic logrotate configuration

Switch via nginx_log_backend variable.

Hands-on Commands

# Test configuration
nginx -t

# Reload (zero downtime)
systemctl reload nginx

# View logs (journald)
journalctl -u nginx -f
journalctl -u nginx -n 100
journalctl -u nginx -p err

# View logs (file)
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log

# List loaded vhosts
ls -la /etc/nginx/conf.d/

# List stream forwarders
ls -la /etc/nginx/streams.d/

Configuration Variables

See defaults/main.yml for all available variables.

References