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
If you liked this, I think you might be interested in some of these related articles:
- DEBIAN - How to clone a server using just rsync
- DEBIAN - Get a nearly fresh debian install without reinstalling
- DEBIAN - Look at that nice looking FreedomBox!
- PYTHON - Automating the creation of an static website
- MISC - New web Design