7.9 KiB
MTA-STS Policy Server
A lightweight nginx-based server that hosts MTA-STS (Mail Transfer Agent Strict Transport Security) policies for multiple domains, ensuring secure email delivery through enforced TLS encryption.
What is MTA-STS?
MTA-STS is a security standard (RFC 8461) that helps protect email in transit by:
- Enforcing TLS encryption for email delivery between mail servers
- Preventing downgrade attacks that could force unencrypted connections
- Validating certificates to ensure emails are delivered to legitimate servers
- Providing a policy that sending servers can cache and enforce
Purpose
This repository serves MTA-STS policies for all domains under the mifi-holdings organization. Each domain has a subdomain mta-sts.<domain> (e.g., mta-sts.mifi.holdings) that serves:
- Policy file:
/.well-known/mta-sts.txt- defines the MTA-STS policy - Landing page:
/- provides information about the service
Domains Covered
This server currently provides MTA-STS protection for:
- mifi.holdings
- mifi.com.br
- mifi.dev
- mifi.ventures
- mifi.vix.br
- mifi.me
- blackice.vix.br
- fitz.guru
- umlautpress.com
- camilla-rena.com
- officelift.net
- mylocalpro.biz
- mylocalpro.online
- happybeardedcarpenter.com
- thenewenglandpalletguy.com
- dining-it.com
Architecture
Components
┌─────────────────────────────────────────┐
│ Docker Container │
│ ┌───────────────────────────────────┐ │
│ │ nginx:alpine │ │
│ │ │ │
│ │ /etc/nginx/conf.d/default.conf │ │
│ │ /usr/share/nginx/html/ │ │
│ │ ├── index.html │ │
│ │ └── .well-known/ │ │
│ │ └── mta-sts.txt │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
▼
┌─────────────┐
│ Traefik │ (Reverse Proxy)
└─────────────┘
│
▼
Multiple mta-sts.* domains
How It Works
- Build: Docker image is built with nginx, custom configuration, and static HTML content baked in
- Registry: Image is pushed to
git.mifi.dev/mifi-holdings/mta-sts - Deploy: Portainer pulls the latest image and restarts the container
- Routing: Traefik routes all
mta-sts.*domains to the container - Serving: nginx serves the policy file and landing page
Project Structure
.
├── Dockerfile # Docker image definition
├── docker-compose.yml # Portainer stack configuration
├── package.json # Build and push scripts
├── nginx/
│ └── default.conf # nginx server configuration
├── html/
│ ├── index.html # Landing page
│ └── .well-known/
│ └── mta-sts.txt # MTA-STS policy file
└── .woodpecker/
├── build.yaml # CI: Build and push image
└── deploy.yaml # CI: Trigger Portainer webhook
Setup & Deployment
Prerequisites
- Docker installed and logged into
git.mifi.dev - pnpm (for manual builds)
- Access to the Gitea package registry
- Portainer webhook configured (for deployments)
Local Development
-
Clone the repository:
git clone <repo-url> cd mta-sts -
Build the Docker image:
pnpm build -
Test locally:
docker run -p 8080:80 git.mifi.dev/mifi-holdings/mta-sts:latestVisit
http://localhost:8080to see the landing page andhttp://localhost:8080/.well-known/mta-sts.txtfor the policy file.
Manual Deployment
-
Build and push:
pnpm build:push -
Trigger Portainer webhook (or let Portainer auto-pull):
curl -X POST <PORTAINER_WEBHOOK_URL>
Automated CI/CD
The repository uses Woodpecker CI for automated builds and deployments:
Build Pipeline (.woodpecker/build.yaml)
Triggers on push to main branch:
- Build Docker image with commit SHA and
latesttags - Send Discord notification about build status
- Push to registry at
git.mifi.dev/mifi-holdings/mta-sts - Send Discord notification about push status
Deploy Pipeline (.woodpecker/deploy.yaml)
Depends on successful build:
- Trigger Portainer webhook to redeploy the stack
- Send Discord notification about deployment status
Updating the Policy
To modify the MTA-STS policy:
- Edit
html/.well-known/mta-sts.txt - Commit and push to
main - Woodpecker CI will automatically build and deploy
Important: The max_age directive in the policy determines how long mail servers will cache the policy. Plan policy changes accordingly.
Adding New Domains
To add MTA-STS support for a new domain:
- Add the domain's MX records to
html/.well-known/mta-sts.txt - Add Traefik labels in
docker-compose.yml:- "traefik.http.routers.mta-sts-<domain-slug>.rule=Host(`mta-sts.<domain>`)" - "traefik.http.routers.mta-sts-<domain-slug>.entrypoints=websecure" - "traefik.http.routers.mta-sts-<domain-slug>.tls=true" - "traefik.http.routers.mta-sts-<domain-slug>.tls.certresolver=letsencrypt" - "traefik.http.routers.mta-sts-<domain-slug>.service=mta-sts-<domain-slug>" - "traefik.http.services.mta-sts-<domain-slug>.loadbalancer.server.port=80" - Ensure DNS is configured with
mta-sts.<domain>pointing to your server - Add a TXT record
_mta-sts.<domain>with valuev=STSv1; id=<unique-id>
Package Scripts
| Script | Description |
|---|---|
pnpm build |
Build Docker image locally |
pnpm push |
Push Docker image to registry |
pnpm build:push |
Build and push in one command |
Technology Stack
- nginx:alpine - Lightweight web server (base image)
- Docker - Containerization
- Traefik - Reverse proxy and SSL termination
- Portainer - Container orchestration
- Woodpecker CI - Automated build and deployment
- Gitea - Package registry
Health Checks
The container includes health checks that:
- Run every 30 seconds
- Check if nginx is responding on port 80
- Allow 10 seconds for startup
- Retry 3 times before marking as unhealthy
Security Considerations
- All content is served over HTTPS (enforced by Traefik)
- MTA-STS policy enforces TLS for mail delivery
- No sensitive data is stored in the container
- nginx runs as non-root user (alpine default)
- Container filesystem is read-only safe (all content is static)
Troubleshooting
Container won't start
- Check Portainer logs for the
mta-stscontainer - Verify the image was pushed successfully to the registry
- Ensure Traefik network exists:
docker network ls | grep traefik
Policy not being respected
- Verify DNS TXT record
_mta-sts.<domain>exists - Check that the policy file is accessible at
https://mta-sts.<domain>/.well-known/mta-sts.txt - Ensure the certificate is valid (no HTTPS errors)
- Wait for cache expiry if you recently changed the policy
Build fails in Woodpecker
- Check Docker daemon is accessible
- Verify registry credentials are configured in Woodpecker secrets
- Review build logs for specific error messages
License
ISC
Contributing
- Create a feature branch
- Make your changes
- Test locally with
pnpm build - Submit a pull request to
main
Changes merged to main will automatically deploy via Woodpecker CI.