Add fail-closed SES sender authentication
The From header and any raw-MIME Authentication-Results copies are
attacker-forgeable, so a forged email to apm@int.seahaven.com or
amazon_po@int.seahaven.com could create or mutate a WO/PO (INFRA-107,
CRITICAL). Both S3-triggered email processors now authenticate the
sender against the Authentication-Results header SES itself prepends
at delivery: only the topmost header is consulted, its authserv-id
must be amazonses.com, and it must carry dkim=pass for a domain in
the per-pipeline ALLOWED_DKIM_DOMAINS env var (comma-separated, set
in CDK so ops can adjust without code changes).
Allowlists come from live traffic observed 2026-07-15 on both ingest
buckets: WO mail arrives via the apm@ Google Groups forward, which
re-signs as seahaven.com (the hxgnsmartcloud.com signature does not
survive the forward); PO mail passes for amazon.coupahost.com.
amazonses.com also passes on PO mail but is deliberately excluded --
every SES customer's outbound mail passes for it.
Every failure path (env var unset, header missing or unparseable,
verdict fail, unaligned domain) rejects the email: a structured
warning with the reason and S3 key is logged and the record skipped
without erroring the invocation, so rejected mail causes no Lambda
retries or DLQ messages. Handler signatures and event sources are
unchanged.
Refs: INFRA-107
2026-07-15 18:53:46 -04:00
|
|
|
"""Unit tests for the fail-closed SES sender authentication (INFRA-107).
|
|
|
|
|
|
|
|
|
|
Fixtures mirror real SES-stamped headers observed on the two ingest
|
|
|
|
|
buckets on 2026-07-15: SES prepends a folded Authentication-Results
|
|
|
|
|
header with authserv-id amazonses.com and reports passing signers as
|
|
|
|
|
``dkim=pass header.i=@<domain>``.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
BODY = "\r\n\r\nWork Order 12345 assigned.\r\n"
|
|
|
|
|
|
|
|
|
|
# Folded exactly like real SES output (continuation lines, header.i form).
|
|
|
|
|
WO_SES_HEADER = (
|
|
|
|
|
"Authentication-Results: amazonses.com;\r\n"
|
|
|
|
|
" spf=pass (spfCheck: domain of seahaven.com designates 209.85.219.70 as"
|
|
|
|
|
" permitted sender) client-ip=209.85.219.70;"
|
|
|
|
|
" envelope-from=apm+bnc@seahaven.com; helo=mail-qv1-f70.google.com;\r\n"
|
|
|
|
|
" dkim=pass header.i=@seahaven.com;\r\n"
|
|
|
|
|
" dmarc=none header.from=hxgnsmartcloud.com;\r\n"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Real PO traffic carries two dkim=pass clauses; amazonses.com must not be
|
|
|
|
|
# sufficient on its own (every SES customer's mail passes for it).
|
|
|
|
|
PO_SES_HEADER = (
|
|
|
|
|
"Authentication-Results: amazonses.com;\r\n"
|
|
|
|
|
" spf=pass (spfCheck: domain of mail.coupahost.com designates"
|
|
|
|
|
" 54.240.41.238 as permitted sender) client-ip=54.240.41.238;\r\n"
|
|
|
|
|
" dkim=pass header.i=@amazonses.com;\r\n"
|
|
|
|
|
" dkim=pass header.i=@amazon.coupahost.com;\r\n"
|
|
|
|
|
" dmarc=pass header.from=amazon.coupahost.com;\r\n"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
FROM_TO = (
|
|
|
|
|
"From: APM <noreply@hxgnsmartcloud.com>\r\n"
|
|
|
|
|
"To: apm@int.seahaven.com\r\n"
|
|
|
|
|
"Subject: WO 12345\r\n"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def raw(*headers: str) -> bytes:
|
|
|
|
|
return ("".join(headers) + FROM_TO + BODY).encode()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestParseAuthenticationResults:
|
|
|
|
|
def test_ses_wo_header(self, ses_auth):
|
|
|
|
|
value = WO_SES_HEADER.split(":", 1)[1]
|
|
|
|
|
authserv_id, passing = ses_auth.parse_authentication_results(value)
|
|
|
|
|
assert authserv_id == "amazonses.com"
|
|
|
|
|
assert passing == frozenset({"seahaven.com"})
|
|
|
|
|
|
|
|
|
|
def test_ses_po_header_multiple_dkim_clauses(self, ses_auth):
|
|
|
|
|
value = PO_SES_HEADER.split(":", 1)[1]
|
|
|
|
|
authserv_id, passing = ses_auth.parse_authentication_results(value)
|
|
|
|
|
assert authserv_id == "amazonses.com"
|
|
|
|
|
assert passing == frozenset({"amazonses.com", "amazon.coupahost.com"})
|
|
|
|
|
|
|
|
|
|
def test_header_d_form(self, ses_auth):
|
|
|
|
|
_, passing = ses_auth.parse_authentication_results(
|
|
|
|
|
"amazonses.com; dkim=pass header.d=Example.COM."
|
|
|
|
|
)
|
|
|
|
|
assert passing == frozenset({"example.com"})
|
|
|
|
|
|
|
|
|
|
def test_case_insensitive_result(self, ses_auth):
|
|
|
|
|
_, passing = ses_auth.parse_authentication_results(
|
|
|
|
|
"amazonses.com; DKIM=Pass HEADER.I=@SeaHaven.COM"
|
|
|
|
|
)
|
|
|
|
|
assert passing == frozenset({"seahaven.com"})
|
|
|
|
|
|
|
|
|
|
def test_dkim_fail_yields_no_domains(self, ses_auth):
|
|
|
|
|
_, passing = ses_auth.parse_authentication_results(
|
|
|
|
|
"amazonses.com; dkim=fail header.i=@seahaven.com"
|
|
|
|
|
)
|
|
|
|
|
assert passing == frozenset()
|
|
|
|
|
|
|
|
|
|
def test_authserv_id_version_token(self, ses_auth):
|
|
|
|
|
authserv_id, _ = ses_auth.parse_authentication_results(
|
|
|
|
|
"amazonses.com 1; dkim=pass header.i=@seahaven.com"
|
|
|
|
|
)
|
|
|
|
|
assert authserv_id == "amazonses.com"
|
|
|
|
|
|
|
|
|
|
def test_garbage_value(self, ses_auth):
|
|
|
|
|
authserv_id, passing = ses_auth.parse_authentication_results(";;;")
|
|
|
|
|
assert authserv_id == ""
|
|
|
|
|
assert passing == frozenset()
|
|
|
|
|
|
2026-07-15 18:55:32 -04:00
|
|
|
def test_result_token_boundary(self, ses_auth):
|
|
|
|
|
# "pass-anything" / "passfail" must never be read as "pass".
|
|
|
|
|
for result in ("pass-fake", "passfail"):
|
|
|
|
|
_, passing = ses_auth.parse_authentication_results(
|
|
|
|
|
f"amazonses.com; dkim={result} header.i=@seahaven.com"
|
|
|
|
|
)
|
|
|
|
|
assert passing == frozenset()
|
|
|
|
|
|
|
|
|
|
def test_result_followed_by_comment(self, ses_auth):
|
|
|
|
|
_, passing = ses_auth.parse_authentication_results(
|
|
|
|
|
"amazonses.com; dkim=pass(good signature) header.i=@seahaven.com"
|
|
|
|
|
)
|
|
|
|
|
assert passing == frozenset({"seahaven.com"})
|
|
|
|
|
|
|
|
|
|
def test_quoted_domain_value(self, ses_auth):
|
|
|
|
|
_, passing = ses_auth.parse_authentication_results(
|
|
|
|
|
'amazonses.com; dkim=pass header.i="@seahaven.com"'
|
|
|
|
|
)
|
|
|
|
|
assert passing == frozenset({"seahaven.com"})
|
|
|
|
|
|
|
|
|
|
def test_folding_inside_dkim_clause(self, ses_auth):
|
|
|
|
|
_, passing = ses_auth.parse_authentication_results(
|
|
|
|
|
"amazonses.com;\r\n dkim=pass\r\n header.i=@seahaven.com"
|
|
|
|
|
)
|
|
|
|
|
assert passing == frozenset({"seahaven.com"})
|
|
|
|
|
|
Harden AR parsing and alarm on sender-auth rejects
The SES-stamped Authentication-Results value echoes attacker-controlled
SMTP-session tokens (envelope-from, helo, header.from) as their own
semicolon-delimited property clauses. A naive split(";") tore an RFC 5321
quoted-local-part MAIL FROM apart and manufactured a forged dkim=pass
clause, so a fully spoofed email was accepted on the genuinely
SES-stamped topmost header. Tokenise comment- and quoted-string-aware
(RFC 8601 / RFC 5322): strip CFWS comments, split clauses only on
semicolons outside a quoted-string, and fail closed on unbalanced
quotes/comments so a ';' inside a quoted pvalue can never start a clause.
Rejected mail returns normally (no error, no retry, no DLQ message), so a
signing-domain drift or a wrong allowlist would silently discard 100% of
legitimate mail while every alarm stayed green. Add a CloudWatch Logs
metric filter + alarm on the sender_auth_rejected warning to both stacks
so a false-reject storm pages instead of vanishing. This is also the
safety net for the WO seahaven.com allowlist assumption, which must be
validated against a live SES-stamped header (a plain Gmail auto-forward
re-signs under the sending Workspace domain, not seahaven.com).
Refs: INFRA-107
2026-07-15 19:13:55 -04:00
|
|
|
def test_quoted_semicolon_in_envelope_from_is_not_split(self, ses_auth):
|
|
|
|
|
# RFC 5321 quoted-local-part MAIL FROM can carry ';' and spaces; SES
|
|
|
|
|
# echoes it into envelope-from=. The ';' inside the quotes must stay
|
|
|
|
|
# part of the one envelope-from clause, never a synthetic dkim clause.
|
|
|
|
|
value = (
|
|
|
|
|
"amazonses.com; spf=pass client-ip=1.2.3.4;"
|
|
|
|
|
' envelope-from="x; dkim=pass header.i=@amazon.coupahost.com"@attacker.com;'
|
|
|
|
|
" helo=mail.attacker.com; dkim=fail header.i=@attacker.com;"
|
|
|
|
|
)
|
|
|
|
|
_, passing = ses_auth.parse_authentication_results(value)
|
|
|
|
|
assert passing == frozenset()
|
|
|
|
|
|
|
|
|
|
def test_quoted_pair_in_envelope_from_is_not_split(self, ses_auth):
|
|
|
|
|
# A quoted-pair (\") inside the quoted local part must not prematurely
|
|
|
|
|
# end the quoted-string and re-expose the smuggled tokens.
|
|
|
|
|
value = (
|
|
|
|
|
'amazonses.com; envelope-from="a\\"; dkim=pass header.d=seahaven.com"@evil.com;'
|
|
|
|
|
" dkim=fail header.i=@evil.com;"
|
|
|
|
|
)
|
|
|
|
|
_, passing = ses_auth.parse_authentication_results(value)
|
|
|
|
|
assert passing == frozenset()
|
|
|
|
|
|
|
|
|
|
def test_helo_injection_is_not_split(self, ses_auth):
|
|
|
|
|
# helo= is attacker-influenced too; a crafted value quoting a fake
|
|
|
|
|
# methodspec must not manufacture a passing clause.
|
|
|
|
|
value = (
|
|
|
|
|
'amazonses.com; helo="h; dkim=pass header.i=@seahaven.com";'
|
|
|
|
|
" dkim=fail header.i=@attacker.com;"
|
|
|
|
|
)
|
|
|
|
|
_, passing = ses_auth.parse_authentication_results(value)
|
|
|
|
|
assert passing == frozenset()
|
|
|
|
|
|
|
|
|
|
def test_comment_embedded_header_i_is_ignored(self, ses_auth):
|
|
|
|
|
# A CFWS comment carrying a fake header.i must be stripped before
|
|
|
|
|
# domain extraction, so only the real (failing) result is considered.
|
|
|
|
|
_, passing = ses_auth.parse_authentication_results(
|
|
|
|
|
"amazonses.com; dkim=fail (header.i=@seahaven.com) header.i=@attacker.com"
|
|
|
|
|
)
|
|
|
|
|
assert passing == frozenset()
|
|
|
|
|
|
|
|
|
|
def test_comment_hiding_semicolon_does_not_split(self, ses_auth):
|
|
|
|
|
# A ';' inside a comment is not a clause separator either.
|
|
|
|
|
_, passing = ses_auth.parse_authentication_results(
|
|
|
|
|
"amazonses.com; spf=pass (note: a; b) client-ip=1.2.3.4;"
|
|
|
|
|
" dkim=pass header.i=@seahaven.com"
|
|
|
|
|
)
|
|
|
|
|
assert passing == frozenset({"seahaven.com"})
|
|
|
|
|
|
|
|
|
|
def test_unbalanced_quote_fails_closed(self, ses_auth):
|
|
|
|
|
# An unterminated quoted-string is malformed; refuse to parse it so a
|
|
|
|
|
# dkim=pass clause "swallowed" by the runaway quote can't be salvaged
|
|
|
|
|
# (and, conversely, a runaway quote can't be used to mis-tokenise).
|
|
|
|
|
authserv_id, passing = ses_auth.parse_authentication_results(
|
|
|
|
|
'amazonses.com; envelope-from="oops@attacker.com;'
|
|
|
|
|
" dkim=pass header.i=@seahaven.com"
|
|
|
|
|
)
|
|
|
|
|
assert authserv_id == ""
|
|
|
|
|
assert passing == frozenset()
|
|
|
|
|
|
|
|
|
|
def test_unbalanced_comment_fails_closed(self, ses_auth):
|
|
|
|
|
# An unterminated comment is malformed and must fail closed.
|
|
|
|
|
authserv_id, passing = ses_auth.parse_authentication_results(
|
|
|
|
|
"amazonses.com; dkim=pass header.i=@seahaven.com (runaway comment"
|
|
|
|
|
)
|
|
|
|
|
assert authserv_id == ""
|
|
|
|
|
assert passing == frozenset()
|
|
|
|
|
|
Add fail-closed SES sender authentication
The From header and any raw-MIME Authentication-Results copies are
attacker-forgeable, so a forged email to apm@int.seahaven.com or
amazon_po@int.seahaven.com could create or mutate a WO/PO (INFRA-107,
CRITICAL). Both S3-triggered email processors now authenticate the
sender against the Authentication-Results header SES itself prepends
at delivery: only the topmost header is consulted, its authserv-id
must be amazonses.com, and it must carry dkim=pass for a domain in
the per-pipeline ALLOWED_DKIM_DOMAINS env var (comma-separated, set
in CDK so ops can adjust without code changes).
Allowlists come from live traffic observed 2026-07-15 on both ingest
buckets: WO mail arrives via the apm@ Google Groups forward, which
re-signs as seahaven.com (the hxgnsmartcloud.com signature does not
survive the forward); PO mail passes for amazon.coupahost.com.
amazonses.com also passes on PO mail but is deliberately excluded --
every SES customer's outbound mail passes for it.
Every failure path (env var unset, header missing or unparseable,
verdict fail, unaligned domain) rejects the email: a structured
warning with the reason and S3 key is logged and the record skipped
without erroring the invocation, so rejected mail causes no Lambda
retries or DLQ messages. Handler signatures and event sources are
unchanged.
Refs: INFRA-107
2026-07-15 18:53:46 -04:00
|
|
|
|
|
|
|
|
class TestEvaluateSenderAuthentication:
|
|
|
|
|
def test_ses_stamped_pass_accepted(self, ses_auth):
|
|
|
|
|
accepted, reason, detail = ses_auth.evaluate_sender_authentication(
|
|
|
|
|
raw(WO_SES_HEADER), {"seahaven.com"}
|
|
|
|
|
)
|
|
|
|
|
assert accepted
|
|
|
|
|
assert reason == "authenticated"
|
|
|
|
|
assert detail["matched_domains"] == ["seahaven.com"]
|
|
|
|
|
|
|
|
|
|
def test_po_pass_accepted_on_coupa_domain(self, ses_auth):
|
|
|
|
|
accepted, reason, _ = ses_auth.evaluate_sender_authentication(
|
|
|
|
|
raw(PO_SES_HEADER), {"amazon.coupahost.com"}
|
|
|
|
|
)
|
|
|
|
|
assert accepted
|
|
|
|
|
assert reason == "authenticated"
|
|
|
|
|
|
|
|
|
|
def test_amazonses_identity_alone_is_not_allowlisted(self, ses_auth):
|
|
|
|
|
# Any SES customer's outbound mail passes DKIM for amazonses.com,
|
|
|
|
|
# so a pass for it must not satisfy a coupahost-only allowlist.
|
|
|
|
|
forged_via_ses = (
|
|
|
|
|
"Authentication-Results: amazonses.com;\r\n"
|
|
|
|
|
" spf=pass client-ip=54.240.41.1;\r\n"
|
|
|
|
|
" dkim=pass header.i=@amazonses.com;\r\n"
|
|
|
|
|
)
|
|
|
|
|
accepted, reason, _ = ses_auth.evaluate_sender_authentication(
|
|
|
|
|
raw(forged_via_ses), {"amazon.coupahost.com"}
|
|
|
|
|
)
|
|
|
|
|
assert not accepted
|
|
|
|
|
assert reason == "dkim_domain_not_allowlisted"
|
|
|
|
|
|
|
|
|
|
def test_forged_ar_below_failing_ses_header_rejected(self, ses_auth):
|
|
|
|
|
# SES's (topmost) header says dkim=fail; the attacker smuggled a
|
|
|
|
|
# perfect-looking AR header inside the message. Only the topmost
|
|
|
|
|
# header may be consulted.
|
|
|
|
|
ses_fail = (
|
|
|
|
|
"Authentication-Results: amazonses.com;\r\n"
|
|
|
|
|
" spf=fail client-ip=203.0.113.7;\r\n"
|
|
|
|
|
" dkim=fail header.i=@seahaven.com;\r\n"
|
|
|
|
|
" dmarc=fail header.from=hxgnsmartcloud.com;\r\n"
|
|
|
|
|
)
|
|
|
|
|
forged = (
|
|
|
|
|
"Authentication-Results: amazonses.com;\r\n"
|
|
|
|
|
" dkim=pass header.i=@seahaven.com;\r\n"
|
|
|
|
|
)
|
|
|
|
|
accepted, reason, _ = ses_auth.evaluate_sender_authentication(
|
|
|
|
|
raw(ses_fail, forged), {"seahaven.com"}
|
|
|
|
|
)
|
|
|
|
|
assert not accepted
|
|
|
|
|
assert reason == "no_passing_dkim_signature"
|
|
|
|
|
|
|
|
|
|
def test_missing_ar_header_rejected(self, ses_auth):
|
|
|
|
|
accepted, reason, _ = ses_auth.evaluate_sender_authentication(
|
|
|
|
|
raw(), {"seahaven.com"}
|
|
|
|
|
)
|
|
|
|
|
assert not accepted
|
|
|
|
|
assert reason == "authentication_results_missing"
|
|
|
|
|
|
|
|
|
|
def test_untrusted_authserv_id_rejected(self, ses_auth):
|
|
|
|
|
attacker_ar = (
|
|
|
|
|
"Authentication-Results: mail.attacker.example;\r\n"
|
|
|
|
|
" dkim=pass header.i=@seahaven.com;\r\n"
|
|
|
|
|
)
|
|
|
|
|
accepted, reason, _ = ses_auth.evaluate_sender_authentication(
|
|
|
|
|
raw(attacker_ar), {"seahaven.com"}
|
|
|
|
|
)
|
|
|
|
|
assert not accepted
|
|
|
|
|
assert reason == "untrusted_authserv_id"
|
|
|
|
|
|
|
|
|
|
def test_unaligned_domain_rejected(self, ses_auth):
|
|
|
|
|
evil = (
|
|
|
|
|
"Authentication-Results: amazonses.com;\r\n"
|
|
|
|
|
" dkim=pass header.i=@evil.example.com;\r\n"
|
|
|
|
|
)
|
|
|
|
|
accepted, reason, _ = ses_auth.evaluate_sender_authentication(
|
|
|
|
|
raw(evil), {"seahaven.com"}
|
|
|
|
|
)
|
|
|
|
|
assert not accepted
|
|
|
|
|
assert reason == "dkim_domain_not_allowlisted"
|
|
|
|
|
|
|
|
|
|
def test_lookalike_domain_rejected(self, ses_auth):
|
|
|
|
|
# Substring containment must not match: notseahaven.com != seahaven.com
|
|
|
|
|
lookalike = (
|
|
|
|
|
"Authentication-Results: amazonses.com;\r\n"
|
|
|
|
|
" dkim=pass header.i=@notseahaven.com;\r\n"
|
|
|
|
|
)
|
|
|
|
|
accepted, _, _ = ses_auth.evaluate_sender_authentication(
|
|
|
|
|
raw(lookalike), {"seahaven.com"}
|
|
|
|
|
)
|
|
|
|
|
assert not accepted
|
|
|
|
|
|
|
|
|
|
def test_empty_allowlist_fails_closed(self, ses_auth):
|
|
|
|
|
accepted, reason, _ = ses_auth.evaluate_sender_authentication(
|
|
|
|
|
raw(WO_SES_HEADER), frozenset()
|
|
|
|
|
)
|
|
|
|
|
assert not accepted
|
|
|
|
|
assert reason == "allowlist_not_configured"
|
|
|
|
|
|
Harden AR parsing and alarm on sender-auth rejects
The SES-stamped Authentication-Results value echoes attacker-controlled
SMTP-session tokens (envelope-from, helo, header.from) as their own
semicolon-delimited property clauses. A naive split(";") tore an RFC 5321
quoted-local-part MAIL FROM apart and manufactured a forged dkim=pass
clause, so a fully spoofed email was accepted on the genuinely
SES-stamped topmost header. Tokenise comment- and quoted-string-aware
(RFC 8601 / RFC 5322): strip CFWS comments, split clauses only on
semicolons outside a quoted-string, and fail closed on unbalanced
quotes/comments so a ';' inside a quoted pvalue can never start a clause.
Rejected mail returns normally (no error, no retry, no DLQ message), so a
signing-domain drift or a wrong allowlist would silently discard 100% of
legitimate mail while every alarm stayed green. Add a CloudWatch Logs
metric filter + alarm on the sender_auth_rejected warning to both stacks
so a false-reject storm pages instead of vanishing. This is also the
safety net for the WO seahaven.com allowlist assumption, which must be
validated against a live SES-stamped header (a plain Gmail auto-forward
re-signs under the sending Workspace domain, not seahaven.com).
Refs: INFRA-107
2026-07-15 19:13:55 -04:00
|
|
|
def test_envelope_from_clause_injection_rejected(self, ses_auth):
|
|
|
|
|
# Full forged email: attacker's quoted MAIL FROM is echoed by SES into
|
|
|
|
|
# its own (topmost, genuinely SES-stamped) Authentication-Results as
|
|
|
|
|
# envelope-from=, trying to smuggle a dkim=pass for an allowlisted
|
|
|
|
|
# domain. The real DKIM verdict is fail. Must fail closed.
|
|
|
|
|
injected = (
|
|
|
|
|
"Authentication-Results: amazonses.com;\r\n"
|
|
|
|
|
" spf=pass (spfCheck: domain of attacker.com designates 1.2.3.4 as"
|
|
|
|
|
" permitted sender) client-ip=1.2.3.4;\r\n"
|
|
|
|
|
' envelope-from="x; dkim=pass header.i=@amazon.coupahost.com"@attacker.com;'
|
|
|
|
|
" helo=mail.attacker.com;\r\n"
|
|
|
|
|
" dkim=fail header.i=@attacker.com;\r\n"
|
|
|
|
|
" dmarc=fail header.from=attacker.com;\r\n"
|
|
|
|
|
)
|
|
|
|
|
accepted, reason, _ = ses_auth.evaluate_sender_authentication(
|
|
|
|
|
raw(injected), {"amazon.coupahost.com", "seahaven.com"}
|
|
|
|
|
)
|
|
|
|
|
assert not accepted
|
|
|
|
|
assert reason == "no_passing_dkim_signature"
|
|
|
|
|
|
Add fail-closed SES sender authentication
The From header and any raw-MIME Authentication-Results copies are
attacker-forgeable, so a forged email to apm@int.seahaven.com or
amazon_po@int.seahaven.com could create or mutate a WO/PO (INFRA-107,
CRITICAL). Both S3-triggered email processors now authenticate the
sender against the Authentication-Results header SES itself prepends
at delivery: only the topmost header is consulted, its authserv-id
must be amazonses.com, and it must carry dkim=pass for a domain in
the per-pipeline ALLOWED_DKIM_DOMAINS env var (comma-separated, set
in CDK so ops can adjust without code changes).
Allowlists come from live traffic observed 2026-07-15 on both ingest
buckets: WO mail arrives via the apm@ Google Groups forward, which
re-signs as seahaven.com (the hxgnsmartcloud.com signature does not
survive the forward); PO mail passes for amazon.coupahost.com.
amazonses.com also passes on PO mail but is deliberately excluded --
every SES customer's outbound mail passes for it.
Every failure path (env var unset, header missing or unparseable,
verdict fail, unaligned domain) rejects the email: a structured
warning with the reason and S3 key is logged and the record skipped
without erroring the invocation, so rejected mail causes no Lambda
retries or DLQ messages. Handler signatures and event sources are
unchanged.
Refs: INFRA-107
2026-07-15 18:53:46 -04:00
|
|
|
|
|
|
|
|
class TestAuthenticateInboundEmail:
|
|
|
|
|
S3_KEY = "s3://bucket/inbound/abc123"
|
|
|
|
|
|
|
|
|
|
def test_accepts_with_configured_allowlist(self, ses_auth, monkeypatch):
|
|
|
|
|
monkeypatch.setenv("ALLOWED_DKIM_DOMAINS", "seahaven.com")
|
|
|
|
|
assert ses_auth.authenticate_inbound_email(raw(WO_SES_HEADER), self.S3_KEY)
|
|
|
|
|
|
|
|
|
|
def test_allowlist_is_comma_separated_and_normalized(self, ses_auth, monkeypatch):
|
2026-07-15 18:55:32 -04:00
|
|
|
# Stray spaces, case, a leading @, and a trailing dot all normalize.
|
|
|
|
|
monkeypatch.setenv("ALLOWED_DKIM_DOMAINS", " Other.Example , @SEAHAVEN.com. ,")
|
Add fail-closed SES sender authentication
The From header and any raw-MIME Authentication-Results copies are
attacker-forgeable, so a forged email to apm@int.seahaven.com or
amazon_po@int.seahaven.com could create or mutate a WO/PO (INFRA-107,
CRITICAL). Both S3-triggered email processors now authenticate the
sender against the Authentication-Results header SES itself prepends
at delivery: only the topmost header is consulted, its authserv-id
must be amazonses.com, and it must carry dkim=pass for a domain in
the per-pipeline ALLOWED_DKIM_DOMAINS env var (comma-separated, set
in CDK so ops can adjust without code changes).
Allowlists come from live traffic observed 2026-07-15 on both ingest
buckets: WO mail arrives via the apm@ Google Groups forward, which
re-signs as seahaven.com (the hxgnsmartcloud.com signature does not
survive the forward); PO mail passes for amazon.coupahost.com.
amazonses.com also passes on PO mail but is deliberately excluded --
every SES customer's outbound mail passes for it.
Every failure path (env var unset, header missing or unparseable,
verdict fail, unaligned domain) rejects the email: a structured
warning with the reason and S3 key is logged and the record skipped
without erroring the invocation, so rejected mail causes no Lambda
retries or DLQ messages. Handler signatures and event sources are
unchanged.
Refs: INFRA-107
2026-07-15 18:53:46 -04:00
|
|
|
assert ses_auth.authenticate_inbound_email(raw(WO_SES_HEADER), self.S3_KEY)
|
|
|
|
|
|
|
|
|
|
def test_env_var_unset_fails_closed(self, ses_auth, monkeypatch, caplog):
|
|
|
|
|
monkeypatch.delenv("ALLOWED_DKIM_DOMAINS", raising=False)
|
|
|
|
|
assert not ses_auth.authenticate_inbound_email(raw(WO_SES_HEADER), self.S3_KEY)
|
|
|
|
|
assert "allowlist_not_configured" in caplog.text
|
|
|
|
|
assert self.S3_KEY in caplog.text
|
|
|
|
|
|
|
|
|
|
def test_rejection_logs_reason_and_key(self, ses_auth, monkeypatch, caplog):
|
|
|
|
|
monkeypatch.setenv("ALLOWED_DKIM_DOMAINS", "seahaven.com")
|
|
|
|
|
assert not ses_auth.authenticate_inbound_email(raw(), self.S3_KEY)
|
|
|
|
|
assert "sender_auth_rejected" in caplog.text
|
|
|
|
|
assert "authentication_results_missing" in caplog.text
|
|
|
|
|
assert self.S3_KEY in caplog.text
|