docker_ssl_endpoint/start.py

253 lines
8.3 KiB
Python

#!/usr/bin/env python3
import os
import json
import sys
import signal
import logging
import time
import hashlib
import threading
import list_domains
from docker import Client
cert_path = '/data/haproxy'
cert_file = cert_path + '/cert.pem'
pid_file = '/haproxy.pid'
max_age = 24 * 60 * 60
delay = 10
def hash_cert_file():
"""Creates the sha256 hash of the certificate file for HAProxy. If the file
does not exist, an empty string is returned.
"""
if not os.path.isfile(cert_file):
return ''
file_obj = open(cert_file, 'rb')
hash_generator = hashlib.sha256()
buf = file_obj.read(65536)
while len(buf) > 0:
hash_generator.update(buf)
buf = file_obj.read(65536)
return hash_generator.digest()
def setup_logging():
"""Sets up logging with a nice format"""
logging.basicConfig(format='%(asctime)s [%(levelname)s]: %(message)s', level=logging.INFO)
def get_pid():
"""This function reads the process id from the given file and returns as int."""
with open(pid_file, 'r') as file:
return int(file.read())
def kill_haproxy():
"""Stops the currently running instance of HAProxy by issuing a kill signal to its pid."""
logging.info('killing HAProxy')
try:
os.kill(get_pid(), signal.SIGKILL)
except OSError:
pass
def start_haproxy_ssl():
"""Start HAProxy with SSL activated. Returns True, if HAProxy is running, False otherwise."""
kill_haproxy()
logging.info('starting HAProxy with SSL')
os.system('/usr/sbin/haproxy -f /haproxy_ssl.conf -p ' + pid_file)
return is_haproxy_running()
def start_haproxy():
"""Start HAProxy without SSL activated. Returns True, if HAProxy is running, False otherwise."""
kill_haproxy()
logging.info('starting HAProxy without SSL')
os.system('/usr/sbin/haproxy -f /haproxy.conf -p ' + pid_file)
return is_haproxy_running()
def is_haproxy_running():
"""Check if HAProxy is running by sending a signal to its pid."""
try:
os.kill(get_pid(), 0)
return True
except OSError:
return False
def ssl_possible():
"""Check if a certificate is available."""
if not os.path.isfile(cert_file):
return False
else:
return True
def read_file(filename):
file=open(filename, 'r')
read_data = file.read()
file.close()
return read_data
def write_file(filename, content):
file=open(filename, 'w')
file.write(content)
file.close()
def create_haproxy_cert():
"""Combines the freshly created fullchain.pem and privkey.pem into /data/haproxy/cert.pem"""
logging.info('updating %s', cert_file)
# make sure the path exists...
if not os.path.exists(cert_path):
logging.info('creating cert_path path: %s', cert_path)
os.mkdir(cert_path)
# find the youngest directory
youngest_modify_time = 0
youngest_directory = ''
for root, directories, files in os.walk('/data/config/live'):
for directory in directories:
modify_time = os.stat('/data/config/live/' + directory).st_mtime
if modify_time > youngest_modify_time:
youngest_modify_time = modify_time
youngest_directory = directory
youngest_directory = '/data/config/live/' + youngest_directory
logging.info('using %s as base dir', youngest_directory)
# read fullchain.pem and privkey.pem
if not os.path.exists(youngest_directory + '/fullchain.pem') or not os.path.exists(youngest_directory + '/privkey.pem'):
logging.info('either fullchain.pem or privkey.pem is missing.')
return
fullchain = read_file(youngest_directory + '/fullchain.pem')
privkey = read_file(youngest_directory + '/privkey.pem')
return fullchain + privkey
def create_cert_data_standalone(domains):
if len(domains) == 0:
logging.info('no domains for SSL found.')
return
domains = " -d ".join(domains)
# we should use tls-sni-01 if ssl is already running!
os.system(
'/letsencrypt/letsencrypt-auto --config letsencrypt.conf certonly --http-01-port 54321 -d ' + domains)
def cert_watcher():
ssl_active = ssl_possible() and is_haproxy_running()
cert_file_hash = hash_cert_file()
while True:
time.sleep(delay)
if not ssl_active:
if ssl_possible():
# we should be able to start with SSL, but ...
kill_haproxy()
start_haproxy_ssl()
if is_haproxy_running():
# running with SSL succeeded
cert_file_hash = hash_cert_file()
logging.info('NON SSL -> SSL')
ssl_active = True
else:
# something went wrong (maybe broken certificate) but without SSL we can run it
start_haproxy()
logging.info('NON SSL -> NON SSL')
else:
# currently not running with SSL but also no cert, so we do not attempt to start with SSL
pass
else:
if cert_file_hash != hash_cert_file():
# we are running with SSL and the certificate has changed so we need to restart haproxy
logging.info('cert has changed')
kill_haproxy()
start_haproxy_ssl()
if is_haproxy_running():
# restart with SSL succeeded, update hash
logging.info('SSL -> SSL')
else:
# restart with SSL failed, so we start without SSL again
start_haproxy()
logging.info('SSL -> NON SSL')
ssl_active = False
cert_file_hash = hash_cert_file()
if __name__ == '__main__':
setup_logging()
logging.info('starting ssl endpoint')
client = Client(base_url='unix://var/run/docker.sock', version='1.15')
# try to start in SSL mode, no problem if that fails
logging.info('try in SSL mode')
if not start_haproxy_ssl():
logging.info('SSL mode failed')
if not is_haproxy_running():
# tried to start haproxy and this failed, so we need to create a certificate and try again:
# - start non ssl haproxy to be able to get a valid cert
logging.info('try in NON SSL mode')
start_haproxy()
# - get all domains
resolved_domains = list_domains.get_resolving_domains_from_containers(client)
# - create cert
create_cert_data_standalone(resolved_domains)
cert_data = create_haproxy_cert()
write_file(cert_file, cert_data)
logging.info('file written')
start_haproxy_ssl()
# now we should have it up and running or something weird happened.
if not is_haproxy_running():
logging.error('could not start after generating cert. See output above.')
sys.exit(1)
t = threading.Thread(target=cert_watcher)
t.start()
cert_cache = {}
for line in client.events():
line_str = line.decode("utf-8")
event = json.loads(line_str)
logging.info('Event: %s', json.dumps(event))
if event['Action'] in ['create']:
# check if there is any domain name configured
container_id = event['id']
logging.info('Create container id: %s', container_id)
if len(list_domains.handle_container(client, container_id)) > 0:
resolved_domains = list_domains.get_resolving_domains_from_containers(client)
resolved_domains = sorted(resolved_domains)
if resolved_domains in cert_cache:
cached_data = cert_cache[resolved_domains]
if time.time() - cached_data['time'] <= max_age:
logging.info('get cached cert: %s s old', time.time() - cached_data['time'])
write_file(cert_file, cached_data['data'])
logging.info('file written')
continue
# no previous data or to old
logging.info('create new cert')
create_cert_data_standalone(resolved_domains)
cert_data = create_haproxy_cert()
cert_cache[resolved_domains] = {
'time': time.time(),
'data': cert_data
}
write_file(cert_file, cert_data)
logging.info('file written')