Mailserver behind Proxy
Using a Reverse Proxy
Guidance is provided via a Traefik config example, however if you're only familiar with configuring a reverse proxy for web services there are some differences to keep in mind.
- A security concern where preserving the client IP is important but needs to be handled at Layer 4 (TCP).
- TLS will be handled differently due protocols like STARTTLS and the need to comply with standards for interoperability with other MTAs.
- The ability to route the same port to different containers by FQDN can be limited.
This reduces many of the benefits for why you might use a reverse proxy, but they can still be useful.
Some deployments may require a service to route traffic (kubernetes) when deploying, in which case the below advice is important to understand well.
The guide here has also been adapted for our Kubernetes docs.
What can go wrong?
Without a reverse proxy involved, a service is typically aware of the client IP for a connection.
However when a reverse proxy routes the connection this information can be lost, and the proxied service mistakenly treats the client IP as the reverse proxy handling the connection.
- That can be problematic when the client IP is meaningful information for the proxied service to act upon, especially when it impacts security.
- The PROXY protocol is a well established solution to preserve the client IP when both the proxy and service have enabled the support.
Technical Details - HTTP vs TCP proxying
A key difference for how the network is proxied relates to the OSI Model:
- Layer 7 (Application layer protocols: SMTP / IMAP / HTTP / etc)
- Layer 4 (Transport layer protocols: TCP / UDP)
When working with Layer 7 and a protocol like HTTP, it is possible to inspect a protocol header like Forwarded
(or it's predecessor: X-Forwarded-For
). At a lower level with Layer 4, that information is not available and we are routing traffic agnostic to the application protocol being proxied.
A proxy can prepend the PROXY protocol header to the TCP/UDP connection as it is routed to the service, which must be configured to be compatible with PROXY protocol (often this adds a restriction that connections must provide the header, otherwise they're rejected).
Beyond your own proxy, traffic may be routed in the network by other means that would also rewrite this information such as Docker's own network management via iptables
and userland-proxy
(NAT). The PROXY header ensures the original source and destination IP addresses, along with their ports is preserved across transit.
Configuration
Reverse Proxy
The below guidance is focused on configuring Traefik, but the advice should be roughly applicable elsewhere (eg: NGINX, Caddy).
- Support requires the capability to proxy TCP (Layer 4) connections with PROXY protocol enabled for the upstream (DMS). The upstream must also support enabling PROXY protocol (which for DMS services rejects any connection not using the protocol).
- TLS should not be terminated at the proxy, that should be delegated to DMS (which should be configured with the TLS certs). Reasoning is covered under the ports section.
Traefik service
The Traefik service config is fairly standard, just define the necessary entrypoints:
services:
reverse-proxy:
image: docker.io/traefik:latest # 2.10 / 3.0
# CAUTION: In production you should configure the Docker API endpoint securely:
# https://doc.traefik.io/traefik/providers/docker/#docker-api-access
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command:
# Docker provider config:
- --providers.docker=true
- --providers.docker.exposedbydefault=false
# DMS ports you want to proxy:
- --entryPoints.mail-smtp.address=:25
- --entryPoints.mail-submission.address=:587
- --entryPoints.mail-submissions.address=:465
- --entryPoints.mail-imap.address=:143
- --entryPoints.mail-imaps.address=:993
- --entryPoints.mail-pop3.address=:110
- --entryPoints.mail-pop3s.address=:995
- --entryPoints.mail-managesieve.address=:4190
# Publish external access ports mapped to traefik entrypoint ports:
ports:
- "25:25"
- "587:587"
- "465:465"
- "143:143"
- "993:993"
- "110:110"
- "995:995"
- "4190:4190"
# An IP is assigned here for other services (Dovecot) to trust for PROXY protocol:
networks:
default:
ipv4_address: 172.16.42.2
# Specifying a subnet to assign a fixed container IP to the reverse proxy:
networks:
default:
name: my-network
ipam:
config:
- subnet: "172.16.42.0/24"
Extra considerations
--providers.docker.network=my-network
is useful when there is more than one network to consider.- If your deployment has any other hops (an edge proxy, load balancer, etc) between the reverse proxy and the client, you'll need PROXY protocol support throughout that chain. For Traefik this additionally requires enabling PROXY protocol on your entry points.
Traefik labels for DMS
services:
dms:
image: ghcr.io/docker-mailserver/docker-mailserver:latest
hostname: mail.example.com
labels:
- traefik.enable=true
# These are examples, configure the equivalent for any additional ports you proxy.
# Explicit TLS (STARTTLS):
- traefik.tcp.routers.mail-smtp.rule=HostSNI(`*`)
- traefik.tcp.routers.mail-smtp.entrypoints=smtp
- traefik.tcp.routers.mail-smtp.service=smtp
- traefik.tcp.services.mail-smtp.loadbalancer.server.port=25
- traefik.tcp.services.mail-smtp.loadbalancer.proxyProtocol.version=2
# Implicit TLS is no different, except for optional HostSNI support:
- traefik.tcp.routers.mail-submissions.rule=HostSNI(`*`)
- traefik.tcp.routers.mail-submissions.entrypoints=smtp-submissions
- traefik.tcp.routers.mail-submissions.service=smtp-submissions
- traefik.tcp.services.mail-submissions.loadbalancer.server.port=465
- traefik.tcp.services.mail-submissions.loadbalancer.proxyProtocol.version=2
# NOTE: Optionally match by SNI rule, this requires TLS passthrough (not compatible with STARTTLS):
#- traefik.tcp.routers.mail-submissions.rule=HostSNI(`mail.example.com`)
#- traefik.tcp.routers.mail-submissions.tls.passthrough=true
PROXY protocol compatibility
Only TCP routers support enabling PROXY Protocol (via proxyProtocol.version=2
)
Postfix and Dovecot are both compatible with PROXY protocol v1 and v2.
Technical Details - Ports (Traefik config)
Explicit TLS (STARTTLS)
Service Ports: mail-smtp
(25), mail-submission
(587), mail-imap
(143), mail-pop3
(110), mail-managesieve
(4190)
- Traefik expects the TCP router to not enable TLS (see "Server First protocols") for these connections. They begin in plaintext and potentially upgrade the connection to TLS, Traefik has no involvement in STARTTLS.
- Without an initial TLS connection, the
HostSNI
router rule is not usable (see "HostSNI & TLS"). This limits routing flexibility for these ports (eg: routing these ports by the FQDN to different DMS containers).
Implicit TLS
Service Ports: mail-submissions
(465), mail-imaps
(993), mail-pop3s
(995)
The HostSNI
router rule could specify the DMS FQDN instead of *
:
- This requires the router to have TLS enabled, so that Traefik can inspect the server name sent by the client.
- Traefik can only match the SNI to
*
when the client does not provide a server name. Some clients must explicitly opt-in, such as CLI clientsopenssl
(-servername
) andswaks
(--tls-sni
). - Add
tls.passthrough=true
to the router (this implicitly enables TLS).- Traefik should not terminate TLS, decryption should occur within DMS instead when proxying to the same implicit TLS ports.
- Passthrough ignores any certificates configured for Traefik; DMS must be configured with the certificates instead (DMS can use
acme.json
from Traefik).
Unlike proxying HTTPS (port 443) to a container via HTTP (port 80), the equivalent for DMS service ports is not supported:
- Port 25 must secure the connection via STARTTLS to be reached publicly.
- STARTTLS ports requiring authentication for Postfix (587) and Dovecot (110, 143, 4190) are configured to only permit authentication over an encrypted connection.
- Support would require routing the implicit TLS ports to their explicit TLS equivalent ports with auth restrictions removed.
tls.passthrough.true
would not be required, additionally port 25 would always be unencrypted (if the proxy exclusively manages TLS/certs), or unreachable by public MTAs attempting delivery if the proxy enables implicit TLS for this port.
DMS (Postfix + Dovecot)
Enable PROXY protocol on existing service ports
This can be handled via our config override support.
Postfix via postfix-master.cf
:
smtp/inet/postscreen_upstream_proxy_protocol=haproxy
submission/inet/smtpd_upstream_proxy_protocol=haproxy
submissions/inet/smtpd_upstream_proxy_protocol=haproxy
postscreen_upstream_proxy_protocol
and smtpd_upstream_proxy_protocol
both specify the protocol type used by a proxy. haproxy
represents the PROXY protocol.
Dovecot via dovecot.cf
:
haproxy_trusted_networks = 172.16.42.2
service imap-login {
inet_listener imap {
haproxy = yes
}
inet_listener imaps {
haproxy = yes
}
}
service pop3-login {
inet_listener pop3 {
haproxy = yes
}
inet_listener pop3s {
haproxy = yes
}
}
service managesieve-login {
inet_listener sieve {
haproxy = yes
}
}
haproxy_trusted_networks
must reference the reverse proxy IP, or a wider subnet using CIDR notation.haproxy = yes
for the TCP listeners of each login service.
Internal traffic (within the network or DMS itself)
- Direct connections to DMS from other containers within the internal network will be rejected when they don't provide the required PROXY header.
- This can also affect services running within the DMS container itself if they attempt to make a connection and aren't PROXY protocol capable.
A solution is to configure alternative service ports that offer PROXY protocol support (as shown next).
Alternatively routing connections to DMS through the local reverse proxy via DNS query rewriting can work too.
Configuring services with separate ports for PROXY protocol
In this example we'll take the original service ports and add 10000
for the new PROXY protocol service ports.
Traefik labels will need to update their service ports accordingly (eg: .loadbalancer.server.port=10465
).
Postfix config now requires our user-patches.sh
support to add new services in /etc/postfix/master.cf
:
#!/bin/bash
# Duplicate the config for the submission(s) service ports (587 / 465) with adjustments for the PROXY ports (10587 / 10465) and `syslog_name` setting:
postconf -Mf submission/inet | sed -e s/^submission/10587/ -e 's/submission/submission-proxyprotocol/' >> /etc/postfix/master.cf
postconf -Mf submissions/inet | sed -e s/^submissions/10465/ -e 's/submissions/submissions-proxyprotocol/' >> /etc/postfix/master.cf
# Enable PROXY Protocol support for these new service variants:
postconf -P 10587/inet/smtpd_upstream_proxy_protocol=haproxy
postconf -P 10465/inet/smtpd_upstream_proxy_protocol=haproxy
# Create a variant for port 25 too (NOTE: Port 10025 is already assigned in DMS to Amavis):
postconf -Mf smtp/inet | sed -e s/^smtp/12525/ >> /etc/postfix/master.cf
# Enable PROXY Protocol support (different setting as port 25 is handled via postscreen), optionally configure a `syslog_name` to distinguish in logs:
postconf -P 12525/inet/postscreen_upstream_proxy_protocol=haproxy 12525/inet/syslog_name=smtp-proxyprotocol
Dovecot is mostly the same as before:
- A new service name instead of targeting one to modify.
- Add the new port assignment.
- Set
ssl = yes
when implicit TLS is needed.
haproxy_trusted_networks = 172.16.42.2
service imap-login {
inet_listener imap-proxied {
haproxy = yes
port = 10143
}
inet_listener imaps-proxied {
haproxy = yes
port = 10993
ssl = yes
}
}
service pop3-login {
inet_listener pop3-proxied {
haproxy = yes
port = 10110
}
inet_listener pop3s-proxied {
haproxy = yes
port = 10995
ssl = yes
}
}
service managesieve-login {
inet_listener sieve-proxied {
haproxy = yes
port = 14190
}
}
Verification
Send an email through the reverse proxy. If you do not use the DNS query rewriting approach, you'll need to do this from an external client.
Sending a generic test mail through swaks
CLI
Run a swaks
command and then check your DMS logs for the expected client IP, it should no longer be using the reverse proxy IP.
# NOTE: It is common to find port 25 is blocked from outbound connections, you may only be able to test the submission(s) ports.
swaks --helo not-relevant.test --server mail.example.com --port 25 -tls --from hello@not-relevant.test --to user@example.com
- You can specify the
--server
as the DMS FQDN or an IP address, where either should connect to the reverse proxy service. not-relevant.test
technically may be subject to some tests, at least for port 25. With the submission(s) ports those should be exempt.-tls
will use STARTTLS on port 25, you can exclude it to send unencrypted, but it would still go through the same port/route being tested.- To test the submission ports use
--port 587 -tls
or--port 465 -tlsc
with your credentials--auth-user user@example.com --auth-password secret
- Add
--tls-sni mail.example.com
if you have configuredHostSNI
in Traefik router rules (SNI routing is only valid for implicit TLS ports).
Do not rely on local testing alone
Testing from the Docker host technically works, however the IP is likely subject to more manipulation via iptables
than an external client.
The IP will likely appear as from the gateway IP of the Docker network associated to the reverse proxy, where that gateway IP then becomes the client IP when writing the PROXY protocol header.
Security concerns
Forgery
Since the PROXY protocol sends a header with the client IP rewritten for software to use instead, this could be abused by bad actors.
Software on the receiving end of the connection often supports configuring an IP or CIDR range of clients to trust receiving the PROXY protocol header from.
Risk exposure
If you trust more than the reverse proxy IP, you must consider the risk exposure:
- Any container within the network that is compromised could impersonate another IP (container or external client) which may have been configured to have additional access/exceptions granted.
- If the reverse proxy is on a separate network/host than DMS, exposure of the PROXY protocol enabled ports outside the network increases the importance of narrowing trust. For example with the known IPv6 to subnet Gateway IP routing gotcha in Docker, trusting the entire subnet DMS belongs to would wrongly trust external clients that have the subnet Gateway IP to impersonate any client IP.
- There is a known risk with Layer 2 switching (applicable to VPC networks, impact varies by cloud vendor):
- Neighbouring hosts can indirectly route to ports published on the interfaces of a separate host system that shouldn't be reachable (eg: localhost
127.0.0.1
, or a private subnet172.16.0.0/12
). - The scope of this in Docker is limited to published ports only when Docker uses
iptables
with the kernel tunablesysctl net.ipv4.ip_forward=1
(enabled implicitly). Port access is viaHOST:CONTAINER
ports published to their respective interface(s), that includes the container IP + port.
- Neighbouring hosts can indirectly route to ports published on the interfaces of a separate host system that shouldn't be reachable (eg: localhost
While some concerns raised above are rather specific, these type of issues aren't exclusive to Docker and difficult to keep on top of as software is constantly changing. Limit the trusted networks where possible.
Postfix has no concept of trusted proxies
Postfix does not appear to have a way to configure trusted proxies like Dovecot does (haproxy_trusted_networks
).
postscreen_access_list
(or smtpd_client_restrictions
with check_client_access
for ports 587/465) can both restrict access by IP via a CIDR lookup table, however the client IP is already rewritten at this point via PROXY protocol.
Thus those settings cannot be used for restricting access to only trusted proxies, only to the actual clients.
A similar setting mynetworks
/ PERMIT_DOCKER
manages elevated trust for bypassing security restrictions. While it is intended for trusted clients, it has no relevance to trusting proxies for the same reasons.
Monitoring
While PROXY protocol works well with the reverse proxy, you may have some containers internally that interact with DMS on behalf of multiple clients.
Roundcube + Fail2Ban
You may have other services with functionality like an API to send mail through DMS that likewise delegates credentials through DMS.
Roundcube is an example of this where authentication is delegated to DMS, which introduces the same concern with loss of client IP.
- While this service does implement some support for preserving the client IP, it is limited.
- This may be problematic when monitoring services like Fail2Ban are enabled that scan logs for multiple failed authentication attempts which triggers a ban on the shared IP address.
You should adjust configuration of these monitoring services to monitor for auth failures from those services directly instead, adding an exclusion for that service IP from any DMS logs monitored (but be mindful of PROXY header forgery risks).