x509 authentication with subject and issuer

This commit is contained in:
Dominik Chilla 2019-05-21 00:26:16 +02:00
parent 4ec4a6996a
commit c4e21a38dd
2 changed files with 70 additions and 35 deletions

View File

@ -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} SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128}
SINGLE-VALUE) 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 attributetype ( 1.3.6.1.4.1.53501.1.1.3
NAME 'allowedSenders' NAME 'allowedSenders'
DESC 'Allowed RFC5321.from' 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}) SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{4096})
attributetype ( 1.3.6.1.4.1.53501.1.1.11 attributetype ( 1.3.6.1.4.1.53501.1.1.11
NAME 'allowedx509CN' NAME 'allowedx509subject'
DESC 'Allowed x509 Common Name - CN' 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 EQUALITY caseExactIA5Match
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64}) 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 objectclass ( 1.3.6.1.4.1.53501.1.2.1
NAME 'lamPolicy' NAME 'lamPolicy'
DESC 'ldap-acl-milter policy' DESC 'ldap-acl-milter policy'
SUP top SUP top STRUCTURAL
STRUCTURAL MUST policyID
MUST ( policyID $ allowedRcpts $ allowedSenders ) MAY ( allowedRcpts $ deniedRcpts $
MAY ( authType $ deniedSenders $ deniedRcpts $ allowedClientAddr $ deniedClientAddr $ allowedSaslUser $ extBLOB $ allowedx509CN )) allowedSenders $ deniedSenders $
allowedClientAddr $ deniedClientAddr $
allowedSaslUser $ extBLOB $
allowedx509subject $ allowedx509issuer )
)

View File

