---
title: MailCow Hardening
description: Securing Mailcow
published: true
date: 2026-02-23T21:56:32.211Z
tags: 
editor: markdown
dateCreated: 2026-02-23T21:56:22.997Z
---

# MailCow Security Hardening

**Service:** MailCow Dockerized
**Host:** 192.168.5.16 (MailCow_Ngnx alias)
**Relay:** MXRoute (outbound only)
**Last Reviewed:** February 2026

---

## Overview

Running MailCow with MXRoute as an outbound relay creates a specific threat model that's different from either a fully self-hosted or fully managed setup. Your server receives inbound directly (MX points to your IP), stores all mailboxes locally, and hands outbound to MXRoute. This means you carry the risk surface of both — inbound SMTP exposure plus the credential and reputation exposure of a relay relationship.

The security areas that matter most for this setup:

| Area | Risk | Priority |
|---|---|---|
| DNS authentication (SPF/DKIM/DMARC) | Spoofing, deliverability failure, relay abuse | 🔴 Critical |
| MTA-STS + TLS-RPT | SMTP downgrade attacks on inbound | 🔴 Critical |
| MXRoute relay credential security | Relay hijacking, spam abuse on your reputation | 🔴 Critical |
| Mailcow admin hardening | Account takeover, open relay creation | 🔴 Critical |
| Postfix TLS hardening | Weak cipher negotiation | 🟡 High |
| Nginx header hardening | XSS, clickjacking on webmail | 🟡 High |
| Rspamd tuning | Inbound spam, outbound policy enforcement | 🟡 High |
| DMARC reporting | Visibility into spoofing and misdelivery | 🟡 High |
| ClamAV / attachment scanning | Malware distribution via your domain | 🟢 Medium |
| Rate limiting | Compromised account spam runs | 🟢 Medium |

---

## DNS Authentication

This is the foundation. If any of these are misconfigured your mail either doesn't deliver or your domain gets spoofed. With MXRoute in the mix the SPF record requires special attention.

### SPF — Include Both Sources

Your SPF must authorize **both** your own IP (for any direct sends) and MXRoute's sending infrastructure:

```dns
@ IN TXT "v=spf1 ip4:YOUR_ATT_MAIL_IP include:mxroute.com ~all"
```

Replace `YOUR_ATT_MAIL_IP` with the static IP you've dedicated to mail (ATT_Mail virtual IP). The `include:mxroute.com` covers MXRoute's sending servers.

> ⚠ Do not use `-all` (hard fail) until you have confirmed all your sending sources are covered. Use `~all` (softfail) initially, then tighten after verifying DMARC reports show no legitimate sources failing.

