Merge pull request #21 from chillout2k/devel

Devel
This commit is contained in:
Dominik Chilla 2019-07-09 10:14:34 +02:00 committed by GitHub
commit 4a0bf680b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 122 additions and 60 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'
@ -77,13 +70,29 @@ attributetype ( 1.3.6.1.4.1.53501.1.1.10
SUBSTR caseIgnoreIA5SubstringsMatch SUBSTR caseIgnoreIA5SubstringsMatch
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
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})
# #
# Objects: 1.3.6.1.4.1.53501.1.2 # Objects: 1.3.6.1.4.1.53501.1.2
# #
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 )) allowedSenders $ deniedSenders $
allowedClientAddr $ deniedClientAddr $
allowedSaslUser $ extBLOB $
allowedx509subject $ allowedx509issuer )
)

View File

@ -76,6 +76,11 @@ services:
#MILTER_SOCKET: inet6:8020 #MILTER_SOCKET: inet6:8020
#MILTER_REJECT_MESSAGE: Message rejected due to security policy #MILTER_REJECT_MESSAGE: Message rejected due to security policy
#MILTER_TMPFAIL_MESSAGE: Message temporary rejected. Please try again later ;) #MILTER_TMPFAIL_MESSAGE: Message temporary rejected. Please try again later ;)
# Expect authentication information from LDAP like allowedClientAddr,
# allowedSaslUser or allowedx509CN. This is usefull if the milter handles
# outbound email traffic, where senders must authenticate before submission.
# Default: False (inbound mode)
MILTER_EXPECT_AUTH: 'True'
hostname: ldap-acl-milter hostname: ldap-acl-milter
volumes: volumes:
- "lam_socket:/socket/:rw" - "lam_socket:/socket/:rw"

View File

