From a9b58917b4274b36dd47b15743c71d3cefb04f32 Mon Sep 17 00:00:00 2001 From: Chris McCormick Date: Sun, 23 Jun 2019 22:47:58 +0800 Subject: [PATCH] RFC: letsencrypt SSL certificate support (#38) * Add Let's Encrypt SSL cert support. This patch has piku use the acme.sh script to request and maintain Let's Encrypt SSL certs rather than generate self-signed certs. For it to work you must install acme.sh as the user piku. Installation instructions here: https://github.com/Neilpang/acme.sh#1-how-to-install The next commit updates piku-bootstrap to install acme.sh by default. If acme.sh is not installed piku continues to default to a self-signed certificate. * Install acme.sh SSL cert wrangler in bootstrap. The previous commit contains details about usage. --- piku-bootstrap | 26 ++++++++++++++++++++++++++ piku.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/piku-bootstrap b/piku-bootstrap index 5ab2756..a9d780a 100755 --- a/piku-bootstrap +++ b/piku-bootstrap @@ -178,6 +178,32 @@ main() { args: creates: ~/.ssh/authorized_keys + - name: Check if acme.sh is already installed + stat: + path: ~/.acme.sh/acme.sh + register: acme_stat_result + + - name: Download acme.sh + get_url: + url: https://raw.githubusercontent.com/Neilpang/acme.sh/6ff3f5d/acme.sh + dest: ~/acme.sh + mode: 0755 + when: acme_stat_result.stat.exists == False + register: acme_installer + + - name: Execute acme.sh installer + shell: ./acme.sh --install + args: + chdir: ~/ + creates: ~/.acme.sh/acme.sh + executable: /bin/bash + when: acme_installer is defined + + - name: Remove acme.sh installer + file: path=~/acme.sh state=absent + when: acme_installer is defined + + - hosts: all become: yes tasks: diff --git a/piku.py b/piku.py index 9b2b26b..a7b9106 100755 --- a/piku.py +++ b/piku.py @@ -49,6 +49,8 @@ UWSGI_AVAILABLE = abspath(join(PIKU_ROOT, "uwsgi-available")) UWSGI_ENABLED = abspath(join(PIKU_ROOT, "uwsgi-enabled")) UWSGI_ROOT = abspath(join(PIKU_ROOT, "uwsgi")) UWSGI_LOG_MAXSIZE = '1048576' +ACME_ROOT = environ.get('ACME_ROOT', join(environ['HOME'],'.acme.sh')) +ACME_WWW = abspath(join(PIKU_ROOT, "acme")) # pylint: disable=anomalous-backslash-in-string NGINX_TEMPLATE = """ @@ -84,6 +86,11 @@ server { $INTERNAL_NGINX_STATIC_MAPPINGS + location ^~ /.well-known/acme-challenge { + allow all; + root ${ACME_WWW}; + } + location / { $INTERNAL_NGINX_UWSGI_SETTINGS proxy_http_version 1.1; @@ -96,6 +103,7 @@ server { proxy_set_header X-Request-Start $msec; $NGINX_ACL } + } """ @@ -107,6 +115,12 @@ server { listen $NGINX_IPV6_ADDRESS:80; listen $NGINX_IPV4_ADDRESS:80; server_name $NGINX_SERVER_NAME; + + location ^~ /.well-known/acme-challenge { + allow all; + root ${ACME_WWW}; + } + return 301 https://$server_name$request_uri; } @@ -152,6 +166,18 @@ server { """ # pylint: enable=anomalous-backslash-in-string +NGINX_ACME_FIRSTRUN_TEMPLATE = """ +server { + listen $NGINX_IPV6_ADDRESS:80; + listen $NGINX_IPV4_ADDRESS:80; + server_name $NGINX_SERVER_NAME; + location ^~ /.well-known/acme-challenge { + allow all; + root ${ACME_WWW}; + } +} +""" + INTERNAL_NGINX_STATIC_MAPPING = """ location $static_url { sendfile on; @@ -506,10 +532,12 @@ def spawn_app(app, deltas={}): nginx_ssl += " http2" elif "--with-http_spdy_module" in nginx and "nginx/1.6.2" not in nginx: # avoid Raspbian bug nginx_ssl += " spdy" + nginx_conf = join(NGINX_ROOT,"{}.conf".format(app)) env.update({ 'NGINX_SSL': nginx_ssl, 'NGINX_ROOT': NGINX_ROOT, + 'ACME_WWW': ACME_WWW, }) # default to reverse proxying to the TCP port we picked @@ -527,7 +555,26 @@ def spawn_app(app, deltas={}): domain = env['NGINX_SERVER_NAME'].split()[0] key, crt = [join(NGINX_ROOT, "{}.{}".format(app,x)) for x in ['key','crt']] + if exists(join(ACME_ROOT, "acme.sh")): + acme = ACME_ROOT + www = ACME_WWW + # if this is the first run there will be no nginx conf yet + # create a basic conf stub just to serve the acme auth + if not exists(nginx_conf): + echo("-----> writing temporary nginx conf") + buffer = expandvars(NGINX_ACME_FIRSTRUN_TEMPLATE, env) + with open(nginx_conf, "w") as h: + h.write(buffer) + if not exists(key) or not exists(join(ACME_ROOT, domain, domain + ".key")): + echo("-----> getting letsencrypt certificate") + call('{acme:s}/acme.sh --issue -d {domain:s} -w {www:s}'.format(**locals()), shell=True) + call('{acme:s}/acme.sh --install-cert -d {domain:s} --key-file {key:s} --fullchain-file {crt:s}'.format(**locals()), shell=True) + else: + echo("-----> letsencrypt certificate already installed") + + # fall back to creating self-signed certificate if acme failed if not exists(key): + echo("-----> generating self-signed certificate") call('openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=NY/L=New York/O=Piku/OU=Self-Signed/CN={domain:s}" -keyout {key:s} -out {crt:s}'.format(**locals()), shell=True) # restrict access to server from CloudFlare IP addresses @@ -573,7 +620,7 @@ def spawn_app(app, deltas={}): echo("-----> nginx will redirect all requests to hostname '{}' to HTTPS".format(env['NGINX_SERVER_NAME'])) else: buffer = expandvars(NGINX_TEMPLATE, env) - with open(join(NGINX_ROOT,"{}.conf".format(app)), "w") as h: + with open(nginx_conf, "w") as h: h.write(buffer) # Configured worker count