From c3664b34d3a598ddaed009a85dd573a1fd2901b1 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Sun, 10 Apr 2016 12:36:43 +0200 Subject: [PATCH 1/7] first step to refactor --- list_domains.py | 9 +++- start.py | 113 ++++++++++++++++++++++++++++-------------------- 2 files changed, 74 insertions(+), 48 deletions(-) diff --git a/list_domains.py b/list_domains.py index 91ba668..2ef3c10 100644 --- a/list_domains.py +++ b/list_domains.py @@ -9,21 +9,24 @@ import os from socket import getaddrinfo import logging + def get_if_available(dict, key, defValue): if key in dict: return dict[key] else: return defValue + def analyse_proxy_data(data): """Extracts the data for the proxy configuration (envrionment variable 'PROXY_DATA' and converts it to a dictionary.""" proxy_data = {} for proxy_var in data['PROXY_DATA'].split(','): - t = proxy_var.split(":",1) + t = proxy_var.split(":", 1) proxy_data[t[0]] = t[1] return proxy_data + def analyse_env_vars(inspect_data): """Extracts the environment variables from the given result of an 'inspect container' call.""" @@ -35,6 +38,7 @@ def analyse_env_vars(inspect_data): env_data[t[0]] = t[1] return env_data + def handle_container(docker_client, id): """This function take a container's id and collects all data required to create a proper proxy configuration. The configuration is then @@ -47,6 +51,7 @@ def handle_container(docker_client, id): return names return [] + def get_resolving_domains_from_containers(docker_client): container_ids = docker_client.containers(quiet=True) @@ -70,8 +75,8 @@ def get_resolving_domains_from_containers(docker_client): return resolved_domains -if __name__ == '__main__': +if __name__ == '__main__': client = Client(base_url='unix://var/run/docker.sock', version='1.15') resolved_domains = get_resolving_domains_from_containers(client) diff --git a/start.py b/start.py index b3449b7..367c440 100644 --- a/start.py +++ b/start.py @@ -12,34 +12,37 @@ import threading import list_domains from docker import Client -cert_path='/data/haproxy' -cert_file='/data/haproxy/cert.pem' -pid_file='/haproxy.pid' - +cert_path = '/data/haproxy' +cert_file = '/data/haproxy/cert.pem' +pid_file = '/haproxy.pid' +delay = 10 def hash_cert_file(): - """Creates the sha256 hash of the certifcate file for haproxy. If the 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 '' - aFile = open(cert_file, 'rb') - hasher = hashlib.sha256() - buf = aFile.read(65536) + file_obj = open(cert_file, 'rb') + hash_generator = hashlib.sha256() + buf = file_obj.read(65536) while len(buf) > 0: - hasher.update(buf) - buf = aFile.read(65536) - return hasher.digest() + 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 issueing a kill signal to its pid.""" logging.info('killing haproxy') @@ -48,16 +51,19 @@ def kill_haproxy(): except OSError: pass + def start_haproxy_ssl(): logging.info('starting haproxy SSL') - os.system('/usr/sbin/haproxy -f /haproxy_ssl.conf -p /haproxy.pid') + os.system('/usr/sbin/haproxy -f /haproxy_ssl.conf -p ' + pid_file) return is_haproxy_running() + def start_haproxy(): logging.info('starting haproxy NON SSL') - os.system('/usr/sbin/haproxy -f /haproxy.conf -p /haproxy.pid') + os.system('/usr/sbin/haproxy -f /haproxy.conf -p ' + pid_file) return False + def is_haproxy_running(): try: os.kill(get_pid(), 0) @@ -65,6 +71,7 @@ def is_haproxy_running(): except OSError: return False + def ssl_possible(): """Check if a certificate is available.""" @@ -73,67 +80,81 @@ def ssl_possible(): else: return True + def create_haproxy_cert(): """Combines the freshly created fullchain.pem and privkey.pem into /data/haproxy/cert.pem""" logging.info('updating %s', cert_file) - os.system('DIR=`ls -td /data/config/live/*/ | head -1`; echo ${DIR}; mkdir -p /data/haproxy; cat ${DIR}/fullchain.pem ${DIR}/privkey.pem > /data/haproxy/cert.pem') + if not os.path.exists(cert_path): + logging.info('creating cert_path path: %s', cert_path) + os.mkdir(cert_path) + os.system( + 'DIR=`ls -td /data/config/live/*/ | head -1`; echo ${DIR}; cat ${DIR}/fullchain.pem ${DIR}/privkey.pem > ' + cert_file) + def create_cert_data_standalone(domains): domains = " -d ".join(domains) - os.system('/letsencrypt/letsencrypt-auto --config letencrypt.conf certonly --expand --force-renewal --duplicate --allow-subset-of-names --standalone-supported-challenges http-01 --http-01-port 54321 -d ' + domains) + os.system( + '/letsencrypt/letsencrypt-auto --config letencrypt.conf certonly --expand --force-renewal --duplicate --allow-subset-of-names --standalone-supported-challenges http-01 --http-01-port 54321 -d ' + domains) + def cert_watcher(): - SSL_RUNNING=True + ssl_active = ssl_possible() and is_haproxy_running() cert_file_hash = hash_cert_file() while True: logging.info('ping') - time.sleep(60) - if ssl_possible() and not SSL_RUNNING: - kill_haproxy() - start_haproxy_ssl() - if is_haproxy_running(): - cert_file_hash = hash_cert_file() - logging.info('NON SSL -> SSL') - SSL_RUNNING=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: - start_haproxy() - SSL_RUNNING=False - if SSL_RUNNING and cert_file_hash != hash_cert_file(): - logging.info('cert has changed') - kill_haproxy() - start_haproxy_ssl() - if is_haproxy_running(): + # 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() - logging.info('SSL -> SSL') - SSL_RUNNING=True - else: - start_haproxy() - logging.info('SSL -> NON SSL') - SSL_RUNNING=False + if __name__ == '__main__': setup_logging() - logging.info('starting') + logging.info('starting ssl endpoint') - if not os.path.exists(cert_path): - logging.info('creating cert_path path: %s', cert_path) - os.mkdir(cert_path) client = Client(base_url='unix://var/run/docker.sock', version='1.15') - cert_file_hash = hash_cert_file() - # try to start in SSL mode, no problem if that fails logging.info('try in SSL mode') - SSL_RUNNING = start_haproxy_ssl() - if not SSL_RUNNING: + 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') - SSL_RUNNING = start_haproxy() + start_haproxy() # - get all domains resolved_domains = list_domains.get_resolving_domains_from_containers(client) # - create cert From b23f1297e61681bff14bea1e8c2e3e795df64e20 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Sun, 10 Apr 2016 12:47:18 +0200 Subject: [PATCH 2/7] Fixed wrong function call --- start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.py b/start.py index 367c440..2f31908 100644 --- a/start.py +++ b/start.py @@ -176,7 +176,7 @@ if __name__ == '__main__': if event['Action'] in ['start', 'destroy']: # check if there is any domain name configured container_id = event['id'] - if len(list_domains.handle_container()) > 0: + if len(list_domains.handle_container(client, container_id)) > 0: resolved_domains = list_domains.get_resolving_domains_from_containers(client) create_cert_data_standalone(resolved_domains) create_haproxy_cert() From 223b49fb53d7da374208bca031027f7899da62e3 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Sun, 10 Apr 2016 14:02:56 +0200 Subject: [PATCH 3/7] Changed actions to react on --- start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.py b/start.py index 2f31908..3642117 100644 --- a/start.py +++ b/start.py @@ -173,7 +173,7 @@ if __name__ == '__main__': line_str = line.decode("utf-8") event = json.loads(line_str) - if event['Action'] in ['start', 'destroy']: + if event['Action'] in ['start', 'stop']: # check if there is any domain name configured container_id = event['id'] if len(list_domains.handle_container(client, container_id)) > 0: From b63cbb80cddc408aaeb83da83edd6968db369f9d Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Tue, 12 Apr 2016 07:02:15 +0200 Subject: [PATCH 4/7] changed spelling of HAProxy --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 91f58a9..34386a0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -# SSL Termination using haproxy -This image translates between plain http and https using haproxy. +# SSL Termination using HAProxy +This image translates between plain http and https using HAProxy. ## How it works + ``` +-------------+ | | From 6d93f9094b78e6437212439e0f023fae251454d0 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Tue, 12 Apr 2016 07:05:24 +0200 Subject: [PATCH 5/7] rework / refactoring / documentation * rename letencrypt.conf to letsencrypt.conf * Move more options to letsencrypt configurations file * Done lot of rework / refactoring / documentation --- Dockerfile | 2 +- letencrypt.conf => letsencrypt.conf | 4 ++ start.py | 59 +++++++++++++++++++++++------ 3 files changed, 53 insertions(+), 12 deletions(-) rename letencrypt.conf => letsencrypt.conf (67%) diff --git a/Dockerfile b/Dockerfile index 6210f10..45b00d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN pip3 install docker-py ADD haproxy_ssl.conf /haproxy_ssl.conf ADD haproxy.conf /haproxy.conf -ADD letencrypt.conf /letencrypt.conf +ADD letsencrypt.conf /letsencrypt.conf ADD start.py /start.py ADD list_domains.py /list_domains.py diff --git a/letencrypt.conf b/letsencrypt.conf similarity index 67% rename from letencrypt.conf rename to letsencrypt.conf index 00cb3ca..15a970a 100644 --- a/letencrypt.conf +++ b/letsencrypt.conf @@ -6,3 +6,7 @@ work-dir=/data/work config-dir=/data/config email=letsencrypt@lusiardi.de agree-tos=TRUE +expand=TRUE +force-renewal=TRUE +duplicate=TRUE +allow-subset-of-names=TRUE diff --git a/start.py b/start.py index 3642117..872179b 100644 --- a/start.py +++ b/start.py @@ -13,12 +13,13 @@ import list_domains from docker import Client cert_path = '/data/haproxy' -cert_file = '/data/haproxy/cert.pem' +cert_file = cert_path + '/cert.pem' pid_file = '/haproxy.pid' delay = 10 + def hash_cert_file(): - """Creates the sha256 hash of the certificate file for haproxy. If the 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): @@ -44,8 +45,8 @@ def get_pid(): def kill_haproxy(): - """Stops the currently running instance of haproxy by issueing a kill signal to its pid.""" - logging.info('killing 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: @@ -53,18 +54,23 @@ def kill_haproxy(): def start_haproxy_ssl(): - logging.info('starting 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(): - logging.info('starting haproxy NON SSL') + """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 False + 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 @@ -74,27 +80,58 @@ def is_haproxy_running(): 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) - os.system( - 'DIR=`ls -td /data/config/live/*/ | head -1`; echo ${DIR}; cat ${DIR}/fullchain.pem ${DIR}/privkey.pem > ' + cert_file) + + # 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(directory).st_mtime + if modify_time > youngest_modify_time: + youngest_modify_time = modify_time + youngest_directory = directory + + logging.info('using %s as base dir', youngest_directory) + + # read fullchain.pem and privkey.pem + fullchain = read_file(youngest_directory + '/fullchain.pem') + privkey = read_file(youngest_directory + '/privkey.pem') + write_file(cert_file, fullchain + privkey) def create_cert_data_standalone(domains): domains = " -d ".join(domains) + + # we should use tls-sni-01 if ssl is already running! os.system( - '/letsencrypt/letsencrypt-auto --config letencrypt.conf certonly --expand --force-renewal --duplicate --allow-subset-of-names --standalone-supported-challenges http-01 --http-01-port 54321 -d ' + domains) + '/letsencrypt/letsencrypt-auto --config letsencrypt.conf certonly --standalone-supported-challenges http-01 --http-01-port 54321 -d ' + domains) def cert_watcher(): From b3e75e564e929639bc3f2ab8078af1fa5bdec824 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Tue, 12 Apr 2016 07:29:39 +0200 Subject: [PATCH 6/7] Fixed issues with relative directories --- start.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/start.py b/start.py index 872179b..84eb9ba 100644 --- a/start.py +++ b/start.py @@ -113,10 +113,11 @@ def create_haproxy_cert(): youngest_directory = '' for root, directories, files in os.walk('/data/config/live'): for directory in directories: - modify_time = os.stat(directory).st_mtime + 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) @@ -124,7 +125,7 @@ def create_haproxy_cert(): fullchain = read_file(youngest_directory + '/fullchain.pem') privkey = read_file(youngest_directory + '/privkey.pem') write_file(cert_file, fullchain + privkey) - + logging.info('file written') def create_cert_data_standalone(domains): domains = " -d ".join(domains) From e2a3438b9386458ce0ccc36b6573b4f04ae4ada9 Mon Sep 17 00:00:00 2001 From: Joachim Lusiardi Date: Tue, 13 Dec 2016 07:09:06 +0100 Subject: [PATCH 7/7] add auto redirect from http to https --- haproxy_ssl.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/haproxy_ssl.conf b/haproxy_ssl.conf index 0b307c8..4729d1f 100644 --- a/haproxy_ssl.conf +++ b/haproxy_ssl.conf @@ -25,6 +25,7 @@ frontend http bind *:80 reqadd X-Forwarded-Proto:\ http acl letsencrypt-acl path_beg /.well-known/acme-challenge/ + redirect scheme https code 301 if !{ ssl_fc } use_backend letsencrypt-backend if letsencrypt-acl default_backend www-backend