@ -29,6 +29,7 @@ g_milter_mode = 'test'
g_milter_default_policy = 'reject' g_milter_default_policy = 'reject'
g_milter_schema = False g_milter_schema = False
g_milter_schema_wildcard_domain = False # works only if g_milter_schema == True g_milter_schema_wildcard_domain = False # works only if g_milter_schema == True
g_milter_expect_auth = False
class LdapAclMilter(Milter.Base): class LdapAclMilter(Milter.Base):
# Each new connection is handled in an own thread # Each new connection is handled in an own thread
@ -39,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
@ -48,9 +50,9 @@ class LdapAclMilter(Milter.Base):
) )
# Not registered/used callbacks # Not registered/used callbacks
@Milter.nocallback #@Milter.nocallback
def hello(self, heloname): #def hello(self, heloname)
return Milter.CONTINUE # return Milter.CONTINUE
@Milter.nocallback @Milter.nocallback
def header(self, name, hval): def header(self, name, hval):
return Milter.CONTINUE return Milter.CONTINUE
@ -70,13 +72,21 @@ class LdapAclMilter(Milter.Base):
def envfrom(self, mailfrom, *str): def envfrom(self, mailfrom, *str):
try: try:
# this may fail, if no x509 client certificate was used # this may fail, if no x509 client certificate was used.
x509cn = self.getsymval('{cert_subject}') # postfix only passes this macro to milters if the TLS connection
if x509cn != None: # with the authenticating client was trusted in a x509 manner!
self.x509_cn = x509cn # http://postfix.1071664.n5.nabble.com/verification-levels-and-Milter-tp91634p91638.html
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: except:
logging.error(self.mconn_id + "/FROM " + 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}')
@ -84,7 +94,7 @@ class LdapAclMilter(Milter.Base):
self.sasl_user = sasl_user self.sasl_user = sasl_user
logging.info(self.mconn_id + "/FROM sasl_user=" + self.sasl_user) logging.info(self.mconn_id + "/FROM sasl_user=" + self.sasl_user)
except: except:
logging.error(self.mconn_id + "/FROM " + traceback.format_exc()) logging.error(self.mconn_id + "/FROM sasl_user " + traceback.format_exc())
mailfrom = mailfrom.replace("<","") mailfrom = mailfrom.replace("<","")
mailfrom = mailfrom.replace(">","") mailfrom = mailfrom.replace(">","")
self.env_from = mailfrom self.env_from = mailfrom
@ -121,16 +131,26 @@ class LdapAclMilter(Milter.Base):
if g_milter_schema == True: if g_milter_schema == True:
# LDAP-ACL-Milter schema # LDAP-ACL-Milter schema
auth_method = '' auth_method = ''
# Authentication order! if g_milter_expect_auth == True:
# 1. x509 client certificate auth_method = "(|(allowedClientAddr="+self.client_addr+")%SASL_AUTH%%X509_AUTH%)"
# 2. SASL authenticated if self.sasl_user:
# if authType sasl_user, check if authenticated user matches sasl_user and auth_method = auth_method.replace(
# check if sender/recipient pair match '%SASL_AUTH%',"(allowedSaslUser="+self.sasl_user+")"
# 3. Client-IP authenticated )
# if authType client_addr, check if client-ip matches client_addr else:
# check if sender/recipient pair match auth_method = auth_method.replace('%SASL_AUTH%','')
# 4. not authenticated if self.x509_subject and self.x509_issuer:
# ldap-search with excluded sasl_user and client_addr attributes! auth_method = auth_method.replace('%X509_AUTH%',
"(&"+
"(allowedx509subject="+self.x509_subject+")"+
"(allowedx509issuer="+self.x509_issuer+")"+
")"
)
else:
auth_method = auth_method.replace('%X509_AUTH%','')
logging.debug(self.mconn_id +
" auth_method: " + auth_method
)
if g_milter_schema_wildcard_domain == True: if g_milter_schema_wildcard_domain == True:
# The asterisk (*) character is in term of local part # The asterisk (*) character is in term of local part
# RFC5322 compliant and expected as a wildcard literal in this code. # RFC5322 compliant and expected as a wildcard literal in this code.
@ -139,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 " +
+ "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 + ')'
@ -160,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
@ -176,17 +201,25 @@ 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
}) })
logging.info(self.mconn_id + "/RCPT " + "policy mismatch " if g_milter_expect_auth == True:
"5321.from=" + self.env_from + ", 5321.rcpt=" + to logging.info(self.mconn_id + "/RCPT " + "policy mismatch "
) "5321.from=" + self.env_from + ", 5321.rcpt=" + to +
", auth_method=" + auth_method
)
else:
logging.info(self.mconn_id + "/RCPT " + "policy mismatch "
"5321.from=" + self.env_from + ", 5321.rcpt=" + to
)
if g_milter_mode == 'reject': if g_milter_mode == 'reject':
logging.info(self.mconn_id + "/RCPT REJECT " logging.info(self.mconn_id + "/RCPT REJECT "
+ g_milter_reject_message + g_milter_reject_message
@ -200,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
@ -215,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 "
@ -236,8 +285,12 @@ class LdapAclMilter(Milter.Base):
logging.warn(self.mconn_id + "/RCPT LDAP: " + str(e)) logging.warn(self.mconn_id + "/RCPT LDAP: " + str(e))
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
except:
logging.error(self.mconn_id + "/RCPT LDAP: " + traceback.format_exc())
self.setreply('451', '4.7.1', g_milter_tmpfail_message)
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
@ -250,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:
@ -338,6 +391,9 @@ if __name__ == "__main__":
g_milter_reject_message = os.environ['MILTER_REJECT_MESSAGE'] g_milter_reject_message = os.environ['MILTER_REJECT_MESSAGE']
if 'MILTER_TMPFAIL_MESSAGE' in os.environ: if 'MILTER_TMPFAIL_MESSAGE' in os.environ:
g_milter_tmpfail_message = os.environ['MILTER_TMPFAIL_MESSAGE'] g_milter_tmpfail_message = os.environ['MILTER_TMPFAIL_MESSAGE']
if 'MILTER_EXPECT_AUTH' in os.environ:
if re.match(r'^true$', os.environ['MILTER_EXPECT_AUTH'], re.IGNORECASE):
g_milter_expect_auth = True
server = Server(g_ldap_server, get_info=NONE) server = Server(g_ldap_server, get_info=NONE)
g_ldap_conn = Connection(server, g_ldap_conn = Connection(server,
g_ldap_binddn, g_ldap_bindpw, g_ldap_binddn, g_ldap_bindpw,

View File

@ -3,7 +3,6 @@
BRANCH="$(/usr/bin/git branch|/bin/grep \*|/usr/bin/awk {'print $2'})" BRANCH="$(/usr/bin/git branch|/bin/grep \*|/usr/bin/awk {'print $2'})"
VERSION="$(/bin/cat VERSION)" VERSION="$(/bin/cat VERSION)"
BASEOS="$(/bin/cat BASEOS)" BASEOS="$(/bin/cat BASEOS)"
#REGISTRY="some-registry.invalid"
GO="" GO=""
while getopts g opt while getopts g opt
@ -22,15 +21,7 @@ fi
IMAGES="ldap-acl-milter" IMAGES="ldap-acl-milter"
for IMAGE in ${IMAGES}; do for IMAGE in ${IMAGES}; do
# --build-arg http_proxy=http://wprx-zdf.zwackl.local:3128 \
# --build-arg https_proxy=http://wprx-zdf.zwackl.local:3128 \
/usr/bin/docker build \ /usr/bin/docker build \
-t "${IMAGE}/${BASEOS}:${VERSION}_${BRANCH}" \ -t "${IMAGE}:${BRANCH}" \
-f "docker/${BASEOS}/Dockerfile" . -f "docker/${BASEOS}/Dockerfile" .
# /usr/bin/docker tag "${IMAGE}/${BASEOS}:${VERSION}_${BRANCH}" "${REGISTRY}/${IMAGE}/${BASEOS}:${VERSION}_${BRANCH}"
done done
#/bin/echo "Push images to registry:"
#for IMAGE in ${IMAGES}; do
# /bin/echo "/usr/bin/docker push ${REGISTRY}/${IMAGE}/${BASEOS}:${VERSION}_${BRANCH}"
#done

View File

@ -2,7 +2,7 @@ ARG http_proxy
ARG https_proxy ARG https_proxy
FROM debian FROM debian
LABEL maintainer="Dominik Chilla <dominik@zwackl.de>" LABEL maintainer="Dominik Chilla <dominik@zwackl.de>"
LABEL git_repo="https://github.com/chillout2k/ldap-acl-milter/tree/devel" LABEL git_repo="https://github.com/chillout2k/ldap-acl-milter"
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
TZ=Europe/Berlin TZ=Europe/Berlin
@ -18,7 +18,8 @@ RUN env; set -ex ; \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY app/*.py /app/ ADD app/*.py /app/
COPY entrypoint.sh /entrypoint.sh ADD entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]
CMD ["/usr/bin/python3", "/app/ldap-acl-milter.py"]

View File

@ -4,4 +4,4 @@ set -x
set -e set -e
umask 0000 umask 0000
ulimit -n 1024 ulimit -n 1024
/usr/bin/python3 /app/ldap-acl-milter.py exec "$@"