Gửi SMTP bằng Python: STARTTLS trên 587 hay TLS trên 465?
STARTTLS trên 587 và TLS ngầm trên 465 đều chuyển một lần gửi từ Python, nhưng chỉ một cái khớp với relay của bạn. Hãy so sánh bắt tay và các kiểu lỗi.
Cả hai chế độ kết nối đều chuyển cùng một thông điệp đến cùng một tài khoản relay, và khác biệt nằm ở chỗ TLS bắt đầu, không phải ở thứ được gửi. Một cái mở bằng văn bản rõ rồi nâng cấp ngay trong kết nối; cái kia được mã hóa từ byte đầu tiên. Chính khác biệt đó quyết định chính sách relay nghiêm ngặt sẽ chấp nhận gì, thiết bị trung gian có thể quan sát gì, và bạn sẽ dành cả buổi chiều đọc thông báo lỗi nào. Đây là cách tôi chọn giữa hai bên khi một ứng dụng trong mạng riêng giao thông điệp cho dịch vụ gửi nội bộ.
Hai chế độ thực sự tách nhau ở đâu
Nhiệm vụ giống nhau trong cả hai trường hợp: xác thực như một tài khoản đã được phê duyệt, nêu người gửi được phép, liệt kê người nhận và chờ relay nhận trách nhiệm. Điều thay đổi là thứ tự thao tác. Cổng 587 bắt đầu như một phiên SMTP văn bản rõ, quảng bá STARTTLS trong phản hồi EHLO và chỉ nâng cấp nếu máy khách yêu cầu. Cổng 465 chờ một lần bắt tay TLS trước khi bất kỳ lệnh SMTP nào được nói ra.
Gửi và relay là hai công việc khác nhau, nên số cổng không phải thói quen có thể hoán đổi. RFC 6409 mô tả việc gửi thông điệp như một dịch vụ riêng, và RFC 8314 bao gồm biến thể TLS ngầm mà hầu hết relay hiện đại cung cấp trên 465. Nếu relay của bạn ghi nhận một trong hai, tài liệu đó đứng trên mọi sở thích bạn mang theo từ nơi làm cũ.
STARTTLS trên 587 mang lại gì cho bạn
Việc nâng cấp diễn ra bên trong cuộc trò chuyện SMTP, nên relay có thể quảng bá nó và máy khách có thể từ chối tiếp tục nếu không có nó. Trong Python, starttls() ném ngoại lệ khi máy chủ không cung cấp phần mở rộng đó, biến một nỗ lực hạ cấp thành thất bại thấy được thay vì đăng nhập văn bản rõ âm thầm. Chính hành vi đó là lập luận chính cho chế độ này, và đáng để dựa vào nó một cách có chủ đích chứ không phải tình cờ.
Cửa sổ phơi nhiễm nhỏ nhưng có thật: trao đổi EHLO ban đầu đọc được, và không có gì trước khi nâng cấp mang thông tin xác thực. Trên một đường riêng giữa mạng con ứng dụng và relay nội bộ, cửa sổ đó thường chấp nhận được. Trên một đường bạn không kiểm soát, chính điều đó khiến một số nhóm khăng khăng dùng chế độ kia.
Thiết bị chặn bắt loại bỏ khả năng STARTTLS tạo ra một lần gửi thất bại, không phải một lần lùi về. Đó là kết quả bạn muốn, và cũng là cách nhanh nhất để phát hiện rằng có gì đó đang viết lại SMTP trên đường truyền.
TLS ngầm trên 465 thay đổi điều gì
Không có giai đoạn văn bản rõ nào cả. Máy khách thực hiện bắt tay TLS ngay khi socket mở, xác thực chứng chỉ với kho tin cậy của mình, và chỉ sau đó mới trao đổi EHLO, AUTH và thông điệp. Không gì có thể hạ cấp phiên vì không tồn tại trạng thái văn bản rõ để thương lượng, và điều đó loại bỏ cả một lớp thất bại do chặn bắt.
Cái giá là khả năng chẩn đoán. Một relay trả lời trên 465 bằng SMTP văn bản rõ, hoặc một bộ cân bằng tải kết thúc TLS với sai tên chứng chỉ, thất bại ở bắt tay và cho bạn lỗi SSL thay vì lỗi thư. Khi điều đó xảy ra, vấn đề nằm dưới ứng dụng, và không lượng dựng thông điệp nào sửa được.
Một hàm gửi, hai cách vận chuyển
Mã hầu như không khác nhau giữa hai chế độ, nên lựa chọn nên được đưa ra từ chính sách relay chứ không phải sự tiện lợi của thư viện. Xuất cấu hình trước, rồi chạy một tập lệnh duy nhất chọn cách vận chuyển theo cổng đã cấu hình.
export SMTP_HOST=smtp.relay.internal export SMTP_PORT=587 export SMTP_USER=svc-notify export [email protected] export [email protected] export SMTP_PASSWORD="$(cat /run/secrets/smtp_password)"
import os, smtplib, ssl
from email.message import EmailMessage
ctx = ssl.create_default_context()
msg = EmailMessage()
msg["From"] = os.environ["MAIL_FROM"]
msg["To"] = os.environ["MAIL_TEST_TO"]
msg["Subject"] = "relay submission test"
msg.set_content("One authorized test message from the app.")
host = os.environ["SMTP_HOST"]
port = int(os.environ.get("SMTP_PORT", "587"))
if port == 465:
client = smtplib.SMTP_SSL(host, port, context=ctx, timeout=15)
else:
client = smtplib.SMTP(host, port, timeout=15)
client.ehlo()
client.starttls(context=ctx)
client.ehlo()
with client:
client.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"])
refused = client.send_message(msg)
print("refused:", refused)
create_default_context() xác minh chuỗi và tên máy chủ, nên CA riêng phải được cài trong kho tin cậy thay vì bị bỏ qua bằng một ngữ cảnh chưa xác minh. send_message() trả về một từ điển những người nhận mà relay từ chối; từ điển rỗng nghĩa là nó đã chấp nhận tất cả. Thời gian chờ mười lăm giây là điểm khởi đầu, không phải giá trị đã tinh chỉnh cho mạng của bạn.
Giữ mật khẩu ngoài image và ngoài nhật ký. Một bí mật được gắn khi chạy và đọc vào môi trường là ổn; sao chép môi trường vào một dòng nhật ký thì không, và dấu vết gỡ lỗi của trao đổi AUTH cũng không.
Chọn theo chính sách relay và đường truyền, không theo thói quen
Ba điều kiện quyết định. Thứ nhất, relay thực sự quảng bá gì: một dịch vụ chỉ lắng nghe trên 587 với STARTTLS không thể tiếp cận trên 465, dù bạn thích TLS ngầm đến đâu. Thứ hai, đường giữa ứng dụng và relay có đủ tin cậy để chấp nhận một trao đổi EHLO đọc được hay không. Thứ ba, có gì ở giữa viết lại hoặc proxy SMTP hay không, vì điều đó quyết định rủi ro hạ cấp là lý thuyết hay thực tế.
Khi relay cung cấp cả hai, tôi chọn 465 cho dịch vụ chạy không giám sát và 587 cho dịch vụ có người theo dõi lần triển khai đầu tiên, chỉ vì các thông báo lỗi chỉ vào những tầng khác nhau. Sở thích đó là của tôi và nó không phải chính sách.
Dù bạn chọn gì, hãy ghi lại cổng, vật liệu tin cậy và miền người gửi cùng với việc triển khai. Sau đó gửi một thông điệp thử nghiệm được cấp phép và xác nhận relay đã chấp nhận nó, giữ kết quả gửi và mọi báo cáo phân phối sau đó như những bản ghi riêng. Việc bàn giao thành công cho relay là điểm kết thúc trách nhiệm của ứng dụng, không phải bằng chứng thông điệp đã đến hộp thư.