[Docker API] Use TLS encryption for communication with "on-the-fly" created key paris (non-exposed)
[Docker API] Create pipe to pass Rspamd UI worker password [Dovecot] Pull Spamassassin ruleset to be read by Rspamd (MANY THANKS to Peer Heinlein!) [Dovecot] Garbage collector for deleted maildirs (set keep time via MAILDIR_GC_TIME which defaults to 1440 minutes) [Web] Flush memcached after mailbox item changes, fixes #1808 [Web] Fix duplicate IDs, fixes #1792 [Compose] Use SQL sockets [PHP-FPM] Update APCu and Redis libs [Dovecot] Encrypt maildir with global key pair in crypt-vol-1 (BACKUP!), also fixes #1791 [Web] Fix deletion of spam aliases [Helper] Add "crypt" to backup script [Helper] Override file for external SQL socket (not supported!) [Compose] New images for Rspamd, PHP-FPM, SOGo, Dovecot, Docker API, Watchdog, ACME, Postfix
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
FROM python:2-alpine
|
||||
FROM alpine:3.8
|
||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
||||
|
||||
RUN apk add -U --no-cache iptables ip6tables tzdata
|
||||
RUN pip install docker==3.0.1 flask flask-restful
|
||||
RUN apk add -U --no-cache python2 python-dev py-pip gcc musl-dev tzdata openssl-dev libffi-dev \
|
||||
&& pip2 install --upgrade docker==3.0.1 flask flask-restful pyOpenSSL \
|
||||
&& apk del python-dev py2-pip gcc
|
||||
|
||||
COPY server.py /
|
||||
|
||||
CMD ["python2", "-u", "/server.py"]
|
||||
|
@@ -3,12 +3,16 @@ from flask_restful import Resource, Api
|
||||
from flask import jsonify
|
||||
from flask import request
|
||||
from threading import Thread
|
||||
from OpenSSL import crypto
|
||||
import docker
|
||||
import uuid
|
||||
import signal
|
||||
import time
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import ssl
|
||||
import socket
|
||||
|
||||
docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
|
||||
app = Flask(__name__)
|
||||
@@ -93,22 +97,74 @@ class container_post(Resource):
|
||||
return sieve_return.output
|
||||
except Exception as e:
|
||||
return jsonify(type='danger', msg=str(e))
|
||||
# not in use...
|
||||
elif request.json['cmd'] == 'mail_crypt_generate' and request.json['username'] and request.json['old_password'] and request.json['new_password']:
|
||||
try:
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
# create if missing
|
||||
crypto_generate = container.exec_run(["/bin/bash", "-c", "/usr/local/bin/doveadm mailbox cryptokey generate -u '" + request.json['username'].replace("'", "'\\''") + "' -URf"], user='vmail')
|
||||
if crypto_generate.exit_code == 0:
|
||||
# open a shell, bind stdin and return socket
|
||||
cryptokey_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='vmail')
|
||||
# command to be piped to shell
|
||||
cryptokey_cmd = "/usr/local/bin/doveadm mailbox cryptokey password -u '" + request.json['username'].replace("'", "'\\''") + "' -n '" + request.json['new_password'].replace("'", "'\\''") + "' -o '" + request.json['old_password'].replace("'", "'\\''") + "'\n"
|
||||
# socket is .output
|
||||
cryptokey_socket = cryptokey_shell.output;
|
||||
try :
|
||||
# send command utf-8 encoded
|
||||
cryptokey_socket.sendall(cryptokey_cmd.encode('utf-8'))
|
||||
# we won't send more data than this
|
||||
cryptokey_socket.shutdown(socket.SHUT_WR)
|
||||
except socket.error:
|
||||
# exit on socket error
|
||||
return jsonify(type='danger', msg=str('socket error'))
|
||||
# read response
|
||||
cryptokey_response = recv_socket_data(cryptokey_socket)
|
||||
crypto_error = re.search('dcrypt_key_load_private.+failed.+error', cryptokey_response)
|
||||
if crypto_error is not None:
|
||||
return jsonify(type='danger', msg=str("dcrypt_key_load_private error"))
|
||||
return jsonify(type='success', msg=str("key pair generated"))
|
||||
else:
|
||||
return jsonify(type='danger', msg=str(crypto_generate.output))
|
||||
except Exception as e:
|
||||
return jsonify(type='danger', msg=str(e))
|
||||
elif request.json['cmd'] == 'maildir_cleanup' and request.json['maildir']:
|
||||
try:
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
sane_name = re.sub(r'\W+', '', request.json['maildir'])
|
||||
maildir_cleanup = container.exec_run(["/bin/bash", "-c", "/bin/mv '/var/vmail/" + request.json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'"], user='vmail')
|
||||
if maildir_cleanup.exit_code == 0:
|
||||
return jsonify(type='success', msg=str("moved to garbage"))
|
||||
else:
|
||||
return jsonify(type='danger', msg=str(maildir_cleanup.output))
|
||||
except Exception as e:
|
||||
return jsonify(type='danger', msg=str(e))
|
||||
elif request.json['cmd'] == 'worker_password' and request.json['raw']:
|
||||
try:
|
||||
for container in docker_client.containers.list(filters={"id": container_id}):
|
||||
hash = container.exec_run(["/bin/bash", "-c", "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null"], user='_rspamd')
|
||||
if hash.exit_code == 0:
|
||||
hash_stdout = str(hash.output)
|
||||
for line in hash_stdout.split("\n"):
|
||||
if '$2$' in line:
|
||||
hash = line.strip()
|
||||
f = open("/access.inc", "w")
|
||||
f.write('enable_password = "' + re.sub('[^0-9a-zA-Z\$]+', '', hash.rstrip()) + '";\n')
|
||||
f.close()
|
||||
container.restart()
|
||||
worker_shell = container.exec_run(["/bin/bash"], stdin=True, socket=True, user='_rspamd')
|
||||
worker_cmd = "/usr/bin/rspamadm pw -e -p '" + request.json['raw'].replace("'", "'\\''") + "' 2> /dev/null\n"
|
||||
worker_socket = worker_shell.output;
|
||||
try :
|
||||
worker_socket.sendall(worker_cmd.encode('utf-8'))
|
||||
worker_socket.shutdown(socket.SHUT_WR)
|
||||
except socket.error:
|
||||
return jsonify(type='danger', msg=str('socket error'))
|
||||
worker_response = recv_socket_data(worker_socket)
|
||||
matched = False
|
||||
for line in worker_response.split("\n"):
|
||||
if '$2$' in line:
|
||||
matched = True
|
||||
hash = line.strip()
|
||||
hash_out = re.search('\$2\$.+$', hash).group(0)
|
||||
f = open("/access.inc", "w")
|
||||
f.write('enable_password = "' + re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip()) + '";\n')
|
||||
f.close()
|
||||
container.restart()
|
||||
if matched:
|
||||
return jsonify(type='success', msg='command completed successfully')
|
||||
else:
|
||||
return jsonify(type='danger', msg='command did not complete, exit code was ' + int(hash.exit_code))
|
||||
return jsonify(type='danger', msg='command did not complete')
|
||||
except Exception as e:
|
||||
return jsonify(type='danger', msg=str(e))
|
||||
elif request.json['cmd'] == 'mailman_password' and request.json['email'] and request.json['passwd']:
|
||||
@@ -137,11 +193,62 @@ class GracefulKiller:
|
||||
signal.signal(signal.SIGINT, self.exit_gracefully)
|
||||
signal.signal(signal.SIGTERM, self.exit_gracefully)
|
||||
|
||||
def exit_gracefully(self,signum, frame):
|
||||
def exit_gracefully(self, signum, frame):
|
||||
self.kill_now = True
|
||||
|
||||
def startFlaskAPI():
|
||||
app.run(debug=False, host='0.0.0.0', port=8080, threaded=True)
|
||||
create_self_signed_cert()
|
||||
try:
|
||||
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
ctx.check_hostname = False
|
||||
ctx.load_cert_chain(certfile='/cert.pem', keyfile='/key.pem')
|
||||
except:
|
||||
print "Cannot initialize TLS, retrying in 5s..."
|
||||
time.sleep(5)
|
||||
app.run(debug=False, host='0.0.0.0', port=443, threaded=True, ssl_context=ctx)
|
||||
|
||||
def recv_socket_data(c_socket, timeout=10):
|
||||
c_socket.setblocking(0)
|
||||
total_data=[];
|
||||
data='';
|
||||
begin=time.time()
|
||||
while True:
|
||||
if total_data and time.time()-begin > timeout:
|
||||
break
|
||||
elif time.time()-begin > timeout*2:
|
||||
break
|
||||
try:
|
||||
data = c_socket.recv(8192)
|
||||
if data:
|
||||
total_data.append(data)
|
||||
#change the beginning time for measurement
|
||||
begin=time.time()
|
||||
else:
|
||||
#sleep for sometime to indicate a gap
|
||||
time.sleep(0.1)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
return ''.join(total_data)
|
||||
|
||||
def create_self_signed_cert():
|
||||
pkey = crypto.PKey()
|
||||
pkey.generate_key(crypto.TYPE_RSA, 2048)
|
||||
cert = crypto.X509()
|
||||
cert.get_subject().O = "mailcow"
|
||||
cert.get_subject().CN = "dockerapi"
|
||||
cert.set_serial_number(int(uuid.uuid4()))
|
||||
cert.gmtime_adj_notBefore(0)
|
||||
cert.gmtime_adj_notAfter(10*365*24*60*60)
|
||||
cert.set_issuer(cert.get_subject())
|
||||
cert.set_pubkey(pkey)
|
||||
cert.sign(pkey, 'sha512')
|
||||
cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||
pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
|
||||
with os.fdopen(os.open('/cert.pem', os.O_WRONLY | os.O_CREAT, 0o644), 'w') as handle:
|
||||
handle.write(cert)
|
||||
with os.fdopen(os.open('/key.pem', os.O_WRONLY | os.O_CREAT, 0o600), 'w') as handle:
|
||||
handle.write(pkey)
|
||||
|
||||
api.add_resource(containers_get, '/containers/json')
|
||||
api.add_resource(container_get, '/containers/<string:container_id>/json')
|
||||
|
Reference in New Issue
Block a user