diff --git a/conf/nginx/server.tpl.conf b/conf/nginx/server.tpl.conf index 5aa84c8d7..d29cb840a 100644 --- a/conf/nginx/server.tpl.conf +++ b/conf/nginx/server.tpl.conf @@ -39,8 +39,12 @@ server { } server { + {% if sni_forward_enabled != "True" %} listen 443 ssl http2; listen [::]:443 ssl http2; + {% else %} + listen 127.0.0.1:443 ssl http2; + {% endif %} server_name {{ domain }}; include /etc/nginx/conf.d/security.conf.inc; diff --git a/conf/nginx/sni_forward.conf b/conf/nginx/sni_forward.conf new file mode 100644 index 000000000..266863e50 --- /dev/null +++ b/conf/nginx/sni_forward.conf @@ -0,0 +1,27 @@ +{% set domain_ip_map = sni_forward_list.split(',') %} +stream { + + map $ssl_preread_server_name $name { + {% for domain_ip in domain_ip_map %} + {{ domain_ip.split(":")[0] }} {{ domain_ip.split(":")[0].replace('.', '') }}; + {% endfor %} + default https_default_backend; + } + + {% for domain_ip in domain_ip_map %} + upstream {{ domain_ip.split(":")[0].replace('.', '') }} { + server {{ domain_ip.split(":")[1] }}:443; + } + {% endfor %} + + upstream https_default_backend { + server 127.0.0.1:443; + } + + server { + listen 443; + listen [::]:443; + proxy_pass $name; + ssl_preread on; + } +} diff --git a/conf/nginx/sni_forward_server.conf b/conf/nginx/sni_forward_server.conf new file mode 100644 index 000000000..e2fbd9356 --- /dev/null +++ b/conf/nginx/sni_forward_server.conf @@ -0,0 +1,38 @@ +# This snippet is only here to redirect traffic to another domain on port 80, +# which is also forwarded for port 443 based on the SNI (which is handled +# differently because of the whole SNI story) + +# We don't explicitly redirect to HTTPS by default and let the forwarded server +# handle the redirection (or not depending on what's configured on the other +# server) + +server { + listen 80; + listen [::]:80; + server_name {{ sni_forward_domain }}; + + location / { + proxy_pass http://{{ sni_forward_ip }}; + + proxy_set_header Host $host; + proxy_set_header X-Original-URL $scheme://$http_host$request_uri; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-Uri $request_uri; + proxy_set_header X-Forwarded-Ssl on; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Connection ""; + + real_ip_header X-Forwarded-For; + real_ip_recursive on; + + send_timeout 5m; + proxy_read_timeout 360; + proxy_send_timeout 360; + proxy_connect_timeout 360; + } + + access_log /var/log/nginx/{{ sni_forward_domain }}-access.log; + error_log /var/log/nginx/{{ sni_forward_domain }}-error.log; +} diff --git a/hooks/conf_regen/15-nginx b/hooks/conf_regen/15-nginx index 9eabcd8b7..6dec52245 100755 --- a/hooks/conf_regen/15-nginx +++ b/hooks/conf_regen/15-nginx @@ -68,6 +68,23 @@ do_pre_regen() { export redirect_to_https="$(yunohost settings get 'security.nginx.nginx_redirect_to_https' | int_to_bool)" export compatibility="$(yunohost settings get 'security.nginx.nginx_compatibility')" export experimental="$(yunohost settings get 'security.experimental.security_experimental_enabled' | int_to_bool)" + export sni_forward_enabled="$(yunohost settings get 'misc.sni_forward.sni_forward_enabled' | int_to_bool)" + export sni_forward_list="$(yunohost settings get 'misc.sni_forward.sni_forward_list')" + + local sni_module="${pending_dir}/etc/nginx/modules-enabled/sni_forward.conf" + if [[ "$sni_forward_enabled" == "True" ]] + then + ynh_render_template "sni_forward.conf" "${sni_module}" + for sni_forward_domain_and_ip in $(echo "$sni_forward_list" | sed 's/,/\n/g') + do + export sni_forward_domain=$(echo $sni_forward_domain_and_ip | awk -F: '{print $1}') + export sni_forward_ip=$(echo $sni_forward_domain_and_ip | awk -F: '{print $2}') + ynh_render_template "sni_forward_server.conf" "${nginx_conf_dir}/${sni_forward_domain}.forward80.conf" + done + else + touch "${sni_module}" + fi + ynh_render_template "security.conf.inc" "${nginx_conf_dir}/security.conf.inc" cert_status=$(yunohost domain cert status --json) @@ -128,6 +145,8 @@ do_pre_regen() { || touch "${nginx_conf_dir}/${file}" done + # FIXME : also add the .forward80 files + # remove old mail-autoconfig files autoconfig_files=$(ls -1 /var/www/.well-known/*/autoconfig/mail/config-v1.1.xml 2>/dev/null || true) for file in $autoconfig_files; do diff --git a/locales/en.json b/locales/en.json index 60f58f639..508407409 100644 --- a/locales/en.json +++ b/locales/en.json @@ -424,6 +424,10 @@ "firewall_rules_cmd_failed": "Some firewall rule commands have failed. More info in log.", "global_settings_reset_success": "Reset global settings", "global_settings_setting_admin_strength": "Admin password strength requirements", + "global_settings_setting_sni_forward_enabled": "Enable SNI-based forwarding", + "global_settings_setting_sni_forward_enabled_help": "This is an advanced feature to reverse-proxy an entire domain to another machine *without* decrypting the traffic. Useful when you want to expose several machines behind the same IP but still allow each machine to handle the SSL termination.", + "global_settings_setting_sni_forward_list": "List of forwarding", + "global_settings_setting_sni_forward_list_help": "Should be a list of DOMAIN:IPv4, such as domain.tld:1.2.3.4", "global_settings_setting_admin_strength_help": "These requirements are only enforced when initializing or changing the password", "global_settings_setting_backup_compress_tar_archives": "Compress backups", "global_settings_setting_backup_compress_tar_archives_help": "When creating new backups, compress the archives (.tar.gz) instead of uncompressed archives (.tar). N.B. : enabling this option means create lighter backup archives, but the initial backup procedure will be significantly longer and heavy on CPU.", diff --git a/share/config_global.toml b/share/config_global.toml index 40b71ab19..b70dfa9e5 100644 --- a/share/config_global.toml +++ b/share/config_global.toml @@ -169,3 +169,18 @@ name = "Other" choices.ipv4 = "IPv4 Only" choices.ipv6 = "IPv6 Only" default = "both" + + [misc.sni_forward] + name = "SNI-based forwarding" + + [misc.sni_forward.sni_forward_enabled] + type = "boolean" + default = false + + [misc.sni_forward.sni_forward_list] + type = "tags" + # Regex is just : + pattern.regexp = '^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,}):((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$' + pattern.error = "You should specify a list of items formatted as DOMAIN:IPv4, such as yolo.test:12.34.56.78" + default = "" + visible = "sni_forward_enabled" diff --git a/src/settings.py b/src/settings.py index 6690ab3fd..9e90935b2 100644 --- a/src/settings.py +++ b/src/settings.py @@ -300,6 +300,8 @@ def regen_ssowatconf(setting_name, old_value, new_value): app_ssowatconf() +@post_change_hook("sni_forward_enabled") +@post_change_hook("sni_forward_list") @post_change_hook("ssowat_panel_overlay_enabled") @post_change_hook("nginx_redirect_to_https") @post_change_hook("nginx_compatibility")