Docs; forget about the dkim-selector

This commit is contained in:
Dominik Chilla 2020-12-05 15:39:51 +01:00
parent a2fbaa9438
commit 368c6b4b77
2 changed files with 40 additions and 56 deletions

View File

@ -53,6 +53,10 @@ Client certificate verification is the job of the underlying MTA. So the **ExOTA
## DKIM - DomainKey Identified Message
Nevertheless, as [Microsoft supports DKIM-signing for outbound email traffic](https://docs.microsoft.com/de-de/microsoft-365/security/office-365-security/use-dkim-to-validate-outbound-email?view=o365-worldwide) the **ExOTA-Milter** can be used to authenticate sending tenants, respectively their sender domains, based on the cryptographic capabilities of [DKIM](https://tools.ietf.org/html/rfc6376). In fact the **ExOTA-Milter** does not validate the DKIM-signatures itself. Instead it simply parses DKIM-specific *Authentication-Results* headers produced by any previously DKIM-validating milter (like [OpenDKIM](http://www.opendkim.org/), [Rspamd](https://rspamd.com/) or [AMavis](https://www.ijs.si/software/amavisd/)) in the chain. I personally prefer OpenDKIM as it´s lightweight and fully focused on DKIM.
**To use DKIM for tenant/sender domain authentication, DKIM must be enabled in the milter as well as in each policy!**
**Worth to know when using OpenDKIM as AR provider:** As Microsoft already signs with 2kRSA keys be sure to use a version of OpenDKIM, which is linked against a DNS resolver library that is able to handle such large DNS responses! Further the resolver library should be aware of DNSSEC! **[libunbound](https://nlnetlabs.nl/documentation/unbound/libunbound/) meets all of these requirements :-)**. A libunbound-linked version of OpenDKIM is provided by [Debian](https://wiki.debian.org/opendkim#DNS_resolution).
*DKIM-Signature* headers appended by the Exchange-Online platform look like this:
```
[...]
@ -63,10 +67,12 @@ DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
b=DYTLJtLFjvVrSZtZQagTwuEe5PQYqrNGi7hR5bkhO[...snip...]
[...]
```
*Authentication-Results* headers provided by OpenDKIM (signature validated) look like this:
*Authentication-Results* headers provided by OpenDKIM (signature valid, public key not DNSSEC signed) look like this:
```
[...]
Authentication-Results: trusted.dkim.validating.relay; dkim=pass header.d=tenantdomain.onmicrosoft.com header.s=selector1-tenantdomain-onmicrosoft-com header.b=mmmjFpv8"
Authentication-Results: trusted.dkim.validating.relay;
dkim=pass (2048-bit key; unprotected) header.d=tenantdomain.onmicrosoft.com header.i=@tenantdomain.onmicrosoft.com header.b=mmmjFpv8";
dkim-atps=neutral
[...]
```

View File

@ -51,22 +51,21 @@ class ExOTAMilter(Milter.Base):
def __init__(self):
self.x509_client_valid = False
self.client_ip = None
self.reset_milter()
self.reset()
def reset_milter(self):
def reset(self):
self.conn_reused = False
self.hdr_from = None
self.hdr_from_domain = None
self.hdr_tenant_id = None
self.hdr_tenant_id_count = 0
self.dkim_results = []
self.dkim_valid = False
self.xar_hdr_count = 0
# https://stackoverflow.com/a/2257449
self.mconn_id = g_milter_name + ': ' + ''.join(
random.choice(string.ascii_lowercase + string.digits) for _ in range(8)
)
logging.debug(self.mconn_id + " reset_milter()")
logging.debug(self.mconn_id + " reset()")
def smfir_reject(self, **kwargs):
message = g_milter_reject_message
@ -76,6 +75,9 @@ class ExOTAMilter(Milter.Base):
message = "queue_id: {0} - {1}".format(kwargs['queue_id'], message)
if 'reason' in kwargs:
message = "{0} - reason: {1}".format(message, kwargs['reason'])
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
": milter_action=reject"
)
self.setreply('550','5.7.1', message)
return Milter.REJECT
@ -104,7 +106,7 @@ class ExOTAMilter(Milter.Base):
if self.conn_reused:
# Milter connection reused!
logging.debug(self.mconn_id + "/FROM connection reused!")
self.reset_milter()
self.reset()
else:
self.conn_reused = True
logging.debug(self.mconn_id + "/FROM client_ip={0}".format(self.client_ip))
@ -115,10 +117,6 @@ class ExOTAMilter(Milter.Base):
logging.debug(self.mconn_id + "/RCPT 5321.rcpt={0}".format(to))
return self.smfir_continue()
def data(self):
logging.debug("DATA")
return self.smfir_continue()
def header(self, name, hval):
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Header: {0}, Value: {1}".format(name, hval)
@ -160,11 +158,8 @@ class ExOTAMilter(Milter.Base):
if ar.authserv_id == g_milter_trusted_authservid:
for ar_result in ar.results:
if ar_result.method == 'dkim':
self.dkim_results.append({
"selector": str(ar_result.header_s),
"from_domain": str(ar_result.header_d),
"result": str(ar_result.result)
})
if ar_result.result == 'pass':
self.dkim_valid = True
else:
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Ignoring authentication results of {0}".format(ar.authserv_id)
@ -271,50 +266,22 @@ class ExOTAMilter(Milter.Base):
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: 5322.from_domain={0} dkim_auth=enabled".format(self.hdr_from_domain)
)
if len(self.dkim_results) > 0:
for dkim_result in self.dkim_results:
if dkim_result['from_domain'] == self.hdr_from_domain:
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Found DKIM authentication result for {0}/{1}".format(
self.hdr_from_domain, dkim_result['selector']
)
)
if dkim_result['result'] == 'pass':
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: dkim_selector={0} result=pass".format(dkim_result['selector'])
)
self.dkim_valid = True
continue
else:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: dkim_selector={0} result=fail".format(dkim_result['selector'])
)
if self.dkim_valid:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Found valid DKIM authentication result for 5322.from_domain={0}".format(
self.hdr_from_domain
)
)
else:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: No DKIM authentication results (AR headers) found - action=reject"
"/EOM: No valid DKIM authentication result found for 5322.from_domain={0}".format(
self.hdr_from_domain
)
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
reason = 'No DKIM authentication results found'
reason = 'No valid DKIM authentication results found'
)
if self.dkim_valid == False:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: DKIM authentication failed - action=reject"
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
reason = 'DKIM failed'
)
if g_milter_dkim_enabled:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Tenant authentication successful (dkim_enabled={0})".format(
str(policy.is_dkim_enabled())
)
)
else:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Tenant successfully authenticated"
)
# Delete all existing X-ExOTA-Authentication-Results headers
for i in range(self.xar_hdr_count, 0, -1):
@ -337,11 +304,22 @@ class ExOTAMilter(Milter.Base):
)
)
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: AR-header added"
"/EOM: AR-header added"
)
except Exception as e:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: addheader(AR) failed: {0}".format(str(e))
"/EOM: addheader(AR) failed: {0}".format(str(e))
)
if g_milter_dkim_enabled:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Tenant successfully authorized (dkim_enabled={0})".format(
str(policy.is_dkim_enabled())
)
)
else:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Tenant successfully authorized"
)
return self.smfir_continue()