diff --git a/LDAP/ldap-acl-milter.schema b/LDAP/ldap-acl-milter.schema index f83c494..352feed 100644 --- a/LDAP/ldap-acl-milter.schema +++ b/LDAP/ldap-acl-milter.schema @@ -15,13 +15,6 @@ attributetype ( 1.3.6.1.4.1.53501.1.1.1 SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} SINGLE-VALUE) -attributetype ( 1.3.6.1.4.1.53501.1.1.2 - NAME 'authType' - DESC 'Authentication type: sasl_user, client_addr, none' - EQUALITY caseExactIA5Match - SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} - SINGLE-VALUE) - attributetype ( 1.3.6.1.4.1.53501.1.1.3 NAME 'allowedSenders' DESC 'Allowed RFC5321.from' @@ -78,8 +71,14 @@ attributetype ( 1.3.6.1.4.1.53501.1.1.10 SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{4096}) attributetype ( 1.3.6.1.4.1.53501.1.1.11 - NAME 'allowedx509CN' - DESC 'Allowed x509 Common Name - CN' + NAME 'allowedx509subject' + DESC 'Allowed x509 Common Name - subject' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64}) + +attributetype ( 1.3.6.1.4.1.53501.1.1.12 + NAME 'allowedx509issuer' + DESC 'Allowed x509 Common Name - issuer' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64}) @@ -89,7 +88,11 @@ attributetype ( 1.3.6.1.4.1.53501.1.1.11 objectclass ( 1.3.6.1.4.1.53501.1.2.1 NAME 'lamPolicy' DESC 'ldap-acl-milter policy' - SUP top - STRUCTURAL - MUST ( policyID $ allowedRcpts $ allowedSenders ) - MAY ( authType $ deniedSenders $ deniedRcpts $ allowedClientAddr $ deniedClientAddr $ allowedSaslUser $ extBLOB $ allowedx509CN )) + SUP top STRUCTURAL + MUST policyID + MAY ( allowedRcpts $ deniedRcpts $ + allowedSenders $ deniedSenders $ + allowedClientAddr $ deniedClientAddr $ + allowedSaslUser $ extBLOB $ + allowedx509subject $ allowedx509issuer ) + ) diff --git a/app/ldap-acl-milter.py b/app/ldap-acl-milter.py index 763bccf..650ef59 100644 --- a/app/ldap-acl-milter.py +++ b/app/ldap-acl-milter.py @@ -40,7 +40,8 @@ class LdapAclMilter(Milter.Base): self.env_from = None self.env_from_domain = None self.sasl_user = None - self.x509_cn = None + self.x509_subject = None + self.x509_issuer = None # recipients list self.env_rcpts = [] # https://stackoverflow.com/a/2257449 @@ -75,12 +76,17 @@ class LdapAclMilter(Milter.Base): # postfix only passes this macro to milters if the TLS connection # with the authenticating client was trusted in a x509 manner! # http://postfix.1071664.n5.nabble.com/verification-levels-and-Milter-tp91634p91638.html - x509cn = self.getsymval('{cert_subject}') - if x509cn != None: - self.x509_cn = x509cn - logging.info(self.mconn_id + "/FROM x509_cn=" + self.x509_cn) + # Unfortunately, postfix only passes the CN-field of the subject/issuer DN :-/ + x509_subject = self.getsymval('{cert_subject}') + if x509_subject != None: + self.x509_subject = x509_subject + logging.info(self.mconn_id + "/FROM x509_subject=" + self.x509_subject) + x509_issuer = self.getsymval('{cert_issuer}') + if x509_issuer != None: + self.x509_issuer = x509_issuer + logging.info(self.mconn_id + "/FROM x509_issuer=" + self.x509_issuer) except: - logging.error(self.mconn_id + "/FROM x509_cn " + traceback.format_exc()) + logging.error(self.mconn_id + "/FROM x509 " + traceback.format_exc()) try: # this may fail, if no SASL authentication preceded sasl_user = self.getsymval('{auth_authen}') @@ -126,19 +132,22 @@ class LdapAclMilter(Milter.Base): # LDAP-ACL-Milter schema auth_method = '' if g_milter_expect_auth == True: - auth_method = "(|(allowedClientAddr="+self.client_addr+")%SASL_AUTH%%X509CN_AUTH%)" + auth_method = "(|(allowedClientAddr="+self.client_addr+")%SASL_AUTH%%X509_AUTH%)" if self.sasl_user: auth_method = auth_method.replace( '%SASL_AUTH%',"(allowedSaslUser="+self.sasl_user+")" ) else: auth_method = auth_method.replace('%SASL_AUTH%','') - if self.x509_cn: - auth_method = auth_method.replace( - '%X509CN_AUTH%',"(allowedx509CN="+self.x509_cn+")" + if self.x509_subject and self.x509_issuer: + auth_method = auth_method.replace('%X509_AUTH%', + "(&"+ + "(allowedx509subject="+self.x509_subject+")"+ + "(allowedx509issuer="+self.x509_issuer+")"+ + ")" ) else: - auth_method = auth_method.replace('%X509CN_AUTH%','') + auth_method = auth_method.replace('%X509_AUTH%','') logging.debug(self.mconn_id + " auth_method: " + auth_method ) @@ -150,16 +159,18 @@ class LdapAclMilter(Milter.Base): # for proper use in LDAP queries. # In this case *@ cannot be a real address! if re.match(r'^\*@.+$', self.env_from, re.IGNORECASE): - logging.info(self.mconn_id + "/RCPT REJECT " - + "Literal wildcard sender (*@) is not allowed in wildcard mode!" + logging.info(self.mconn_id + "/RCPT REJECT " + + "Literal wildcard sender (*@) is not " + + "allowed in wildcard mode!" ) self.setreply('550','5.7.1', g_milter_reject_message + ' (' + self.mconn_id + ')' ) return Milter.REJECT if re.match(r'^\*@.+$', to, re.IGNORECASE): - logging.info(self.mconn_id + "/RCPT REJECT " - + "Wildcard recipient (*@) is not allowed in wildcard mode!" + logging.info(self.mconn_id + "/RCPT REJECT " + + "Literal wildcard recipient (*@) is not " + + "allowed in wildcard mode!" ) self.setreply('550','5.7.1', g_milter_reject_message + ' (' + self.mconn_id + ')' @@ -171,12 +182,15 @@ class LdapAclMilter(Milter.Base): "(|"+ "(allowedRcpts="+to+")"+ "(allowedRcpts=\\2a@"+rcpt_domain+")"+ + "(allowedRcpts=\\2a@\\2a)"+ ")"+ "(|"+ "(allowedSenders="+self.env_from+")"+ "(allowedSenders=\\2a@"+self.env_from_domain+")"+ - ")"+ - ")" + "(allowedSenders=\\2a@\\2a)"+ + ")"+ + ")", + attributes=['policyID'] ) else: # Asterisk must be ASCII-HEX encoded for LDAP queries @@ -187,12 +201,14 @@ class LdapAclMilter(Milter.Base): auth_method + "(allowedRcpts="+query_to+")" + "(allowedSenders="+query_from+")" + - ")" + ")", + attributes=['policyID'] ) time_end = timer() if len(self.ldap_conn.entries) == 0: + # Policy not found in LDAP self.env_rcpts.append({ - "rcpt": to, "reason": g_milter_reject_message, + "rcpt": to, "action": g_milter_reject_message, "time_start":time_start, "time_end":time_end }) if g_milter_expect_auth == True: @@ -217,6 +233,22 @@ class LdapAclMilter(Milter.Base): g_milter_reject_message ) return Milter.CONTINUE + elif len(self.ldap_conn.entries) == 1: + # Policy found in LDAP, but which one? + entry = self.ldap_conn.entries[0] + logging.info(self.mconn_id + + "/RCPT Policy match: " + entry.policyID.value + ) + elif len(self.ldap_conn.entries) > 1: + # Something went wrong!? There shouldn´t be more than one entries! + logging.warn(self.mconn_id + "/RCPT More than one policies found! "+ + "5321.from=" + self.env_from + ", 5321.rcpt=" + to + + ", auth_method=" + auth_method + ) + self.setreply('550','5.7.1', + g_milter_reject_message + ' (' + self.mconn_id + ')' + ) + return Milter.REJECT else: # Custom LDAP schema # 'build' a LDAP query per recipient @@ -232,7 +264,7 @@ class LdapAclMilter(Milter.Base): time_end = timer() if len(self.ldap_conn.entries) == 0: self.env_rcpts.append({ - "rcpt": to, "reason": g_milter_reject_message, + "rcpt": to, "action": g_milter_reject_message, "time_start":time_start, "time_end":time_end }) logging.info(self.mconn_id + "/RCPT " + "policy mismatch " @@ -258,7 +290,7 @@ class LdapAclMilter(Milter.Base): self.setreply('451', '4.7.1', g_milter_tmpfail_message) return Milter.TEMPFAIL self.env_rcpts.append({ - "rcpt": to, "reason":'pass',"time_start":time_start,"time_end":time_end + "rcpt": to, "action":'pass',"time_start":time_start,"time_end":time_end }) return Milter.CONTINUE @@ -271,7 +303,7 @@ class LdapAclMilter(Milter.Base): duration = rcpt['time_end'] - rcpt['time_start'] logging.info(self.mconn_id + "/DATA " + self.queue_id + ": 5321.from=" + self.env_from + " 5321.rcpt=" + - rcpt['rcpt'] + " reason=" + rcpt['reason'] + + rcpt['rcpt'] + " action=" + rcpt['action'] + " duration=" + str(duration) + "sec." ) except: