# Basic configuration
user nginx;
worker_processes auto;
error_log /dev/stderr notice;
pid /var/run/nginx.pid;
load_module "modules/ngx_mail_module.so";
events {
worker_connections 1024;
}
http {
# Standard HTTP configuration with slight hardening
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server_tokens off;
absolute_redirect off;
resolver {{ RESOLVER }} valid=30s;
{% if REAL_IP_HEADER %}
#real_ip_header {{ REAL_IP_HEADER }};
{% endif %}
{% if REAL_IP_FROM %}{% for from_ip in REAL_IP_FROM.split(',') %}
set_real_ip_from {{ from_ip }};
{% endfor %}{% endif %}
real_ip_header proxy_protocol;
proxy_set_header X-Real-IP $proxy_protocol_addr;
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
real_ip_recursive on;
# Header maps
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
default $http_x_forwarded_proto;
'' $scheme;
}
map $uri $expires {
default off;
~*\.(ico|css|js|gif|jpeg|jpg|png|woff2?|ttf|otf|svg|tiff|eot|webp)$ 97d;
}
map $request_uri $loggable {
/health 0;
/auth/email 0;
default 1;
}
log_format main '$proxy_protocol_addr $remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" "$gzip_ratio"';
#access_log /var/log/nginx/access.log main;
access_log /dev/stdout main;# if=$loggable;
# compression
gzip on;
gzip_static on;
gzip_types text/plain text/css application/xml application/javascript
gzip_min_length 1024;
# TODO: figure out how to server pre-compressed assets from admin container
{% if KUBERNETES_INGRESS != 'true' and TLS_FLAVOR in [ 'letsencrypt', 'cert' ] %}
# Enable the proxy for certbot if the flavor is letsencrypt and not on kubernetes
#
server {
# Listen over HTTP
listen 80;
listen [::]:80;
{% if TLS_FLAVOR == 'letsencrypt' %}
location ^~ /.well-known/acme-challenge/ {
proxy_pass http://127.0.0.1:8008;
}
{% endif %}
# redirect to https
location / {
return 301 https://$host$request_uri;
}
}
{% endif %}
# Main HTTP server
server {
# Favicon stuff
root /static;
# Variables for proxifying
set $admin {{ ADMIN_ADDRESS }};
set $antispam {{ ANTISPAM_WEBUI_ADDRESS }};
{% if WEBMAIL_ADDRESS %}
set $webmail {{ WEBMAIL_ADDRESS }};
{% endif %}
{% if WEBDAV_ADDRESS %}
set $webdav {{ WEBDAV_ADDRESS }};
{% endif %}
client_max_body_size {{ MESSAGE_SIZE_LIMIT|int + 8388608 }};
# Listen on HTTP only in kubernetes or behind reverse proxy
{% if KUBERNETES_INGRESS == 'true' or TLS_FLAVOR in [ 'mail-letsencrypt', 'notls', 'mail' ] %}
listen 80;
listen [::]:80;
{% endif %}
# Only enable HTTPS if TLS is enabled with no error and not on kubernetes
{% if KUBERNETES_INGRESS != 'true' and TLS and not TLS_ERROR %}
listen 443 ssl http2;
listen [::]:443 ssl http2;
real_ip_header proxy_protocol;
proxy_set_header X-Real-IP $proxy_protocol_addr;
real_ip_recursive on;
include /etc/nginx/tls.conf;
ssl_stapling on;
ssl_stapling_verify on;
ssl_session_cache shared:SSLHTTP:50m;
add_header Strict-Transport-Security 'max-age=31536000';
{% if not TLS_FLAVOR in [ 'mail', 'mail-letsencrypt' ] %}
if ($proxy_x_forwarded_proto = http) {
return 301 https://$host$request_uri;
}
{% endif %}
{% endif %}
# Remove headers to prevent duplication and information disclosure
proxy_hide_header X-XSS-Protection;
proxy_hide_header X-Powered-By;
add_header X-Frame-Options 'SAMEORIGIN';
add_header X-Content-Type-Options 'nosniff';
add_header X-Permitted-Cross-Domain-Policies 'none';
add_header X-XSS-Protection '1; mode=block';
add_header Referrer-Policy 'same-origin';
{% if TLS_FLAVOR == 'mail-letsencrypt' %}
location ^~ /.well-known/acme-challenge/ {
proxy_pass http://127.0.0.1:8008;
}
{% endif %}
# If TLS is failing, prevent access to anything except certbot
{% if KUBERNETES_INGRESS != 'true' and TLS_ERROR and not (TLS_FLAVOR in [ 'mail-letsencrypt', 'mail' ]) %}
location / {
return 403;
}
{% else %}
include /overrides/*.conf;
# Actual logic
{% if ADMIN == 'true' or WEBMAIL != 'none' %}
location ~ ^/(sso|static)/ {
include /etc/nginx/proxy.conf;
proxy_set_header X-Real-IP $proxy_protocol_addr;
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
proxy_pass http://$admin;
}
{% endif %}
{% if WEB_WEBMAIL != '/' and WEBROOT_REDIRECT != 'none' %}
location / {
expires $expires;
{% if WEBROOT_REDIRECT %}
try_files $uri {{ WEBROOT_REDIRECT }};
{% else %}
try_files $uri =404;
{% endif %}
}
{% endif %}
{% if WEBMAIL != 'none' %}
location {{ WEB_WEBMAIL }} {
{% if WEB_WEBMAIL != '/' %}
rewrite ^({{ WEB_WEBMAIL }})$ $1/ permanent;
rewrite ^{{ WEB_WEBMAIL }}/(.*) /$1 break;
{% endif %}
include /etc/nginx/proxy.conf;
auth_request /internal/auth/user;
error_page 403 @webmail_login;
proxy_set_header X-Real-IP $proxy_protocol_addr;
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
proxy_pass http://$webmail;
}
{% if WEB_WEBMAIL == '/' %}
location /sso.php {
{% else %}
location {{ WEB_WEBMAIL }}/sso.php {
{% endif %}
{% if WEB_WEBMAIL != '/' %}
rewrite ^({{ WEB_WEBMAIL }})$ $1/ permanent;
rewrite ^{{ WEB_WEBMAIL }}/(.*) /$1 break;
{% endif %}
include /etc/nginx/proxy.conf;
proxy_set_header X-Real-IP $proxy_protocol_addr;
auth_request /internal/auth/user;
auth_request_set $user $upstream_http_x_user;
auth_request_set $token $upstream_http_x_user_token;
proxy_set_header X-Remote-User $user;
proxy_set_header X-Remote-User-Token $token;
error_page 403 @webmail_login;
proxy_pass http://$webmail;
}
location @webmail_login {
return 302 /sso/login;
}
{% endif %}
{% if ADMIN == 'true' %}
location {{ WEB_ADMIN }} {
include /etc/nginx/proxy.conf;
proxy_pass http://$admin;
proxy_set_header X-Real-IP $proxy_protocol_addr;
expires $expires;
}
location {{ WEB_ADMIN }}/antispam {
rewrite ^{{ WEB_ADMIN }}/antispam/(.*) /$1 break;
auth_request /internal/auth/admin;
proxy_set_header X-Real-IP "";
proxy_set_header X-Forwarded-For "";
proxy_pass http://$antispam;
}
{% endif %}
{% if WEBDAV != 'none' %}
location /webdav {
rewrite ^/webdav/(.*) /$1 break;
auth_request /internal/auth/basic;
auth_request_set $user $upstream_http_x_user;
include /etc/nginx/proxy.conf;
proxy_set_header X-Remote-User $user;
proxy_set_header X-Script-Name /webdav;
proxy_pass http://$webdav;
}
location ~ ^/.well-known/(carddav|caldav) {
return 301 /webdav/;
}
{% endif %}
{% endif %}
location /internal {
internal;
#proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-IP $proxy_protocol_addr;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_pass http://$admin;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
}
location /health {
return 204;
}
}
# Forwarding authentication server
server {
# Variables for proxifying
set $admin {{ ADMIN_ADDRESS }};
listen 127.0.0.1:8000;
location / {
proxy_pass http://$admin/internal$request_uri;
}
}
}
mail {
server_name {{ HOSTNAMES.split(",")[0] }};
auth_http http://127.0.0.1:8000/auth/email;
proxy_pass_error_message on;
resolver {{ RESOLVER }} valid=30s;
error_log /dev/stderr debug;
#error_log /var/log/nginx/mail_error.log info;
{% if TLS and not TLS_ERROR %}
include /etc/nginx/tls.conf;
ssl_session_cache shared:SSLMAIL:50m;
{% endif %}
#proxy_protocol on;
# Advertise real capabilites of backends (postfix/dovecot)
smtp_capabilities PIPELINING SIZE {{ MESSAGE_SIZE_LIMIT }} ETRN ENHANCEDSTATUSCODES 8BITMIME DSN;
pop3_capabilities TOP UIDL RESP-CODES PIPELINING AUTH-RESP-CODE USER;
imap_capabilities IMAP4 IMAP4rev1 UIDPLUS SASL-IR LOGIN-REFERRALS ID ENABLE IDLE LITERAL+;
# Default SMTP server for the webmail (no encryption, but authentication)
server {
listen 10025;
protocol smtp;
smtp_auth plain;
auth_http_header Auth-Port 10025;
}
# Default IMAP server for the webmail (no encryption, but authentication)
server {
listen 10143;
protocol imap;
smtp_auth plain;
auth_http_header Auth-Port 10143;
}
# SMTP is always enabled, to avoid losing emails when TLS is failing
server {
listen 25 proxy_protocol;
listen [::]:25 proxy_protocol;
{% if TLS and not TLS_ERROR %}
{% if TLS_FLAVOR in ['letsencrypt','mail-letsencrypt'] %}
ssl_certificate /certs/letsencrypt/live/mailu/fullchain.pem;
ssl_certificate /certs/letsencrypt/live/mailu-ecdsa/fullchain.pem;
{% endif %}
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA;
ssl_prefer_server_ciphers on;
starttls on;
{% endif %}
protocol smtp;
smtp_auth none;
auth_http_header Auth-Port 25;
}
# All other protocols are disabled if TLS is failing
{% if not TLS_ERROR %}
server {
listen 143 proxy_protocol;
listen [::]:143 proxy_protocol;
{% if TLS %}
starttls only;
{% endif %}
protocol imap;
imap_auth plain;
auth_http_header Auth-Port 143;
}
server {
listen 110 proxy_protocol;
listen [::]:110 proxy_protocol;
{% if TLS %}
starttls only;
{% endif %}
protocol pop3;
pop3_auth plain;
auth_http_header Auth-Port 110;
}
server {
listen 587 proxy_protocol;
listen [::]:587 proxy_protocol;
{% if TLS %}
starttls only;
{% endif %}
protocol smtp;
smtp_auth plain login;
auth_http_header Auth-Port 587;
}
{% if TLS %}
server {
listen 465 ssl proxy_protocol;
listen [::]:465 ssl proxy_protocol;
protocol smtp;
smtp_auth plain login;
auth_http_header Auth-Port 465;
}
server {
listen 993 ssl proxy_protocol;
listen [::]:993 ssl proxy_protocol;
protocol imap;
imap_auth plain;
auth_http_header Auth-Port 993;
}
server {
listen 995 ssl proxy_protocol;
listen [::]:995 ssl proxy_protocol;
protocol pop3;
pop3_auth plain;
auth_http_header Auth-Port 995;
}
{% endif %}
{% endif %}
}
主要修改部分就是给 mail 的 server 对外部分添加上 proxy_protocol, 因为外网的 nginx 的 stream 添加了,因此这部分也需要添加上,不然服务器会转发失败的。
mailu 的授权主要在 admin 部分,因此我们只要修改这部分就可以了。这部分默认读取的 IP 是从 Client-Ip 字段中来的,现在真实的 IP 保存在”Proxy-Protocol-Addr”中,在 nginx.py 和 auth.py 中搜索 Client-Ip, 并在合适的位置添加上下面的内容:
if headers.get("Proxy-Protocol-Addr"):
client_ip = flask.request.headers["Proxy-Protocol-Addr"]
但 admin 的日志中显示的还是错误的地址,为了配合 fail2ban 的使用,我在所有授权失败的地方添加上了 这样的错误日志的输出:
app.logger.error(f'Auth Fail user ip {client_ip!r} Auth not supported 403')
主要是添加 fail2ban 可识别的正则表达式的格式。admin 部分默认的日志输出都是在 stderr 或者 stdout 的, 我也修改了 start.py 一下,让日志保存到/var/log 中
start_command="".join([
"gunicorn --threads ", str(os.cpu_count()),
" -b :80 ",
"--access-logfile /var/log/access.log " if (log.root.level<=log.INFO) else "",
"--error-logfile /var/log/error.log ",
"--preload ",
"'mailu:create_app()'"])
os.system(start_command)