aboutsummaryrefslogtreecommitdiff
path: root/articles/2025/Mailcow-with-Nginx-reverse-proxy.html
blob: 657b2147d5521b2533d8d0a9a3a59736030d8e13 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
<!doctype html>
<html lang="EN">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="/style.css">
    <title>Mailcow with Nginx reverse proxy</title>
  </head>
  <body>
    <header>
      <h1>Bloggings</h1>
      <a href="/index.php">Back</a>
      
    </header>
    
<main>
<article>
<h2>Mailcow with Nginx reverse proxy (updated)</h2>
<h3>2025-04-19</h3>
<h4>Using other web services with Mailcow</h4>
<p>This is roughly what I did to have an Nginx web server on the same machine as dockerized 
<a href="https://mailcow.email/">Mailcow</a></p>
<p><b>Note:</b> mail.xxxxx.com is your mail server (MX), yourdomain.com is whatever domain you wish to use.</p> 
<ol>
<li>Change the default http and https ports (for example 8480 and 8443)<br>
and set <b>SKIP_LETS_ENCRYPT=y</b> in /opt/mailcow-dockerized/mailcow.conf</li>
<li>Restart Mailcow by executing <br>
<b>cd /opt/mailcow-dockerized;docker compose restart</b></li>
<li>Make sure the DNS entry mail.xxxxx.com points to the server</li>
<li><b>mkdir -p /var/www/html/letsencrypt/.well-known/acme-challenge</b></li>
<li>Create /etc/nginx/letsencrypt_path. This can be re-used in your other domains.
<pre># url for letsencrypt
location ^~ /.well-known/acme-challenge/ {
  allow all;
  default_type "text/plain";
  # Path can be used for cert-validation on all domains
  root /var/www/html/letsencrypt;
  break;
}
</pre>
</li> 
<li>Stop the nginx server <b>systemctl stop nginx</b></li>
<li>Run <b>certbot certonly -d mail.xxxxx.com</b> (select 2 standalone)</li>
<li>Create /etc/nginx/sites-available/mail.xxxxx.com</li>
<pre>server {
  listen 443 ssl;
  listen [::]:443 ssl;
  http2 on;

  server_name mail.xxxxx.com;

  charset UTF-8;  
  access_log /var/log/nginx/access.mail.xxxxx.com;  
  error_log /var/log/nginx/error.mail.xxxxx.com;  
  include snippets/error_pages.conf;
  ssl_certificate /etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem; 
  ssl_certificate_key /etc/letsencrypt/live/mail.xxxxx.com/privkey.pem; 
  include /etc/letsencrypt/options-ssl-nginx.conf;
  ssl_dhparam /etc/letsencrypt/ssl-dhparams-2048.pem;  

  include /etc/nginx/letsencrypt_path;

  location / {
    proxy_pass      http://127.0.0.1:8480;
    proxy_buffering off;
    include /etc/nginx/proxy_params;
    
  }

}

server {

    listen 80;
    listen [::]:80;
    server_name mail.xxxxx.com;
    return 301 https://mail.xxxxx.com;
}
</pre>
<li><b>ln -s /etc/nginx/sites-available/mail.xxxxx.com <br>/etc/nginx/sites-enabled/mail.xxxxx.com</b></li> 
<li>Test with <b>nginx -t</b>, if all is well run <b>sudo systemctl start nginx</b></li>
<li>Pointing your browser to https://mail.xxxxx.com should show you the Mailcow login page.</li>
</ol>
<h3>Problem</h3>
<p>The problem is that Mailcow can no longer use port 80 to update it's ssl certificates that
are used by postfix and dovecot. Instead certbot puts them in /etc/letsencrypt/live.  
To fix this the following script runs from crontab daily.</p>
<pre>#!/usr/bin/env bash

# we are running behind an nginx proxy, where certbot is run  by systemd,
# so the mail.xxxxx.com certificates are updated, but not copied to the mailcow folder
# in case the certificate isn't renewed automatically, run this:
# sudo certbot -n certonly --webroot -w /var/www/html/letsencrypt -d mail.xxxxx.com

t1="/etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem"
t2="/opt/mailcow-dockerized/data/assets/ssl/mail.xxxxx.com/cert.pem" 

# test if certificate has been updated
differ=$(cmp -b $t1 $t2 | grep -c "differ")

[[ "$differ" = "0" ]] && exit 0

# these files are required in /data/assets/ssh/mail.xxxxx.com
cp /etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem /opt/mailcow-dockerized/data/assets/ssl/mail.xxxxx.com/cert.pem
cp /etc/letsencrypt/live/mail.xxxxx.com/privkey.pem /opt/mailcow-dockerized/data/assets/ssl/mail.xxxxx.com/key.pem

