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}
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 )
)

View File

@ -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 *@<domain> cannot be a real address!
if re.match(r'^\*@.+$', self.env_from, re.IGNORECASE):
logging.info(self.mconn_id + "/RCPT REJECT "
+ "Literal wildcard sender (*@<domain>) is not allowed in wildcard mode!"
logging.info(self.mconn_id + "/RCPT REJECT " +
"Literal wildcard sender (*@<domain>) 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 (*@<domain>) is not allowed in wildcard mode!"
logging.info(self.mconn_id + "/RCPT REJECT " +
"Literal wildcard recipient (*@<domain>) 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: