diff --git a/ssl-update.py b/ssl-update.py index 56f0d03..9e98203 100755 --- a/ssl-update.py +++ b/ssl-update.py @@ -19,7 +19,17 @@ import sys import time supported_services = [ - 'git', 'plex', 'jellyfin', 'photoprism', 'nextcloud', 'read', 'www', 'chat', 'sync'] + 'chat', + 'git', + 'irc', + 'jellyfin', + 'nextcloud', + 'photoprism', + 'plex', + 'read', + 'sync', + 'www', +] restart_delay = { 'plex': 10 @@ -31,15 +41,18 @@ pfx_key_path = { } # Cert owning user if different than the name of the service +# set to disable to not chown at all users = { 'git': 'gitea', 'read': 'http', 'chat': 'synapse', 'sync': 'syncv3', + 'irc': 'disable', } # systemd service names that don't match the service name # service : systemd_service +# user service "disable" to not attempt to restart any service systemd_services = { 'git': 'gitea', 'plex': 'plexmediaserver', @@ -47,6 +60,7 @@ systemd_services = { 'nextcloud': 'php-fpm', 'chat': 'synapse', 'sync': 'sliding-sync', + 'irc': 'disable', } @@ -117,6 +131,10 @@ def restart(service): systemd_service = systemd_services[service] except KeyError: systemd_service = service + if systemd_service == 'disable': + log.info(f'will perform no service action for {service}') + return + log.info(f'going to restart service: {service}') cmd = ['/usr/bin/systemctl', 'restart', systemd_service] log.info(f'cmd to restart service: "{" ".join(cmd)}"') @@ -179,21 +197,7 @@ def pfx_gen(service): log.info(f'this did not explicitly fail') -def main(args): - # logging.basicConfig(level=os.environ.get("LOGLEVEL", "WARNING")) - logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) - - log.info(f'program start: {sys.argv}') - if len(args) != 1: - sys.exit(f'Give a service to renew: {", ".join(supported_services)} ') - service = args[0] - if service not in supported_services: - sys.exit(f'Give a service to renew: {", ".join(supported_services)} ') - - uid = os.getuid() - if uid != 0: - sys.exit('Run as root') - +def get_decrypt_pp(server_user): log.info('Get SSH decryption pw') cmd = ['/usr/bin/su', '-l', server_user, '/usr/bin/pass', 'show', 'ssh/autofirewall'] @@ -204,8 +208,14 @@ def main(args): sys.exit('Could not get decryption passpharase') log.info('Got SSH decryption pw') - challenge_path = pathlib.Path( - '/usr/share/nginx/html/.well-known/acme-challenge/') + +def root_check(): + uid = os.getuid() + if uid != 0: + sys.exit('Run as root') + + +def remake_challenge_path(challenge_path): if challenge_path.is_dir(): recurse_rmdir(challenge_path) log.info('Challenge path deleted') @@ -214,96 +224,163 @@ def main(args): challenge_path.chmod(0o755) log.info('Challenge path chmodded') - fqdn = f'{service}.drheck.dev' + +class Certbot: + def __init__(self): + self.fqdn = '' + self.service = '' + self.skip = False + self.cb = None + self.data = None + self.next_step = None + + def set_service(self, service): + self.service = service + self.fqdn = f'{service}.drheck.dev' + + def certbot_spawn(self): + if not self.service: + sys.exit('Set service first') + cmd = ['/usr/bin/certbot', 'certonly', '--manual', '-d', self.fqdn] + log.info(f'certbot cmd: "{" ".join(cmd)}"') + self.cb = pexpect.spawnu(' '.join(cmd)) + self.cb.logfile = sys.stderr + + def certbot_init(self): + expected_results = { + 0: 'Create a file containing just this data:\r\n\r\n([^\r]+)\r', + 1: ('You have an existing certificate that has exactly the ' + "same domains or certificate name you requested and isn't " + 'close to expiry'), + 2: '\(U\)pdate key type\/\(K\)eep existing key type:', + 3: pexpect.TIMEOUT, + 4: pexpect.EOF, + } + res_list = [] + dict_len = len(expected_results.keys()) + for i in range(dict_len): + res_list.append(expected_results[i]) + + res = self.cb.expect(res_list, timeout=20) + if res > 2: + log.error(p.before) + sys.exit('Timed out. did not see any expected output') + if res == 2: + self.cb.sendline('U') + continue + if res == 0: + self.data = self.cb.match.group(1) + self.next_step = 'update cert' + break + if res == 1: + log.info('Current cert is not yet expired') + res = self.cb.expect_exact(['cancel):', pexpect.TIMEOUT, pexpect.EOF]) + if res > 0: + sys.exit('Timed out in setup with existing cert') + self.cb.sendline('1') + self.next_step = 'post cert' + break + + +def main(args): + logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) + + root_check() + log.info(f'program start: {sys.argv}') + + if len(args) == 1: + sys.exit('Give a service(s) to renew: ', + ', '.join(supported_services)) + service = args[0] + if service not in supported_services: + sys.exit(f'Give a service to renew: {", ".join(supported_services)} ') + + # pass phrase for ssh key decryption + decrypt_pp = get_decrypt_pp() + + challenge_path = pathlib.Path( + '/usr/share/nginx/html/.well-known/acme-challenge/') + # delete any crud from here and make sure correct permissions + remake_challenge_path(challenge_path) + log.info(f'fqdn: {fqdn}') # Dry run: # cmd = ['/usr/bin/certbot', '--dry-run', 'certonly', '--manual', '-d', fqdn] - # Real run: - cmd = ['/usr/bin/certbot', 'certonly', '--manual', '-d', fqdn] - log.info(f'certbot cmd: "{" ".join(cmd)}"') - cb = pexpect.spawnu(' '.join(cmd)) - cb.logfile = sys.stderr + + # TODO: finish this function v + + certbot = Certbot(fqdn) + certbot.set_service(service) + certbot.certbot_spawn() while True: - res = cb.expect( - ['Create a file containing just this data:\r\n\r\n([^\r]+)\r', - ('You have an existing certificate that has exactly the ' - "same domains or certificate name you requested and isn't " - 'close to expiry'),'\(U\)pdate key type\/\(K\)eep existing key type:', - pexpect.TIMEOUT, pexpect.EOF], timeout=20) - if res > 2: - sys.exit('Timed out') - if res == 2: - cb.sendline('U') - continue - if res == 1: - log.info('Current cert is not yet expired') - res = cb.expect_exact(['cancel):', pexpect.TIMEOUT, pexpect.EOF]) - if res > 0: - sys.exit('Timed out in setup with existing cert') - cb.sendline('2') - res = cb.expect( - ['Create a file containing just this data:\r\n\r\n([^\r]+)\r', - pexpect.TIMEOUT, pexpect.EOF], timeout=20) - if res > 1: - sys.exit('Timed out') - if res == 0: + certbot.certbot_init() + if certbot.next_step: break + if certbot.next_step == 'update cert': + certbot.write_secret() + nginx.enable_secret_http() + firewall.open(service) + certbot.secret_ready() - data = cb.match.group(1) - log.info(f'secret data: {data}') - log.info('the data string and location for the shared secret are known') - 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 = cb.expect([long_match % (fqdn,), pexpect.TIMEOUT, pexpect.EOF]) - if res > 0: - sys.exit('Timed out') - filename = cb.match.group(1) - log.info(f'filename of secret: {filename}') - res = cb.expect(['Press Enter to Continue', pexpect.EOF], timeout=0) + update_firewall + certbot.create_secret() + - 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}') - log.info('created secret file with secret data') - data_file.chmod(0o644) - log.info('and chmodded') + def create_secret(self): + log.info(f'secret data: {data}') + log.info('the data string and location for the shared secret are known') + 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 = cb.expect([long_match % (fqdn,), pexpect.TIMEOUT, pexpect.EOF]) + if res > 0: + sys.exit('Timed out') + filename = cb.match.group(1) + log.info(f'filename of secret: {filename}') + res = cb.expect(['Press Enter to Continue', pexpect.EOF], timeout=0) - symlink_name = pathlib.Path(f'{service}-le') - log.info(f'nginx symlink: {symlink_name}') - nx_conf = pathlib.Path('/etc/nginx') - avail_path = nx_conf / pathlib.Path('sites-available') - enabled_path = nx_conf / pathlib.Path('sites-enabled') + 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}') + log.info('created secret file with secret data') + data_file.chmod(0o644) + log.info('and chmodded') - service_available_file = avail_path / symlink_name - log.info(f'nginx service avail symlink: {service_available_file}') - service_enabled_symlink = enabled_path / symlink_name - log.info(f'nginx service enabled symlink: {service_enabled_symlink}') - if not service_enabled_symlink.is_symlink(): - service_enabled_symlink.symlink_to(service_available_file) - log.info('created symlink to enable service') + symlink_name = pathlib.Path(f'{service}-le') + log.info(f'nginx symlink: {symlink_name}') + nx_conf = pathlib.Path('/etc/nginx') + avail_path = nx_conf / pathlib.Path('sites-available') + enabled_path = nx_conf / pathlib.Path('sites-enabled') - log.info(f'open port 80 to {service}') - firewall_mod('HTTP_UP', service, decrypt_pp) - restart('nginx') + service_available_file = avail_path / symlink_name + log.info(f'nginx service avail symlink: {service_available_file}') + service_enabled_symlink = enabled_path / symlink_name + log.info(f'nginx service enabled symlink: {service_enabled_symlink}') + if not service_enabled_symlink.is_symlink(): + service_enabled_symlink.symlink_to(service_available_file) + log.info('created symlink to enable service') - cb.sendline() - log.info(f'sent to certbot to continue process') - res = cb.expect([pexpect.EOF]) - log.info(f'cerbot completed. final output: {cb.before}') + log.info(f'open port 80 to {service}') + firewall_mod('HTTP_UP', service, decrypt_pp) + restart('nginx') - if 'failed' in cb.before: - sys.exit('Something went wrong') + cb.sendline() + log.info(f'sent to certbot to continue process') + res = cb.expect([pexpect.EOF]) + log.info(f'cerbot completed. final output: {cb.before}') - log.info(f'open port 80 to {service}') - firewall_mod('HTTP_DOWN', service, decrypt_pp) + if 'failed' in cb.before: + sys.exit('Something went wrong') - service_enabled_symlink.unlink() - log.info('removed symlink in nginx to disable HTTP') + log.info(f'open port 80 to {service}') + firewall_mod('HTTP_DOWN', service, decrypt_pp) - restart('nginx') + service_enabled_symlink.unlink() + log.info('removed symlink in nginx to disable HTTP') + + restart('nginx') key_path = pathlib.Path('/etc/letsencrypt') live = key_path / pathlib.Path(f'live/{service}.drheck.dev')