Enforce outbound DKIM and SRS policy
Debian Trixie package bundle / packages (push) Successful in 21m55s

This commit is contained in:
Mario Fetka
2026-07-27 07:05:23 +02:00
parent d69c29a9a0
commit 4d157aeecb
9 changed files with 121 additions and 44 deletions
+4 -1
View File
@@ -30,7 +30,10 @@ one-off files in `/tmp`:
relay hostname on non-standard ports. It proves verified STARTTLS without
authentication, verified implicit TLS with optional authentication, a
temporary `535` failure, durable Queue retention, credential repair and
explicit retry. The exact SMTP configuration is restored afterward.
explicit retry. Every probe enters through the trusted internal-relay
listener, as a Mailman return message would, and the captured external
message must contain Bongo's DKIM signature. The exact SMTP configuration
is restored afterward.
- `smtp-internal-relay-check.py` temporarily lowers the trusted port 26
limits, verifies allowed and denied source addresses plus connection,
recipient, message, and byte limits, then restores the exact SMTP
+6 -1
View File
@@ -380,6 +380,10 @@ def perform_test() -> None:
SMTP07.replace_smtp_configuration(original)
if originally_active:
restart_bongo()
# A delivery attempt may race with the first cleanup while the
# temporary relay configuration is still active. Once the
# original service is back, remove any remaining SMTP-09 entry.
cleanup_marked_queue()
if fixture_started:
SMTP4DEV.stop(FIXTURE_ROOT)
PASSWORD_FILE.unlink(missing_ok=True)
@@ -388,7 +392,8 @@ def perform_test() -> None:
print(
"SMTP-09 PASS hostname=localhost ports=12525/12465 "
"starttls=no-auth implicit-tls=auth failure=retained recovery=delivered "
"dkim=verified queue-clean=yes config-restored=yes"
"trusted-port26=dkim-signed dkim=verified "
"queue-clean=yes config-restored=yes"
)
+10
View File
@@ -31,6 +31,16 @@ The relevant `smtp` configuration keys are:
* `srs_forward`, `srs_reverse`, `srs_domain`, and `srs_secret_file` control
SRS forwarding and bounce reversal.
Every delivery which leaves Bongo for an external SMTP peer is authenticated
before transmission. Local, authenticated, trusted-internal-relay and
Bongo-generated bounce traffic receives a Bongo DKIM signature. This includes
mail returned by Mailman over the restricted internal SMTP listener and mail
sent through a system or per-user relayhost. An unauthenticated incoming
message forwarded to another external domain receives an SRS envelope sender
instead; Bongo defers that delivery if SRS is disabled or unusable rather than
leaking the original sender unchanged. Internal Store delivery and configured
LMTP delivery do not add DKIM or SRS.
The SRS secret and DKIM private keys must not be readable by other users. SRS
addresses are envelope addresses such as `SRS0=...@example.org`; SRS is not a
message-header format.
+1 -1
View File
@@ -136,7 +136,7 @@ inputs and outputs is absent from the ZIP.
| SMTP-07 | Internet delivery prefers STARTTLS and follows opportunistic fallback | [PASS](test-evidence/0.7-r1.md#smtp-07) | | |
| SMTP-08 | Required TLS and `REQUIRETLS` defer rather than downgrade | [PASS](test-evidence/0.7-r1.md#smtp-08) | | |
| SMTP-27 | DANE takes precedence; MTA-STS enforce/testing/cache and MX wildcard policy through complete BIND/Unbound, Technitium/Technitium, and PowerDNS/PowerDNS Recursor DNS stacks | [PASS](test-evidence/0.7-r1.md#smtp-27) | | |
| SMTP-09 | Relayhost hostname, port, TLS, optional authentication, failure, and recovery | | | |
| SMTP-09 | Trusted port-26/Mailman-style return receives DKIM through relayhost hostname, port, TLS, optional authentication, failure, and recovery | | | |
| SMTP-10 | Open-relay tests reject unauthenticated/untrusted third-party relay | | | |
| SMTP-11 | `SIZE` exact limit, over-limit, absent size, and disk-space responses | | | |
| SMTP-12 | `PIPELINING` ordering and error recovery | | | |
+9
View File
@@ -34,6 +34,15 @@ SMTPOutboundIdentityTrusted(unsigned long flags, const char *auth_sender)
auth_sender[0] != '-';
}
int
SMTPOutboundRequiresDKIM(unsigned long flags, const char *auth_sender,
const char *sender)
{
return SMTPOutboundIdentityTrusted(flags, auth_sender) ||
sender == NULL || sender[0] == '\0' ||
(sender[0] == '-' && sender[1] == '\0');
}
int
SMTPOutboundNeedsSRS(unsigned long flags, const char *auth_sender,
const char *sender)
+2
View File
@@ -23,6 +23,8 @@
#define BONGO_SMTP_OUTBOUND_POLICY_H
int SMTPOutboundIdentityTrusted(unsigned long flags, const char *auth_sender);
int SMTPOutboundRequiresDKIM(unsigned long flags, const char *auth_sender,
const char *sender);
int SMTPOutboundNeedsSRS(unsigned long flags, const char *auth_sender,
const char *sender);
+78 -41
View File
@@ -232,7 +232,7 @@ ParseClientMetadata(char *line, SMTPClient *queue)
static BongoMailAuthStatus
GetEnvelopeSender(const SMTPClient *queue, char *rewritten, size_t rewritten_size,
const char **sender)
const char **sender, BOOL externalTransport)
{
BOOL forwarded;
@@ -241,11 +241,22 @@ GetEnvelopeSender(const SMTPClient *queue, char *rewritten, size_t rewritten_siz
}
if (queue->sender == NULL) return BONGO_MAILAUTH_PROTOCOL_ERROR;
*sender = strcmp(queue->sender, "-") == 0 ? "" : queue->sender;
if (!externalTransport) {
return BONGO_MAILAUTH_OK;
}
forwarded = SMTPOutboundNeedsSRS(
(unsigned long) queue->flags, queue->authSender, queue->sender);
if (!MailAuth.srs_forward || !forwarded) {
if (!forwarded) {
return BONGO_MAILAUTH_OK;
}
/*
* An unauthenticated Internet message which is forwarded externally
* must not leave Bongo with its original envelope sender. Deferring is
* safer than silently breaking SPF/DMARC when SRS is disabled.
*/
if (!MailAuth.srs_forward) {
return BONGO_MAILAUTH_INVALID_ARGUMENT;
}
if (MailAuth.srs_domain == NULL || MailAuth.srs_domain[0] == '\0' ||
MailAuth.srs_secret_file == NULL || MailAuth.srs_secret_file[0] == '\0') {
return BONGO_MAILAUTH_INVALID_ARGUMENT;
@@ -282,21 +293,21 @@ GenerateDKIMSignature(SMTPClient *queue)
const char *domain;
const char *at;
char keyPath[XPL_MAX_PATH];
char *messageLine = NULL;
unsigned long messageLineSize = 0;
unsigned char messageChunk[8192];
long long remaining;
BongoDKIMSigner *signer = NULL;
BongoMailAuthStatus status;
queue->dkimPrepared = TRUE;
queue->hasDKIMSignature = FALSE;
/* External submission providers apply their own aligned DKIM signature. */
if (!MailAuth.dkim_sign_outgoing || queue->hasExternalRelay) {
/* Sign local, authenticated, explicitly trusted relay mail, and DSNs. */
if (!SMTPOutboundRequiresDKIM(
(unsigned long) queue->flags, queue->authSender,
queue->sender)) {
return BONGO_MAILAUTH_OK;
}
/* Sign only local, authenticated, or explicitly trusted relay mail. */
if (!SMTPOutboundIdentityTrusted(
(unsigned long) queue->flags, queue->authSender)) {
return BONGO_MAILAUTH_OK;
if (!MailAuth.dkim_sign_outgoing) {
return BONGO_MAILAUTH_INVALID_ARGUMENT;
}
domain = MailAuth.dkim_signing_domain;
if (domain == NULL || domain[0] == '\0') {
@@ -327,27 +338,24 @@ GenerateDKIMSignature(SMTPClient *queue)
goto cleanup;
}
remaining = atoll(&queue->line[5]);
if (remaining < 0) {
status = BONGO_MAILAUTH_PROTOCOL_ERROR;
goto cleanup;
}
while (remaining > 0) {
size_t lineLength;
size_t consumed;
size_t chunk = remaining < (long long) sizeof(messageChunk)
? (size_t) remaining : sizeof(messageChunk);
ConnReadToAllocatedBuffer(queue->conn, &messageLine,
&messageLineSize);
if (messageLine == NULL) {
if (ConnReadCount(queue->conn, messageChunk, (int) chunk) !=
(int) chunk) {
status = BONGO_MAILAUTH_IO_ERROR;
goto cleanup;
}
lineLength = strlen(messageLine);
status = BongoMailAuthDKIMSignerUpdate(signer, messageLine, lineLength);
if (status == BONGO_MAILAUTH_OK) {
status = BongoMailAuthDKIMSignerUpdate(signer, "\r\n", 2U);
}
status = BongoMailAuthDKIMSignerUpdate(signer, messageChunk, chunk);
if (status != BONGO_MAILAUTH_OK) {
goto cleanup;
}
consumed = lineLength + 2U;
remaining -= remaining > (long long) consumed
? (long long) consumed : remaining;
remaining -= (long long) chunk;
}
ConnReadAnswer(queue->conn, queue->line, CONN_BUFSIZE);
if (atoi(queue->line) != 1000) {
@@ -359,13 +367,27 @@ GenerateDKIMSignature(SMTPClient *queue)
queue->hasDKIMSignature = status == BONGO_MAILAUTH_OK;
cleanup:
if (messageLine != NULL) {
MemFree(messageLine);
}
BongoMailAuthDKIMSignerFree(signer);
return status;
}
static BongoMailAuthStatus
PrepareExternalMessage(SMTPClient *queue)
{
if (queue == NULL) {
return BONGO_MAILAUTH_INVALID_ARGUMENT;
}
if (queue->dkimPrepared) {
return queue->hasDKIMSignature
? BONGO_MAILAUTH_OK : (
SMTPOutboundNeedsSRS(
(unsigned long) queue->flags, queue->authSender,
queue->sender)
? BONGO_MAILAUTH_OK : BONGO_MAILAUTH_PROTOCOL_ERROR);
}
return GenerateDKIMSignature(queue);
}
static int
InspectQueueMessageHeaders(SMTPClient *queue)
{
@@ -904,8 +926,16 @@ DeliverRelay(SMTPClient *queue, RecipStruct *recipient,
queue != NULL && queue->qID != NULL ? queue->qID : "(unknown)");
goto done;
}
senderStatus = PrepareExternalMessage(queue);
if (senderStatus != BONGO_MAILAUTH_OK) {
Log(LOG_ERROR,
"Cannot DKIM-sign external relay message %s (status %d)",
queue->qID, (int)senderStatus);
goto done;
}
senderStatus = GetEnvelopeSender(
queue, rewrittenSender, sizeof(rewrittenSender), &envelopeSender);
queue, rewrittenSender, sizeof(rewrittenSender), &envelopeSender,
TRUE);
if (senderStatus != BONGO_MAILAUTH_OK) {
Log(LOG_ERROR,
"Cannot prepare relay envelope sender for message %s (status %d)",
@@ -1627,7 +1657,8 @@ beginConversation:
/* Apply SRS only to unauthenticated externally sourced messages which
* Bongo is forwarding. Local submissions and bounces are not rewritten. */
if (GetEnvelopeSender(Queue, rewrittenSender, sizeof(rewrittenSender),
&envelopeSender) != BONGO_MAILAUTH_OK) {
&envelopeSender, !Remote->isLMTP) !=
BONGO_MAILAUTH_OK) {
Log(LOG_ERROR,
"SRS forwarding failed for queue %s; delivery deferred",
Queue->qID);
@@ -1799,7 +1830,7 @@ beginConversation:
Recip->Result = DELIVER_TRY_LATER;
goto finalization;
}
if (Queue->hasDKIMSignature) {
if (Queue->hasDKIMSignature && !Remote->isLMTP) {
ConnWriteF(Remote->conn, "DKIM-Signature: %s\r\n",
Queue->dkimSignature);
}
@@ -2039,17 +2070,6 @@ ProcessEntry(void *clientp, Connection *conn)
g_array_free(Recipients, TRUE);
return -1;
}
{
BongoMailAuthStatus dkimStatus = GenerateDKIMSignature(Queue);
if (dkimStatus != BONGO_MAILAUTH_OK) {
Log(LOG_ERROR,
"DKIM signing preparation failed for queue %s (status %d)",
Queue->qID, (int) dkimStatus);
PreserveRemoteRecipientsForRetry(Queue, Recipients);
g_array_free(Recipients, TRUE);
return -1;
}
}
g_array_sort(Recipients, (ArrayCompareFunc)RecipientCompare);
/* now i can skip over any duplicates an do lookups once per remote domain */
@@ -2110,7 +2130,24 @@ ProcessEntry(void *clientp, Connection *conn)
continue;
}
DeliverMessage(Queue, &Remote, &CurrentRecip);
if (!Remote.isLMTP) {
BongoMailAuthStatus dkimStatus =
PrepareExternalMessage(Queue);
if (dkimStatus != BONGO_MAILAUTH_OK) {
Log(LOG_ERROR,
"DKIM signing preparation failed for queue %s "
"(status %d)",
Queue->qID, (int) dkimStatus);
CurrentRecip.Result = DELIVER_TRY_LATER;
ConnClose(Remote.conn);
ConnFree(Remote.conn);
Remote.conn = NULL;
} else {
DeliverMessage(Queue, &Remote, &CurrentRecip);
}
} else {
DeliverMessage(Queue, &Remote, &CurrentRecip);
}
if (Remote.conn != NULL) {
ConnClose(Remote.conn);
ConnFree(Remote.conn);
+1
View File
@@ -52,6 +52,7 @@ typedef struct {
int flags;
char line[CONN_BUFSIZE + 1];
char qID[16]; /* holds the queueid pulled during handshake */
BOOL dkimPrepared;
BOOL hasDKIMSignature;
char dkimSignature[8192];
BOOL hasExternalRelay;
@@ -34,6 +34,16 @@ main(void)
assert(SMTPOutboundIdentityTrusted(
MSG_FLAG_SOURCE_EXTERNAL | MSG_FLAG_TRUSTED_RELAY, "-"));
assert(SMTPOutboundRequiresDKIM(0U, NULL, "sender@example.test"));
assert(SMTPOutboundRequiresDKIM(
MSG_FLAG_SOURCE_EXTERNAL, "test1", "sender@example.test"));
assert(SMTPOutboundRequiresDKIM(
MSG_FLAG_SOURCE_EXTERNAL | MSG_FLAG_TRUSTED_RELAY, "-",
"sender@example.test"));
assert(SMTPOutboundRequiresDKIM(MSG_FLAG_SOURCE_EXTERNAL, "-", "-"));
assert(!SMTPOutboundRequiresDKIM(
MSG_FLAG_SOURCE_EXTERNAL, "-", "sender@example.test"));
assert(SMTPOutboundNeedsSRS(
MSG_FLAG_SOURCE_EXTERNAL, "-", "sender@example.test"));
assert(!SMTPOutboundNeedsSRS(