@ -40,7 +40,8 @@ class LdapAclMilter(Milter.Base):
self.env_from = None self.env_from = None
self.env_from_domain = None self.env_from_domain = None
self.sasl_user = None self.sasl_user = None
self.x509_cn = None self.x509_subject = None
self.x509_issuer = None
# recipients list # recipients list
self.env_rcpts = [] self.env_rcpts = []
# https://stackoverflow.com/a/2257449 # https://stackoverflow.com/a/2257449
@ -75,12 +76,17 @@ class LdapAclMilter(Milter.Base):
# postfix only passes this macro to milters if the TLS connection # postfix only passes this macro to milters if the TLS connection
# with the authenticating client was trusted in a x509 manner! # with the authenticating client was trusted in a x509 manner!
# http://postfix.1071664.n5.nabble.com/verification-levels-and-Milter-tp91634p91638.html # http://postfix.1071664.n5.nabble.com/verification-levels-and-Milter-tp91634p91638.html
x509cn = self.getsymval('{cert_subject}') # Unfortunately, postfix only passes the CN-field of the subject/issuer DN :-/
if x509cn != None: x509_subject = self.getsymval('{cert_subject}')
self.x509_cn = x509cn if x509_subject != None:
logging.info(self.mconn_id + "/FROM x509_cn=" + self.x509_cn) 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: except:
logging.error(self.mconn_id + "/FROM x509_cn " + traceback.format_exc()) logging.error(self.mconn_id + "/FROM x509 " + traceback.format_exc())
try: try:
# this may fail, if no SASL authentication preceded # this may fail, if no SASL authentication preceded
sasl_user = self.getsymval('{auth_authen}') sasl_user = self.getsymval('{auth_authen}')
@ -126,19 +132,22 @@ class LdapAclMilter(Milter.Base):
# LDAP-ACL-Milter schema # LDAP-ACL-Milter schema
auth_method = '' auth_method = ''
if g_milter_expect_auth == True: 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: if self.sasl_user:
auth_method = auth_method.replace( auth_method = auth_method.replace(
'%SASL_AUTH%',"(allowedSaslUser="+self.sasl_user+")" '%SASL_AUTH%',"(allowedSaslUser="+self.sasl_user+")"
) )
else: else:
auth_method = auth_method.replace('%SASL_AUTH%','') auth_method = auth_method.replace('%SASL_AUTH%','')
if self.x509_cn: if self.x509_subject and self.x509_issuer:
auth_method = auth_method.replace( auth_method = auth_method.replace('%X509_AUTH%',
'%X509CN_AUTH%',"(allowedx509CN="+self.x509_cn+")" "(&"+
"(allowedx509subject="+self.x509_subject+")"+
"(allowedx509issuer="+self.x509_issuer+")"+
")"
) )
else: else:
auth_method = auth_method.replace('%X509CN_AUTH%','') auth_method = auth_method.replace('%X509_AUTH%','')
logging.debug(self.mconn_id + logging.debug(self.mconn_id +
" auth_method: " + auth_method " auth_method: " + auth_method
) )
@ -150,16 +159,18 @@ class LdapAclMilter(Milter.Base):
# for proper use in LDAP queries. # for proper use in LDAP queries.
# In this case *@<domain> cannot be a real address! # In this case *@<domain> cannot be a real address!
if re.match(r'^\*@.+$', self.env_from, re.IGNORECASE): if re.match(r'^\*@.+$', self.env_from, re.IGNORECASE):
logging.info(self.mconn_id + "/RCPT REJECT " logging.info(self.mconn_id + "/RCPT REJECT " +
+ "Literal wildcard sender (*@<domain>) is not allowed in wildcard mode!" "Literal wildcard sender (*@<domain>) is not " +
"allowed in wildcard mode!"
) )
self.setreply('550','5.7.1', self.setreply('550','5.7.1',
g_milter_reject_message + ' (' + self.mconn_id + ')' g_milter_reject_message + ' (' + self.mconn_id + ')'
) )
return Milter.REJECT return Milter.REJECT
if re.match(r'^\*@.+$', to, re.IGNORECASE): if re.match(r'^\*@.+$', to, re.IGNORECASE):
logging.info(self.mconn_id + "/RCPT REJECT " logging.info(self.mconn_id + "/RCPT REJECT " +
+ "Wildcard recipient (*@<domain>) is not allowed in wildcard mode!" "Literal wildcard recipient (*@<domain>) is not " +
"allowed in wildcard mode!"
) )
self.setreply('550','5.7.1', self.setreply('550','5.7.1',
g_milter_reject_message + ' (' + self.mconn_id + ')' g_milter_reject_message + ' (' + self.mconn_id + ')'
@ -171,12 +182,15 @@ class LdapAclMilter(Milter.Base):
"(|"+ "(|"+
"(allowedRcpts="+to+")"+ "(allowedRcpts="+to+")"+
"(allowedRcpts=\\2a@"+rcpt_domain+")"+ "(allowedRcpts=\\2a@"+rcpt_domain+")"+
"(allowedRcpts=\\2a@\\2a)"+
")"+ ")"+
"(|"+ "(|"+
"(allowedSenders="+self.env_from+")"+ "(allowedSenders="+self.env_from+")"+
"(allowedSenders=\\2a@"+self.env_from_domain+")"+ "(allowedSenders=\\2a@"+self.env_from_domain+")"+
")"+ "(allowedSenders=\\2a@\\2a)"+
")" ")"+
")",
attributes=['policyID']
) )
else: else:
# Asterisk must be ASCII-HEX encoded for LDAP queries # Asterisk must be ASCII-HEX encoded for LDAP queries
@ -187,12 +201,14 @@ class LdapAclMilter(Milter.Base):
auth_method + auth_method +
"(allowedRcpts="+query_to+")" + "(allowedRcpts="+query_to+")" +
"(allowedSenders="+query_from+")" + "(allowedSenders="+query_from+")" +
")" ")",
attributes=['policyID']
) )
time_end = timer() time_end = timer()
if len(self.ldap_conn.entries) == 0: if len(self.ldap_conn.entries) == 0:
# Policy not found in LDAP
self.env_rcpts.append({ 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 "time_start":time_start, "time_end":time_end
}) })
if g_milter_expect_auth == True: if g_milter_expect_auth == True:
@ -217,6 +233,22 @@ class LdapAclMilter(Milter.Base):
g_milter_reject_message g_milter_reject_message
) )
return Milter.CONTINUE 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: else:
# Custom LDAP schema # Custom LDAP schema
# 'build' a LDAP query per recipient # 'build' a LDAP query per recipient
@ -232,7 +264,7 @@ class LdapAclMilter(Milter.Base):
time_end = timer() time_end = timer()
if len(self.ldap_conn.entries) == 0: if len(self.ldap_conn.entries) == 0:
self.env_rcpts.append({ 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 "time_start":time_start, "time_end":time_end
}) })
logging.info(self.mconn_id + "/RCPT " + "policy mismatch " 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) self.setreply('451', '4.7.1', g_milter_tmpfail_message)
return Milter.TEMPFAIL return Milter.TEMPFAIL
self.env_rcpts.append({ 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 return Milter.CONTINUE
@ -271,7 +303,7 @@ class LdapAclMilter(Milter.Base):
duration = rcpt['time_end'] - rcpt['time_start'] duration = rcpt['time_end'] - rcpt['time_start']
logging.info(self.mconn_id + "/DATA " + self.queue_id + logging.info(self.mconn_id + "/DATA " + self.queue_id +
": 5321.from=" + self.env_from + " 5321.rcpt=" + ": 5321.from=" + self.env_from + " 5321.rcpt=" +
rcpt['rcpt'] + " reason=" + rcpt['reason'] + rcpt['rcpt'] + " action=" + rcpt['action'] +
" duration=" + str(duration) + "sec." " duration=" + str(duration) + "sec."
) )
except: except: