Proxy Pro/Ent only

In some strict environments, Internet access should be done via a proxy server.

DNF proxy

DNF is the package manager used by Red Hat-based Linux distributions.

To configure the back-end to use a configured proxy, follow these steps:

Create and Ansible override file at /etc/ciphermail/ansible/group_vars/all/override.yml.

Insert the required lines into your configuration, then replace each placeholder with the actual values for your environment (for example, hostnames, IP addresses, service names, usernames, or passwords).

# the proxy configuration for dnf
ciphermail_appliance__dnf_proxy_host: '<scheme>://<ip-or-hostname>[:port]'
ciphermail_appliance__dnf_proxy_username: ''
ciphermail_appliance__dnf_proxy_password: ''

After creating the override file, you need to run the playbook to apply the changes:

sudo cm-run-playbook

Back-end proxy

To configure the back-end to use a configured proxy, follow these steps:

Create an Ansible override file at /etc/ciphermail/ansible/group_vars/all/override.yml.

Insert the required lines into your configuration, then replace each placeholder with the actual values for your environment.

# the http proxy host config for the back-ends
ciphermail_common__http_proxy_host: ''
ciphermail_common__http_proxy_port: 80
ciphermail_common__https_proxy_host: ''
ciphermail_common__https_proxy_port: 443

After creating the override file, you need to run the playbook to apply the changes:

sudo cm-run-playbook

Tip

The back-end does not support authenticated proxies because Java does not provide a standardized method for proxy authentication. If your environment requires proxy authentication, please install and configure the tinyproxy package on your local machine. tinyproxy will act as a local forwarding proxy to handle authentication. For detailed setup instructions, please contact us.

Behind a proxy

If the gateway is configured behind a proxy, for example a load balancer or a reverse proxy like HAProxy, the gateway by default sees the IP address of the proxy and not the real IP address of the client.

The real client IP address is passed on by the proxy in the X-Forwarded-For HTTP header. Because any client can set this header itself, and thereby spoof its IP address, X-Forwarded-For is stripped by default. It should therefore be configured which IP addresses are trusted to set the header.

The client IP address is used for the audit log, for restricting an admin account to specific IP addresses and for rate limiting. If the gateway sees the address of the proxy instead of the address of the client, all clients behind that proxy are treated as one: the login failure limit locks out a user for every client at once, the limits per address, such as the budget of failed sign ins per address (see Failed sign ins per address), are shared by every client, and an admin restricted to IP addresses must list the proxy. An IP load balancer which forwards nothing cannot be trusted this way; raise the limits per address or switch them off instead.

Configure the trusted proxies

To add a range of trusted IP addresses, create an Ansible override file at /etc/ciphermail/ansible/group_vars/all/override.yml.

# the IP addresses or ranges of the proxy servers which are allowed to
# set the X-Forwarded-For header
ciphermail_ui__trusted_proxy_ips:
  - 10.0.0.0/8
  - 172.16.0.0/12
  - 192.168.0.0/16

Replace the IP ranges by the ranges used by the forwarding proxy server.

After creating the override file, you need to run the playbook to apply the changes:

sudo cm-run-playbook

If the connecting IP address comes from a trusted IP address, the X-Forwarded-For header will not be stripped and the gateway will use the client IP address from the header.

Warning

Every address in ciphermail_ui__trusted_proxy_ips is trusted to claim any client IP address it likes. Only add the addresses of the proxy servers themselves and prefer the exact addresses of the proxies over a complete RFC 1918 range.

Note

The address which must be trusted is the address the gateway sees the proxy connect from, which is not always the address the proxy is configured with. If the proxy connects over IPv6, or if the connection is translated (NAT), the address will be different from what is expected. The section Testing the proxy configuration below shows how to determine the address.

Forwarded header handling

CipherMail sets the Spring Boot property server.forward-headers-strategy to native, which means that the handling of the forwarded headers is delegated to Tomcat.

