feat: forward tcp traffic easily

This commit is contained in:
Clément Désiles 2025-12-15 22:14:46 +01:00
parent bd2e806aa1
commit ebeb6d5c6b
No known key found for this signature in database
8 changed files with 287 additions and 2 deletions

View File

@ -206,11 +206,44 @@ The role implements proper data isolation for both database backends:
The compose file is deployed to `{{ podman_projects_dir }}/immich/docker-compose.yml` and managed via a systemd service. The compose file is deployed to `{{ podman_projects_dir }}/immich/docker-compose.yml` and managed via a systemd service.
## Nginx Reverse Proxy with ACME/Let's Encrypt
The role includes an Nginx vhost template with native ACME support for automatic HTTPS certificate management.
**Prerequisites:**
1. Nginx role deployed with `acme_email` configured
2. Port 80/443 accessible from internet (for ACME HTTP-01 challenge)
3. DNS pointing to your server
**Configuration:**
```yaml
# Enable Nginx reverse proxy
immich_nginx_enabled: true
immich_nginx_hostname: "blog.hello.com"
# In nginx role configuration (host_vars or group_vars)
acme_email: "admin@carabosse.cloud"
```
**What it does:**
- Deploys HTTPS vhost with automatic Let's Encrypt certificate
- HTTP → HTTPS redirect
- Proxies to Immich container on localhost
- Handles WebSocket upgrades for live photos
- Large file upload support (50GB max)
**ACME automatic features:**
- Certificate issuance on first deployment
- Automatic renewal
- HTTP-01 challenge handling
## Post-Installation ## Post-Installation
After deployment: After deployment:
1. Access Immich at `http://<host-ip>:2283` 1. Access Immich at:
- **With Nginx enabled**: `https://{{ immich_nginx_hostname }}`
- **Without Nginx**: `http://<host-ip>:{{ immich_port }}`
2. Create an admin account on first login 2. Create an admin account on first login
3. Configure mobile/desktop apps to point to your server 3. Configure mobile/desktop apps to point to your server

View File

