Base on Stretch, add Zeyple
parent
34bc242554
commit
2b955d08ab
|
@ -1,4 +1,4 @@
|
||||||
FROM ubuntu:xenial
|
FROM debian:testing-slim
|
||||||
MAINTAINER Andre Peters <andre.peters@servercow.de>
|
MAINTAINER Andre Peters <andre.peters@servercow.de>
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
@ -19,10 +19,19 @@ RUN apt-get install -y --no-install-recommends supervisor \
|
||||||
postfix-pcre \
|
postfix-pcre \
|
||||||
syslog-ng \
|
syslog-ng \
|
||||||
syslog-ng-core \
|
syslog-ng-core \
|
||||||
ca-certificates
|
ca-certificates \
|
||||||
|
gnupg \
|
||||||
|
python-gpgme \
|
||||||
|
sudo \
|
||||||
|
dirmngr
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 600 zeyple
|
||||||
|
RUN adduser --system --home /var/lib/zeyple --no-create-home --uid 600 --gid 600 --disabled-login zeyple
|
||||||
|
RUN touch /var/log/zeyple.log && chown zeyple: /var/log/zeyple.log
|
||||||
RUN sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf
|
RUN sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf
|
||||||
|
|
||||||
|
COPY zeyple.py /usr/local/bin/zeyple.py
|
||||||
|
COPY zeyple.conf /etc/zeyple.conf
|
||||||
COPY supervisord.conf /etc/supervisor/supervisord.conf
|
COPY supervisord.conf /etc/supervisor/supervisord.conf
|
||||||
COPY postfix.sh /opt/postfix.sh
|
COPY postfix.sh /opt/postfix.sh
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ user = ${DBUSER}
|
||||||
password = ${DBPASS}
|
password = ${DBPASS}
|
||||||
hosts = mysql
|
hosts = mysql
|
||||||
dbname = ${DBNAME}
|
dbname = ${DBNAME}
|
||||||
query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', 'DUNNO') AS 'tls_enforce_in';
|
query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', NULL) AS 'tls_enforce_in';
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cat <<EOF > /opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf
|
cat <<EOF > /opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf
|
||||||
|
@ -25,7 +25,7 @@ user = ${DBUSER}
|
||||||
password = ${DBPASS}
|
password = ${DBPASS}
|
||||||
hosts = mysql
|
hosts = mysql
|
||||||
dbname = ${DBNAME}
|
dbname = ${DBNAME}
|
||||||
query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', 'DUNNO') AS 'tls_enforce_out';
|
query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', NULL) AS 'tls_enforce_out';
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf
|
cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf
|
||||||
|
@ -92,11 +92,21 @@ dbname = ${DBNAME}
|
||||||
query = SELECT goto FROM spamalias WHERE address='%s' AND validity >= UNIX_TIMESTAMP()
|
query = SELECT goto FROM spamalias WHERE address='%s' AND validity >= UNIX_TIMESTAMP()
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
# Reset GPG key permissions
|
||||||
|
mkdir -p /var/lib/zeyple/keys
|
||||||
|
chmod 700 /var/lib/zeyple/keys
|
||||||
|
chown -R 600:600 /var/lib/zeyple/keys
|
||||||
|
|
||||||
|
# Fix Postfix permissions
|
||||||
|
chgrp -R postdrop /var/spool/postfix/public
|
||||||
|
chgrp -R postdrop /var/spool/postfix/maildrop
|
||||||
|
postfix set-permissions
|
||||||
postconf -c /opt/postfix/conf
|
postconf -c /opt/postfix/conf
|
||||||
if [[ $? != 0 ]]; then
|
if [[ $? != 0 ]]; then
|
||||||
echo "Postfix configuration error, refusing to start."
|
echo "Postfix configuration error, refusing to start."
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
postfix -c /opt/postfix/conf start
|
postfix -c /opt/postfix/conf start
|
||||||
|
supervisorctl restart postfix-maillog
|
||||||
sleep 126144000
|
sleep 126144000
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -12,6 +12,17 @@ command=/opt/postfix.sh
|
||||||
autorestart=true
|
autorestart=true
|
||||||
|
|
||||||
[program:postfix-maillog]
|
[program:postfix-maillog]
|
||||||
command=/usr/bin/tail -f /var/log/mail.log
|
command=/bin/tail -f /var/log/zeyple.log /var/log/mail.log
|
||||||
stdout_logfile=/dev/fd/1
|
stdout_logfile=/dev/stdout
|
||||||
stdout_logfile_maxbytes=0
|
stdout_logfile_maxbytes=0
|
||||||
|
|
||||||
|
[unix_http_server]
|
||||||
|
file=/var/tmp/supervisord.sock
|
||||||
|
chmod=0770
|
||||||
|
chown=nobody:nogroup
|
||||||
|
|
||||||
|
[supervisorctl]
|
||||||
|
serverurl=unix:///var/tmp/supervisord.sock
|
||||||
|
|
||||||
|
[rpcinterface:supervisor]
|
||||||
|
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
[zeyple]
|
||||||
|
log_file = /var/log/zeyple.log
|
||||||
|
|
||||||
|
[gpg]
|
||||||
|
home = /var/lib/zeyple/keys
|
||||||
|
|
||||||
|
[relay]
|
||||||
|
host = localhost
|
||||||
|
port = 10026
|
|
@ -0,0 +1,274 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import email
|
||||||
|
import email.mime.multipart
|
||||||
|
import email.mime.application
|
||||||
|
import email.encoders
|
||||||
|
import smtplib
|
||||||
|
import copy
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
try:
|
||||||
|
from configparser import SafeConfigParser # Python 3
|
||||||
|
except ImportError:
|
||||||
|
from ConfigParser import SafeConfigParser # Python 2
|
||||||
|
|
||||||
|
import gpgme
|
||||||
|
|
||||||
|
# Boiler plate to avoid dependency on six
|
||||||
|
# BBB: Python 2.7 support
|
||||||
|
PY3K = sys.version_info > (3, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def message_from_binary(message):
|
||||||
|
if PY3K:
|
||||||
|
return email.message_from_bytes(message)
|
||||||
|
else:
|
||||||
|
return email.message_from_string(message)
|
||||||
|
|
||||||
|
|
||||||
|
def as_binary_string(email):
|
||||||
|
if PY3K:
|
||||||
|
return email.as_bytes()
|
||||||
|
else:
|
||||||
|
return email.as_string()
|
||||||
|
|
||||||
|
|
||||||
|
def encode_string(string):
|
||||||
|
if isinstance(string, bytes):
|
||||||
|
return string
|
||||||
|
else:
|
||||||
|
return string.encode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
__title__ = 'Zeyple'
|
||||||
|
__version__ = '1.2.0'
|
||||||
|
__author__ = 'Cédric Félizard'
|
||||||
|
__license__ = 'AGPLv3+'
|
||||||
|
__copyright__ = 'Copyright 2012-2016 Cédric Félizard'
|
||||||
|
|
||||||
|
|
||||||
|
class Zeyple:
|
||||||
|
"""Zeyple Encrypts Your Precious Log Emails"""
|
||||||
|
|
||||||
|
def __init__(self, config_fname='zeyple.conf'):
|
||||||
|
self.config = self.load_configuration(config_fname)
|
||||||
|
|
||||||
|
log_file = self.config.get('zeyple', 'log_file')
|
||||||
|
logging.basicConfig(
|
||||||
|
filename=log_file, level=logging.DEBUG,
|
||||||
|
format='%(asctime)s %(process)s %(levelname)s %(message)s'
|
||||||
|
)
|
||||||
|
logging.info("Zeyple ready to encrypt outgoing emails")
|
||||||
|
|
||||||
|
def load_configuration(self, filename):
|
||||||
|
"""Reads and parses the config file"""
|
||||||
|
|
||||||
|
config = SafeConfigParser()
|
||||||
|
config.read([
|
||||||
|
os.path.join('/etc/', filename),
|
||||||
|
filename,
|
||||||
|
])
|
||||||
|
if not config.sections():
|
||||||
|
raise IOError('Cannot open config file.')
|
||||||
|
return config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gpg(self):
|
||||||
|
protocol = gpgme.PROTOCOL_OpenPGP
|
||||||
|
|
||||||
|
if self.config.has_option('gpg', 'executable'):
|
||||||
|
executable = self.config.get('gpg', 'executable')
|
||||||
|
else:
|
||||||
|
executable = None # Default value
|
||||||
|
|
||||||
|
home_dir = self.config.get('gpg', 'home')
|
||||||
|
|
||||||
|
ctx = gpgme.Context()
|
||||||
|
ctx.set_engine_info(protocol, executable, home_dir)
|
||||||
|
ctx.armor = True
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
def process_message(self, message_data, recipients):
|
||||||
|
"""Encrypts the message with recipient keys"""
|
||||||
|
message_data = encode_string(message_data)
|
||||||
|
|
||||||
|
in_message = message_from_binary(message_data)
|
||||||
|
logging.info(
|
||||||
|
"Processing outgoing message %s", in_message['Message-id'])
|
||||||
|
|
||||||
|
if not recipients:
|
||||||
|
logging.warn("Cannot find any recipients, ignoring")
|
||||||
|
|
||||||
|
sent_messages = []
|
||||||
|
for recipient in recipients:
|
||||||
|
logging.info("Recipient: %s", recipient)
|
||||||
|
|
||||||
|
key_id = self._user_key(recipient)
|
||||||
|
logging.info("Key ID: %s", key_id)
|
||||||
|
|
||||||
|
if key_id:
|
||||||
|
out_message = self._encrypt_message(in_message, key_id)
|
||||||
|
|
||||||
|
# Delete Content-Transfer-Encoding if present to default to
|
||||||
|
# "7bit" otherwise Thunderbird seems to hang in some cases.
|
||||||
|
del out_message["Content-Transfer-Encoding"]
|
||||||
|
else:
|
||||||
|
logging.warn("No keys found, message will be sent unencrypted")
|
||||||
|
out_message = copy.copy(in_message)
|
||||||
|
|
||||||
|
self._add_zeyple_header(out_message)
|
||||||
|
self._send_message(out_message, recipient)
|
||||||
|
sent_messages.append(out_message)
|
||||||
|
|
||||||
|
return sent_messages
|
||||||
|
|
||||||
|
def _get_version_part(self):
|
||||||
|
ret = email.mime.application.MIMEApplication(
|
||||||
|
'Version: 1\n',
|
||||||
|
'pgp-encrypted',
|
||||||
|
email.encoders.encode_noop,
|
||||||
|
)
|
||||||
|
ret.add_header(
|
||||||
|
'Content-Description',
|
||||||
|
"PGP/MIME version identification",
|
||||||
|
)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _get_encrypted_part(self, payload):
|
||||||
|
ret = email.mime.application.MIMEApplication(
|
||||||
|
payload,
|
||||||
|
'octet-stream',
|
||||||
|
email.encoders.encode_noop,
|
||||||
|
name="encrypted.asc",
|
||||||
|
)
|
||||||
|
ret.add_header('Content-Description', "OpenPGP encrypted message")
|
||||||
|
ret.add_header(
|
||||||
|
'Content-Disposition',
|
||||||
|
'inline',
|
||||||
|
filename='encrypted.asc',
|
||||||
|
)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _encrypt_message(self, in_message, key_id):
|
||||||
|
if in_message.is_multipart():
|
||||||
|
# get the body (after the first \n\n)
|
||||||
|
payload = in_message.as_string().split("\n\n", 1)[1].strip()
|
||||||
|
|
||||||
|
# prepend the Content-Type including the boundary
|
||||||
|
content_type = "Content-Type: " + in_message["Content-Type"]
|
||||||
|
payload = content_type + "\n\n" + payload
|
||||||
|
|
||||||
|
message = email.message.Message()
|
||||||
|
message.set_payload(payload)
|
||||||
|
|
||||||
|
payload = message.get_payload()
|
||||||
|
|
||||||
|
else:
|
||||||
|
payload = in_message.get_payload()
|
||||||
|
payload = encode_string(payload)
|
||||||
|
|
||||||
|
quoted_printable = email.charset.Charset('ascii')
|
||||||
|
quoted_printable.body_encoding = email.charset.QP
|
||||||
|
|
||||||
|
message = email.mime.nonmultipart.MIMENonMultipart(
|
||||||
|
'text', 'plain', charset='utf-8'
|
||||||
|
)
|
||||||
|
message.set_payload(payload, charset=quoted_printable)
|
||||||
|
|
||||||
|
mixed = email.mime.multipart.MIMEMultipart(
|
||||||
|
'mixed',
|
||||||
|
None,
|
||||||
|
[message],
|
||||||
|
)
|
||||||
|
|
||||||
|
# remove superfluous header
|
||||||
|
del mixed['MIME-Version']
|
||||||
|
|
||||||
|
payload = as_binary_string(mixed)
|
||||||
|
|
||||||
|
encrypted_payload = self._encrypt_payload(payload, [key_id])
|
||||||
|
|
||||||
|
version = self._get_version_part()
|
||||||
|
encrypted = self._get_encrypted_part(encrypted_payload)
|
||||||
|
|
||||||
|
out_message = copy.copy(in_message)
|
||||||
|
out_message.preamble = "This is an OpenPGP/MIME encrypted " \
|
||||||
|
"message (RFC 4880 and 3156)"
|
||||||
|
|
||||||
|
if 'Content-Type' not in out_message:
|
||||||
|
out_message['Content-Type'] = 'multipart/encrypted'
|
||||||
|
else:
|
||||||
|
out_message.replace_header(
|
||||||
|
'Content-Type',
|
||||||
|
'multipart/encrypted',
|
||||||
|
)
|
||||||
|
|
||||||
|
out_message.set_param('protocol', 'application/pgp-encrypted')
|
||||||
|
out_message.set_payload([version, encrypted])
|
||||||
|
|
||||||
|
return out_message
|
||||||
|
|
||||||
|
def _encrypt_payload(self, payload, key_ids):
|
||||||
|
"""Encrypts the payload with the given keys"""
|
||||||
|
payload = encode_string(payload)
|
||||||
|
|
||||||
|
plaintext = BytesIO(payload)
|
||||||
|
ciphertext = BytesIO()
|
||||||
|
|
||||||
|
self.gpg.armor = True
|
||||||
|
|
||||||
|
recipient = [self.gpg.get_key(key_id) for key_id in key_ids]
|
||||||
|
|
||||||
|
self.gpg.encrypt(recipient, gpgme.ENCRYPT_ALWAYS_TRUST,
|
||||||
|
plaintext, ciphertext)
|
||||||
|
|
||||||
|
return ciphertext.getvalue()
|
||||||
|
|
||||||
|
def _user_key(self, email):
|
||||||
|
"""Returns the GPG key for the given email address"""
|
||||||
|
logging.info("Trying to encrypt for %s", email)
|
||||||
|
keys = [key for key in self.gpg.keylist(email)]
|
||||||
|
|
||||||
|
if keys:
|
||||||
|
key = keys.pop() # NOTE: looks like keys[0] is the master key
|
||||||
|
key_id = key.subkeys[0].keyid
|
||||||
|
return key_id
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _add_zeyple_header(self, message):
|
||||||
|
if self.config.has_option('zeyple', 'add_header') and \
|
||||||
|
self.config.getboolean('zeyple', 'add_header'):
|
||||||
|
message.add_header(
|
||||||
|
'X-Zeyple',
|
||||||
|
"processed by {0} v{1}".format(__title__, __version__)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _send_message(self, message, recipient):
|
||||||
|
"""Sends the given message through the SMTP relay"""
|
||||||
|
logging.info("Sending message %s", message['Message-id'])
|
||||||
|
|
||||||
|
smtp = smtplib.SMTP(self.config.get('relay', 'host'),
|
||||||
|
self.config.get('relay', 'port'))
|
||||||
|
|
||||||
|
smtp.sendmail(message['From'], recipient, message.as_string())
|
||||||
|
smtp.quit()
|
||||||
|
|
||||||
|
logging.info("Message %s sent", message['Message-id'])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
recipients = sys.argv[1:]
|
||||||
|
|
||||||
|
# BBB: Python 2.7 support
|
||||||
|
binary_stdin = sys.stdin.buffer if PY3K else sys.stdin
|
||||||
|
message = binary_stdin.read()
|
||||||
|
|
||||||
|
zeyple = Zeyple()
|
||||||
|
zeyple.process_message(message, recipients)
|
Loading…
Reference in New Issue