diff --git a/README.md b/README.md index 3c48a67..da51517 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ -# ssl-update +# mycertbot +my process for updating certs \ No newline at end of file diff --git a/remote/authorized_keys b/remote/authorized_keys new file mode 100644 index 0000000..3e97bb4 --- /dev/null +++ b/remote/authorized_keys @@ -0,0 +1 @@ +command="/usr/local/bin/ssl-update.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIORHtLf6y6rF7EQ9UdPhILDUOLybxffwjyHzxsmOk735 autofirewall diff --git a/remote/ssl-update.sh b/remote/ssl-update.sh new file mode 100644 index 0000000..5a09654 --- /dev/null +++ b/remote/ssl-update.sh @@ -0,0 +1,55 @@ +#!/bin/ksh +# install to /usr/local/bin on firewall +# OpenBSD pdksh + +hostname_file='/etc/myname' + +if [ "$(id -u)" -ne 0 ]; then + printf 'Must be run as root.\n' >&2 + exit 1 +fi +if [ ! -f "$hostname_file" ]; then + printf 'No hostname file. Is this the right server?\n' >&2 + exit 1 +fi +if [ "$(<"$hostname_file")" != 'danknasty' ]; then + printf 'Only designed to be run on danknasty.\n' >&2 + exit 1 +fi + +fw_config="/etc/pf_${ssl_service}.conf" + +if [ ! -f "$fw_config" ]; then + printf 'No firewall config found for %s at %s.\n' "$ssl_service" "$fw_config" >&2 + exit 1 +fi + +printf 'Setting "%s" for "%s".\n' "$state" "$ssl_service" +printf 'Firewall config: "%s"\n' "$fw_config" + +if [ "$state" == 'HTTP_UP' ]; then + printf 'Removing comment\n' + sed -i 's/^# pass/pass/' "$fw_config" + if [ $? != 0 ]; then + printf 'Failed to configure %s http port up.\n' "$ssl_service" >&2 + exit 1 + fi +elif [ "$state" == 'HTTP_DOWN' ]; then + printf 'Adding comment\n' + sed -i 's/^pass/# pass/' "$fw_config" + if [ $? != 0 ]; then + printf 'Failed to configure %s http port down.\n' "$ssl_service" >&2 + exit 1 + fi +else + printf 'Invalid state: %s.\n' "$state" >&2 + exit 1 +fi + +/usr/local/bin/pfhup +if [ $? != 0 ]; then + printf 'Failed to restart firewall. Check config immediately.\n' >&2 + exit 1 +fi + +printf 'success\n' diff --git a/update_cert.py b/update_cert.py new file mode 100755 index 0000000..3ce4ea8 --- /dev/null +++ b/update_cert.py @@ -0,0 +1,132 @@ +#!/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:])