> ⚠ SPF has a **10 DNS lookup limit**. Each `include:` costs lookups. If you add more includes (e.g. transactional services), check your SPF lookup count at [mxtoolbox.com/spf](https://mxtoolbox.com/spf.aspx).

### DKIM — Two Selectors for Two Signers

Because MXRoute re-signs outbound mail with their own DKIM key, you need a DKIM record for both signers:

| Selector | Signer | Where to get the key |
|---|---|---|
| `mailcow._domainkey` | MailCow (inbound, internal sends) | MailCow UI → Configuration → ARC/DKIM Keys |
| `mxroute._domainkey` (or `x._domainkey`) | MXRoute (outbound relay) | MXRoute control panel |

Add both as TXT records. Having both means DMARC passes regardless of which path the mail took.

> ✓ MailCow lets you choose the DKIM selector name. Use `mailcow` as the selector to avoid confusion with the MXRoute selector.

### DMARC — Start Monitoring, Then Enforce

DMARC ties SPF and DKIM together and tells receiving servers what to do with failures. Start in monitoring mode, review reports for 2–4 weeks, then advance to enforcement.

**Phase 1 — Monitor (add immediately):**
```dns
_dmarc IN TXT "v=DMARC1; p=none; rua=mailto:dmarc-reports@yourdomain.com; ruf=mailto:dmarc-failures@yourdomain.com; fo=1"
```

**Phase 2 — Quarantine (after reviewing reports, no legitimate failures):**
```dns
_dmarc IN TXT "v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc-reports@yourdomain.com; fo=1"
```

**Phase 3 — Reject (final enforcement):**
```dns
_dmarc IN TXT "v=DMARC1; p=reject; pct=100; rua=mailto:dmarc-reports@yourdomain.com; fo=1"
```

> ✓ `fo=1` requests forensic reports on any authentication failure — more detail for debugging.

**DMARC Report Processing:** Raw DMARC reports are XML and not human-readable. Use one of these free tools to process them:
- [Postmark DMARC](https://dmarc.postmarkapp.com/) — free, email-based weekly digest
- [dmarcian.com](https://dmarcian.com) — free tier, dashboard view
- Self-hosted: [Parsedmarc](https://github.com/domainaware/parsedmarc) → send to Graylog/Grafana

---

## MTA-STS (MailCow September 2025+)

MTA-STS forces other mail servers to use TLS when delivering to you, preventing downgrade attacks that try to force plaintext SMTP. The September 2025 MailCow update added the `postfix-tlspol-mailcow` container which enforces MTA-STS on **outbound** connections too.

### What You Need

**1. DNS records** — three records for each domain:

```dns
# For your mail server's hostname domain (e.g. netgrimoire.com)
mta-sts       IN CNAME   mail.netgrimoire.com.
_mta-sts      IN TXT     "v=STSv1; id=20260223"
_smtp._tls    IN TXT     "v=TLSRPTv1; rua=mailto:tls-reports@netgrimoire.com"
```

The `id` value in `_mta-sts` is a version string — update it (e.g. to today's date) whenever you change your MTA-STS policy.

**2. Policy file** — served by MailCow's nginx at `https://mta-sts.yourdomain.com/.well-known/mta-sts.txt`:

```bash
# On your MailCow host:
mkdir -p /opt/mailcow-dockerized/data/web/.well-known/
cat > /opt/mailcow-dockerized/data/web/.well-known/mta-sts.txt << 'EOF'
version: STSv1
mode: enforce
max_age: 86400
mx: mail.netgrimoire.com
EOF
```

Start with `mode: testing` for the first week, then switch to `mode: enforce`.

**3. For additional domains** — add CNAMEs pointing to your primary domain's records:

```dns
# For each additional mail domain you host on MailCow:
mta-sts.otherdomain.com    IN CNAME   mail.netgrimoire.com.
_mta-sts.otherdomain.com   IN CNAME   _mta-sts.netgrimoire.com.
_smtp._tls.otherdomain.com IN CNAME   _smtp._tls.netgrimoire.com.
```

> ✓ TLS-RPT (`_smtp._tls` TXT record) sends you reports about TLS failures when other servers connect to you. Pipe these to Graylog or Postmark for visibility.

---

## MXRoute Relay Security

This is the most overlooked area. Your MXRoute credentials can send mail as your domain — if they're compromised, someone else is spamming from your reputation.

### Credential Hardening

- Use a **unique, strong password** for your MXRoute account — not shared with anything else
- Store the MXRoute SMTP credentials in MailCow's relay configuration only, not in any config file or environment variable that gets committed to git
- If MXRoute supports API tokens or app passwords, use those instead of your main account password

### Relay Configuration in MailCow

In MailCow UI: **Configuration → Routing → Sender-Dependent Transports**

Verify the relay is configured to authenticate via TLS (port 587 with STARTTLS or port 465 with SSL). Do not relay over port 25 without authentication.

```
# What the relay entry should look like in Postfix terms:
# relayhost = [smtp.mxroute.com]:587
# smtp_sasl_auth_enable = yes
# smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
# smtp_tls_security_level = encrypt   ← ensures TLS is required, not optional
```

> ⚠ Set `smtp_tls_security_level = encrypt` (not `may`) so the connection to MXRoute is always encrypted. If the TLS negotiation fails, Postfix should reject rather than fall back to plaintext.

### Rate Limiting (Prevent Relay Abuse if Account Compromised)

Add rate limits in MailCow UI: **Configuration → Mail Setup → Domains → [your domain] → Rate Limit**

| Setting | Recommended Value | Notes |
|---|---|---|
| Outbound messages/hour | 500 | Adjust for your actual sending volume |
| Outbound messages/day | 2000 | A sudden spike above this = red flag |

This doesn't stop abuse but limits blast radius if a mailbox is compromised and starts spamming through MXRoute.

---

## MailCow Admin Hardening

### Two-Factor Authentication

Enable 2FA on the admin account and all mailbox accounts that have access to the admin panel.

MailCow UI: **Edit mailbox → Two-Factor Authentication → TOTP**

> ⚠ There was a session fixation vulnerability in the MailCow web panel (GHSA-23c8-4wwr-g3c6, January 2025) and a critical SSTI vulnerability (GHSA-8p7g-6cjj-wr9m, July 2025). Both require staying current on updates. Enable auto-updates or check the MailCow blog monthly.

### Restrict Admin UI to Internal Network

The MailCow admin panel should not be reachable from the public internet. Access should require being on your internal network or connected via WireGuard.

In OPNsense, add a firewall rule blocking external access to port 443 on 192.168.5.16 except from your static admin IP or WireGuard peers.

Alternatively, configure MailCow's nginx to restrict the admin path by IP:

```nginx
# In data/conf/nginx/includes/site-defaults.conf
# Add inside the server block for the admin panel:
location /admin {
    allow 192.168.3.0/24;
    allow 192.168.5.0/24;
    allow 192.168.32.0/24;  # WireGuard peers
    deny all;
}
```

### API Key Rotation

If you use the MailCow API (for automation or Netgrimoire tooling), generate a dedicated read-only key where possible, and rotate keys annually or after any suspected compromise.

---

## Postfix TLS Hardening

Add to `/opt/mailcow-dockerized/data/conf/postfix/extra.cf`:

```ini
# Enforce TLS 1.2+ and strong ciphers
tls_high_cipherlist = ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
tls_preempt_cipherlist = yes

# Inbound SMTP (smtpd) — receiving from other mail servers
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_ciphers = high
smtpd_tls_mandatory_ciphers = high

# Outbound SMTP (smtp) — delivery to MXRoute and direct sends
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_ciphers = high
smtp_tls_mandatory_ciphers = high

# Require encryption on the MXRoute relay connection
smtp_tls_security_level = encrypt
```

After editing, restart Postfix:
```bash
cd /opt/mailcow-dockerized
docker compose restart postfix-mailcow
```

---

## Nginx Header Hardening

Add to `/opt/mailcow-dockerized/data/conf/nginx/includes/site-defaults.conf`:

```nginx
# Strong SSL ciphers only
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;
ssl_conf_command Options PrioritizeChaCha;

# HSTS — include subdomains if all your services use HTTPS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";

# Disable X-XSS-Protection (deprecated, CSP replaces it)
add_header X-XSS-Protection "0";

# Deny unused browser permissions
add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()";

# Content Security Policy — if NOT using Gravatar with SOGo
add_header Content-Security-Policy "default-src 'none'; connect-src 'self' https://api.github.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content; base-uri 'none'";

# Cross-origin isolation headers
add_header Cross-Origin-Resource-Policy same-origin;
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;

# Disable gzip to prevent BREACH attack
# Change gzip on; → gzip off; in the main nginx conf
```

> ⚠ The December 2025 MailCow update already removed the deprecated `X-XSS-Protection` header from defaults. If you're current, you may already have this. Check before duplicating.

After editing, restart nginx:
```bash
docker compose restart nginx-mailcow
```

---

## Rspamd Tuning

Rspamd is MailCow's spam filter. The defaults are reasonable but a few adjustments improve both inbound protection and outbound policy enforcement.

### Key Settings to Review

Navigate to **MailCow UI → Configuration → Rspamd UI** (or directly at `https://mail.yourdomain.com/rspamd/`)

**Actions → Score Thresholds:**

| Action | Default | Recommended |
|---|---|---|
| Greylist | 4 | 3 |
| Add header | 6 | 5 |
| Reject | 15 | 12 |

Lowering the reject threshold from 15 to 12 catches more aggressive spam while avoiding false positives.

**Modules to enable/verify:**

| Module | Purpose |
|---|---|
| DKIM verification | Verify incoming DKIM signatures |
| SPF | Verify incoming SPF |
| DMARC | Enforce DMARC on inbound |
| MX Check | Verify sending domain has a valid MX |
| RBL (Realtime Blacklists) | Check sending IPs against blocklists |
| Greylisting | Temporary reject new senders (forces retry) |

### Add CrowdSec as an Rspamd Feed

If you also have the CrowdSec bouncer running on the MailCow host (or can reach it), you can feed CrowdSec decisions into Rspamd to reject mail from banned IPs. This is advanced but powerful — see the [CrowdSec Bouncer for Rspamd](https://hub.crowdsec.net) hub entry.

---

## Deliverability Verification

Run these checks after making any DNS or config changes:

| Tool | What It Checks | URL |
|---|---|---|
| MXToolbox | SPF, DKIM, DMARC, MX, PTR, blacklists | mxtoolbox.com |
| mail-tester.com | Send a test email, get a 1–10 score | mail-tester.com |
| Port25 verifier | Send to check-auth@verifier.port25.com | Email-based |
| DKIM validator | Validates DKIM signature | dkimvalidator.com |
| Google Postmaster Tools | Gmail reputation monitoring (requires setup) | postmaster.google.com |
| Microsoft SNDS | Outlook/Hotmail reputation | sendersupport.olc.protection.outlook.com |

> ✓ Aim for 9–10/10 on mail-tester.com. Anything below 8 indicates a misconfiguration that will hurt deliverability.

---

## Keeping MailCow Updated

MailCow has had several critical security vulnerabilities in 2025 (session fixation, SSTI, password reset poisoning). Staying current is non-negotiable.

```bash
cd /opt/mailcow-dockerized

# Pull latest images
docker compose pull

# Apply update
./update.sh

# Or if using the newer helper:
docker compose up -d
```

> ✓ Subscribe to the [MailCow blog](https://mailcow.email/posts/) or watch the [GitHub releases](https://github.com/mailcow/mailcow-dockerized/releases) for security advisories. The update cadence is roughly monthly.

Set up a cron job or Monit check to alert you when MailCow is more than 30 days behind the latest release.

---

## Checklist Summary

| Item | Status |
|---|---|
| SPF includes both own IP and mxroute.com | ☐ |
| Two DKIM selectors (mailcow + mxroute) | ☐ |
| DMARC in monitoring mode, advancing to reject | ☐ |
| DMARC reports being processed (Postmark/dmarcian) | ☐ |
| MTA-STS policy published and enforced | ☐ |
| TLS-RPT record in DNS | ☐ |
| MXRoute relay connection uses TLS/encrypt level | ☐ |
| Admin UI restricted to internal network | ☐ |
| 2FA on admin and all privileged accounts | ☐ |
| Postfix TLS 1.2+ enforced via extra.cf | ☐ |
| Nginx security headers added | ☐ |
| Rate limits set on outbound per-domain | ☐ |
| MailCow updated to latest (monthly check) | ☐ |
| Rspamd thresholds reviewed | ☐ |
| PTR/rDNS record matches mail hostname | ☐ |

---

## Related Documentation

- [OPNsense Firewall](./opnsense-firewall) — dedicated ATT_Mail virtual IP, port NAT
- [CrowdSec](./crowdsec) — IP reputation blocking at firewall level
- [Graylog](./graylog) — DMARC report and TLS-RPT ingestion target
- [Caddy Reverse Proxy](./caddy-reverse-proxy) — if MailCow webmail is proxied through Caddy
