move
This commit is contained in:
parent
b1a2672c76
commit
e55070398b
65 changed files with 0 additions and 0 deletions
522
Netgrimoire/Keystone-Grimoire/Docker/Caddy.md
Normal file
522
Netgrimoire/Keystone-Grimoire/Docker/Caddy.md
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
---
|
||||
title: Caddy Reverse Proxy
|
||||
description: Curreent and future config
|
||||
published: true
|
||||
date: 2026-02-25T01:50:20.558Z
|
||||
tags:
|
||||
editor: markdown
|
||||
dateCreated: 2026-02-23T22:09:16.106Z
|
||||
---
|
||||
|
||||
# Caddy Reverse Proxy
|
||||
|
||||
**Host:** znas (Docker Swarm node)
|
||||
**Internal IP:** 192.168.5.10
|
||||
**Data Path:** `/export/Docker/caddy/`
|
||||
**Networks:** `netgrimoire` (service network), `vpn`
|
||||
**Ports:** 80 (mapped to host 8900), 443
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Caddy serves as the primary reverse proxy for all public and internal web services. It uses the `caddy-docker-proxy` pattern, which allows services to register themselves with Caddy by adding Docker labels to their compose files — no manual Caddyfile edits required per service.
|
||||
|
||||
Configuration is **hybrid**: some services are defined entirely via Docker labels, others are defined statically in the Caddyfile, and most use both (labels for routing, Caddyfile for shared snippets). The `caddy-docker-proxy` container merges both sources at runtime.
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
### Image
|
||||
|
||||
```yaml
|
||||
image: lucaslorentz/caddy-docker-proxy:ci-alpine
|
||||
```
|
||||
|
||||
This image provides the Docker Proxy module only. It has no CrowdSec, GeoIP, or rate limiting built in.
|
||||
|
||||
### Docker Compose (`/export/Docker/caddy/docker-compose.yml`)
|
||||
|
||||
```yaml
|
||||
configs:
|
||||
caddy-basic-content:
|
||||
file: ./Caddyfile
|
||||
labels:
|
||||
caddy:
|
||||
|
||||
services:
|
||||
caddy:
|
||||
image: lucaslorentz/caddy-docker-proxy:ci-alpine
|
||||
ports:
|
||||
- 8900:80
|
||||
- 443:443
|
||||
environment:
|
||||
- CADDY_INGRESS_NETWORKS=netgrimoire
|
||||
networks:
|
||||
- netgrimoire
|
||||
- vpn
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /export/Docker/caddy/Caddyfile:/etc/caddy/Caddyfile
|
||||
- /export/Docker/caddy:/data
|
||||
#- /export/Docker/caddy/logs:/var/log/caddy # Placeholder for CrowdSec log mount
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == znas
|
||||
|
||||
networks:
|
||||
netgrimoire:
|
||||
external: true
|
||||
vpn:
|
||||
external: true
|
||||
```
|
||||
|
||||
### Caddyfile (`/export/Docker/caddy/Caddyfile`)
|
||||
|
||||
The Caddyfile defines shared authentication snippets and static site blocks. These snippets are available to all services — including label-defined ones — via `import`.
|
||||
|
||||
```caddyfile
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# AUTH SNIPPETS
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
(authentik) {
|
||||
route /outpost.goauthentik.io/* {
|
||||
reverse_proxy http://authentik:9000
|
||||
}
|
||||
|
||||
forward_auth http://authentik:9000 {
|
||||
uri /outpost.goauthentik.io/auth/caddy
|
||||
header_up X-Forwarded-URI {http.request.uri}
|
||||
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email \
|
||||
X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt \
|
||||
X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider \
|
||||
X-Authentik-Meta-App X-Authentik-Meta-Version
|
||||
}
|
||||
}
|
||||
|
||||
(authelia) {
|
||||
forward_auth http://authelia:9091 {
|
||||
uri /api/verify?rd=https://login.wasted-bandwidth.net/
|
||||
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
||||
}
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# MAIL SNIPPETS
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
(email-proxy) {
|
||||
redir https://mail.netgrimoire.com/sogo 301
|
||||
}
|
||||
|
||||
(mailcow-proxy) {
|
||||
reverse_proxy nginx-mailcow:80
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# STATIC SITE BLOCKS — NETGRIMOIRE.COM
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
cloud.netgrimoire.com {
|
||||
reverse_proxy http://nextcloud-aio-apache:11000
|
||||
}
|
||||
|
||||
log.netgrimoire.com {
|
||||
reverse_proxy http://graylog:9000
|
||||
}
|
||||
|
||||
win.netgrimoire.com {
|
||||
reverse_proxy http://192.168.5.10:8006
|
||||
}
|
||||
|
||||
docker.netgrimoire.com {
|
||||
reverse_proxy http://portainer:9000
|
||||
}
|
||||
|
||||
immich.netgrimoire.com {
|
||||
reverse_proxy http://192.168.5.10:2283
|
||||
}
|
||||
|
||||
npm.netgrimoire.com {
|
||||
reverse_proxy http://librenms:8000
|
||||
}
|
||||
|
||||
#jellyfin.netgrimoire.com {
|
||||
# reverse_proxy http://jellyfin:8096
|
||||
#}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# AUTHENTICATED — NETGRIMOIRE.COM
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
dozzle.netgrimoire.com {
|
||||
import authentik
|
||||
reverse_proxy http://192.168.4.72:8043
|
||||
}
|
||||
|
||||
dns.netgrimoire.com {
|
||||
import authentik
|
||||
reverse_proxy http://192.168.5.7:5380
|
||||
}
|
||||
|
||||
webtop.netgrimoire.com {
|
||||
import authentik
|
||||
reverse_proxy http://webtop:3000
|
||||
}
|
||||
|
||||
jackett.netgrimoire.com {
|
||||
import authentik
|
||||
reverse_proxy http://gluetun:9117
|
||||
}
|
||||
|
||||
transmission.netgrimoire.com {
|
||||
import authentik
|
||||
reverse_proxy http://gluetun:9091
|
||||
}
|
||||
|
||||
scrutiny.netgrimoire.com {
|
||||
import authentik
|
||||
reverse_proxy http://192.168.5.10:8081
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# AUTHENTICATED — WASTED-BANDWIDTH.NET (Authelia)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
stash.wasted-bandwidth.net {
|
||||
import authelia
|
||||
reverse_proxy http://192.168.5.10:9999
|
||||
}
|
||||
|
||||
namer.wasted-bandwidth.net {
|
||||
import authelia
|
||||
reverse_proxy http://192.168.5.10:6980
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# PUBLIC — PNCHARRIS.COM / WASTED-BANDWIDTH.NET
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fish.pncharris.com {
|
||||
reverse_proxy http://web
|
||||
}
|
||||
|
||||
www.wasted-bandwidth.net {
|
||||
reverse_proxy http://web
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# MAILCOW — MULTI-DOMAIN
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
mail.netgrimoire.com, autodiscover.netgrimoire.com, autoconfig.netgrimoire.com, \
|
||||
mail.wasted-bandwidth.net, autodiscover.wasted-bandwidth.net, autoconfig.wasted-bandwidth.net, \
|
||||
mail.gnarlypandaproductions.com, autodiscover.gnarlypandaproductions.com, autoconfig.gnarlypandaproductions.com, \
|
||||
mail.pncfishandmore.com, autodiscover.pncfishandmore.com, autoconfig.pncfishandmore.com, \
|
||||
mail.pncharrisenterprises.com, autodiscover.pncharrisenterprises.com, autoconfig.pncharrisenterprises.com, \
|
||||
mail.pncharris.com, autodiscover.pncharris.com, autoconfig.pncharris.com, \
|
||||
mail.florosafd.org, autodiscover.florosafd.org, autoconfig.florosafd.org {
|
||||
import mailcow-proxy
|
||||
}
|
||||
```
|
||||
|
||||
### Docker Label Pattern (label-defined services)
|
||||
|
||||
Services not in the Caddyfile are registered via labels on their own containers. The snippet defined in the Caddyfile is available to them via `caddy.import`:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
- caddy=homepage.netgrimoire.com
|
||||
- caddy.import=authentik
|
||||
- caddy.reverse_proxy={{upstreams 3000}}
|
||||
```
|
||||
|
||||
For services that need no auth:
|
||||
```yaml
|
||||
labels:
|
||||
- caddy=myservice.netgrimoire.com
|
||||
- caddy.reverse_proxy={{upstreams 8080}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication Layers
|
||||
|
||||
Two identity proxies are in use, each serving different domains/use cases:
|
||||
|
||||
| Provider | Domain Pattern | Snippet |
|
||||
|----------|----------------|---------|
|
||||
| Authentik | `*.netgrimoire.com` internal tools | `import authentik` |
|
||||
| Authelia | `*.wasted-bandwidth.net` | `import authelia` |
|
||||
|
||||
Services without an auth import are either public (e.g. `fish.pncharris.com`) or carry their own authentication (e.g. Nextcloud, Graylog, Portainer).
|
||||
|
||||
---
|
||||
|
||||
## Current Security Posture
|
||||
|
||||
CrowdSec protection exists only at the **OPNsense firewall level** — IP reputation blocking before traffic reaches Caddy. CrowdSec does not currently inspect HTTP traffic at the application layer. This means:
|
||||
|
||||
- Known-bad IPs are blocked at the perimeter
|
||||
- Application-layer attacks (SQLi in URLs, malicious paths, bad user agents, brute force on specific endpoints) are not blocked at the Caddy level
|
||||
- Services behind Authentik/Authelia have an additional protection layer; unauthenticated public services do not
|
||||
|
||||
---
|
||||
|
||||
## Future State: CrowdSec + GeoIP + Rate Limiting
|
||||
|
||||
### Target Image
|
||||
|
||||
```yaml
|
||||
image: ghcr.io/serfriz/caddy-crowdsec-geoip-ratelimit-security-dockerproxy:latest
|
||||
```
|
||||
|
||||
This is a drop-in replacement for `lucaslorentz/caddy-docker-proxy`. All existing Docker labels and Caddyfile site blocks continue to work unchanged. The image is automatically rebuilt monthly when Caddy releases updates — no custom image maintenance required.
|
||||
|
||||
**Included modules:**
|
||||
- `caddy-docker-proxy` — same label-based config as current
|
||||
- `caddy-crowdsec-bouncer` — inline HTTP blocking based on CrowdSec decisions
|
||||
- `caddy-geoip` — GeoIP filtering at the application layer
|
||||
- `caddy-ratelimit` — per-endpoint rate limiting
|
||||
- `caddy-security` — additional auth/security middleware
|
||||
|
||||
### Updated Compose
|
||||
|
||||
```yaml
|
||||
configs:
|
||||
caddy-basic-content:
|
||||
file: ./Caddyfile
|
||||
labels:
|
||||
caddy:
|
||||
|
||||
services:
|
||||
caddy:
|
||||
image: ghcr.io/serfriz/caddy-crowdsec-geoip-ratelimit-security-dockerproxy:latest
|
||||
ports:
|
||||
- 8900:80
|
||||
- 443:443
|
||||
environment:
|
||||
- CADDY_INGRESS_NETWORKS=netgrimoire
|
||||
- CADDY_DOCKER_EVENT_THROTTLE_INTERVAL=2000 # Prevents non-deterministic reload with CrowdSec module
|
||||
- CROWDSEC_API_KEY=BYSLg/wKOa7wlHYzChJpBVJA06Ukc7G6fKJCvBwjyZg
|
||||
networks:
|
||||
- netgrimoire
|
||||
- vpn
|
||||
- crowdsec_net
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /export/Docker/caddy/Caddyfile:/etc/caddy/Caddyfile
|
||||
- /export/Docker/caddy:/data
|
||||
- caddy-logs:/var/log/caddy
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == znas
|
||||
|
||||
crowdsec:
|
||||
image: crowdsecurity/crowdsec
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
COLLECTIONS: "crowdsecurity/caddy crowdsecurity/http-cve crowdsecurity/whitelist-good-actors"
|
||||
BOUNCER_KEY_CADDY: BYSLg/wKOa7wlHYzChJpBVJA06Ukc7G6fKJCvBwjyZg # Pre-registers the Caddy bouncer automatically
|
||||
volumes:
|
||||
- crowdsec-db:/var/lib/crowdsec/data
|
||||
- ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml
|
||||
- caddy-logs:/var/log/caddy:ro
|
||||
networks:
|
||||
- crowdsec_net
|
||||
deploy:
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == znas
|
||||
|
||||
volumes:
|
||||
caddy-logs:
|
||||
crowdsec-db:
|
||||
|
||||
networks:
|
||||
netgrimoire:
|
||||
external: true
|
||||
vpn:
|
||||
external: true
|
||||
crowdsec_net:
|
||||
driver: overlay # Swarm overlay network
|
||||
```
|
||||
|
||||
### CrowdSec Log Acquisition (`./crowdsec/acquis.yaml`)
|
||||
|
||||
```yaml
|
||||
filenames:
|
||||
- /var/log/caddy/access.log
|
||||
labels:
|
||||
type: caddy
|
||||
```
|
||||
|
||||
### Environment File (`.env`)
|
||||
|
||||
```env
|
||||
CROWDSEC_API_KEY=<generate-with-cscli-or-set-before-first-boot>
|
||||
```
|
||||
|
||||
The `BOUNCER_KEY_CADDY` env var in the CrowdSec container pre-registers the bouncer key at startup. Set the same value in `.env` as `CROWDSEC_API_KEY` and both sides will be in sync on first boot — no need to run `cscli bouncers add` manually.
|
||||
|
||||
### Updated Caddyfile Additions
|
||||
|
||||
Add a global block at the top of the Caddyfile and a new `crowdsec` snippet. All other existing content remains unchanged.
|
||||
|
||||
```caddyfile
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# GLOBAL BLOCK — add this at the very top before any snippets
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
{
|
||||
crowdsec {
|
||||
api_url http://crowdsec:8080
|
||||
api_key {$CROWDSEC_API_KEY}
|
||||
}
|
||||
log {
|
||||
output file /var/log/caddy/access.log {
|
||||
roll_size 50mb
|
||||
roll_keep 5
|
||||
}
|
||||
format json
|
||||
}
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# CROWDSEC SNIPPET — add alongside existing auth snippets
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
(crowdsec) {
|
||||
route {
|
||||
crowdsec
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Applying CrowdSec to Existing Services
|
||||
|
||||
Once the snippet exists, add `import crowdsec` to site blocks and container labels. This is a **gradual rollout** — services without it remain fully functional, just without Caddy-level CrowdSec inspection (they still have OPNsense perimeter protection).
|
||||
|
||||
**In the Caddyfile:**
|
||||
```caddyfile
|
||||
# Before
|
||||
cloud.netgrimoire.com {
|
||||
reverse_proxy http://nextcloud-aio-apache:11000
|
||||
}
|
||||
|
||||
# After
|
||||
cloud.netgrimoire.com {
|
||||
import crowdsec
|
||||
reverse_proxy http://nextcloud-aio-apache:11000
|
||||
}
|
||||
|
||||
# With auth
|
||||
dozzle.netgrimoire.com {
|
||||
import crowdsec
|
||||
import authentik
|
||||
reverse_proxy http://192.168.4.72:8043
|
||||
}
|
||||
```
|
||||
|
||||
**In Docker labels:**
|
||||
```yaml
|
||||
labels:
|
||||
- caddy=homepage.netgrimoire.com
|
||||
- caddy.import=crowdsec
|
||||
- caddy.import=authentik
|
||||
- caddy.reverse_proxy={{upstreams 3000}}
|
||||
```
|
||||
|
||||
### CrowdSec Rollout Priority
|
||||
|
||||
Roll out `import crowdsec` in this order based on risk exposure:
|
||||
|
||||
**High priority — do first (public, no auth):**
|
||||
- `cloud.netgrimoire.com` (Nextcloud)
|
||||
- `immich.netgrimoire.com`
|
||||
- `docker.netgrimoire.com` (Portainer)
|
||||
- `fish.pncharris.com`
|
||||
- `www.wasted-bandwidth.net`
|
||||
|
||||
**Medium priority — high value behind auth:**
|
||||
- `log.netgrimoire.com` (Graylog)
|
||||
- `win.netgrimoire.com` (Proxmox)
|
||||
- All `dozzle`, `dns`, `webtop`, `jackett`, `transmission`, `scrutiny`
|
||||
|
||||
**Lower priority — already protected by Authelia/Authentik:**
|
||||
- `stash.wasted-bandwidth.net`
|
||||
- `namer.wasted-bandwidth.net`
|
||||
- All label-defined services behind auth
|
||||
|
||||
**Skip:**
|
||||
- Mailcow block — handled by nginx-mailcow, different threat model
|
||||
|
||||
### Behavior if CrowdSec Container Goes Down
|
||||
|
||||
The bouncer is designed to **fail open** by default. If `crowdsec` is unreachable, Caddy continues serving traffic normally — enforcement is temporarily suspended but the site stays up. This is the safe default for a homelab. To change this behavior, set `enable_hard_fails true` in the global crowdsec block (will cause 500 errors if CrowdSec is down — not recommended for homelab).
|
||||
|
||||
---
|
||||
|
||||
## Bootstrap Steps
|
||||
|
||||
When ready to migrate to the new image:
|
||||
|
||||
**Step 1 — Add the CrowdSec global block and snippet to the Caddyfile** before changing the image. This ensures the Caddyfile is valid for the new image on startup.
|
||||
|
||||
**Step 2 — Create `./crowdsec/acquis.yaml`** with the content above.
|
||||
|
||||
**Step 3 — Create `.env`** with a strong random value for `CROWDSEC_API_KEY`:
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
**Step 4 — Update the image and add the CrowdSec service to the compose file**, then redeploy:
|
||||
```bash
|
||||
docker stack deploy -c docker-compose.yml caddy
|
||||
```
|
||||
|
||||
**Step 5 — Verify CrowdSec is reading Caddy logs:**
|
||||
```bash
|
||||
docker exec <crowdsec_container> cscli metrics
|
||||
```
|
||||
Look for the `Acquisition Metrics` table showing hits from `/var/log/caddy/access.log`.
|
||||
|
||||
**Step 6 — Test a ban manually:**
|
||||
```bash
|
||||
docker exec <crowdsec_container> cscli decisions add --ip 1.2.3.4 --duration 5m
|
||||
# Verify the IP gets a 403 from Caddy
|
||||
curl -I https://yoursite.com --resolve yoursite.com:443:1.2.3.4
|
||||
docker exec <crowdsec_container> cscli decisions delete --ip 1.2.3.4
|
||||
```
|
||||
|
||||
**Step 7 — Gradually add `import crowdsec`** to site blocks and labels per the priority order above.
|
||||
|
||||
---
|
||||
|
||||
## File Layout
|
||||
|
||||
```
|
||||
/export/Docker/caddy/
|
||||
├── Caddyfile # Shared snippets and static site blocks
|
||||
├── docker-compose.yml # Caddy + CrowdSec services
|
||||
├── .env # CROWDSEC_API_KEY (future)
|
||||
├── data/ # Caddy data volume (TLS certs, etc.)
|
||||
├── logs/ # caddy-logs volume mount point (future)
|
||||
└── crowdsec/
|
||||
└── acquis.yaml # Tells CrowdSec where to read Caddy logs (future)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Known Issues / Notes
|
||||
|
||||
- Port 80 is mapped to host port 8900 — this is intentional for Swarm. OPNsense NAT handles the external 80→8900 translation.
|
||||
- The `CADDY_DOCKER_EVENT_THROTTLE_INTERVAL=2000` setting is **required** with the CrowdSec module to prevent non-deterministic domain matching behavior during container label reloads (see [issue #61](https://github.com/hslatman/caddy-crowdsec-bouncer/issues/61)).
|
||||
- Jellyfin is commented out in the Caddyfile — likely served via a different path or disabled temporarily.
|
||||
- The `web` upstream referenced by `fish.pncharris.com` and `www.wasted-bandwidth.net` resolves to a container named `web` on the `netgrimoire` network.
|
||||
- Authelia redirect URL is `https://login.wasted-bandwidth.net/` — update if this changes.
|
||||
- The serfriz image is rebuilt on the **1st of each month** for module updates, and on every new Caddy release. Force a module update by recreating the container: `docker service update --force caddy_caddy`.
|
||||
144
Netgrimoire/Keystone-Grimoire/Docker/Swarm-Template.md
Normal file
144
Netgrimoire/Keystone-Grimoire/Docker/Swarm-Template.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
title: Docker Swarm Template Standard
|
||||
description: Canonical YAML template and label rules for all Netgrimoire swarm services
|
||||
published: true
|
||||
date: 2026-04-12T00:00:00.000Z
|
||||
tags: keystone, docker, swarm
|
||||
editor: markdown
|
||||
dateCreated: 2026-04-12T00:00:00.000Z
|
||||
---
|
||||
|
||||
# Docker Swarm Template Standard
|
||||
|
||||
All Swarm YAML files in `services/swarm/` and `services/swarm/stack/` must follow this standard. The Gremlin audit workflow checks compliance weekly.
|
||||
|
||||
---
|
||||
|
||||
## Canonical Template
|
||||
|
||||
```yaml
|
||||
# Deploy: docker stack deploy -c <service>.yaml <service>
|
||||
services:
|
||||
<servicename>:
|
||||
image: <image>:latest
|
||||
environment:
|
||||
TZ: America/Chicago
|
||||
volumes:
|
||||
- /DockerVol/<servicename>:/config
|
||||
# - /data/nfs/znas/Docker/<servicename>:/data
|
||||
networks:
|
||||
- netgrimoire
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: any
|
||||
delay: 5s
|
||||
max_attempts: 3
|
||||
window: 120s
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == znas
|
||||
- node.platform.arch != aarch64
|
||||
- node.platform.arch != arm
|
||||
labels:
|
||||
# Caddy
|
||||
caddy: <servicename>.netgrimoire.com
|
||||
caddy.reverse_proxy: <servicename>:<PORT>
|
||||
caddy.import: crowdsec
|
||||
caddy.import_1: authentik
|
||||
|
||||
# Uptime Kuma
|
||||
kuma.<servicename>.http.name: <Service Name>
|
||||
kuma.<servicename>.http.url: https://<servicename>.netgrimoire.com
|
||||
|
||||
# Homepage
|
||||
homepage.group: <Group>
|
||||
homepage.name: <Service Name>
|
||||
homepage.icon: <service>.png
|
||||
homepage.href: https://<servicename>.netgrimoire.com
|
||||
homepage.description: <Description>
|
||||
|
||||
# DIUN
|
||||
diun.enable: "true"
|
||||
|
||||
networks:
|
||||
netgrimoire:
|
||||
external: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Forbidden Fields
|
||||
|
||||
Never use these at the service level:
|
||||
|
||||
| Field | Reason |
|
||||
|-------|--------|
|
||||
| `version:` | Deprecated in Compose v2+ |
|
||||
| `container_name:` | Incompatible with Swarm replicas |
|
||||
| `restart:` | Use `deploy.restart_policy` instead |
|
||||
| `depends_on:` | Not supported in Swarm mode |
|
||||
| `endpoint_mode: dnsrr` | Breaks internal DNS — always use VIP |
|
||||
|
||||
---
|
||||
|
||||
## Volume Path Rules
|
||||
|
||||
| Path | When to Use |
|
||||
|------|-------------|
|
||||
| `/DockerVol/<service>` | Config, SQLite DBs, small app state. **Only valid with a `node.hostname` placement constraint.** |
|
||||
| `/data/nfs/znas/Docker/<service>` | Bulk data, media, or any service without a hostname constraint |
|
||||
|
||||
---
|
||||
|
||||
## Placement Constraints
|
||||
|
||||
**Default (all services):**
|
||||
```yaml
|
||||
constraints:
|
||||
- node.hostname == znas
|
||||
- node.platform.arch != aarch64
|
||||
- node.platform.arch != arm
|
||||
```
|
||||
|
||||
ARM exclusion prevents accidental scheduling on Pi vault/worker nodes. Override only if the service is ARM-specific.
|
||||
|
||||
For services pinned to docker4 (Gremlin stack):
|
||||
```yaml
|
||||
constraints:
|
||||
- node.hostname == docker4
|
||||
- node.platform.arch != aarch64
|
||||
- node.platform.arch != arm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Caddy Label Rules
|
||||
|
||||
```yaml
|
||||
caddy: servicename.netgrimoire.com # no https:// prefix
|
||||
caddy.reverse_proxy: servicename:PORT # container name:port, NOT {{upstreams PORT}}
|
||||
caddy.import: crowdsec # always both
|
||||
caddy.import_1: authentik # always both, no exceptions
|
||||
```
|
||||
|
||||
Never use `{{upstreams PORT}}` — it breaks during `docker stack config` preprocessing.
|
||||
|
||||
**Wasted-bandwidth services** use `wasted-bandwidth.net` domain and `caddy.import_1: authelia` instead of authentik.
|
||||
|
||||
---
|
||||
|
||||
## Deploy Workflow
|
||||
|
||||
```bash
|
||||
# From services repo root
|
||||
git add . && git commit -m "Add/update <service>" && git push
|
||||
|
||||
# On znas (or docker4 for Gremlin services)
|
||||
cd ~/services && git pull
|
||||
cd swarm/stack/<StackName>
|
||||
set -a && source .env && set +a
|
||||
docker stack config --compose-file <service>.yaml > resolved.yml
|
||||
docker stack deploy --compose-file resolved.yml <service>
|
||||
rm resolved.yml
|
||||
docker stack services <service>
|
||||
```
|
||||
59
Netgrimoire/Keystone-Grimoire/Hosts/Host-Inventory.md
Normal file
59
Netgrimoire/Keystone-Grimoire/Hosts/Host-Inventory.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
title: Host Inventory
|
||||
description: All Netgrimoire nodes — roles, IPs, services, hardware
|
||||
published: true
|
||||
date: 2026-04-12T00:00:00.000Z
|
||||
tags: keystone, hosts
|
||||
editor: markdown
|
||||
dateCreated: 2026-04-12T00:00:00.000Z
|
||||
---
|
||||
|
||||
# Host Inventory
|
||||
|
||||
## Swarm Cluster
|
||||
|
||||
| Host | Hostname | IP | Role | Runtime |
|
||||
|------|----------|----|------|---------|
|
||||
| znas | znas | 192.168.5.10 | NAS + Primary Swarm manager | Swarm manager + Compose |
|
||||
| docker2 | — | — | VPN gateway | Compose only |
|
||||
| docker3 | — | — | LibreNMS | Compose only |
|
||||
| docker4 | hermes | 192.168.5.16 | Mail server + AI worker | Compose + Swarm worker |
|
||||
| docker5 | — | 192.168.5.18 | Media host | Compose only |
|
||||
| Pi nodes | various | various | Swarm workers + vault nodes | Swarm workers |
|
||||
|
||||
## Other Infrastructure
|
||||
|
||||
| Device | IP | Purpose |
|
||||
|--------|----|---------|
|
||||
| OPNsense firewall | 192.168.3.4 | Firewall, dual-WAN, NAT, WireGuard |
|
||||
| Internal DNS | 192.168.5.7 | Technitium DNS |
|
||||
| ISPConfig | 192.168.4.11 | Web/DNS hosting control panel |
|
||||
|
||||
## WAN
|
||||
|
||||
| Interface | IP | Status | Purpose |
|
||||
|-----------|----|----|---------|
|
||||
| ATT (`igc1`) | 107.133.34.145/28 | Primary | 5 static IPs allocated |
|
||||
| Cox | — | Retiring | Legacy WAN |
|
||||
|
||||
**ATT Static IP Assignments:**
|
||||
|
||||
| IP | Assigned To |
|
||||
|----|-------------|
|
||||
| .145 | Admin / default |
|
||||
| .146 | Web services |
|
||||
| .147 | Jellyfin |
|
||||
| .148 | Mail (ATT_Mail — pending) |
|
||||
| .149 | WireGuard / Spare |
|
||||
|
||||
## Pinned Services by Host
|
||||
|
||||
**znas** — Caddy, Forgejo, Wiki.js, Homepage, Uptime Kuma, AutoKuma, ntfy, Portainer, Authentik, LLDAP, Kopia, Vault, Nextcloud AIO, Immich, Joplin, n8n (Gremlin), all arr services, all media services
|
||||
|
||||
**docker4 (hermes)** — MailCow (Compose), Ollama, Open WebUI, Qdrant (Swarm, pinned docker4), Roundcube
|
||||
|
||||
**docker5** — Jellyfin, Jellyfinx (Compose)
|
||||
|
||||
**docker2** — Gluetun, Jackett, Transmission (Compose)
|
||||
|
||||
**docker3** — LibreNMS (Compose)
|
||||
401
Netgrimoire/Keystone-Grimoire/Mail/Domain-Setup.md
Normal file
401
Netgrimoire/Keystone-Grimoire/Mail/Domain-Setup.md
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
---
|
||||
title: Sample Domain Setup
|
||||
description: Graymutt@nucking-futz.com
|
||||
published: true
|
||||
date: 2026-03-16T00:34:08.387Z
|
||||
tags:
|
||||
editor: markdown
|
||||
dateCreated: 2026-02-25T22:02:27.719Z
|
||||
---
|
||||
|
||||
# Mail Setup — nucking-futz.com
|
||||
|
||||
## Part 0 — OPNsense: Configure ATT_Mail Secondary IP
|
||||
|
||||
Before configuring DNS or Mailcow, the secondary AT&T static IP must be configured in OPNsense as a virtual IP on the WAN interface and NAT rules must be set so only raw SMTP traffic (ports 25, 465, 587, 993, 143) uses this address. Webmail, the Mailcow admin UI, and all other traffic continue to use the primary WAN IP (107.133.34.145).
|
||||
|
||||
| Address | Purpose |
|
||||
|---------|---------|
|
||||
| 107.133.34.145 | Primary WAN — web, admin, everything else |
|
||||
| 107.133.34.146 | ATT_Mail — SMTP/IMAP inbound and outbound only |
|
||||
|
||||
### Step 0.1 — Add Virtual IP
|
||||
|
||||
1. Go to **Interfaces → Virtual IPs → Settings**
|
||||
2. Click **+ Add**
|
||||
3. Set the following:
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Mode | IP Alias |
|
||||
| Interface | WAN (igc1) |
|
||||
| Network / Address | `107.133.34.146 / 28` |
|
||||
| Description | `ATT_Mail` |
|
||||
|
||||
4. Click **Save**, then **Apply changes**
|
||||
|
||||
> The /28 subnet mask matches the AT&T block (107.133.34.144/28). All 5 static IPs in the block share this mask.
|
||||
|
||||
### Step 0.2 — Outbound NAT for SMTP Traffic
|
||||
|
||||
This ensures Mailcow's outbound SMTP connections leave through the ATT_Mail IP rather than the primary WAN IP. OPNsense must be in **Hybrid** or **Manual** outbound NAT mode.
|
||||
|
||||
1. Go to **Firewall → NAT → Outbound**
|
||||
2. Confirm mode is set to **Hybrid Outbound NAT** (or Manual — either works)
|
||||
3. Click **Add** to create a new rule
|
||||
|
||||
**Rule for outbound SMTP (port 587 relay to MXRoute):**
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Interface | WAN |
|
||||
| TCP/IP Version | IPv4 |
|
||||
| Protocol | TCP |
|
||||
| Source | `192.168.5.16 / 32` (Mailcow host) |
|
||||
| Source Port | any |
|
||||
| Destination | any |
|
||||
| Destination Port | 587 |
|
||||
| Translation / Target | `107.133.34.146` (ATT_Mail) |
|
||||
| Description | `Mailcow outbound relay via ATT_Mail` |
|
||||
|
||||
4. Repeat for port **25** (direct outbound SMTP, if used) and port **465** (SMTPS)
|
||||
5. Click **Save** and **Apply changes**
|
||||
|
||||
### Step 0.3 — Inbound NAT (Port Forwards) for Mail Ports
|
||||
|
||||
Route inbound connections on mail ports to Mailcow using the ATT_Mail IP as the external address.
|
||||
|
||||
1. Go to **Firewall → NAT → Port Forward**
|
||||
2. Create rules for each mail port:
|
||||
|
||||
| External IP | Port(s) | Forward to | Description |
|
||||
|-------------|---------|-----------|-------------|
|
||||
| 107.133.34.146 | 25 | 192.168.5.16:25 | SMTP inbound |
|
||||
| 107.133.34.146 | 465 | 192.168.5.16:465 | SMTPS inbound |
|
||||
| 107.133.34.146 | 587 | 192.168.5.16:587 | Submission inbound |
|
||||
| 107.133.34.146 | 993 | 192.168.5.16:993 | IMAPS |
|
||||
| 107.133.34.146 | 143 | 192.168.5.16:143 | IMAP (if needed) |
|
||||
|
||||
> **Do not** add port forwards for 80, 443, or 3443 (Mailcow admin/webmail ports) on this IP. Those remain on the primary WAN IP via Caddy.
|
||||
|
||||
3. Click **Save** and **Apply changes**
|
||||
|
||||
### Step 0.4 — Firewall Rules
|
||||
|
||||
Ensure the WAN firewall rules permit inbound traffic on the mail ports to the ATT_Mail IP. If you have a default deny-all WAN rule (recommended), add explicit pass rules:
|
||||
|
||||
1. Go to **Firewall → Rules → WAN**
|
||||
2. Add pass rules for each port in the table above with destination `107.133.34.146`
|
||||
|
||||
### Step 0.5 — Verify
|
||||
|
||||
```bash
|
||||
# From outside your network, confirm the mail IP is live
|
||||
telnet 107.133.34.146 25
|
||||
# Should see: 220 hermes.netgrimoire.com ESMTP
|
||||
|
||||
# Confirm primary WAN IP does NOT respond on port 25
|
||||
telnet 107.133.34.145 25
|
||||
# Should time out or be refused
|
||||
|
||||
# Check that Mailcow outbound connections leave from the ATT_Mail IP
|
||||
# Send a test to check-auth@verifier.port25.com and inspect the Return-Path
|
||||
# or check the Received: header — the sending IP should be 107.133.34.146
|
||||
```
|
||||
|
||||
> ⚠ If the verify step shows port 25 still responding on 107.133.34.145, check that no leftover port forward rules exist on the primary WAN IP for mail ports.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This guide covers complete mail setup for `nucking-futz.com` using MXRoute as the inbound gateway and Mailcow as the mailbox host. MXRoute receives all inbound mail from the internet (solving residential IP filtering issues with banks and financial institutions) and forwards to Mailcow for storage and retrieval. Mailcow handles outbound mail via the MXRoute SMTP relay.
|
||||
|
||||
**Architecture:**
|
||||
|
||||
```
|
||||
Inbound: Internet → MXRoute (commercial IP) → Mailcow (192.168.5.16)
|
||||
Outbound: Mailcow → MXRoute SMTP relay → Internet
|
||||
```
|
||||
|
||||
**Why two domains in Mailcow:**
|
||||
MXRoute forwarders require a valid destination email address. You cannot forward `graymutt@nucking-futz.com` back to `graymutt@nucking-futz.com` — that loops. The solution is to have Mailcow own a subdomain (`mail.nucking-futz.com`) with its own MX record pointing directly to your server. MXRoute forwards to `graymutt@mail.nucking-futz.com`, Mailcow delivers locally, and an alias domain maps `nucking-futz.com` back so users only ever see and use `graymutt@nucking-futz.com`.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- MXRoute account active with DirectAdmin access
|
||||
- Mailcow running at 192.168.5.16
|
||||
- DNS management access for nucking-futz.com
|
||||
- Your MXRoute server hostname from your MXRoute welcome email (e.g. `arrow.mxrouting.net`)
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — DNS Records
|
||||
|
||||
Create all DNS records before configuring either service. Keep TTL at 300 during setup — raise to 3600 once confirmed working.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Required DNS Records
|
||||
|
||||
| Type | Host | Value | Notes |
|
||||
|------|------|-------|-------|
|
||||
| A | `mail` | `YOUR_ATT_MAIL_IP` | Points to Mailcow — MXRoute forwards to this server |
|
||||
| MX | `@` | `heracles.mxrouting.net (Priority 10)` | Check MXRoute welcome email for exact hostname |
|
||||
| MX | `@` | `heracles-relay.mxrouting.net (Priority 20)` (priority 20) | Secondary MXRoute server from welcome email |
|
||||
| MX | `mail` | `mail.nucking-futz.com` (priority 10) | Mailcow handles this subdomain directly |
|
||||
| CNAME | `imap` | `mail.nucking-futz.com` | Client autoconfiguration |
|
||||
| CNAME | `smtp` | `mail.nucking-futz.com` | Client autoconfiguration |
|
||||
| CNAME | `webmail` | `mail.nucking-futz.com` | Roundcube access |
|
||||
| CNAME | `autodiscover` | `mail.nucking-futz.com` | Outlook autodiscover |
|
||||
| CNAME | `autoconfig` | `mail.nucking-futz.com` | Thunderbird autoconfig |
|
||||
| TXT | `@` | `v=spf1 ip4:YOUR_ATT_MAIL_IP include:mxroute.com -all` | SPF — authorizes both Mailcow direct and MXRoute relay |
|
||||
| TXT | `mail` | `v=spf1 ip4:YOUR_ATT_MAIL_IP -all` | SPF for subdomain — Mailcow sends directly from here |
|
||||
| TXT | `_dmarc` | `v=DMARC1; p=reject; rua=mailto:admin@netgrimoire.com` | DMARC enforcement |
|
||||
|
||||
> DKIM TXT records (two selectors) are added in Steps 2 and 3 after generating keys in Mailcow and MXRoute.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Mailcow Configuration
|
||||
|
||||
### 2.1 Add the Subdomain as Primary Domain
|
||||
|
||||
Mailcow owns `mail.nucking-futz.com` as its active mail domain. Mailboxes live internally on this subdomain.
|
||||
|
||||
1. Log into Mailcow admin UI → **Mail Setup → Domains**
|
||||
2. Click **Add domain**
|
||||
3. Set **Domain:** `mail.nucking-futz.com`
|
||||
4. Leave all other settings as default
|
||||
5. Click **Add domain**
|
||||
|
||||
### 2.2 Add the Alias Domain
|
||||
|
||||
This makes Mailcow accept mail addressed to `@nucking-futz.com` and deliver it to the matching `@mail.nucking-futz.com` mailbox. Users send and receive as `@nucking-futz.com` — the subdomain is invisible to them.
|
||||
|
||||
1. Go to **Mail Setup → Alias Domains**
|
||||
2. Click **Add alias domain**
|
||||
3. Set **Alias Domain:** `nucking-futz.com`
|
||||
4. Set **Target Domain:** `mail.nucking-futz.com`
|
||||
5. Click **Add**
|
||||
|
||||
### 2.3 Create Mailbox
|
||||
|
||||
1. Go to **Mail Setup → Mailboxes**
|
||||
2. Click **Add mailbox**
|
||||
3. Set **Username:** `graymutt`
|
||||
4. Set **Domain:** `mail.nucking-futz.com`
|
||||
5. Set a strong password
|
||||
6. Set quota as needed
|
||||
7. Click **Add**
|
||||
|
||||
The mailbox is internally `graymutt@mail.nucking-futz.com`. The alias domain from Step 2.2 means Mailcow also accepts and delivers mail for `graymutt@nucking-futz.com` to this same mailbox.
|
||||
|
||||
### 2.4 Generate DKIM Key
|
||||
|
||||
1. Go to **Configuration → Configuration & Diagnostics → Configuration**
|
||||
2. Click **ARC/DKIM Keys** tab
|
||||
3. Select domain `mail.nucking-futz.com`
|
||||
4. Set **Selector:** `mailcow`
|
||||
5. Set **Key length:** 2048
|
||||
6. Click **Generate**
|
||||
7. Copy the full TXT record value — needed for DNS
|
||||
|
||||
### 2.5 Add Mailcow DKIM DNS Record
|
||||
|
||||
| Type | Host | Value |
|
||||
|------|------|-------|
|
||||
| TXT | `mailcow._domainkey.mail` | *(full key string from Mailcow — begins with `v=DKIM1;`)* |
|
||||
|
||||
### 2.6 Add MXRoute to Trusted Networks
|
||||
|
||||
Prevents Mailcow from applying spam scoring to forwarded mail arriving from MXRoute's IPs.
|
||||
|
||||
1. Go to **Configuration → Configuration & Diagnostics → Configuration**
|
||||
2. Click **Extra Postfix configuration** tab
|
||||
3. Add to `extra.cf`:
|
||||
|
||||
```
|
||||
# Trust MXRoute forwarding IPs
|
||||
mynetworks = 127.0.0.1/8 [::1]/128 192.168.5.0/24 69.167.160.0/19 198.54.120.0/22
|
||||
```
|
||||
|
||||
> Verify current MXRoute IP ranges in your MXRoute account documentation — these may change.
|
||||
|
||||
4. Click **Save**
|
||||
5. Click **Restart affected containers**
|
||||
|
||||
### 2.7 Configure Outbound Relay
|
||||
|
||||
Routes outbound mail through MXRoute for best deliverability.
|
||||
|
||||
1. Go to **Configuration → Routing → Sender-Dependent Transports**
|
||||
2. Click **Add transport**
|
||||
3. Set **Domain:** `nucking-futz.com`
|
||||
4. Set **Relay host:** `[smtp.mxroute.com]:587` (confirm SMTP hostname from MXRoute welcome email)
|
||||
5. Set **Username:** your MXRoute relay username
|
||||
6. Set **Password:** your MXRoute relay password
|
||||
7. Click **Add**
|
||||
8. Repeat for domain `mail.nucking-futz.com` using the same relay credentials
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — MXRoute Configuration
|
||||
|
||||
### 3.1 Add Domain in DirectAdmin
|
||||
|
||||
1. Log into MXRoute DirectAdmin
|
||||
2. Go to **Account Manager → Domain Setup**
|
||||
3. Add domain: `nucking-futz.com`
|
||||
4. Complete the domain wizard
|
||||
|
||||
### 3.2 Create Forwarder
|
||||
|
||||
MXRoute does not support domain-level remote MX routing — forwarders must be created per address. The destination must be on a domain whose MX resolves to Mailcow, not back to MXRoute.
|
||||
|
||||
1. Go to **Forwarders** in the MXRoute control panel
|
||||
2. Click **Create New Forwarder**
|
||||
3. Set **Forwarder Name:** `graymutt` (the `@nucking-futz.com` part is shown automatically)
|
||||
4. Set **Destination Type:** `Forward to Email(s)`
|
||||
5. Set **Recipients:** `graymutt@mail.nucking-futz.com`
|
||||
6. Click **Create Forwarder**
|
||||
|
||||
> Every new mailbox requires a matching forwarder entry. The pattern is always `user@nucking-futz.com` → `user@mail.nucking-futz.com`. See the Adding a New Mailbox section below.
|
||||
|
||||
### 3.3 Get MXRoute DKIM Key
|
||||
|
||||
1. Go to **Email Manager → DKIM Keys** for `nucking-futz.com`
|
||||
2. Generate or view the DKIM key — note the selector name assigned (often `x`)
|
||||
3. Copy the full TXT record value
|
||||
|
||||
### 3.4 Add MXRoute DKIM DNS Record
|
||||
|
||||
| Type | Host | Value |
|
||||
|------|------|-------|
|
||||
| TXT | `x._domainkey` *(replace `x` with MXRoute's actual selector)* | *(full key string from MXRoute DirectAdmin)* |
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Verify DNS
|
||||
|
||||
Once DNS has propagated, verify all records:
|
||||
|
||||
```bash
|
||||
# MX for main domain — should show MXRoute servers
|
||||
dig MX nucking-futz.com +short
|
||||
|
||||
# MX for subdomain — should show mail.nucking-futz.com
|
||||
dig MX mail.nucking-futz.com +short
|
||||
|
||||
# A record — should show your ATT IP
|
||||
dig A mail.nucking-futz.com +short
|
||||
|
||||
# SPF
|
||||
dig TXT nucking-futz.com +short
|
||||
dig TXT mail.nucking-futz.com +short
|
||||
|
||||
# DMARC
|
||||
dig TXT _dmarc.nucking-futz.com +short
|
||||
|
||||
# DKIM — Mailcow
|
||||
dig TXT mailcow._domainkey.mail.nucking-futz.com +short
|
||||
|
||||
# DKIM — MXRoute (replace x with your selector)
|
||||
dig TXT x._domainkey.nucking-futz.com +short
|
||||
```
|
||||
|
||||
Run a full check at [https://mxtoolbox.com](https://mxtoolbox.com) → Email Health for `nucking-futz.com`.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Test Mail Flow
|
||||
|
||||
### Inbound Test
|
||||
|
||||
Send a test email to `graymutt@nucking-futz.com` from an external Gmail or Outlook account. Verify:
|
||||
|
||||
- Mail arrives in the Mailcow mailbox
|
||||
- Headers show the MXRoute → Mailcow forwarding path (two `Received:` hops)
|
||||
- No spam flagging
|
||||
|
||||
In Roundcube open the test message → **More → View Source** and check the `Received:` chain.
|
||||
|
||||
### Outbound Test
|
||||
|
||||
Send from `graymutt@nucking-futz.com` to an external Gmail address. Run through [https://mail-tester.com](https://mail-tester.com) for a full delivery score.
|
||||
|
||||
### DKIM/SPF/DMARC Test
|
||||
|
||||
Send a test to `check-auth@verifier.port25.com` — you will receive an automated reply confirming pass/fail for SPF, DKIM, and DMARC.
|
||||
|
||||
### Bank/Financial Test
|
||||
|
||||
Send from a bank address to `graymutt@nucking-futz.com` and confirm delivery. This is the primary goal — banks see MXRoute's commercial IPs in the MX record, not your residential AT&T IP.
|
||||
|
||||
---
|
||||
|
||||
## Email Client Settings
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| Email address | `graymutt@nucking-futz.com` |
|
||||
| IMAP server | `mail.nucking-futz.com` |
|
||||
| IMAP port | `993` (SSL/TLS) |
|
||||
| SMTP server | `mail.nucking-futz.com` |
|
||||
| SMTP port | `465` (SSL/TLS) |
|
||||
| Username | `graymutt@nucking-futz.com` |
|
||||
| Password | *(mailbox password set in Step 2.3)* |
|
||||
|
||||
> Users log in and send as `graymutt@nucking-futz.com`. Mailcow resolves this to the internal `mail.nucking-futz.com` mailbox transparently via the alias domain.
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Mailbox
|
||||
|
||||
Every new address on `nucking-futz.com` requires entries in both Mailcow and MXRoute.
|
||||
|
||||
**In Mailcow:**
|
||||
1. Mail Setup → Mailboxes → Add mailbox
|
||||
2. Username: `newuser`, Domain: `mail.nucking-futz.com`
|
||||
|
||||
**In MXRoute control panel:**
|
||||
1. Forwarders → Create New Forwarder
|
||||
2. Forwarder Name: `newuser`, Destination Type: `Forward to Email(s)`, Recipients: `newuser@mail.nucking-futz.com`
|
||||
|
||||
---
|
||||
|
||||
## Credentials Reference
|
||||
|
||||
| Service | Account | Password |
|
||||
|---------|---------|----------|
|
||||
| Mailcow mailbox | `graymutt@mail.nucking-futz.com` | *(set during mailbox creation)* |
|
||||
| MXRoute relay | *(from MXRoute welcome email)* | *(from MXRoute welcome email)* |
|
||||
| MXRoute DirectAdmin | *(from MXRoute welcome email)* | *(from MXRoute welcome email)* |
|
||||
|
||||
---
|
||||
|
||||
## Known Gotchas
|
||||
|
||||
**Forwarder destination must not loop.** Never set the MXRoute forwarder destination to an address on the same domain that has MXRoute as its MX. `graymutt@nucking-futz.com` → `graymutt@nucking-futz.com` will loop. Always forward to `@mail.nucking-futz.com` which has its own MX resolving directly to Mailcow.
|
||||
|
||||
**Two DKIM selectors required.** `mailcow._domainkey.mail.nucking-futz.com` covers mail Mailcow sends directly from the subdomain. `x._domainkey.nucking-futz.com` (MXRoute selector) covers outbound mail relayed through MXRoute. Both must exist for DMARC to pass on all paths.
|
||||
|
||||
**New mailboxes need matching MXRoute forwarders.** MXRoute has no catch-all forwarding to remote servers. Every address that needs to receive mail must have an explicit forwarder in DirectAdmin. Add the MXRoute forwarder step to your mailbox creation checklist.
|
||||
|
||||
**Alias domain vs. alias mailbox.** The alias domain in Step 2.2 maps the entire `nucking-futz.com` domain to `mail.nucking-futz.com`. Do not also create individual alias mailboxes for the same addresses — this creates duplicate delivery and may cause unexpected behavior.
|
||||
|
||||
**SPF differs between the two domains.** The main domain SPF includes `include:mxroute.com` because MXRoute relay sends outbound from there. The subdomain SPF (`mail.nucking-futz.com`) only needs your ATT IP — Mailcow sends directly from that domain without going through MXRoute. Two different records for two different send paths.
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [MailCow Configuration](./mailcow)
|
||||
- [MXRoute Outbound Relay Setup](./mxroute-outbound-relay)
|
||||
- [OPNsense Firewall](./opnsense-firewall) — static IP allocation for ATT_Mail
|
||||
391
Netgrimoire/Keystone-Grimoire/Mail/Hardening.md
Normal file
391
Netgrimoire/Keystone-Grimoire/Mail/Hardening.md
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
---
|
||||
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
|
||||
490
Netgrimoire/Keystone-Grimoire/Mail/Install.md
Normal file
490
Netgrimoire/Keystone-Grimoire/Mail/Install.md
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
---
|
||||
title: Mailcow Dockerized Install and Config
|
||||
description:
|
||||
published: true
|
||||
date: 2026-02-25T21:05:48.256Z
|
||||
tags:
|
||||
editor: markdown
|
||||
dateCreated: 2026-02-25T21:05:38.864Z
|
||||
---
|
||||
|
||||
# MailCow — Installation & Configuration
|
||||
|
||||
**Host:** docker4 (192.168.5.16)
|
||||
**Hostname:** hermes.netgrimoire.com
|
||||
**Admin URL:** https://mail.netgrimoire.com
|
||||
**Version:** 2025-10a (update 2026-01 available as of documentation date)
|
||||
**Installed:** /opt/mailcow-dockerized
|
||||
**Timezone:** America/Chicago
|
||||
**Architecture:** x86_64
|
||||
**CPU:** 16 cores
|
||||
**RAM:** 30.63 GB
|
||||
**Disk:** /dev/nvme0n1p2 — 442G / 502G used (93% — monitor this)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Mailcow runs as a Docker stack on docker4, attached to the `netgrimoire` overlay network. All containers use `restart: unless-stopped` via a compose override. Outbound mail routes through MXRoute via sender-dependent transports. Inbound mail arrives from MXRoute which acts as the public-facing inbound gateway (solving residential AT&T IP filtering issues with banks).
|
||||
|
||||
See [MXRoute Master Configuration](./mxroute-master) for full inbound/outbound/DNS detail per domain.
|
||||
|
||||
---
|
||||
|
||||
## Installation Paths
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `/opt/mailcow-dockerized/` | Mailcow root |
|
||||
| `/opt/mailcow-dockerized/mailcow.conf` | Primary configuration file |
|
||||
| `/opt/mailcow-dockerized/docker-compose.yml` | Base compose (do not edit) |
|
||||
| `/opt/mailcow-dockerized/docker-compose.override.yml` | Local overrides — network and restart policy |
|
||||
| `/opt/mailcow-dockerized/data/conf/postfix/extra.cf` | Persistent Postfix overrides |
|
||||
| `/opt/mailcow-dockerized/data/conf/postfix/main.cf` | Postfix base config (managed by Mailcow) |
|
||||
| `/opt/mailcow-dockerized/data/conf/rspamd/` | Rspamd configuration |
|
||||
| `/opt/mailcow-dockerized/data/assets/ssl/` | TLS certificates |
|
||||
|
||||
---
|
||||
|
||||
## mailcow.conf — Key Settings
|
||||
|
||||
```ini
|
||||
MAILCOW_HOSTNAME=hermes.netgrimoire.com
|
||||
MAILCOW_PASS_SCHEME=BLF-CRYPT
|
||||
|
||||
# Database
|
||||
DBNAME=mailcow
|
||||
DBUSER=mailcow
|
||||
DBPASS=mg7Z8W9UsPlOh0S6vF7TmmPb6n1s
|
||||
DBROOT=JdymsZFFACHkDcOdziQ53QruCTG2
|
||||
|
||||
# Redis
|
||||
REDISPASS=6AduWQsmBYGMKfOi1CNEGQfTE3RH
|
||||
|
||||
# Ports — HTTPS runs on 3443, proxied through Caddy
|
||||
HTTP_PORT=80
|
||||
HTTP_BIND=
|
||||
HTTPS_PORT=3443
|
||||
HTTPS_BIND=
|
||||
HTTP_REDIRECT=n
|
||||
|
||||
# Mail ports (standard)
|
||||
SMTP_PORT=25
|
||||
SMTPS_PORT=465
|
||||
SUBMISSION_PORT=587
|
||||
IMAP_PORT=143
|
||||
IMAPS_PORT=993
|
||||
POP_PORT=110
|
||||
POPS_PORT=995
|
||||
SIEVE_PORT=4190
|
||||
|
||||
# Internal ports (localhost only)
|
||||
DOVEADM_PORT=127.0.0.1:19991
|
||||
SQL_PORT=127.0.0.1:13306
|
||||
REDIS_PORT=127.0.0.1:7654
|
||||
|
||||
# TLS cert coverage
|
||||
ADDITIONAL_SAN=smtp.*,imap.*
|
||||
AUTODISCOVER_SAN=y
|
||||
|
||||
# ACME / Let's Encrypt
|
||||
SKIP_LETS_ENCRYPT=n
|
||||
SKIP_IP_CHECK=y
|
||||
SKIP_HTTP_VERIFICATION=y
|
||||
|
||||
# Services — all enabled
|
||||
SKIP_CLAMD=n
|
||||
SKIP_OLEFY=n
|
||||
SKIP_SOGO=n
|
||||
SKIP_FTS=n
|
||||
|
||||
# FTS (Flatcurve/Xapian)
|
||||
FTS_HEAP=128
|
||||
FTS_PROCS=1
|
||||
|
||||
# Watchdog
|
||||
USE_WATCHDOG=y
|
||||
WATCHDOG_NOTIFY_START=y
|
||||
WATCHDOG_NOTIFY_BAN=n
|
||||
WATCHDOG_EXTERNAL_CHECKS=n
|
||||
|
||||
# Networking
|
||||
IPV4_NETWORK=172.22.1
|
||||
IPV6_NETWORK=fd4d:6169:6c63:6f77::/64
|
||||
ENABLE_IPV6=false
|
||||
|
||||
# Misc
|
||||
MAILDIR_GC_TIME=7200
|
||||
MAILDIR_SUB=Maildir
|
||||
SOGO_EXPIRE_SESSION=480
|
||||
SOGO_URL_ENCRYPTION_KEY=ojmPfhnM4MYMsA2f
|
||||
ACL_ANYONE=disallow
|
||||
ALLOW_ADMIN_EMAIL_LOGIN=n
|
||||
DOCKER_COMPOSE_VERSION=native
|
||||
COMPOSE_PROJECT_NAME=mailcow
|
||||
LOG_LINES=9999
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## docker-compose.override.yml
|
||||
|
||||
All services are attached to the external `netgrimoire` overlay network and set to `restart: unless-stopped`. The override does not change any image versions or environment variables — it only adds network membership and restart policy.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
unbound-mailcow:
|
||||
networks:
|
||||
netgrimoire:
|
||||
restart: unless-stopped
|
||||
|
||||
mysql-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
redis-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
clamd-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
rspamd-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
php-fpm-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
sogo-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
dovecot-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
postfix-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
postfix-tlspol-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
memcached-mailcow:
|
||||
restart: unless-stopped
|
||||
|
||||
nginx-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
acme-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
watchdog-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
dockerapi-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
olefy-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
ofelia-mailcow:
|
||||
networks:
|
||||
- netgrimoire
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
netgrimoire:
|
||||
external: true
|
||||
driver: overlay
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Container Image Versions
|
||||
|
||||
From `docker-compose.yml` (base file — version 2025-10a):
|
||||
|
||||
| Service | Image |
|
||||
|---------|-------|
|
||||
| unbound-mailcow | ghcr.io/mailcow/unbound:1.24 |
|
||||
| mysql-mailcow | mariadb:10.11 |
|
||||
| redis-mailcow | redis:7.4.6-alpine |
|
||||
| clamd-mailcow | ghcr.io/mailcow/clamd:1.71 |
|
||||
| rspamd-mailcow | ghcr.io/mailcow/rspamd:2.4 |
|
||||
| php-fpm-mailcow | ghcr.io/mailcow/phpfpm:1.94 |
|
||||
| sogo-mailcow | ghcr.io/mailcow/sogo:1.136 |
|
||||
| dovecot-mailcow | ghcr.io/mailcow/dovecot:2.35 |
|
||||
| postfix-mailcow | ghcr.io/mailcow/postfix:1.81 |
|
||||
| postfix-tlspol-mailcow | ghcr.io/mailcow/postfix-tlspol:1.0 |
|
||||
| memcached-mailcow | memcached:alpine |
|
||||
| nginx-mailcow | ghcr.io/mailcow/nginx:1.05 |
|
||||
| acme-mailcow | ghcr.io/mailcow/acme:1.94 |
|
||||
| netfilter-mailcow | ghcr.io/mailcow/netfilter:1.63 |
|
||||
| watchdog-mailcow | ghcr.io/mailcow/watchdog:2.09 |
|
||||
| dockerapi-mailcow | ghcr.io/mailcow/dockerapi:2.11 |
|
||||
| olefy-mailcow | ghcr.io/mailcow/olefy:1.15 |
|
||||
| ofelia-mailcow | mcuadros/ofelia:latest |
|
||||
|
||||
---
|
||||
|
||||
## Postfix Configuration
|
||||
|
||||
### extra.cf
|
||||
|
||||
```
|
||||
myhostname = hermes.netgrimoire.com
|
||||
```
|
||||
|
||||
> The MXRoute trusted network entries should also be here. Current extra.cf only contains myhostname — confirm mynetworks is set correctly or add the MXRoute IP ranges if not already present via the UI.
|
||||
|
||||
### Key Postfix Settings (from running config)
|
||||
|
||||
```
|
||||
mynetworks = 127.0.0.0/8 172.22.1.0/24 10.0.1.0/24 [::1]/128 [fd4d:6169:6c63:6f77::]/64 [fe80::]/64
|
||||
message_size_limit = 104857600 # 100MB
|
||||
mailbox_size_limit = 0 # unlimited
|
||||
bounce_queue_lifetime = 1d
|
||||
maximal_queue_lifetime = 5d
|
||||
delay_warning_time = 4h
|
||||
postscreen_dnsbl_threshold = 6
|
||||
postscreen_dnsbl_action = enforce
|
||||
postscreen_greet_action = enforce
|
||||
smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, defer_unauth_destination
|
||||
disable_vrfy_command = yes
|
||||
broken_sasl_auth_clients = yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Domains
|
||||
|
||||
10 domains configured. All active.
|
||||
|
||||
| Domain | Mailboxes | Sender-Dependent Transport | Created |
|
||||
|--------|-----------|---------------------------|---------|
|
||||
| bamalady.com | 0 / 10 | *(not confirmed)* | — |
|
||||
| bill740.com | 1 / 10 | *(not confirmed)* | — |
|
||||
| florosafd.org | 4 / 10 | ID 4: heracles.mxrouting.net:587 (relay@florosafd.org) | 2025-11-21 |
|
||||
| gnarlypandaproductions.com | 2 / 10 | ID 5: heracles.mxrouting.net:587 (relay@gnarlypandaproductions.com) | 2025-11-21 |
|
||||
| netgrimoire.com | 2 / 10 | ID 2: heracles.mxrouting.net:587 (relay@netgrimoire.com) | 2025-11-21 |
|
||||
| nucking-futz.net | 0 / 10 | *(not confirmed)* | — |
|
||||
| pncfishandmore.com | 4 / 10 | ID 6: heracles.mxrouting.net:587 (relay@pncfishandmore.com) | — |
|
||||
| pncharris.com | 4 / 10 | ID 3: heracles.mxrouting.net:587 (passer@pncharris.com) | 2025-11-21 |
|
||||
| pncharrisenterprises.com | 2 / 10 | *(not confirmed from screenshots)* | — |
|
||||
| wasted-bandwidth.net | 1 / 10 | ID 1: heracles.mxrouting.net:587 (relay@wasted-bandwidth.net) | — |
|
||||
|
||||
> MXRoute relay hostname is `heracles.mxrouting.net:587` — note this differs from the generic `smtp.mxroute.com` placeholder used in setup docs. Always use `heracles.mxrouting.net:587` for this account.
|
||||
|
||||
---
|
||||
|
||||
## Mailboxes
|
||||
|
||||
19 active mailboxes across all domains:
|
||||
|
||||
| Mailbox | Messages | Domain |
|
||||
|---------|----------|--------|
|
||||
| bill@bill740.com | 1 | bill740.com |
|
||||
| chieflee@florosafd.org | 2124 | florosafd.org |
|
||||
| cindy@pncfishandmore.com | 1109 | pncfishandmore.com |
|
||||
| cindy@pncharris.com | 33797 | pncharris.com |
|
||||
| cindy@pncharrisenterprises.com | 819 | pncharrisenterprises.com |
|
||||
| dads_attic@pncharris.com | 0 | pncharris.com |
|
||||
| jim.harris@florosafd.org | 8 | florosafd.org |
|
||||
| kyle@gnarlypandaproductions.com | 486 | gnarlypandaproductions.com |
|
||||
| kyle@pncfishandmore.com | 110 | pncfishandmore.com |
|
||||
| kyle@pncharris.com | 31182 | pncharris.com |
|
||||
| phil@florosafd.org | 5 | florosafd.org |
|
||||
| phil@gnarlypandaproductions.com | 5 | gnarlypandaproductions.com |
|
||||
| phil@netgrimoire.com | 1 | netgrimoire.com |
|
||||
| phil@pncfishandmore.com | 10 | pncfishandmore.com |
|
||||
| phil@pncharris.com | 3210 | pncharris.com |
|
||||
| phil@pncharrisenterprises.com | 1 | pncharrisenterprises.com |
|
||||
| times@florosafd.org | 191 | florosafd.org |
|
||||
| traveler@netgrimoire.com | 3 | netgrimoire.com |
|
||||
| traveler@wasted-bandwidth.net | 138 | wasted-bandwidth.net |
|
||||
|
||||
---
|
||||
|
||||
## Aliases
|
||||
|
||||
| ID | Alias | Target Domain | Internal |
|
||||
|----|-------|---------------|---------|
|
||||
| 7 | cindy@bamalady.com | bamalady.com | No |
|
||||
|
||||
---
|
||||
|
||||
## Sender-Dependent Transports
|
||||
|
||||
All outbound relay routes through `heracles.mxrouting.net:587`. This is your MXRoute server hostname — use this exact value when adding new transports.
|
||||
|
||||
| ID | Host | Username | Password |
|
||||
|----|------|----------|----------|
|
||||
| 1 | heracles.mxrouting.net:587 | relay@wasted-bandwidth.net | dZ4yLYznVvgSJtqWZJFA |
|
||||
| 2 | heracles.mxrouting.net:587 | relay@netgrimoire.com | TVGCnJp9SxRbWU8EhkMw |
|
||||
| 3 | heracles.mxrouting.net:587 | passer@pncharris.com | bBJtPhrGkHvvhxhukkae |
|
||||
| 4 | heracles.mxrouting.net:587 | relay@florosafd.org | 2Fe8XMyaeh6Z5dvdHYdq |
|
||||
| 5 | heracles.mxrouting.net:587 | relay@gnarlypandaproductions.com | vG5ZsUQhRWD2UyzLPsqA |
|
||||
| 6 | heracles.mxrouting.net:587 | relay@pncfishandmore.com | *(confirm from MXRoute panel)* |
|
||||
|
||||
---
|
||||
|
||||
## DKIM Keys
|
||||
|
||||
Two DKIM selectors are configured per domain — one for Mailcow (selector: `dkim`) and one added separately for MXRoute outbound signing. The Mailcow-managed keys use selector `dkim._domainkey`.
|
||||
|
||||
### pncharris.com
|
||||
```
|
||||
v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqhgQV7r+KKQwJceWenZ3FNq8AsllgW6cIm/0jpsLT62vF1yy0nh2MdhjYgQAX2MK9HHYzNZcCB3+OPpqBbXeNbSDckxB/dC+z/vboMHrJmYonfaSYshZjSR80V/a2Yoq+hiXQ9eBcuOggENtMm4XvEsl/vOWLBMfasqe+X11gzQBeRv1tTaXJB0C4i7tAcfi0O/AxH8QFTr2099+k2iepn8J15ukk1zu4zemBJj4Z3uFTNnBP8YpgKbYoUDyMVIKIxGjANVBBypcrMKavpQ4F1JLhgGFhWAsAuFRwZsnOaftZyMuzAZxM37DTd/bF2WanmK3Xe75SN5uOnEXjuzW/wIDAQAB
|
||||
```
|
||||
|
||||
### netgrimoire.com
|
||||
```
|
||||
v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoJ9YKqV9+6gOcVKI+UJ0TRcMmergxU8HLO+mwTMfqOhblsEcDPO60c8ya24iIXg51AA2k5Xcbb0bLScaaIi0P/TRzP/bonAZkPS1Y8Fx1se9dikTsA9Lazho u6DvoFkkV/IPH1ZNg68Cd9teAD5tvoY18OSneJJsocXwFo57c+XccUaTxjpV7eReuT4da7iNHMmUmZNfKenxVMKD740zrDJAeAsXtEb/71CochHYSm+qAvuG9/WPixJbMsJLF/iVhV3Byp0LCrB+CwGTwnsiUcd7QpuD6rRs/7zzdGBtoN22m/j390GimFstYvB61I20h8sHWGAG66dLko6Sgvs47wIDAQAB
|
||||
```
|
||||
|
||||
### gnarlypandaproductions.com
|
||||
```
|
||||
v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
|
||||
```
|
||||
*(scroll cut off in screenshot — retrieve full key from Mailcow UI → Edit domain → bottom of page)*
|
||||
|
||||
> All other domain DKIM keys should be retrieved from the Mailcow domain edit page and recorded here for disaster recovery completeness.
|
||||
|
||||
---
|
||||
|
||||
## Network Configuration
|
||||
|
||||
Mailcow containers join the `netgrimoire` external overlay network, allowing communication with other Docker Swarm services (Caddy reverse proxy, etc.) without exposing ports directly to the host network.
|
||||
|
||||
**Internal Docker network:** `172.22.1.0/24`
|
||||
|
||||
Key container IPs within the mailcow-network:
|
||||
- unbound: 172.22.1.254
|
||||
- redis: 172.22.1.249
|
||||
- sogo: 172.22.1.248
|
||||
- dovecot: 172.22.1.250
|
||||
- postfix: 172.22.1.253
|
||||
|
||||
**IPv6:** disabled (`ENABLE_IPV6=false`)
|
||||
|
||||
---
|
||||
|
||||
## Caddy Reverse Proxy
|
||||
|
||||
Mailcow's nginx listens on HTTPS port 3443 internally. Caddy proxies external requests to it. Mailcow handles its own TLS for direct mail client connections (IMAP 993, SMTP 465/587).
|
||||
|
||||
The admin UI at `mail.netgrimoire.com` is proxied through Caddy on the `netgrimoire` overlay network.
|
||||
|
||||
---
|
||||
|
||||
## Updating Mailcow
|
||||
|
||||
```bash
|
||||
cd /opt/mailcow-dockerized
|
||||
|
||||
# Pull latest
|
||||
git fetch origin
|
||||
git checkout origin/master
|
||||
|
||||
# Update containers
|
||||
docker compose pull
|
||||
./update.sh
|
||||
```
|
||||
|
||||
> As of documentation date, version 2026-01 is available. Current running version is 2025-10a. Update when convenient — check the [MailCow changelog](https://github.com/mailcow/mailcow-dockerized/releases) for breaking changes first.
|
||||
|
||||
Monthly update check is recommended. MailCow had multiple security vulnerabilities in 2025 — staying current is important.
|
||||
|
||||
---
|
||||
|
||||
## Common Operations
|
||||
|
||||
### Restart all containers
|
||||
```bash
|
||||
cd /opt/mailcow-dockerized
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
### Restart single container (e.g. after extra.cf change)
|
||||
```bash
|
||||
docker compose restart postfix-mailcow
|
||||
```
|
||||
|
||||
### View logs
|
||||
```bash
|
||||
# Postfix
|
||||
docker compose logs postfix-mailcow -f
|
||||
|
||||
# Dovecot
|
||||
docker compose logs dovecot-mailcow -f
|
||||
|
||||
# All containers
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
### Check queue
|
||||
```bash
|
||||
docker exec mailcow-postfix-mailcow-1 postqueue -p
|
||||
```
|
||||
|
||||
### Flush queue
|
||||
```bash
|
||||
docker exec mailcow-postfix-mailcow-1 postqueue -f
|
||||
```
|
||||
|
||||
### Check container health
|
||||
```bash
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Known Gotchas
|
||||
|
||||
**Disk usage is at 93%.** The nvme0n1p2 volume has 442G used of 502G. This needs attention — vmail storage grows over time and garbage collection runs hourly but only removes items older than 7200 minutes (5 days). Monitor this and consider quota enforcement per mailbox if growth continues.
|
||||
|
||||
**extra.cf is minimal.** The MXRoute trusted network IPs should be confirmed in the running Postfix config. The `mynetworks` value from `postconf` shows `10.0.1.0/24` is already trusted — confirm whether MXRoute IP ranges `69.167.160.0/19` and `198.54.120.0/22` are included. If not, add them to extra.cf and restart postfix.
|
||||
|
||||
**MXRoute relay hostname.** The actual relay hostname for this account is `heracles.mxrouting.net:587` — not the generic `smtp.mxroute.com` placeholder. All 6 transports use `heracles.mxrouting.net:587`. Use this exact hostname for any new transport entries.
|
||||
|
||||
**pncharris.com uses passer@ not relay@.** Transport ID 3 for pncharris.com authenticates as `passer@pncharris.com`, not `relay@pncharris.com`. This is intentional — the relay@ account exists but passer@ is the current active relay credential.
|
||||
|
||||
**HTTPS on port 3443.** Mailcow's web UI is not on the standard 443 — it binds to 3443 and Caddy handles the public-facing 443 proxy. Direct access to the UI requires going through Caddy or using the internal port.
|
||||
|
||||
**nucking-futz.net vs nucking-futz.com.** The domains list shows `nucking-futz.net` but the intended new domain is `nucking-futz.com`. Verify which is actually configured and correct if needed.
|
||||
|
||||
**bamalady.com and bill740.com** have no transport assigned in the screenshots. Confirm whether these domains need MXRoute relay configured.
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [MXRoute Master Configuration](./mxroute-master) — per-domain DNS, inbound forwarding, outbound relay credentials
|
||||
- [Mail Setup — nucking-futz.com](./mail-setup-nucking-futz) — new domain setup guide
|
||||
- [MailCow Security Hardening](./mailcow-security-hardening)
|
||||
- [Caddy Reverse Proxy](./caddy-reverse-proxy) — proxies mail.netgrimoire.com to port 3443
|
||||
- [OPNsense Firewall](./opnsense-firewall) — ATT_Mail static IP, port forwarding rules
|
||||
430
Netgrimoire/Keystone-Grimoire/Mail/MXRoute-Integration.md
Normal file
430
Netgrimoire/Keystone-Grimoire/Mail/MXRoute-Integration.md
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
---
|
||||
title: Integrating MXRoute with MailCow
|
||||
description:
|
||||
published: true
|
||||
date: 2026-02-25T21:04:37.135Z
|
||||
tags:
|
||||
editor: markdown
|
||||
dateCreated: 2026-02-25T19:22:31.514Z
|
||||
---
|
||||
|
||||
# MXRoute — Master Configuration Reference
|
||||
|
||||
## Overview
|
||||
|
||||
MXRoute serves two roles in Netgrimoire mail infrastructure:
|
||||
|
||||
- **Inbound gateway** — MX records for all domains point to MXRoute's commercial IPs, solving residential AT&T IP filtering by banks and financial institutions. MXRoute receives mail and forwards to Mailcow via per-address forwarders.
|
||||
- **Outbound relay** — Mailcow sends all outbound mail through MXRoute via sender-dependent transports for improved deliverability.
|
||||
|
||||
**Mail flow:**
|
||||
|
||||
```
|
||||
Inbound: Internet → MXRoute (commercial IP) → Mailcow (192.168.5.16)
|
||||
Outbound: Mailcow (192.168.5.16) → MXRoute SMTP relay → Internet
|
||||
```
|
||||
|
||||
**Mailcow host:** 192.168.5.16
|
||||
**MXRoute control panel:** confirm server hostname from MXRoute welcome email (e.g. `arrow.mxrouting.net`)
|
||||
**MXRoute SMTP relay:** confirm from welcome email (e.g. `smtp.mxroute.com:587`)
|
||||
|
||||
---
|
||||
|
||||
## Architecture — Why Two Domains Per Hosted Domain
|
||||
|
||||
MXRoute forwarders require a valid destination email address. Forwarding `user@domain.com` back to `user@domain.com` creates a mail loop because MXRoute would look up the MX for `domain.com` and find itself. The solution is a `mail.domain.com` subdomain with its own MX record pointing directly to Mailcow. MXRoute forwards to `user@mail.domain.com`, Mailcow accepts and delivers, and an alias domain maps `@domain.com` back so users only ever see `@domain.com`.
|
||||
|
||||
```
|
||||
domain.com MX → MXRoute (public-facing, receives from internet)
|
||||
mail.domain.com MX → 192.168.5.16 (internal, MXRoute forwards here)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MXRoute Control Panel
|
||||
|
||||
**Login:** confirm URL from MXRoute welcome email
|
||||
**Interface:** MXRoute 4.0 (new UI — not old DirectAdmin)
|
||||
|
||||
### Creating a Forwarder
|
||||
|
||||
1. Go to **Forwarders**
|
||||
2. Click **Create New Forwarder**
|
||||
3. Set **Forwarder Name:** `username` (domain shown automatically)
|
||||
4. Set **Destination Type:** `Forward to Email(s)`
|
||||
5. Set **Recipients:** `username@mail.domain.com`
|
||||
6. Click **Create Forwarder**
|
||||
|
||||
> Recipients field accepts multiple addresses comma or newline separated.
|
||||
|
||||
---
|
||||
|
||||
## Mailcow Configuration
|
||||
|
||||
### Adding a New Domain (One-Time Per Domain)
|
||||
|
||||
1. **Mail Setup → Domains → Add domain**
|
||||
- Domain: `mail.domain.com` (the subdomain Mailcow owns)
|
||||
- Leave relay settings as default
|
||||
|
||||
2. **Mail Setup → Alias Domains → Add alias domain**
|
||||
- Alias Domain: `domain.com`
|
||||
- Target Domain: `mail.domain.com`
|
||||
- This makes Mailcow accept and deliver mail for `@domain.com` to `@mail.domain.com` mailboxes
|
||||
|
||||
3. **Configuration → ARC/DKIM Keys**
|
||||
- Select domain `mail.domain.com`
|
||||
- Selector: `mailcow`
|
||||
- Key length: 2048
|
||||
- Generate and copy TXT record for DNS
|
||||
|
||||
4. **Configuration → Extra Postfix configuration → extra.cf**
|
||||
|
||||
```
|
||||
# Trust MXRoute forwarding IPs — prevents SPF scoring on forwarded mail
|
||||
mynetworks = 127.0.0.1/8 [::1]/128 192.168.5.0/24 69.167.160.0/19 198.54.120.0/22
|
||||
```
|
||||
|
||||
Restart affected containers after saving.
|
||||
|
||||
### Adding a New Mailbox
|
||||
|
||||
1. **Mail Setup → Mailboxes → Add mailbox**
|
||||
- Username: `user`
|
||||
- Domain: `mail.domain.com`
|
||||
|
||||
2. **MXRoute control panel → Forwarders → Create New Forwarder**
|
||||
- Forwarder: `user@domain.com`
|
||||
- Destination: `user@mail.domain.com`
|
||||
|
||||
### Outbound Relay — Sender-Dependent Transports
|
||||
|
||||
One transport entry per domain. **Configuration → Routing → Sender-Dependent Transports**
|
||||
|
||||
| Domain | Relay Host | Username | Password |
|
||||
|--------|-----------|----------|----------|
|
||||
| pncharris.com | `[smtp.mxroute.com]:587` | relay@pncharris.com | H@rv3yD)G123 |
|
||||
| wasted-bandwidth.net | `[smtp.mxroute.com]:587` | relay@wasted-bandwidth.net | dZ4yLYznVvgSJtqWZJFA |
|
||||
| netgrimoire.com | `[smtp.mxroute.com]:587` | relay@netgrimoire.com | TVGCnJp9SxRbWU8EhkMw |
|
||||
| florosafd.org | `[smtp.mxroute.com]:587` | relay@florosafd.org | 2Fe8XMyaeh6Z5dvdHYdq |
|
||||
| gnarlypandaproductions.com | `[smtp.mxroute.com]:587` | relay@gnarlypandaproductions.com | vG5ZsUQhRWD2UyzLPsqA |
|
||||
|
||||
> Confirm SMTP relay hostname from MXRoute welcome email — substitute actual hostname for `smtp.mxroute.com` if different.
|
||||
|
||||
### Email Client Settings (All Domains)
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| IMAP server | `mail.domain.com` |
|
||||
| IMAP port | `993` (SSL/TLS) |
|
||||
| SMTP server | `mail.domain.com` |
|
||||
| SMTP port | `465` (SSL/TLS) |
|
||||
| Username | `user@domain.com` |
|
||||
|
||||
> Users log in with `@domain.com`. Mailcow resolves to the internal `@mail.domain.com` mailbox via alias domain — transparent to the user.
|
||||
|
||||
---
|
||||
|
||||
## DNS Reference — All Domains
|
||||
|
||||
### DNS Pattern (Apply to Every Domain)
|
||||
|
||||
Two sets of MX records are required — one for the public domain (pointing to MXRoute) and one for the mail subdomain (pointing directly to Mailcow).
|
||||
|
||||
| Type | Host | Value | Notes |
|
||||
|------|------|-------|-------|
|
||||
| A | `mail` | `YOUR_ATT_MAIL_IP` | Mailcow server — MXRoute forwards here |
|
||||
| MX | `@` | MXRoute primary (priority 10) | From MXRoute welcome email |
|
||||
| MX | `@` | MXRoute secondary (priority 20) | From MXRoute welcome email |
|
||||
| MX | `mail` | `mail.domain.com` (priority 10) | Mailcow handles subdomain directly |
|
||||
| CNAME | `imap` | `mail.domain.com` | Client autoconfiguration |
|
||||
| CNAME | `smtp` | `mail.domain.com` | Client autoconfiguration |
|
||||
| CNAME | `webmail` | `mail.domain.com` | Roundcube access |
|
||||
| CNAME | `autodiscover` | `mail.domain.com` | Outlook autodiscover |
|
||||
| CNAME | `autoconfig` | `mail.domain.com` | Thunderbird autoconfig |
|
||||
| TXT | `@` | `v=spf1 ip4:YOUR_ATT_MAIL_IP include:mxroute.com -all` | SPF — both Mailcow direct and MXRoute relay |
|
||||
| TXT | `mail` | `v=spf1 ip4:YOUR_ATT_MAIL_IP -all` | SPF for subdomain — Mailcow direct only |
|
||||
| TXT | `_dmarc` | `v=DMARC1; p=reject; rua=mailto:admin@netgrimoire.com` | DMARC enforcement |
|
||||
| TXT | `mailcow._domainkey.mail` | *(generated in Mailcow ARC/DKIM Keys)* | Mailcow DKIM selector |
|
||||
| TXT | `x._domainkey` | *(from MXRoute control panel)* | MXRoute DKIM selector — confirm actual selector name |
|
||||
|
||||
---
|
||||
|
||||
### pncharris.com
|
||||
|
||||
| Type | Host | Value |
|
||||
|------|------|-------|
|
||||
| A | `mail` | YOUR_ATT_MAIL_IP |
|
||||
| MX | `@` | MXRoute primary (priority 10) |
|
||||
| MX | `@` | MXRoute secondary (priority 20) |
|
||||
| MX | `mail` | `mail.pncharris.com` (priority 10) |
|
||||
| CNAME | `imap` | `mail.pncharris.com` |
|
||||
| CNAME | `smtp` | `mail.pncharris.com` |
|
||||
| CNAME | `webmail` | `mail.pncharris.com` |
|
||||
| CNAME | `autodiscover` | `mail.pncharris.com` |
|
||||
| CNAME | `autoconfig` | `mail.pncharris.com` |
|
||||
| TXT | `@` | `v=spf1 ip4:YOUR_ATT_MAIL_IP include:mxroute.com -all` |
|
||||
| TXT | `mail` | `v=spf1 ip4:YOUR_ATT_MAIL_IP -all` |
|
||||
| TXT | `_dmarc` | `v=DMARC1; p=reject; rua=mailto:admin@netgrimoire.com` |
|
||||
| TXT | `mailcow._domainkey.mail` | *(from Mailcow ARC/DKIM Keys for mail.pncharris.com)* |
|
||||
| TXT | `x._domainkey` | *(from MXRoute control panel)* |
|
||||
|
||||
**Mailcow domains:** `mail.pncharris.com` (primary), `pncharris.com` (alias domain → mail.pncharris.com)
|
||||
|
||||
**Relay credentials:**
|
||||
|
||||
| Account | Password | Notes |
|
||||
|---------|----------|-------|
|
||||
| relay@pncharris.com | H@rv3yD)G123 | Current relay account |
|
||||
| forwarder@pncharris.com | *(see password history below)* | Legacy account |
|
||||
| passer@pncharris.com | bBJtPhrGkHvvhxhukkae | Current |
|
||||
| kylr pncharris | -,68,incTeR | |
|
||||
| G4@rlyf1ng3r | *(Feb 14)* | |
|
||||
|
||||
**passer@pncharris.com password history** (most recent last):
|
||||
- !5!,_\*zDyLEhhR4
|
||||
- sh7dXWnTPqbkDGsTcwtn
|
||||
- MY3V8p69b2HYksygxhXX
|
||||
- RS6U2GU6rcYe3THKKgYx
|
||||
- yzqNysrd73yzWptVEZ5H (current)
|
||||
|
||||
---
|
||||
|
||||
### wasted-bandwidth.net
|
||||
|
||||
| Type | Host | Value |
|
||||
|------|------|-------|
|
||||
| A | `mail` | YOUR_ATT_MAIL_IP |
|
||||
| MX | `@` | MXRoute primary (priority 10) |
|
||||
| MX | `@` | MXRoute secondary (priority 20) |
|
||||
| MX | `mail` | `mail.wasted-bandwidth.net` (priority 10) |
|
||||
| CNAME | `imap` | `mail.wasted-bandwidth.net` |
|
||||
| CNAME | `smtp` | `mail.wasted-bandwidth.net` |
|
||||
| CNAME | `webmail` | `mail.wasted-bandwidth.net` |
|
||||
| CNAME | `autodiscover` | `mail.wasted-bandwidth.net` |
|
||||
| CNAME | `autoconfig` | `mail.wasted-bandwidth.net` |
|
||||
| TXT | `@` | `v=spf1 ip4:YOUR_ATT_MAIL_IP include:mxroute.com -all` |
|
||||
| TXT | `mail` | `v=spf1 ip4:YOUR_ATT_MAIL_IP -all` |
|
||||
| TXT | `_dmarc` | `v=DMARC1; p=reject; rua=mailto:admin@netgrimoire.com` |
|
||||
| TXT | `mailcow._domainkey.mail` | *(from Mailcow ARC/DKIM Keys for mail.wasted-bandwidth.net)* |
|
||||
| TXT | `x._domainkey` | *(from MXRoute control panel)* |
|
||||
|
||||
**Mailcow domains:** `mail.wasted-bandwidth.net` (primary), `wasted-bandwidth.net` (alias domain)
|
||||
|
||||
**Relay credentials:**
|
||||
|
||||
| Account | Password |
|
||||
|---------|----------|
|
||||
| relay@wasted-bandwidth.net | dZ4yLYznVvgSJtqWZJFA |
|
||||
|
||||
---
|
||||
|
||||
### netgrimoire.com
|
||||
|
||||
| Type | Host | Value |
|
||||
|------|------|-------|
|
||||
| A | `mail` | YOUR_ATT_MAIL_IP |
|
||||
| MX | `@` | MXRoute primary (priority 10) |
|
||||
| MX | `@` | MXRoute secondary (priority 20) |
|
||||
| MX | `mail` | `mail.netgrimoire.com` (priority 10) |
|
||||
| CNAME | `imap` | `mail.netgrimoire.com` |
|
||||
| CNAME | `smtp` | `mail.netgrimoire.com` |
|
||||
| CNAME | `webmail` | `mail.netgrimoire.com` |
|
||||
| CNAME | `autodiscover` | `mail.netgrimoire.com` |
|
||||
| CNAME | `autoconfig` | `mail.netgrimoire.com` |
|
||||
| TXT | `@` | `v=spf1 ip4:YOUR_ATT_MAIL_IP include:mxroute.com -all` |
|
||||
| TXT | `mail` | `v=spf1 ip4:YOUR_ATT_MAIL_IP -all` |
|
||||
| TXT | `_dmarc` | `v=DMARC1; p=reject; rua=mailto:admin@netgrimoire.com` |
|
||||
| TXT | `mailcow._domainkey.mail` | *(from Mailcow ARC/DKIM Keys for mail.netgrimoire.com)* |
|
||||
| TXT | `x._domainkey` | *(from MXRoute control panel)* |
|
||||
|
||||
**Mailcow domains:** `mail.netgrimoire.com` (primary), `netgrimoire.com` (alias domain)
|
||||
|
||||
**Relay credentials:**
|
||||
|
||||
| Account | Password |
|
||||
|---------|----------|
|
||||
| relay@netgrimoire.com | TVGCnJp9SxRbWU8EhkMw |
|
||||
|
||||
---
|
||||
|
||||
### florosafd.org
|
||||
|
||||
| Type | Host | Value |
|
||||
|------|------|-------|
|
||||
| A | `mail` | YOUR_ATT_MAIL_IP |
|
||||
| MX | `@` | MXRoute primary (priority 10) |
|
||||
| MX | `@` | MXRoute secondary (priority 20) |
|
||||
| MX | `mail` | `mail.florosafd.org` (priority 10) |
|
||||
| CNAME | `imap` | `mail.florosafd.org` |
|
||||
| CNAME | `smtp` | `mail.florosafd.org` |
|
||||
| CNAME | `webmail` | `mail.florosafd.org` |
|
||||
| CNAME | `autodiscover` | `mail.florosafd.org` |
|
||||
| CNAME | `autoconfig` | `mail.florosafd.org` |
|
||||
| TXT | `@` | `v=spf1 ip4:YOUR_ATT_MAIL_IP include:mxroute.com -all` |
|
||||
| TXT | `mail` | `v=spf1 ip4:YOUR_ATT_MAIL_IP -all` |
|
||||
| TXT | `_dmarc` | `v=DMARC1; p=reject; rua=mailto:admin@netgrimoire.com` |
|
||||
| TXT | `mailcow._domainkey.mail` | *(from Mailcow ARC/DKIM Keys for mail.florosafd.org)* |
|
||||
| TXT | `x._domainkey` | *(from MXRoute control panel)* |
|
||||
|
||||
**Mailcow domains:** `mail.florosafd.org` (primary), `florosafd.org` (alias domain)
|
||||
|
||||
**Relay credentials:**
|
||||
|
||||
| Account | Password |
|
||||
|---------|----------|
|
||||
| relay@florosafd.org | 2Fe8XMyaeh6Z5dvdHYdq |
|
||||
|
||||
---
|
||||
|
||||
### gnarlypandaproductions.com
|
||||
|
||||
| Type | Host | Value |
|
||||
|------|------|-------|
|
||||
| A | `mail` | YOUR_ATT_MAIL_IP |
|
||||
| MX | `@` | MXRoute primary (priority 10) |
|
||||
| MX | `@` | MXRoute secondary (priority 20) |
|
||||
| MX | `mail` | `mail.gnarlypandaproductions.com` (priority 10) |
|
||||
| CNAME | `imap` | `mail.gnarlypandaproductions.com` |
|
||||
| CNAME | `smtp` | `mail.gnarlypandaproductions.com` |
|
||||
| CNAME | `webmail` | `mail.gnarlypandaproductions.com` |
|
||||
| CNAME | `roundcube` | `roundcube.netgrimoire.com` |
|
||||
| CNAME | `autodiscover` | `mail.gnarlypandaproductions.com` |
|
||||
| CNAME | `autoconfig` | `mail.gnarlypandaproductions.com` |
|
||||
| TXT | `@` | `v=spf1 ip4:YOUR_ATT_MAIL_IP include:mxroute.com -all` |
|
||||
| TXT | `mail` | `v=spf1 ip4:YOUR_ATT_MAIL_IP -all` |
|
||||
| TXT | `_dmarc` | `v=DMARC1; p=reject; rua=mailto:admin@gnarlypandaproductions.com` |
|
||||
| TXT | `mailcow._domainkey.mail` | *(from Mailcow ARC/DKIM Keys for mail.gnarlypandaproductions.com)* |
|
||||
| TXT | `default._domainkey` | `v=DKIM1; t=s; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3D3vyPoBHB4eMSMq8HygVWHzYbketRX4yjk9wV4bdaar0/c89dK230FMOW6zVXEsY1sXKFk1kBxerHVw0wY8qnQyooHgINEQcEXrtB/x93Sl/cqBQXk+PHOIOymQwgni8WCUhCSnvunxXK8qX5f9J56qzd0/wpY2WSEHho+XrnQjc+c7HMvkcC3+nKJe59ZNgvQW/Y9B/L6zFDjAp+QOUYp9wwX4L+j1T4fQSygYxAJZ0aIoR8FsbOuXc38pht99HyUnYwH08HoK7xv3DL2BrVo3KVZ7xMe2S4YMxd1HkJz2evbV/ziNsJcKW/le3fFS7mza09yJXDLDcLOKLXbYUQIDAQAB` |
|
||||
| TXT | `x._domainkey` | *(from MXRoute control panel — confirm actual selector)* |
|
||||
|
||||
**Mailcow domains:** `mail.gnarlypandaproductions.com` (primary), `gnarlypandaproductions.com` (alias domain)
|
||||
|
||||
**Relay credentials:**
|
||||
|
||||
| Account | Password |
|
||||
|---------|----------|
|
||||
| relay@gnarlypandaproductions.com | vG5ZsUQhRWD2UyzLPsqA |
|
||||
|
||||
---
|
||||
|
||||
### nucking-futz.com
|
||||
|
||||
New domain — see [Mail Setup — nucking-futz.com](./mail-setup-nucking-futz) for full setup guide.
|
||||
|
||||
| Type | Host | Value |
|
||||
|------|------|-------|
|
||||
| A | `mail` | YOUR_ATT_MAIL_IP |
|
||||
| MX | `@` | MXRoute primary (priority 10) |
|
||||
| MX | `@` | MXRoute secondary (priority 20) |
|
||||
| MX | `mail` | `mail.nucking-futz.com` (priority 10) |
|
||||
| CNAME | `imap` | `mail.nucking-futz.com` |
|
||||
| CNAME | `smtp` | `mail.nucking-futz.com` |
|
||||
| CNAME | `webmail` | `mail.nucking-futz.com` |
|
||||
| CNAME | `autodiscover` | `mail.nucking-futz.com` |
|
||||
| CNAME | `autoconfig` | `mail.nucking-futz.com` |
|
||||
| TXT | `@` | `v=spf1 ip4:YOUR_ATT_MAIL_IP include:mxroute.com -all` |
|
||||
| TXT | `mail` | `v=spf1 ip4:YOUR_ATT_MAIL_IP -all` |
|
||||
| TXT | `_dmarc` | `v=DMARC1; p=reject; rua=mailto:admin@netgrimoire.com` |
|
||||
| TXT | `mailcow._domainkey.mail` | *(from Mailcow ARC/DKIM Keys for mail.nucking-futz.com)* |
|
||||
| TXT | `x._domainkey` | *(from MXRoute control panel)* |
|
||||
|
||||
**Mailcow domains:** `mail.nucking-futz.com` (primary), `nucking-futz.com` (alias domain)
|
||||
|
||||
**Relay credentials:**
|
||||
|
||||
| Account | Password |
|
||||
|---------|----------|
|
||||
| relay@nucking-futz.com | *(set during MXRoute domain creation)* |
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Domain — Checklist
|
||||
|
||||
Use this checklist every time a new domain is added to the stack.
|
||||
|
||||
**DNS (at registrar):**
|
||||
- [ ] A record: `mail.newdomain.com` → YOUR_ATT_MAIL_IP
|
||||
- [ ] MX records: `@` → MXRoute servers
|
||||
- [ ] MX record: `mail` → `mail.newdomain.com`
|
||||
- [ ] CNAME records: imap, smtp, webmail, autodiscover, autoconfig
|
||||
- [ ] SPF TXT: `@` — includes both ATT IP and `include:mxroute.com`
|
||||
- [ ] SPF TXT: `mail` — ATT IP only
|
||||
- [ ] DMARC TXT: `_dmarc`
|
||||
- [ ] DKIM TXT: `mailcow._domainkey.mail` — after generating in Mailcow
|
||||
- [ ] DKIM TXT: `x._domainkey` — after retrieving from MXRoute
|
||||
|
||||
**Mailcow:**
|
||||
- [ ] Add domain: `mail.newdomain.com`
|
||||
- [ ] Add alias domain: `newdomain.com` → `mail.newdomain.com`
|
||||
- [ ] Generate DKIM key (selector: `mailcow`) for `mail.newdomain.com`
|
||||
- [ ] Add sender-dependent transport for `newdomain.com`
|
||||
- [ ] Add sender-dependent transport for `mail.newdomain.com`
|
||||
- [ ] Create mailboxes as `user@mail.newdomain.com`
|
||||
|
||||
**MXRoute:**
|
||||
- [ ] Add domain in control panel
|
||||
- [ ] Create forwarder for each mailbox: `user@newdomain.com` → `user@mail.newdomain.com`
|
||||
- [ ] Retrieve DKIM key for DNS
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Mail not delivering inbound (not reaching Mailcow)
|
||||
|
||||
- Check MX records for `@` point to MXRoute servers: `dig MX domain.com +short`
|
||||
- Check MX record for `mail` subdomain points to Mailcow: `dig MX mail.domain.com +short`
|
||||
- Verify MXRoute forwarder exists for the address in the control panel
|
||||
- Check Mailcow logs: **Logs → Postfix** — look for the delivery attempt and any rejection reason
|
||||
- Verify MXRoute IP ranges are in Mailcow `extra.cf` trusted networks
|
||||
|
||||
### Mail not delivering inbound (banks / financial institutions)
|
||||
|
||||
- This is the residential AT&T IP problem — confirm MX records point to MXRoute, not directly to your IP
|
||||
- Run `dig MX domain.com +short` — should show MXRoute servers, not your IP
|
||||
- If MX still points to your ATT IP, update DNS and wait for propagation
|
||||
|
||||
### Outbound mail rejected or going to spam
|
||||
|
||||
- Verify sender-dependent transport is configured for the domain in Mailcow
|
||||
- Check relay credentials are current in the transport entry
|
||||
- Run an SPF check: `dig TXT domain.com +short` — confirm `include:mxroute.com` is present
|
||||
- Send test to check-auth@verifier.port25.com for full SPF/DKIM/DMARC report
|
||||
- Run through https://mail-tester.com for a deliverability score
|
||||
|
||||
### DKIM verification failing
|
||||
|
||||
- Confirm both selectors are published in DNS:
|
||||
- `dig TXT mailcow._domainkey.mail.domain.com +short`
|
||||
- `dig TXT x._domainkey.domain.com +short` (substitute actual MXRoute selector)
|
||||
- Allow up to 48 hours for DNS propagation after adding records
|
||||
- Verify selector names match exactly what Mailcow and MXRoute are using to sign
|
||||
|
||||
### DMARC failures
|
||||
|
||||
- SPF and DKIM must both pass and align with the From: domain
|
||||
- Check DMARC reports sent to `admin@netgrimoire.com` — use [Postmark DMARC](https://dmarc.postmarkapp.com/) or [dmarcian.com](https://dmarcian.com) to parse raw XML reports
|
||||
- Common cause: outbound mail going through MXRoute but `include:mxroute.com` missing from SPF
|
||||
|
||||
### Forwarded mail getting spam-scored
|
||||
|
||||
- Confirm MXRoute IP ranges are in Mailcow `extra.cf` mynetworks
|
||||
- Check that Mailcow trusted networks were saved and containers restarted
|
||||
- Verify SRS is working: in Roundcube open a forwarded message → More → View Source → `Return-Path` should begin with `SRS0=`
|
||||
|
||||
### New mailbox not receiving mail
|
||||
|
||||
- Two steps are required — confirm both were done:
|
||||
1. Mailbox created in Mailcow as `user@mail.domain.com`
|
||||
2. Forwarder created in MXRoute as `user@domain.com` → `user@mail.domain.com`
|
||||
- If the MXRoute forwarder is missing, inbound mail silently goes nowhere
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [MailCow Configuration](./mailcow)
|
||||
- [MailCow Security Hardening](./mailcow-security-hardening)
|
||||
- [Mail Setup — nucking-futz.com](./mail-setup-nucking-futz)
|
||||
- [OPNsense Firewall](./opnsense-firewall) — ATT_Mail static IP allocation
|
||||
85
Netgrimoire/Keystone-Grimoire/Mail/MailCow-Overview.md
Normal file
85
Netgrimoire/Keystone-Grimoire/Mail/MailCow-Overview.md
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
title: MailCow Overview
|
||||
description: Self-hosted mail stack — architecture, domains, and key decisions
|
||||
published: true
|
||||
date: 2026-04-12T00:00:00.000Z
|
||||
tags: keystone, mail, mailcow
|
||||
editor: markdown
|
||||
dateCreated: 2026-04-12T00:00:00.000Z
|
||||
---
|
||||
|
||||
# MailCow Overview
|
||||
|
||||
MailCow runs on `docker4` (hermes, 192.168.5.16) via Docker Compose — not Swarm. It manages mail for all 8 domains.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
| Component | Role |
|
||||
|-----------|------|
|
||||
| MailCow stack | Postfix, Dovecot, Rspamd, ClamAV, SOGo, Roundcube, nginx-mailcow |
|
||||
| MXRoute | Inbound filtering + outbound relay for all domains |
|
||||
| nginx-mailcow | Only MailCow container connected to `netgrimoire` overlay |
|
||||
|
||||
**Critical:** Only `nginx-mailcow` is attached to the `netgrimoire` overlay network. All other MailCow containers stay on the internal `mailcow-network` bridge. Connecting other containers to the overlay causes Redis and PHP-FPM to resolve to wrong IPs, breaking the entire stack.
|
||||
|
||||
---
|
||||
|
||||
## Domains
|
||||
|
||||
`netgrimoire.com` · `pncharris.com` · `wasted-bandwidth.net` · `nucking-futz.com` · `florosafd.org` · `gnarlypandaproductions.com` · `pncfishandmore.com` · `pncharrisenterprises.com`
|
||||
|
||||
---
|
||||
|
||||
## Mail Flow
|
||||
|
||||
**Inbound:** MXRoute filters → forwards to MailCow → Dovecot delivers
|
||||
|
||||
**Outbound:** Postfix → MXRoute relay → recipient
|
||||
|
||||
**SRS rewriting:** MXRoute rewrites the envelope sender on forwarded mail. All domains using MXRoute inbound forwarding **must** have catch-all aliases configured in MailCow, or `reject_unlisted_sender` will reject the rewritten addresses.
|
||||
|
||||
---
|
||||
|
||||
## DKIM
|
||||
|
||||
Two selectors required:
|
||||
|
||||
| Selector | Purpose |
|
||||
|----------|---------|
|
||||
| `mailcow` | Direct sends from MailCow |
|
||||
| `mxroute` | MXRoute relay path |
|
||||
|
||||
---
|
||||
|
||||
## Key Limits (must match across all three)
|
||||
|
||||
Attachment size limits must be set identically in Postfix, Rspamd, and ClamAV. Changing only Postfix is insufficient — Rspamd and ClamAV reject large messages before Postfix processes them.
|
||||
|
||||
---
|
||||
|
||||
## Roundcube SSL
|
||||
|
||||
Internal connections to Dovecot use self-signed certs. In `config.inc.php`:
|
||||
|
||||
```php
|
||||
$config['imap_conn_options'] = ['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Docs
|
||||
|
||||
- [MXRoute Integration](/Keystone-Grimoire/Mail/MXRoute-Integration)
|
||||
- [Domain Setup](/Keystone-Grimoire/Mail/Domain-Setup)
|
||||
- [MailCow Hardening](/Keystone-Grimoire/Mail/Hardening)
|
||||
- [MailCow Backup](/Vault-Grimoire/Backups/MailCow-Backup)
|
||||
|
||||
---
|
||||
|
||||
## Pending
|
||||
|
||||
- [ ] Dedicated ATT_Mail static IP for outbound mail (OPNsense outbound NAT rule)
|
||||
- [ ] Second DKIM selector (`mxroute`) validation
|
||||
- [ ] MTA-STS validation (supported since Sep 2025 update)
|
||||
60
Netgrimoire/Keystone-Grimoire/Network/Port-Assignments.md
Normal file
60
Netgrimoire/Keystone-Grimoire/Network/Port-Assignments.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
title: Port Assignments
|
||||
description:
|
||||
published: true
|
||||
date: 2026-02-20T04:21:52.996Z
|
||||
tags:
|
||||
editor: markdown
|
||||
dateCreated: 2026-01-27T03:42:58.945Z
|
||||
---
|
||||
|
||||
# Physical Paths
|
||||
|
||||
|Device|IP|Room|Home Infra|DLink|TPLink|Closet|Inter Rack|Rack|Ubiquity|
|
||||
|------|--|----|------|------|-------|------|----|----|--------|
|
||||
|Dlink |5.2 |Office | |1| | | | |1 |
|
||||
|ZNAS |5.10 | | |2| | | | | |
|
||||
|Docker3 | | | |3| | | | | |
|
||||
|Docker5 | | | |4| | | | | |
|
||||
|DockerPi1 | | | |5| | | | | |
|
||||
|DNS |5.7 | | |6| | | | | |
|
||||
|Docker4 | | | | | | |W:7 |19|4 |
|
||||
|Docker2 | | Office | | | | |W:5 |17|11|
|
||||
|Time Machine| | | | | | |W:6 |18|12|
|
||||
|Deco Satt | |Room 1 |1 | | | | | |15|
|
||||
|Deco AP | |Office(E)|10-24| | |24|W:9 |21|20|
|
||||
|TP Link | | | | |1|22|W:10|22|23|
|
||||
|OpnSense |3.4 | | | | |23|W:11|23|24|
|
||||
|OPnSense-Cox| | | | | | | | | |
|
||||
| | | | | | | | | | |
|
||||
| | |Room 2 |2 | | | | |2 | |
|
||||
| | |Room 3 |3 | | | | |3 | |
|
||||
| | |Living(E)|4 | | | | |4 | |
|
||||
| | |Living(W)|5 | | | | |5 | |
|
||||
| | |Family |6 | | | | |6 | |
|
||||
| | |Pantry |7 | | | | |7 | |
|
||||
| | |Room 4 |8 | | | | |8 | |
|
||||
| | |Gym |9 | | | | |9 | |
|
||||
| | |Office(S)|11 | | | | |11| |
|
||||
| | |Office(W)|12 | | | | |12| |
|
||||
| | |Office(W)|13 | | | | |13| |
|
||||
| | |Office(W)|14 | | | | |14| |
|
||||
| | |Office(W)|15 | | | | |15| |
|
||||
| | |Office(W)|16 | | | | |16| |
|
||||
| | |Office(N)|17 | | | | |17| |
|
||||
| | |Office(N)|18 | | | | |18| |
|
||||
| | |Office(N)|19 | | | | |19| |
|
||||
| | |Office(N)|20 | | | | |20| |
|
||||
|
||||
Note: For rooms N,E,S,W are compass directions
|
||||
For InterRack, W - wall, H - Hallway
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
49
Netgrimoire/Keystone-Grimoire/Network/Topology.md
Normal file
49
Netgrimoire/Keystone-Grimoire/Network/Topology.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
title: Network Topology
|
||||
description: Netgrimoire network layout — VLANs, subnets, routing
|
||||
published: true
|
||||
date: 2026-04-12T00:00:00.000Z
|
||||
tags: keystone, network
|
||||
editor: markdown
|
||||
dateCreated: 2026-04-12T00:00:00.000Z
|
||||
---
|
||||
|
||||
# Network Topology
|
||||
|
||||
## Subnets
|
||||
|
||||
| Subnet | Purpose |
|
||||
|--------|---------|
|
||||
| 192.168.3.0/24 | OPNsense / firewall management |
|
||||
| 192.168.4.0/24 | ISPConfig / web hosting |
|
||||
| 192.168.5.0/24 | Primary LAN — all Docker hosts |
|
||||
| 192.168.8.0/24 | Pocket Grimoire (GL.iNet Beryl AX) |
|
||||
| 192.168.32.0/24 | WireGuard VPN peers |
|
||||
|
||||
## WireGuard Peers
|
||||
|
||||
| Peer | IP | Device |
|
||||
|------|----|--------|
|
||||
| Obie | 192.168.32.2 | — |
|
||||
| pncfishandmore | 192.168.32.3 | — |
|
||||
| GLNet | 192.168.32.4 | GL.iNet router |
|
||||
| PortaPotty | 192.168.32.5 | Pocket Grimoire laptop |
|
||||
| GLNet | 192.168.32.6 | Second GL.iNet |
|
||||
|
||||
## DNS
|
||||
|
||||
Internal DNS runs on Technitium at `192.168.5.7` (`dns.netgrimoire.com`), behind Authentik.
|
||||
|
||||
All `*.netgrimoire.com` and `*.wasted-bandwidth.net` internal hostnames resolve via Technitium. Public DNS managed via ISPConfig and domain registrars.
|
||||
|
||||
## Docker Overlay Network
|
||||
|
||||
All Swarm services share the `netgrimoire` external overlay network (VIP mode). This is the only overlay network in use.
|
||||
|
||||
```
|
||||
Name: netgrimoire
|
||||
Driver: overlay
|
||||
Mode: VIP (always — dnsrr is banned)
|
||||
```
|
||||
|
||||
See [Docker Swarm Template](/Keystone-Grimoire/Docker/Swarm-Template) for attachment rules.
|
||||
36
Netgrimoire/Keystone-Grimoire/Overview.md
Normal file
36
Netgrimoire/Keystone-Grimoire/Overview.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
title: Keystone Grimoire
|
||||
description: Architecture — the dwarven runesmith's blueprints
|
||||
published: true
|
||||
date: 2026-04-12T00:00:00.000Z
|
||||
tags: keystone, architecture
|
||||
editor: markdown
|
||||
dateCreated: 2026-04-12T00:00:00.000Z
|
||||
---
|
||||
|
||||
# Keystone Grimoire
|
||||
|
||||

|
||||
|
||||
The Keystone Grimoire holds the architectural blueprints of Netgrimoire — how everything is wired together, how traffic flows, why decisions were made. Remove the keystone and the arch falls. This is the arch.
|
||||
|
||||
---
|
||||
|
||||
## Sections
|
||||
|
||||
| Section | Contents |
|
||||
|---------|----------|
|
||||
| [Hosts](/Keystone-Grimoire/Hosts/Host-Inventory) | Node inventory, roles, IPs, pinned services, hardware |
|
||||
| [Network](/Keystone-Grimoire/Network/Topology) | Topology, VLANs, DNS, WireGuard, OpenVPN, port assignments |
|
||||
| [Docker](/Keystone-Grimoire/Docker/Swarm-Template) | Swarm template standard, overlay network, label rules, volume paths |
|
||||
| [Mail](/Keystone-Grimoire/Mail/MailCow-Overview) | MailCow, MXRoute, DKIM, SRS, domain setup, hardening |
|
||||
|
||||
---
|
||||
|
||||
## Key Principles
|
||||
|
||||
- **Caddy is the single entry point** for all web traffic. Every public service goes through Caddy. No exceptions.
|
||||
- **Docker labels drive routing** — services register themselves with Caddy via `deploy.labels`. Static Caddyfile entries only for Compose stacks where label pickup is unreliable.
|
||||
- **Never mix label and static routing for the same hostname** — caddy-docker-proxy merges them into a broken upstream pool.
|
||||
- **Always VIP endpoint mode** — `endpoint_mode: dnsrr` is banned. It breaks internal DNS resolution.
|
||||
- **ARM nodes are excluded by default** — all swarm services carry `node.platform.arch != aarch64` and `node.platform.arch != arm` constraints unless explicitly ARM-specific.
|
||||
Loading…
Add table
Add a link
Reference in a new issue