@ -1,7 +1,41 @@
# Immich vhost with Let's Encrypt (Certbot)
# Managed by Ansible - DO NOT EDIT MANUALLY
server { server {
listen 80; listen 80;
server_name {{ immich_nginx_hostname }}; server_name {{ immich_nginx_hostname }};
# 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;
server_name {{ immich_nginx_hostname }};
# Let's Encrypt certificates (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/{{ immich_nginx_hostname }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ immich_nginx_hostname }}/privkey.pem;
# SSL configuration
ssl_protocols {{ nginx_ssl_protocols }};
ssl_prefer_server_ciphers {{ 'on' if nginx_ssl_prefer_server_ciphers else 'off' }};
{% if nginx_log_backend == 'journald' %}
access_log syslog:server=unix:/dev/log,nohostname,tag=nginx_immich;
error_log syslog:server=unix:/dev/log,nohostname,tag=nginx_immich;
{% else %}
access_log /var/log/nginx/{{ immich_nginx_hostname }}_access.log main;
error_log /var/log/nginx/{{ immich_nginx_hostname }}_error.log;
{% endif %}
client_max_body_size 50000M; client_max_body_size 50000M;
location / { location / {

View File

@ -9,6 +9,8 @@ Installs and configures Nginx as a reverse proxy for web applications with modul
- Configurable logging backend (journald or traditional files) - Configurable logging backend (journald or traditional files)
- Automatic logrotate for file-based logging - Automatic logrotate for file-based logging
- SSL/TLS configuration - SSL/TLS configuration
- **Native ACME/Let's Encrypt support** (Nginx 1.25.0+)
- **Transparent proxy forwarding** (HTTP/HTTPS to other hosts)
## Service Integration Pattern ## Service Integration Pattern
@ -32,6 +34,33 @@ Each service role should deploy its own vhost config:
notify: Reload nginx 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:**
```yaml
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 ## Logging Backends
**journald (default):** **journald (default):**
@ -64,10 +93,19 @@ tail -f /var/log/nginx/error.log
# List loaded vhosts # List loaded vhosts
ls -la /etc/nginx/conf.d/ ls -la /etc/nginx/conf.d/
# List stream forwarders
ls -la /etc/nginx/streams.d/
``` ```
## Configuration Variables
See [defaults/main.yml](defaults/main.yml) for all available variables.
## References ## References
- [Nginx Documentation](https://nginx.org/en/docs/) - [Nginx Documentation](https://nginx.org/en/docs/)
- [Nginx ACME Support](https://blog.nginx.org/blog/native-support-for-acme-protocol)
- [Nginx Stream Module](https://nginx.org/en/docs/stream/ngx_stream_core_module.html)
- [Nginx Logging](https://nginx.org/en/docs/syslog.html) - [Nginx Logging](https://nginx.org/en/docs/syslog.html)
- [Nginx SSL/TLS](https://nginx.org/en/docs/http/configuring_https_servers.html) - [Nginx SSL/TLS](https://nginx.org/en/docs/http/configuring_https_servers.html)

View File

@ -2,6 +2,9 @@
# Nginx configuration directory for service vhosts # Nginx configuration directory for service vhosts
nginx_conf_dir: /etc/nginx/conf.d nginx_conf_dir: /etc/nginx/conf.d
# Nginx stream configuration directory (TCP/UDP proxies)
nginx_streams_dir: /etc/nginx/streams.d
# Worker processes (auto = number of CPU cores) # Worker processes (auto = number of CPU cores)
nginx_worker_processes: auto nginx_worker_processes: auto
@ -23,3 +26,16 @@ nginx_log_backend: journald
nginx_logrotate_rotate: 14 # Keep 14 days of logs nginx_logrotate_rotate: 14 # Keep 14 days of logs
nginx_logrotate_frequency: daily # daily|weekly|monthly nginx_logrotate_frequency: daily # daily|weekly|monthly
nginx_logrotate_compress: true # Compress rotated logs nginx_logrotate_compress: true # Compress rotated logs
# Forwarder configuration (transparent proxy to other hosts)
# Example:
# nginx_forwarder:
# "blog.hello.com":
# forward_to: "you.domain.org"
# http: true # Forward port 80 (default: true)
# https: true # Forward port 443 (default: true)
nginx_forwarder: {}
# Let's Encrypt / Certbot configuration
# acme_email: "" # Required for Let's Encrypt - intentionally undefined
# Set this variable to enable Certbot installation and certificate management

View File

@ -9,11 +9,54 @@
ansible.builtin.set_fact: ansible.builtin.set_fact:
nginx_user: "{{ nginx_user | default('www-data') }}" nginx_user: "{{ nginx_user | default('www-data') }}"
- name: Add Nginx official APT signing key (Debian/Ubuntu)
ansible.builtin.get_url:
url: https://nginx.org/keys/nginx_signing.key
dest: /etc/apt/keyrings/nginx-archive-keyring.asc
mode: "0644"
when:
- ansible_facts['os_family'] == 'Debian'
- name: Add Nginx official repository (Debian/Ubuntu)
ansible.builtin.deb822_repository:
name: nginx-official
types: deb
uris: http://nginx.org/packages/mainline/debian/
suites: "{{ ansible_facts['distribution_release'] }}"
components: nginx
signed_by: /etc/apt/keyrings/nginx-archive-keyring.asc
state: present
when:
- ansible_facts['os_family'] == 'Debian'
- name: Install nginx - name: Install nginx
ansible.builtin.package: ansible.builtin.package:
name: nginx name: nginx
state: present state: present
- name: Install nginx stream module (Debian)
ansible.builtin.package:
name: libnginx-mod-stream
state: present
when:
- ansible_facts['os_family'] == 'Debian'
- nginx_forwarder is defined
- nginx_forwarder | length > 0
- name: Install Certbot
ansible.builtin.package:
name: certbot
state: present
when: acme_email is defined
- name: Enable Certbot renewal timer
ansible.builtin.systemd:
name: certbot-renew.timer
enabled: true
state: started
when: acme_email is defined
ignore_errors: true
- name: Ensure nginx conf.d directory exists - name: Ensure nginx conf.d directory exists
ansible.builtin.file: ansible.builtin.file:
path: "{{ nginx_conf_dir }}" path: "{{ nginx_conf_dir }}"
@ -22,6 +65,23 @@
group: root group: root
mode: "0755" mode: "0755"
- name: Ensure nginx streams.d directory exists
ansible.builtin.file:
path: "{{ nginx_streams_dir }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Ensure Certbot webroot directory exists
ansible.builtin.file:
path: /var/www/certbot
state: directory
owner: "{{ nginx_user }}"
group: "{{ nginx_user }}"
mode: "0755"
when: acme_email is defined
- name: Deploy nginx main configuration - name: Deploy nginx main configuration
ansible.builtin.template: ansible.builtin.template:
src: nginx.conf.j2 src: nginx.conf.j2
@ -32,6 +92,31 @@
validate: nginx -t -c %s validate: nginx -t -c %s
notify: Reload nginx notify: Reload nginx
- name: Deploy stream forwarder configurations
ansible.builtin.template:
src: forwarder.conf.j2
dest: "{{ nginx_streams_dir }}/forwarder-{{ domain | replace('.', '_') }}.conf"
owner: root
group: root
mode: "0644"
loop: "{{ nginx_forwarder | dict2items }}"
loop_control:
loop_var: item
vars:
domain: "{{ item.key }}"
config: "{{ item.value }}"
when:
- nginx_forwarder is defined
- nginx_forwarder | length > 0
notify: Reload nginx
- name: Validate nginx configuration after stream forwarder deployment
ansible.builtin.command: nginx -t
changed_when: false
when:
- nginx_forwarder is defined
- nginx_forwarder | length > 0
- name: Deploy logrotate configuration for nginx - name: Deploy logrotate configuration for nginx
ansible.builtin.template: ansible.builtin.template:
src: logrotate-nginx.j2 src: logrotate-nginx.j2

View File

@ -0,0 +1,25 @@
# TCP stream forwarder for {{ domain }}
# Managed by Ansible - DO NOT EDIT MANUALLY
# Transparent TCP proxy (no protocol inspection)
{% if config.http | default(true) %}
upstream {{ domain | replace('.', '_') | replace('-', '_') }}_http {
server {{ config.forward_to }}:80;
}
server {
listen 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;
proxy_pass {{ domain | replace('.', '_') | replace('-', '_') }}_https;
}
{% endif %}

View File

@ -9,6 +9,11 @@ pid /run/nginx.pid;
include /usr/share/nginx/modules/*.conf; include /usr/share/nginx/modules/*.conf;
{% if nginx_forwarder and nginx_forwarder | length > 0 %}
# Load stream module for TCP/UDP proxying
load_module modules/ngx_stream_module.so;
{% endif %}
events { events {
worker_connections {{ nginx_worker_connections }}; worker_connections {{ nginx_worker_connections }};
} }
@ -48,3 +53,11 @@ http {
# Load modular configuration files from the conf.d directory # Load modular configuration files from the conf.d directory
include {{ nginx_conf_dir }}/*.conf; include {{ nginx_conf_dir }}/*.conf;
} }
{% if nginx_forwarder and nginx_forwarder | length > 0 %}
# Stream block for TCP/UDP proxying
stream {
# Load stream configurations
include {{ nginx_streams_dir }}/*.conf;
}
{% endif %}

View File

@ -0,0 +1,41 @@
# HTTPS vhost with Let's Encrypt (Certbot) for {{ server_name }}
# Managed by Ansible - DO NOT EDIT MANUALLY
server {
listen 80;
server_name {{ server_name }};
# 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;
server_name {{ server_name }};
# Let's Encrypt certificates (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/{{ server_name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ server_name }}/privkey.pem;
# SSL configuration
ssl_protocols {{ nginx_ssl_protocols }};
ssl_prefer_server_ciphers {{ 'on' if nginx_ssl_prefer_server_ciphers else 'off' }};
{% if nginx_log_backend == 'journald' %}
access_log syslog:server=unix:/dev/log,nohostname,tag=nginx_{{ server_name | replace('.', '_') }};
error_log syslog:server=unix:/dev/log,nohostname,tag=nginx_{{ server_name | replace('.', '_') }};
{% else %}
access_log /var/log/nginx/{{ server_name }}_access.log main;
error_log /var/log/nginx/{{ server_name }}_error.log;
{% endif %}
# Service-specific configuration included below
{{ vhost_config | default('') }}
}