partial rewrite

This commit is contained in:
LuKe Tidd 2024-02-24 17:13:05 -05:00
parent bfccb1d4a3
commit d692c184b3
Signed by: luke
GPG Key ID: 75D6600BEF4E8E8F

View File

@ -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')
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')
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)
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')
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')
log.info(f'open port 80 to {service}')
firewall_mod('HTTP_UP', service, decrypt_pp)
restart('nginx')
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')
cb.sendline()
log.info(f'sent <enter> to certbot to continue process')
res = cb.expect([pexpect.EOF])
log.info(f'cerbot completed. final output: {cb.before}')
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')
if 'failed' in cb.before:
sys.exit('Something went wrong')
log.info(f'open port 80 to {service}')
firewall_mod('HTTP_UP', service, decrypt_pp)
restart('nginx')
log.info(f'open port 80 to {service}')
firewall_mod('HTTP_DOWN', service, decrypt_pp)
cb.sendline()
log.info(f'sent <enter> to certbot to continue process')
res = cb.expect([pexpect.EOF])
log.info(f'cerbot completed. final output: {cb.before}')
service_enabled_symlink.unlink()
log.info('removed symlink in nginx to disable HTTP')
if 'failed' in cb.before:
sys.exit('Something went wrong')
restart('nginx')
log.info(f'open port 80 to {service}')
firewall_mod('HTTP_DOWN', service, decrypt_pp)
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')