#!/usr/bin/env python3 """Automation for cert renewal. assumptions: * router is danknasty * generate key in /root/.ssh/id_autofirewall on server * firewall has config from `authorized_keys` file * firewall sshd config contains: `AcceptEnv ssl_service state` * firewall has `ssl-update.sh` copied to /usr/local/bin and chmod +x """ import getpass import os import pathlib import pexpect import subprocess import sys services = ['git', 'plex', 'jellyfin', 'photoprism'] def rmdir(directory): directory = pathlib.Path(directory) for item in directory.iterdir(): if item.is_dir(): rmdir(item) else: item.unlink() directory.rmdir() def main(args): # Get SSH decryption password cmd = ['/usr/bin/su', '-l', 'luke', '/usr/bin/pass', 'show', 'ssh/autofirewall'] p = subprocess.run(cmd, capture_output=True) decrypt_pp = p.stdout.decode('UTF-8').strip() if not decrypt_pp: sys.exit('Couldnt get decryption passpharase') # Start Certbot, get data to send to service challenge_path = pathlib.Path('/usr/share/nginx/html/' '.well-known/acme-challenge/') if challenge_path.is_dir(): rmdir(challenge_path) challenge_path.mkdir() challenge_path.chmod(0o755) if len(args) != 1: sys.exit(f'Give a service to renew: {", ".join(services)} ') service = args[0] if service not in services: sys.exit(f'Give a service to renew: {", ".join(services)} ') fqdn = f'{service}.drheck.dev' cmd = ['/usr/bin/certbot', 'certonly', '--manual', '-d', fqdn] p = pexpect.spawnu(' '.join(cmd)) # p.logfile = sys.stderr res = p.expect(['Create a file containing just this data:\r\n\r\n([^\r]+)\r', pexpect.TIMEOUT, pexpect.EOF], timeout=20) if res > 0: sys.exit('Timed out') data = p.match.group(1) long_match = ('And make it available on your web server at this URL:' '\r\n\r\nhttp://%s/.well-known/acme-challenge/([^\r]+)\r') res = p.expect([long_match % (fqdn,), pexpect.TIMEOUT, pexpect.EOF]) if res > 0: sys.exit('Timed out') filename = p.match.group(1) res = p.expect(['Press Enter to Continue', pexpect.EOF], timeout=0) # Certbot is paused. Got the data # put data in acme-challenge file data_file = challenge_path / pathlib.Path(filename) try: with open(data_file, 'w') as f: f.write(data) except: sys.exit(f'Failed to write {data_file}') data_file.chmod(0o644) # put symlink in nginx enabled sites symlink_name = pathlib.Path(f'{service}-le') nx_conf = pathlib.Path('/etc/nginx') avail_path = nx_conf / pathlib.Path('sites-available') enabled_path = nx_conf / pathlib.Path('sites-enabled') service_available_file = avail_path / symlink_name service_enabled_symlink = enabled_path / symlink_name if not service_enabled_symlink.is_symlink(): service_enabled_symlink.symlink_to(service_available_file) # open port 80 to ${service}.drheck.dev os.environ['state'] = 'HTTP_DOWN' os.environ['ssl_service'] = service cmd = ['/usr/bin/ssh', '-i', '/root/.ssh/id_autofirewall', '-o', 'SendEnv=state', '-o', 'SendEnv=ssl_service', '-l', 'luke131', 'danknasty', 'doas', '-n', '/usr/local/bin/ssl-update.sh'] print(f'cmd: {cmd}') p = pexpect.spawnu(' '.join(cmd)) p.logfile = sys.stderr res = p.expect(["""Enter passphrase for key '/root/.ssh/id_autofirewall':""", pexpect.TIMEOUT, pexpect.EOF]) if res > 0: sys.exit('Couldnt send decryption key to ssh.') p.sendline(decrypt_pp) res = p.expect(['success', pexpect.TIMEOUT, pexpect.EOF]) if res > 0: sys.exit(f'Failed. error: {p.before}') print(f'Turned up HTTP for {service}') # restart nginx # continue with certbot # close port 80 to git.drheck.dev # remove symlink in nginx enabled sites # restart nginx # chmod # /etc/letsencrypt/live/git.drheck.dev # /etc/letsencrypt/archive/git.drheck.dev # restart gitea # p.expect(r'up\s+(.*?),\s+([0-9]+) users?,\s+load averages?: ([0-9]+\.[0-9][0-9]),?\s+([0-9]+\.[0-9][0-9]),?\s+([0-9]+\.[0-9][0-9])') # duration, users, av1, av5, av15 = p.match.groups() if __name__ == '__main__': main(sys.argv[1:])