421 Service Unavailable — STARTTLS Required
Síntomas
- Server disconnects immediately after EHLO with "421 Must issue a STARTTLS command first"
- Plain-text SMTP connection on port 587 is accepted but all commands after EHLO are rejected
- Application log shows: `SMTPException: (421, b"4.7.0 Must issue a STARTTLS command first")`
- Email delivery works from one server but fails from another (one has TLS configured, the other does not)
- EHLO response lists STARTTLS but the client ignores it and proceeds with AUTH
- Plain-text SMTP connection on port 587 is accepted but all commands after EHLO are rejected
- Application log shows: `SMTPException: (421, b"4.7.0 Must issue a STARTTLS command first")`
- Email delivery works from one server but fails from another (one has TLS configured, the other does not)
- EHLO response lists STARTTLS but the client ignores it and proceeds with AUTH
Causas raíz
- SMTP client library not configured to call STARTTLS before authentication
- Client connecting to port 587 (STARTTLS) but using plain SMTP_SSL code meant for port 465 (implicit TLS)
- Server enforcing MTA-STS policy or operator-level mandatory TLS
- Self-signed certificate on the server causing TLS negotiation to fail silently and the client falling back to plain text
- Outdated SMTP library that predates STARTTLS support connecting without upgrading the session
Diagnóstico
**Step 1 — Check the EHLO banner for STARTTLS**
```bash
# Connect in plain text and observe the EHLO response
telnet smtp.example.com 587
EHLO testclient.example.com
# Look for:
# 250-STARTTLS ← server supports it
# 250-AUTH LOGIN PLAIN ← only shown after TLS if server requires it
```
**Step 2 — Test TLS negotiation with openssl**
```bash
openssl s_client -connect smtp.example.com:587 -starttls smtp
# Successful output includes:
# SSL handshake has read ... verify return:1
# Cipher: TLS_AES_256_GCM_SHA384
```
**Step 3 — Check if a self-signed cert is blocking TLS**
```bash
openssl s_client -connect smtp.example.com:587 -starttls smtp 2>&1 | grep 'Verify return code'
# 0 (ok) = valid certificate
# 18, 19, 21 = self-signed or untrusted CA — client may silently fail
```
```bash
# Connect in plain text and observe the EHLO response
telnet smtp.example.com 587
EHLO testclient.example.com
# Look for:
# 250-STARTTLS ← server supports it
# 250-AUTH LOGIN PLAIN ← only shown after TLS if server requires it
```
**Step 2 — Test TLS negotiation with openssl**
```bash
openssl s_client -connect smtp.example.com:587 -starttls smtp
# Successful output includes:
# SSL handshake has read ... verify return:1
# Cipher: TLS_AES_256_GCM_SHA384
```
**Step 3 — Check if a self-signed cert is blocking TLS**
```bash
openssl s_client -connect smtp.example.com:587 -starttls smtp 2>&1 | grep 'Verify return code'
# 0 (ok) = valid certificate
# 18, 19, 21 = self-signed or untrusted CA — client may silently fail
```
Resolución
**Fix 1 — Enable STARTTLS in your SMTP client**
```python
# Python smtplib
import smtplib, ssl
with smtplib.SMTP('smtp.example.com', 587) as s:
s.ehlo()
s.starttls(context=ssl.create_default_context()) # ← key line
s.ehlo()
s.login(user, password)
s.sendmail(sender, recipients, msg)
```
**Fix 2 — Django settings for STARTTLS (port 587)**
```python
EMAIL_HOST = 'smtp.example.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True # activates STARTTLS
EMAIL_USE_SSL = False # mutually exclusive with USE_TLS
```
**Fix 3 — Accept self-signed certs in development only**
```python
# DEVELOPMENT ONLY — never use in production
import ssl
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
```
**Fix 4 — Install a valid TLS certificate on your mail server**
```bash
# Postfix with Let's Encrypt (certbot)
sudo certbot certonly --standalone -d mail.yourdomain.com
# /etc/postfix/main.cf
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.yourdomain.com/privkey.pem
sudo systemctl reload postfix
```
```python
# Python smtplib
import smtplib, ssl
with smtplib.SMTP('smtp.example.com', 587) as s:
s.ehlo()
s.starttls(context=ssl.create_default_context()) # ← key line
s.ehlo()
s.login(user, password)
s.sendmail(sender, recipients, msg)
```
**Fix 2 — Django settings for STARTTLS (port 587)**
```python
EMAIL_HOST = 'smtp.example.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True # activates STARTTLS
EMAIL_USE_SSL = False # mutually exclusive with USE_TLS
```
**Fix 3 — Accept self-signed certs in development only**
```python
# DEVELOPMENT ONLY — never use in production
import ssl
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
```
**Fix 4 — Install a valid TLS certificate on your mail server**
```bash
# Postfix with Let's Encrypt (certbot)
sudo certbot certonly --standalone -d mail.yourdomain.com
# /etc/postfix/main.cf
smtpd_tls_cert_file = /etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/mail.yourdomain.com/privkey.pem
sudo systemctl reload postfix
```
Prevención
- Always configure your SMTP client with `USE_TLS=True` (port 587) or `USE_SSL=True` (port 465) — never send credentials over plain text
- Use a TLS certificate from a public CA (Let's Encrypt) on your mail server, not a self-signed cert — client TLS failures are often silent
- Test your SMTP configuration with openssl before deploying to catch TLS handshake issues
- Enable SMTP debug logging in development to see the full session and verify STARTTLS is being negotiated
- Check MTA-STS policies for recipient domains before bulk sending to ensure your server meets their TLS requirements
- Use a TLS certificate from a public CA (Let's Encrypt) on your mail server, not a self-signed cert — client TLS failures are often silent
- Test your SMTP configuration with openssl before deploying to catch TLS handshake issues
- Enable SMTP debug logging in development to see the full session and verify STARTTLS is being negotiated
- Check MTA-STS policies for recipient domains before bulk sending to ensure your server meets their TLS requirements