From b8d2daf09789bc9b4ac6a42bd5bb6786d2279f53 Mon Sep 17 00:00:00 2001 From: Luke Tidd Date: Sun, 3 Jul 2022 13:11:28 -0400 Subject: [PATCH] jellyfin completed first complete update --- README.md | 25 ++++++- nfo/success_msg | 14 ++++ update_cert.py | 168 ++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 184 insertions(+), 23 deletions(-) create mode 100644 nfo/success_msg diff --git a/README.md b/README.md index da51517..509e6bd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,24 @@ -# mycertbot +ssl-update +automation for cert renewal with local hooks + +given a service: + * start letsencrypt's certbot "manually", getting ownership proof data + * write proof into nginx's serving path + * log into the firewall, allow http for the given service + * enable http for the given service + * instruct let's encrypt to check the proof + * get new keys + * disable http for the service + * log into firewall, block http for the given service + * set permissions and ownership on new keys + * perform service specific hooks + * jellyfin: generating a pkcs12 key + +All secrets are GPG encrypted and one password prompt allows for script access +to all secrets necessary. + +State: + * Only jellyfin is tested and working + * Can only really test when keys come closer to expiring + * code is ugly, could be a nice class or something -my process for updating certs \ No newline at end of file diff --git a/nfo/success_msg b/nfo/success_msg new file mode 100644 index 0000000..d0bc11e --- /dev/null +++ b/nfo/success_msg @@ -0,0 +1,14 @@ +Successfully received certificate. +Certificate is saved at: /etc/letsencrypt/live/jellyfin.drheck.dev/fullchain.pem +Key is saved at: /etc/letsencrypt/live/jellyfin.drheck.dev/privkey.pem +This certificate expires on 2022-10-01. +These files will be updated when the certificate renews. + +NEXT STEPS: +- This certificate will not be renewed automatically. Autorenewal of --manual certificates requires the use of an authentication hook script (--manual-auth-hook) but one was not provided. To renew this certificate, repeat this same certbot command before the certificate's expiry date. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +If you like Certbot, please consider supporting our work by: + * Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate + * Donating to EFF: https://eff.org/donate-le +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/update_cert.py b/update_cert.py index 3ce4ea8..5ae0e1b 100755 --- a/update_cert.py +++ b/update_cert.py @@ -13,11 +13,29 @@ import getpass import os import pathlib import pexpect +import pwd import subprocess import sys +import time services = ['git', 'plex', 'jellyfin', 'photoprism'] +users = { + 'jellyfin': 'jellyfin', + 'git': 'gitea', + 'plex': 'plex', + 'photoprism': 'photoprism', +} + +service_systemd = { + 'jellyfin': 'jellyfin', + 'git': 'gitea', + 'plex': 'plexmediaserver', + 'photoprism': 'photoprism', +} + +cert_files = ['privkey1.pem', 'fullchain1.pem', 'chain1.pem', 'cert1.pem'] + def rmdir(directory): directory = pathlib.Path(directory) @@ -29,10 +47,38 @@ def rmdir(directory): directory.rmdir() +def jellyfin(): + cmd = ['/usr/bin/su', '-l', 'luke', '/usr/bin/pass', 'show', 'ssl/jellyfin'] + p = subprocess.run(cmd, capture_output=True) + export_pw = p.stdout.decode('UTF-8').strip() + if not export_pw: + sys.exit('Couldnt get ssl export password for jellyfin') + cmd = ['/usr/bin/openssl', 'pkcs12', '-export', '-out', + '/etc/letsencrypt/live/jellyfin.drheck.dev/jellyfin.pfx', + '-inkey', '/etc/letsencrypt/live/jellyfin.drheck.dev/privkey.pem', + '-in', '/etc/letsencrypt/live/jellyfin.drheck.dev/cert.pem', + '-certfile', '/etc/letsencrypt/live/jellyfin.drheck.dev/chain.pem'] + p = pexpect.spawnu(' '.join(cmd)) + res = p.expect(['Enter Export Password:', pexpect.EOF, pexpect.TIMEOUT]) + if res > 0: + sys.exit('Failed to run openssl to generate ' + f'pkcs12 keys for jellyfin: {p.before}') + p.sendline(export_pw) + res = p.expect(['Verifying - Enter Export Password:', + pexpect.EOF, pexpect.TIMEOUT]) + if res > 0: + sys.exit('Failed to run openssl to generate ' + f'pkcs12 keys for jellyfin: {p.before}') + p.sendline(export_pw) + res = p.expect([pexpect.EOF, pexpect.TIMEOUT]) + if res > 0: + sys.exit(f'Failed to run openssl to generate ' + 'pkcs12 keys for jellyfin: {p.before}') + + 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() @@ -40,7 +86,6 @@ def main(args): 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(): @@ -55,22 +100,19 @@ def main(args): 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', + cb = pexpect.spawnu(' '.join(cmd)) + res = cb.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) + data = cb.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]) + res = cb.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 + filename = cb.match.group(1) + res = cb.expect(['Press Enter to Continue', pexpect.EOF], timeout=0) # put data in acme-challenge file data_file = challenge_path / pathlib.Path(filename) @@ -82,7 +124,6 @@ def main(args): 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') @@ -94,8 +135,7 @@ def main(args): service_enabled_symlink.symlink_to(service_available_file) # open port 80 to ${service}.drheck.dev - - os.environ['state'] = 'HTTP_DOWN' + os.environ['state'] = 'HTTP_UP' os.environ['ssl_service'] = service cmd = ['/usr/bin/ssh', '-i', '/root/.ssh/id_autofirewall', '-o', 'SendEnv=state', '-o', 'SendEnv=ssl_service', '-l', 'luke131', @@ -103,7 +143,8 @@ def main(args): 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]) + 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) @@ -114,18 +155,103 @@ def main(args): print(f'Turned up HTTP for {service}') # restart nginx + cmd = ['/usr/bin/systemctl', 'restart', 'nginx'] + p = subprocess.run(cmd, capture_output=True) + time.sleep(5) + +# get nginx status + cmd = ['/usr/bin/systemctl', 'status', 'nginx'] + p = subprocess.run(cmd, capture_output=True) + stdout = p.stdout.decode('UTF-8').split('\n') + success = False + for line in stdout: + if 'Active: active (running)' in line: + success = True + if not success: + sys.exit('nginx did not restart properly') + print('nginx restarted properly') + # continue with certbot -# close port 80 to git.drheck.dev + cb.sendline() + res = cb.expect([pexpect.EOF]) + print(cb.before) + +# close 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 down HTTP for {service}') + # remove symlink in nginx enabled sites + service_enabled_symlink.unlink() + # restart nginx + cmd = ['/usr/bin/systemctl', 'restart', 'nginx'] + p = subprocess.run(cmd, capture_output=True) + time.sleep(5) + +# get nginx status + cmd = ['/usr/bin/systemctl', 'status', 'nginx'] + p = subprocess.run(cmd, capture_output=True) + stdout = p.stdout.decode('UTF-8').split('\n') + success = False + for line in stdout: + if 'Active: active (running)' in line: + success = True + if not success: + sys.exit('nginx did not restart properly') + print('nginx restarted properly') + # 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() + key_path = pathlib.Path('/etc/letsencrypt') + live = key_path / pathlib.Path(f'live/{service}.drheck.dev') + archive = key_path / pathlib.Path(f'archive/{service}.drheck.dev') + user = users[service] + uid = pwd.getpwnam(user).pw_uid + gid = pwd.getpwnam(user).pw_gid + os.chown(live, uid, gid) + os.chown(archive, uid, gid) + for cert_file in cert_files: + os.chown(archive / pathlib.Path(cert_file), uid, gid) + +# service specific work + if service in globals(): + eval(f'{service}()') + print(f'{service} specific work complete.') + + # restart $service + systemd = service_systemd[service] + cmd = ['/usr/bin/systemctl', 'restart', systemd] + p = subprocess.run(cmd) + time.sleep(10) + +# get $service status + cmd = ['/usr/bin/systemctl', 'status', systemd] + p = subprocess.run(cmd, capture_output=True) + stdout = p.stdout.decode('UTF-8').split('\n') + success = False + for line in stdout: + if 'Active: active (running)' in line: + success = True + if not success: + sys.exit(f'{service} did not restart properly') + print(f'{service} restarted properly') if __name__ == '__main__':