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
.
├── .devcontainer/
│ └── devcontainer.json # Dev Container config (pnpm + Docker)
├── .dockerignore # Docker build context exclusions
├── .gitignore # Git exclusions
├── Dockerfile # Docker image definition
├── docker-compose.yml # Portainer stack configuration
├── package.json # Build and push scripts (pnpm)
├── 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) — see Package manager
- Access to the Gitea package registry
- Portainer webhook configured (for deployments)
Package Manager
This project uses pnpm as the package manager. The version is pinned in package.json via the packageManager field. If you use Corepack (bundled with Node.js 22+), run corepack enable once and the correct pnpm version will be used automatically.
Dev Container (Optional)
A Dev Container configuration is included for consistent local development. It provides:
- Node.js 24 with pnpm 10.28
- Docker CLI with access to the host Docker daemon (so you can run
pnpm buildand test images) - VS Code extensions: Docker, Prettier
To use: Open the repo in VS Code or Cursor, install the "Dev Containers" extension if needed, and run Dev Containers: Reopen in Container from the command palette. The first build may take a few minutes.
Local Development
-
Clone the repository:
git clone <repo-url> cd mta-sts -
Preview the site locally (optional):
pnpm install pnpm devOpen http://localhost:3000 for the landing page and http://localhost:3000/.well-known/mta-sts.txt for the policy file.
-
Build the Docker image:
pnpm build -
Test locally (with the built image):
docker run -p 8080:80 git.mifi.dev/mifi-holdings/mta-sts:latestVisit http://localhost:8080 to see the landing page and http://localhost:8080/.well-known/mta-sts.txt for 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 dev |
Start local dev server for html/ at http://localhost:3000 |
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
pnpm install shows DEP0169 / url.parse() deprecation warning
On Node.js 24+, you may see:
DeprecationWarning: `url.parse()` behavior is not standardized...
This comes from pnpm (and its dependencies), not from this repo. The project has no runtime dependencies, so the warning is harmless and can be ignored. To hide it for a single run:
NODE_OPTIONS='--no-deprecation' pnpm install
To suppress it for your shell session: export NODE_OPTIONS='--no-deprecation'. See pnpm#9492 for upstream progress.
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.