Why I Set Up Rate Limiting on My SMTP Relay
I run a self-hosted SMTP relay on my Proxmox homelab. It handles outbound email for several services: monitoring alerts from n8n, backup notifications, and occasional manual sends from scripts. The relay itself sits behind Traefik, which I use to manage all my internal routing and TLS termination.
A few months ago, I noticed something odd in my logs. Someone had found my relay endpoint and was attempting to use it as an open proxy for sending spam. The requests weren't getting through because I had authentication enabled, but they kept hammering the service. Hundreds of connection attempts per minute, all failing, but still consuming CPU cycles and filling up logs.
This wasn't a sophisticated attack. Just automated scripts probing for misconfigured mail servers. But it was enough to make me realize I needed a better gate at the front door.
My Setup Before Rate Limiting
My SMTP relay runs in a Docker container on Proxmox, exposed through Traefik. The basic configuration looked like this:
- Traefik handles TLS termination with Let's Encrypt certificates
- The SMTP service accepts connections on port 587 (submission)
- Authentication is required for all sends
- No rate limiting or IP filtering at the proxy layer
The authentication requirement meant nobody could actually send mail through my relay without credentials. But they could still attempt connections, trigger authentication failures, and waste resources. I wanted to stop these attempts before they even reached the SMTP service.
Understanding Traefik's Rate Limiting
Traefik includes a built-in rate limiting middleware that uses the Token Bucket algorithm. I chose this because it's simple to configure and handles exactly what I needed: limiting connection attempts per source IP.
The Token Bucket approach works like this: you define a bucket size (burst capacity) and a refill rate. Each incoming request consumes one token. If the bucket is empty, the request is rejected. Tokens refill at a steady rate, so legitimate traffic can continue even after a burst.
For my SMTP relay, I needed to balance two things:
- Allow legitimate services to send multiple emails in quick succession
- Block abusive connection patterns from unknown sources
What I Configured
I created a rate limiting middleware in my Traefik dynamic configuration. Here's the actual setup I'm using:
http:
middlewares:
smtp-ratelimit:
rateLimit:
average: 10
period: 1s
burst: 20
sourceCriterion:
ipStrategy:
depth: 1
This configuration means:
- Each IP address gets 10 requests per second on average
- Burst capacity allows up to 20 simultaneous requests
- After the burst, requests are limited to 10 per second
- IP detection uses the first address in X-Forwarded-For
I chose these numbers based on my actual usage patterns. My monitoring system sends at most 5-6 alerts per minute during normal operations. Even during an incident, it rarely exceeds 20 emails in a short burst. So 10 requests per second with a burst of 20 gives plenty of headroom for legitimate use while blocking abuse.
The IP Strategy Problem
The ipStrategy setting was trickier than I expected. My Traefik instance sits behind my router, which does NAT. This means all internal traffic appears to come from the same IP address when viewed from Traefik's perspective.
I initially set depth: 0, which told Traefik to use the direct client IP. This worked fine for external connections but broke rate limiting for internal services because they all shared the same source IP.
I changed it to depth: 1 to use the X-Forwarded-For header. This required configuring my router to add the real client IP to this header. Without this, rate limiting would have blocked all internal traffic after the first burst, which would have defeated the purpose.
For anyone else doing this: if you have proxies or NAT between your clients and Traefik, you need to think carefully about how IP addresses are tracked. The default settings assume direct connections.
Applying the Middleware
Once the middleware was defined, I attached it to my SMTP relay's router configuration:
http:
routers:
smtp-relay:
rule: "Host(`smtp.internal.vipinpg.com`)"
service: smtp-relay-service
middlewares:
- smtp-ratelimit
tls:
certResolver: letsencrypt
This applies rate limiting to all traffic hitting that hostname before it reaches the SMTP service. The order matters: Traefik evaluates middlewares in the order they're listed, so rate limiting happens before the request is forwarded to the backend.
What Worked
After deploying this configuration, the abuse attempts dropped off immediately. Not because they stopped happening, but because Traefik was rejecting them at the proxy layer. My SMTP service logs became clean again, showing only legitimate connection attempts.
The rate limiting also had an unexpected benefit: it made debugging easier. When I was troubleshooting an n8n workflow that was stuck in a loop sending failed email notifications, the rate limiter caught it before it could flood my relay. I saw the 429 errors in Traefik's logs and knew exactly what was happening.
Legitimate traffic continued without issues. My monitoring system, backup scripts, and manual sends all stayed within the configured limits. The burst capacity handled occasional spikes without dropping valid requests.
What Didn't Work
My first attempt at setting the rate limit was too aggressive. I started with average: 5 and burst: 10, thinking I'd be conservative. This broke immediately during a test where I triggered multiple monitoring alerts in quick succession. The rate limiter blocked legitimate traffic because my burst capacity was too low.
I also initially forgot to configure the X-Forwarded-For header on my router. This meant all internal traffic appeared to come from the router's IP, and rate limiting applied to everything as a single source. I only noticed this when my backup notifications started failing after the first few went through.
The fix was straightforward once I understood the problem: increase the limits and configure proper IP forwarding. But it took a few iterations to get the numbers right.
Limitations I Accepted
This setup only protects against volume-based abuse. It doesn't stop someone from slowly probing my relay at 9 requests per second forever. For that, I'd need additional layers like fail2ban or IP allowlisting.
I also can't share rate limit buckets across multiple Traefik instances. Each instance maintains its own token buckets, so if I ever scale to multiple Traefik containers, the limits would apply per instance, not globally. Traefik Hub offers distributed rate limiting, but I don't need that complexity for my homelab.
The rate limiting applies at the HTTP layer, not at the SMTP protocol level. This means it protects the proxy but not the underlying mail service from protocol-specific attacks. If someone managed to bypass Traefik and connect directly to the SMTP port, the rate limiting wouldn't help.
Key Takeaways
Rate limiting is effective for stopping basic abuse patterns without blocking legitimate traffic. The Token Bucket algorithm in Traefik is simple to configure and works well for services with predictable traffic patterns.
Getting the IP strategy right is critical. If you have proxies or NAT in your network, you need to configure X-Forwarded-For handling carefully or rate limiting will behave unpredictably.
Start with generous limits and tighten them based on actual usage. It's easier to reduce limits gradually than to debug why legitimate traffic is being blocked.
Rate limiting is one layer of defense, not a complete solution. It stops volume-based abuse but doesn't replace authentication, encryption, or proper service configuration.
For my homelab SMTP relay, this setup solved the immediate problem of automated abuse attempts while staying simple enough to maintain. It's been running without issues for several months now.