Tomcat only accepts a X-Forwarded-For header from addresses which match server.tomcat.remoteip.internal-proxies, and it also skips any address in the header itself which matches, so that the chain of proxies is walked through to the client. CipherMail sets the property to the loopback addresses: the web server resolves the client address itself before the request is passed on, and it connects to the back-end over loopback. This also holds in portal split mode: the portal back-end runs on the front-end host next to the web server, and HAProxy on the back-end host connects to the gateway back-end over loopback.

Loopback only, and not the Spring Boot default, which covers the RFC 1918 private ranges, link-local, carrier-grade NAT and the IPv6 unique-local and link-local ranges as well. With that default, every address in the header which is in one of those ranges is skipped. Behind a trusted proxy the header is passed on as the client sent it, with the address of the client appended, so a client with a private address could claim any address by sending the header itself: Tomcat skips the real, private address and takes the claimed one. The IP restrictions on an admin account and the rate limits would then be checked against the claimed address. With loopback only, the address the web server appends is the one taken. The web server also removes everything in the header to the left of the client address it resolved, so the back-end always receives exactly one address, also when a trusted proxy reports the client as the loopback address itself.

Note

The web server resolves a host name in the header with a DNS lookup. A proxy which appends the address of its client, as every proxy does, puts an address at the right, where the web server stops, so only the proxy itself can put a name there. A name which does not resolve delays the request until the lookup times out, after which the address of the proxy is used.

Warning

If server.tomcat.remoteip.internal-proxies is changed, note that the value replaces the shipped one instead of extending it, so the loopback range must always be included. When loopback is not trusted the X-Forwarded-For header is ignored completely and the back-end reports 127.0.0.1 for every request. Do not add a range which clients can have an address in.

The property is a Java regular expression. It only needs to be changed when the web server connects to the back-end from another address than loopback, which a standard installation does not do. It is set from the Ansible variable ciphermail_backend__remoteip_internal_proxies, in the same Ansible override file. The example keeps loopback and adds the single address 10.0.1.5:

ciphermail_backend__remoteip_internal_proxies: '127\.\d{1,3}\.\d{1,3}\.\d{1,3}|::1|0:0:0:0:0:0:0:1|10\.0\.1\.5'

The portal and the company portal run as separate back-ends and have their own variables, ciphermail_portal_backend__remoteip_internal_proxies and ciphermail_company_portal_backend__remoteip_internal_proxies.

Note

Earlier versions of this chapter set the property as a system property, in ciphermail_backend__user_defined_system_properties. A system property overrides the variable, so remove such an entry when switching to the variable, or the variable has no effect.

Testing the proxy configuration

First determine the address the gateway sees the proxy connect from. Every request is logged by the web server with two addresses: the first field is the client address and the peer field at the end is the address the connection was actually made from.

sudo journalctl -t httpd --since '10 minutes ago' | tail

The peer field is the address which must be covered by ciphermail_ui__trusted_proxy_ips. If it is not, the header is ignored and stripped, and the client address will never reach the back-end.

203.0.113.99 - - [05/Aug/2026:10:12:31 +0200] "GET /api/... HTTP/1.1" 200 41 "-" "curl/8.0" peer=10.0.0.5

In the example above the proxy at 10.0.0.5 is trusted, so the client address 203.0.113.99 was taken from X-Forwarded-For. When the two fields are the same, the header was not used: either the request was not forwarded, or the address in peer is not a trusted proxy.

Next, check that a forwarded address reaches the back-end. Run the following from the proxy server, so the request has the same source address as a real forwarded request:

curl -k -H 'X-Forwarded-For: 203.0.113.99' https://example.com:8443/api/auth/getOAuth2Clients

Then inspect the audit log on the gateway:

sudo journalctl -t ciphermail-backend-audit --since '5 minutes ago' | grep API

The logged request contains the address which the back-end resolved:

