두 연결 방식은 같은 메시지를 같은 릴레이 계정으로 나르며, 차이는 무엇이 전송되는지가 아니라 TLS가 어디서 시작하는지에 있습니다. 하나는 평문으로 열려 연결 안에서 승격되고, 다른 하나는 첫 바이트부터 암호화됩니다. 이 하나의 차이가 엄격한 릴레이 정책이 무엇을 받아들일지, 중간 장비가 무엇을 관찰할 수 있는지, 그리고 어떤 오류 메시지를 반나절 동안 읽게 될지를 결정합니다. 사설 네트워크의 애플리케이션이 내부 전송 서비스에 메시지를 넘길 때 제가 둘 중 하나를 고르는 방식을 보여 드립니다.

두 방식이 실제로 갈라지는 지점

두 경우 모두 작업은 같습니다. 승인된 계정으로 인증하고, 허용된 발신자를 밝히고, 수신자를 나열하고, 릴레이가 책임을 인수하기를 기다립니다. 달라지는 것은 작업의 순서입니다. 포트 587은 평문 SMTP 세션으로 시작해 EHLO 응답에서 STARTTLS를 알리고, 클라이언트가 요청할 때만 승격됩니다. 포트 465는 어떤 SMTP 명령도 말하기 전에 TLS 핸드셰이크를 기대합니다.

전송과 릴레이는 다른 일이므로 포트 번호는 서로 바꿔 쓸 습관이 아닙니다. RFC 6409는 메시지 전송을 별도의 서비스로 설명하고, RFC 8314는 대부분의 현대 릴레이가 465에서 제공하는 암시적 TLS 방식을 다룹니다. 릴레이가 둘 중 하나를 문서화한다면 그 문서는 이전 직장에서 얻은 어떤 선호보다 우선합니다.

587의 STARTTLS가 주는 것

승격은 SMTP 대화 안에서 일어나므로 릴레이는 이를 알릴 수 있고 클라이언트는 그것 없이 계속하기를 거부할 수 있습니다. Python에서는 서버가 그 확장을 제공하지 않을 때 starttls()가 예외를 던지며, 이는 다운그레이드 시도를 조용한 평문 로그인이 아니라 눈에 보이는 실패로 바꿉니다. 이 동작이 이 방식의 핵심 근거이며, 우연이 아니라 의도적으로 그것에 기대는 것이 좋습니다.

노출 창은 작지만 실제입니다. 초기 EHLO 교환은 읽을 수 있고, 승격 전에는 아무것도 자격 증명을 나르지 않습니다. 애플리케이션 서브넷과 내부 릴레이 사이의 사설 경로에서는 그 창이 보통 받아들여집니다. 여러분이 통제하지 않는 경로에서는 바로 그것이 일부 팀이 다른 방식을 고집하는 이유입니다.

STARTTLS 기능을 제거하는 차단 장비는 대체 경로가 아니라 실패한 전송을 만듭니다. 그것이 원하는 결과이며, 경로에서 무언가가 SMTP를 다시 쓰고 있음을 알아내는 가장 빠른 방법이기도 합니다.

465의 암시적 TLS가 바꾸는 것

평문 단계가 전혀 없습니다. 클라이언트는 소켓이 열리자마자 TLS 핸드셰이크를 수행하고, 자체 신뢰 저장소에 대해 인증서를 검증한 다음에야 EHLO, AUTH, 메시지를 교환합니다. 협상할 평문 상태가 없으므로 세션을 다운그레이드할 수 없고, 이는 가로채기 실패의 한 부류를 통째로 없앱니다.

그 대가는 진단 가능성입니다. 465에서 평문 SMTP로 응답하는 릴레이, 또는 잘못된 인증서 이름으로 TLS를 종료하는 로드 밸런서는 핸드셰이크에서 실패하고 메일 오류 대신 SSL 오류를 줍니다. 그런 일이 생기면 문제는 애플리케이션 아래에 있으며, 메시지를 아무리 잘 구성해도 고쳐지지 않습니다.

하나의 전송 함수, 두 개의 전송 수단

코드는 두 방식 사이에서 거의 다르지 않으므로, 선택은 라이브러리의 편의가 아니라 릴레이 정책에서 이루어져야 합니다. 먼저 설정을 내보내고, 그다음 구성된 포트에서 전송 수단을 고르는 단일 스크립트를 실행하십시오.

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()는 체인과 호스트 이름을 검증하므로 사설 CA는 검증하지 않은 컨텍스트로 우회하지 말고 신뢰 저장소에 설치해야 합니다. send_message()는 릴레이가 거부한 수신자의 사전을 반환하며, 빈 사전은 모두 받아들였다는 뜻입니다. 열다섯 초 타임아웃은 출발점이지 여러분의 네트워크에 맞춰 조정된 값이 아닙니다.

비밀번호는 이미지 밖에, 로그 밖에 두십시오. 런타임에 마운트되어 환경으로 읽히는 비밀은 괜찮지만, 환경을 로그 줄에 그대로 찍는 것은 안 되며 AUTH 교환의 디버그 추적도 마찬가지입니다.

습관이 아니라 릴레이 정책과 경로로 고르십시오

세 가지 조건이 결정합니다. 첫째, 릴레이가 실제로 무엇을 알리는가입니다. 587에서 STARTTLS로만 수신하는 서비스는 암시적 TLS를 아무리 선호해도 465로 도달할 수 없습니다. 둘째, 애플리케이션과 릴레이 사이의 경로가 읽을 수 있는 EHLO 교환을 받아들일 만큼 신뢰할 수 있는가입니다. 셋째, 중간에서 무언가가 SMTP를 다시 쓰거나 프록시하는가이며, 이것이 다운그레이드 위험을 이론적인지 실제적인지 결정합니다.

릴레이가 둘 다 제공할 때 저는 무인으로 도는 서비스에는 465를, 사람이 첫 배포를 지켜보는 서비스에는 587을 택합니다. 실패 메시지가 서로 다른 계층을 가리킨다는 이유만으로 그렇게 합니다. 그 선호는 제 것이며 정책은 아닙니다.

무엇을 고르든 포트, 신뢰 자료, 발신자 도메인을 배포와 함께 기록하십시오. 그런 다음 승인된 테스트 메시지를 하나 보내고 릴레이가 받아들였는지 확인하며, 전송 결과와 이후의 배달 보고서는 별도의 기록으로 보관하십시오. 릴레이로의 성공적인 인계는 애플리케이션 책임의 끝이며, 메시지가 받은편지함에 도달했다는 증거는 아닙니다.

관련 글