the f*ck rants about stuff

Automatize wildcard cert renewal

problem definition

I host one instance of sandstorm. Id like to use my own domain AND HTTPS

Sandstorm uses a new unguessable throw-away host name for every session as part of its security strategy, so in order to host your own under your own domain, you need a wildcard DNS entry and a wildcard cert for it (a cert with a *.yourdomain that will be valid for all your subdomains)

I use certbot (aka letsencrypt) to generate my certificates. Unfortunately, they have stated that will not emit wildcard certificates. Not now, and very likely, not in the future

Sandstorm offers a free DNS service using sandcats.io with batteries included (free wildcard cert). But this makes the whole site looks like they are not running under your control when you share a link to it to a third party (even tho is not true). This being one of the main points of running my own instance makes this solution not suitable for me

For reasons that deserver its own rant, I will not buy a wildcard cert

This only left me with the option of running sandstorm in a local port, have my apache proxy petitions and present the right certs. I will be using the sandcats.io DNS + wilcard cert for websockets, which are virtually invisible to the final user

The certbot cert renovation is easy enough to automate, but I need to automate the renewal of the sandcats.io cert, which lasts for 9 days

solution

A service will run weekly to renew the cert. For this, It will use a configuration faking using one of those free sandcats.io free certs so sandstorm renew the cert. Parse the new cert and tell apache to use it

shortcomings

Disclaimer: This setup is not officially supported by sandstorm

The reason is that some apps doesnt work well due to some browsers security policies. Just like sandstorm guys, I had to make a compromise. The stuff I use works for me and I have to test it before I use something new :)

code
updatecert.py

#!/usr/bin/env python3
import json
from subprocess import call,check_call
from glob import glob
from shutil import copy
from time import sleep
from timeout import timeout

TIMEOUT = 120

SSPATH = '/opt/sandstorm'
CONF = SSPATH + '/sandstorm.conf'
GOODCONF = SSPATH + '/sandstorm.good.conf'
CERTCONF = SSPATH + '/sandstorm.certs.conf'
CERTSPATH = SSPATH + '/var/sandcats/https/server.sandcats.io/'
APACHECERT = '/etc/apache2/tls/cert'
APACHECERTPUB = APACHECERT + '.crt'
APACHECERTKEY = APACHECERT + '.key'

RESTART_APACHE_CMD = 'systemctl restart apache2'.split()
RESTART_SS_CMD = 'systemctl restart sandstorm'.split()

@timeout(TIMEOUT, "ERROR: Cert didnt renew in {} secs".format(TIMEOUT))
def check_cert_reply(files_before):
    found = None
    print("waiting for new cert in" + CERTCONF, end="")
    while not found:
        print(".", end="", flush=True)
        sleep(5)
        files_after = set(glob(CERTSPATH + '*.response-json'))

        found = files_after - files_before
    else:
        print("")
    return found.pop()

def renew_cert():
    files_before = set(glob(CERTSPATH + '*.response-json'))
    copy(CERTCONF, CONF)
    call(RESTART_SS_CMD)
    try:
        new_cert = check_cert_reply(files_before)
    finally:
        print("Restoring sandstorm conf and restarting it")
        copy(GOODCONF, CONF)
        call(RESTART_SS_CMD)
        print("Restoring done")
    return new_cert

def parse_cert(certfile):
    with open(certfile) as f:
        certs = json.load(f)

    with open(APACHECERTPUB, 'w') as cert:

        cert.write(certs['cert'])

        ca = certs['ca']
        ca.reverse()
        for i in ca:
            cert.write('\n')
            cert.write(i)

    copy(certfile[:-len('.response-json')], APACHECERTKEY)

if __name__ == '__main__':
    new_cert = renew_cert()
    parse_cert(new_cert)
    try:
        check_call(RESTART_APACHE_CMD)
    except:
        # one reason for apache to fail is to try to parse the json before is completely written
        # try once again just in case
        print("failed to restart apache with the new cert. Trying once more")
        sleep(1)
        parse_cert(new_cert)
        call(RESTART_APACHE_CMD)
updatecert.service

[Unit]
Description=tries to renew ss cert
OnFailure=status-email-admin@%n.service

[Service]
Type=oneshot
ExecStart=/root/updatecert.py
updatecert.timer

[Unit]
Description=runs ss cert renewal once a week

[Timer]
Persistent=true
OnCalendar=weekly
Unit=updatecert.service

[Install]
WantedBy=default.target
comments?

If you liked this, I think you might be interested in some of these related articles:

¡ En Español !