[API] Method: GET, Path: /api/auth/getOAuth2Clients, IP: 203.0.113.99, User: anonymousUser, Params: , Status: 200

Compare the result against the address which was sent:

IP: 203.0.113.99 means the whole chain works.

Repeat the request without the X-Forwarded-For header to confirm the difference, and run it once from a machine which is not a trusted proxy to confirm that a spoofed header is ignored there.

For the portal use port 443 and for the company portal port 8444, instead of the administration port 8443 used above.

Azure Front Door

Azure Front Door ends the connection of the client and connects to the gateway itself. It appends the address the client connected from to the X-Forwarded-For header, after any address the client sent itself, and the rule sets of Front Door cannot change the header. The gateway can therefore take the client address from the header, once the addresses Front Door connects from are trusted.

Trust the Front Door addresses

Front Door connects from the addresses of the AzureFrontDoor.Backend service tag. Download “Azure IP Ranges and Service Tags - Public Cloud” from https://www.microsoft.com/download/details.aspx?id=56519 and list every prefix of AzureFrontDoor.Backend, IPv4 and IPv6, for example with jq:

jq -r '.values[] | select(.name == "AzureFrontDoor.Backend") | .properties.addressPrefixes[] | "  - \(.)"' ServiceTags_Public_*.json

Add the prefixes to ciphermail_ui__trusted_proxy_ips in the Ansible override file and run the playbook:

ciphermail_ui__trusted_proxy_ips:
  - 192.0.2.0/26
  - 2001:db8:1::/48
  # ... every prefix of AzureFrontDoor.Backend

Warning

Microsoft changes the addresses of the service tag regularly, so update the list, and run the playbook again, when the service tag changes. An address which is missing from the list is not trusted: the header is removed, and every client which Front Door forwards from that address is seen as the Front Door address, so those clients share the limits per address. An address which Microsoft no longer uses for Front Door stays trusted until it is removed from the list.

Limit the ports of the web UIs which are behind Front Door to the AzureFrontDoor.Backend service tag in the network security group of the gateway, so a client cannot reach them without Front Door.

Only accept your own Front Door

The Front Door profiles of other Azure customers connect from the same addresses. A request which someone else’s Front Door forwards still ends with the address its client connected from, so the client address cannot be spoofed this way, but the request bypasses the web application firewall, rate limits and other rules of your own profile. Microsoft therefore advises to only accept requests which carry the ID of your own profile.

Front Door sends the ID of the profile in the X-Azure-FDID header. The ID is shown as Front Door ID on the overview page of the profile in the Azure portal. Refuse every other request with additional web server configuration, in the variable of every web UI which is behind Front Door. The example does this for the user portal:

ciphermail_ui__portal_additional_config: |
  <If "%{HTTP:X-Azure-FDID} != '00000000-0000-0000-0000-000000000000'">
      Require all denied
  </If>

A request without the ID is answered with 403, also a request from a client on the internal network, so only add the check to the web UIs which are reached through Front Door. ciphermail_ui__additional_config adds it to every web UI at once, and to port 80 as well, where the ACME challenge then only works through Front Door too. The redirect to https still answers.

Testing through Front Door

The test in Testing the proxy configuration above sends a request from the proxy itself, which is not possible with Front Door. Send a request through Front Door instead, from a machine whose public address you know, with another address in the header:

curl -H 'X-Forwarded-For: 203.0.113.99' https://portal.example.com/

Then look up the request in the log of the web server on the gateway:

sudo journalctl -t httpd --since '5 minutes ago' | tail

The first field must be the public address of your machine, and not 203.0.113.99: Front Door appended the address you connected from, and the web server took that one and passes it on to the back-end. The peer field shows a Front Door address, or a NAT IP address with Private Link. When the first field is the same as peer, that address is not trusted, see the warning about missing addresses above. When the request is not logged at all, Front Door answered it from its cache; switch caching off for the route.