# these files are required in /data/assets/ssl/ (turns out sending failed otherwise)
cp /etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem /opt/mailcow-dockerized/data/assets/ssl/cert.pem
cp /etc/letsencrypt/live/mail.xxxxx.com/privkey.pem /opt/mailcow-dockerized/data/assets/ssl/key.pem

# update postfix & docker
docker exec $(/usr/bin/docker ps -qaf name=postfix-mailcow) postfix reload
docker exec $(/usr/bin/docker ps -qaf name=dovecot-mailcow) dovecot reload
</pre>
<p>Inspiration: <a href="https://felixmoessbauer.com/blog-reader/mailcow-reverse-proxy-letsencrypt.html">Felix Moesbauer</a></p>
<p>Don't forget to check and correct if necessary your DANE and MTA-STS records if you use them</p>
<h4>DANE</h4>
<p>On your mail server:</p>
<pre>apt install hash-slinger -y</pre>
<p>Create a TLSA record:</p>
<pre>tlsa --create --selector 1 -p 25 --certificate /etc/letsencrypt/live/mail.xxxxx.com/fullchain.pem mail.xxxxx.com
</pre>
<p>The last line or the result will be something like:</p>
<pre>_25._tcp.mail.xxxxx.com. IN TLSA 3 1 1 443ac7c5c70fbfbc...</pre>
<p>Enter this TLSA line in your mail server's DNS record.</p>
<p>Do the same for the following ports:</p>
<pre>_110._tcp.mail.xxxxx.com
_143._tcp.mail.xxxxx.com
_465._tcp.mail.xxxxx.com
_587._tcp.mail.xxxxx.com
_993._tcp.mail.xxxxx.com
_995._tcp.mail.xxxxx.com
</pre>
<p>You can check your DANE records at <a href="https://www.huque.com/bin/danecheck-smtp">huque.com</a></p>
<p><img src="/images/blog/dane-success.png" alt="dane success"></p> 
<h4>MTA-STS</h4>
<p>This is a fallback to DANE, you can run both.</p>
<p><b>Note:</b> https access to mta-sts.yourdomain.com is obligatory</p>
<ol>
<li>Create /var/www/html/mta-sts/.well-known/mta-sts.txt with the following content:<br>
<pre>version: STSv1
mode: enforce
max_age: 172800
mx: mail.xxxxx.com
</pre>
</li>
<li>Create the DNS record _mta-sts.mail.xxxxx.com as TXT with "v=STSv1; id=<b>INSERT AN ID, EX. 202511101644</b>"</li>
<li>Create the DNS record _mta-sts.yourdomain.com  as CNAME pointing to _mta-sts.mail.xxxxx.com (your mail server)</li>
<li>Create the DNS record mta-sts.yourdomain.com pointing to the ip of the web server</li>
<li>You may need to wait for the DNS records to propagate</li>
<li>Stop Nginx with <b>systemctl stop nginx</b></li>
<li>Create a letsencrypt certificate with <b>certbot certonly -d mta-sts.yourdomain.com</b> (select 2 standalone)</li>
<li>Create mta-sts.yourdomain.com in Nginx:</li>
<li>/etc/sites-available/mta-sts.yourdomain.com<br>
<pre>server {
   listen 443 ssl;
   listen [::]:443 ssl;
   http2 on;

   charset UTF-8;  
   access_log /var/log/nginx/access.mts-sta.yourdomain.com;  
   error_log /var/log/nginx/error.mts-sta.yourdomain.com;  
   include snippets/error_pages.conf;     
   ssl_certificate /etc/letsencrypt/live/mta-sts.yourdomain.com/fullchain.pem;
   ssl_certificate_key /etc/letsencrypt/live/mta-sts.yourdomain.com/privkey.pem;
   include /etc/letsencrypt/options-ssl-nginx.conf;
   ssl_dhparam /etc/letsencrypt/ssl-dhparams-2048.pem;

   include /etc/nginx/letsencrypt_path;
  
   root /var/www/html/mta-sts;
   index index.html index.htm;

   server_name mta-sts.yourdomain.com;

   location / {
      try_files $uri $uri/ =404;
   }
}
server {
    listen 80;
    listen [::]:80;
    server_name mta-sts.yourdomain.com;
    return 301 https://mta-sts.yourdomain.com;
}
</pre>
</li>
<li><b>ln -s /etc/nginx/sites-available/mta-sts.yourdomain.com <br>/etc/nginx/sites-enabled/mta-sts.yourdomain.com</b></li> 
<li>Test with nginx -t and if all goes well start nginx <b>systemctl start nginx</b></li>
</ol>
<p>You can check your domain's MTA-STS at <a href="https://mxtoolbox.com/mta-sts.aspx">mxtoolbox.com</a><p>
<p>&nbsp;</p>
<p>All this worked for me :-) I hope it does for you!</p>

</article>
</main>
<footer>

<p>&nbsp;</p>

<p>§</p>
  </footer>
</body>
</html>