From f98fbe7ff2d9f8a594727b9a6a7393addf6ec40d Mon Sep 17 00:00:00 2001 From: Dominik Chilla Date: Tue, 12 Feb 2019 01:51:03 +0100 Subject: [PATCH 1/2] first aproach --- README.md | 37 ++++++++++ app/ldap-acl-milter.py | 152 +++++++++++++++++++++++++++++++++++++++ docker/debian/Dockerfile | 1 + entrypoint.sh | 7 ++ 4 files changed, 197 insertions(+) create mode 100755 entrypoint.sh diff --git a/README.md b/README.md index f073ca3..897e5ed 100644 --- a/README.md +++ b/README.md @@ -1 +1,38 @@ # ldap-acl-milter + +### docker-compose.yml + +``` +version: '3' + +volumes: + lam_socket: + +services: + ldap-acl-milter: + image: "ldap-acl-milter/debian:19.02_devel" + restart: unless-stopped + environment: + LDAP_SERVER: ldap://ldap-slave.example.org:389 + LDAP_BINDDN: uid=lam,ou=apps,dc=example,dc=org + LDAP_BINDPW: TopSecret1! + LDAP_BASE: ou=users,dc=example,dc=org + LDAP_QUERY: (&(mail=%rcpt%)(whitelistSender=%from%)) + # Socket default: /socket/ldap-acl-milter + # MILTER_SOCKET: inet6:8020 + MILTER_REJECT_MESSAGE: Rejected due to security policy violation + hostname: ldap-acl-milter + volumes: + - "lam_socket:/socket/:rw" + postfix: + depends_on: + - ldap-acl-milter + image: "postfix/alpine/amd64" + restart: unless-stopped + hostname: postfix + ports: + - "25:25" + volumes: + - "./config/postfix:/etc/postfix:rw" + - "lam_socket:/socket/:rw" +``` diff --git a/app/ldap-acl-milter.py b/app/ldap-acl-milter.py index e69de29..f2f218a 100644 --- a/app/ldap-acl-milter.py +++ b/app/ldap-acl-milter.py @@ -0,0 +1,152 @@ +import Milter +from ldap3 import Server,Connection,NONE,LDAPOperationResult +import sys +import os +import logging +from multiprocessing import Process,Queue + +log_queue = Queue(maxsize=32) +g_milter_name = 'ldap-acl-milter' +g_milter_socket = '/socket/' + g_milter_name +g_milter_reject_message = 'Absender/Empfaenger passen nicht!' +g_ldap_conn = None +g_ldap_server = 'ldap://127.0.0.1:389' +g_ldap_binddn = 'cn=ldap-reader,ou=binds,dc=example,dc=org' +g_ldap_bindpw = 'TopSecret;-)' +g_ldap_base = 'ou=users,dc=example,dc=org' +g_ldap_query = '(&(mail=%rcpt%)(allowedEnvelopeSender=%from%))' +logging.basicConfig( + filename=None, # log to stdout + format='%(asctime)s: %(levelname)s %(message)s', + level=logging.INFO +) + +class LdapAclMilter(Milter.Base): + def __init__(self): # A new instance with each new connection. + self.id = Milter.uniqueID() + self.ldap_conn = g_ldap_conn + self.R = [] + + # Not registered callbacks + @Milter.nocallback + def connect(self, IPname, family, hostaddr): + return self.CONTINUE + @Milter.nocallback + def hello(self, heloname): + return self.CONTINUE + @Milter.nocallback + def header(self, name, hval): + return self.CONTINUE + @Milter.nocallback + def eoh(self): + return self.CONTINUE + @Milter.nocallback + def body(self, chunk): + return self.CONTINUE + + def envfrom(self, mailfrom, *str): + if mailfrom == '<>': + self.setreply('550','5.7.1','Envelope null-sender not allowed!') + Milter.REJECT + mailfrom = mailfrom.replace("<","") + mailfrom = mailfrom.replace(">","") + self.F = mailfrom + return Milter.CONTINUE + + def envrcpt(self, to, *str): + to = to.replace("<","") + to = to.replace(">","") + try: + query = g_ldap_query.replace("%rcpt%",to) + query = query.replace("%from%", self.F) + self.ldap_conn.search(g_ldap_base, query) + if len(self.ldap_conn.entries) == 0: + self.R.append({"rcpt": to, "reason": g_milter_reject_message}) + self.setreply('550','5.7.1',g_milter_reject_message) + return Milter.REJECT + except LDAPOperationResult as e: + self.log("LDAP Exception (envrcpt): " + str(e)) + self.setreply('451','4.7.1', str(e)) + return Milter.TEMPFAIL + self.R.append({"rcpt": to, "reason":'pass'}) + return Milter.CONTINUE + + def data(self): + # This callback is used only to log with queue-id + for rcpt in self.R: + self.log(self.getsymval('i') + + ": 5321.from=<" + self.F + "> 5321.rcpt=<" + + rcpt['rcpt'] + "> reason: " + rcpt['reason'] + ) + return Milter.CONTINUE + + def eom(self): + # EOM will always be called - non-optional + return Milter.CONTINUE + + def abort(self): + # client disconnected prematurely + return Milter.CONTINUE + + def close(self): + # always called, even when abort is called. Clean up + # any external resources here. + return Milter.CONTINUE + + def log(self,msg): + log_queue.put(msg) + +def logger_proc_loop(): + while True: + qmsg = log_queue.get() + if not qmsg: break + logging.info(qmsg) + +if __name__ == "__main__": + try: + if 'LDAP_SERVER' not in os.environ: + logging.error("Missing ENV[LDAP_SERVER], e.g. " + g_ldap_server) + sys.exit(1) + g_ldap_server = os.environ['LDAP_SERVER'] + if 'LDAP_BINDDN' in os.environ: + g_ldap_binddn = os.environ['LDAP_BINDDN'] + if 'LDAP_BINDPW' in os.environ: + g_ldap_bindpw = os.environ['LDAP_BINDPW'] + if 'LDAP_BASE' not in os.environ: + logging.error("Missing ENV[LDAP_BASE], e.g. " + g_ldap_base) + sys.exit(1) + g_ldap_base = os.environ['LDAP_BASE'] + if 'LDAP_QUERY' not in os.environ: + logging.error("Missing ENV[LDAP_QUERY], e.g. " + g_ldap_query) + sys.exit(1) + g_ldap_query = os.environ['LDAP_QUERY'] + if 'MILTER_SOCKET' in os.environ: + g_milter_socket = os.environ['MILTER_SOCKET'] + if 'MILTER_REJECT_MESSAGE' in os.environ: + g_milter_reject_message = os.environ['MILTER_REJECT_MESSAGE'] + server = Server(g_ldap_server, get_info=NONE) + g_ldap_conn = Connection(server, + g_ldap_binddn, g_ldap_bindpw, + auto_bind=True, raise_exceptions=True, + client_strategy='RESTARTABLE' + ) + logging.info("Connected to LDAP-server: " + g_ldap_server) + except LDAPOperationResult as e: + logging.error("LDAP Exception: " + str(e)) + sys.exit(1) + try: + logger_proc = Process(target=logger_proc_loop) + logger_proc.start() + timeout = 600 + # Register to have the Milter factory create instances of your class: + Milter.factory = LdapAclMilter + # Tell the MTA which features we use + flags = Milter.ADDHDRS + Milter.set_flags(flags) + logging.info("Startup " + g_milter_name + "@"+ g_milter_socket) + Milter.runmilter(g_milter_name,g_milter_socket,timeout,True) + log_queue.put(None) + logger_proc.join() + logging.info("Shutdown " + g_milter_name) + except: + logging.error("MAIN-EXCEPTION: " + str(sys.exc_info())) diff --git a/docker/debian/Dockerfile b/docker/debian/Dockerfile index d1f3c50..f1032b9 100644 --- a/docker/debian/Dockerfile +++ b/docker/debian/Dockerfile @@ -18,5 +18,6 @@ RUN env; set -ex ; \ && rm -rf /var/lib/apt/lists/* COPY app/*.py /app/ +COPY entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..4aea782 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +set -x +set -e +umask 0000 +ulimit -n 1024 +/usr/bin/python3 /app/ldap-acl-milter.py From bbbaa64db8e248a2d8306ab24707816fcc2baa89 Mon Sep 17 00:00:00 2001 From: Dominik Chilla Date: Tue, 12 Feb 2019 01:58:32 +0100 Subject: [PATCH 2/2] . --- config/config.json | 49 ---------------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 config/config.json diff --git a/config/config.json b/config/config.json deleted file mode 100644 index 244f5ee..0000000 --- a/config/config.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "logging": { - "__level": "default: WARNING. Possible: INFO,ERROR,CRITICAL,DEBUG", - "level": "DEBUG", - "__filename": "default: empty string (stdout).", - "filename": "" - }, - "daemon":{ - "listen_host": "127.0.0.1", - "listen_port": 5001 - }, - "trusted_proxies": { - "rprx01":[ - "172.16.100.5", "fd00:100::5" - ], - "rprx02":[ - "172.16.100.6", "fd00:100::6" - ] - }, - "api_keys": { - "HIGHLY_SECURE_API_KEY": { - "user": "GULAG APP" - } - }, - "uri_prefixes": { - "root": "http://127.0.0.1:9090/api/v1/", - "mailrelays": "http://127.0.0.1:9090/api/v1/mailrelays/", - "mailboxes": "http://127.0.0.1:9090/api/v1/mailboxes/", - "quarmails": "http://127.0.0.1:9090/api/v1/quarmails/", - "attachments": "http://127.0.0.1:9090/api/v1/attachments/", - "uris": "http://127.0.0.1:9090/api/v1/uris/" - }, - "dos_protection": { - "max_body_bytes": 8388608 - }, - "db":{ - "unix_socket": "/mysqld/mysqld.sock", - "user": "root", - "password": "", - "name": "Gulag" - }, - "cleaner":{ - "retention_period": "12 hour", - "interval": 10 - }, - "importer":{ - "interval": 10 - } -}