Files
IKAR/tool/renew_ip_certificate.py

78 lines
3.8 KiB
Python

"""Issue/renew IKAR's IP certificate without stopping the shared Caddy."""
from pathlib import Path
import datetime, grp, hashlib, json, os, subprocess, sys
ROOT = Path('/mnt/data/ikar_server')
WORK = ROOT / 'shared/caddy-ip'
IP = '195.216.241.165'
IMAGE = 'certbot/certbot@sha256:c23159d30afdd9c97960578aa4654f5901de6cae394958f894074dedd55e599d'
NAME = 'ikar-public-ip'
assert os.geteuid() == 0 and os.path.ismount('/mnt/data')
assert ROOT.resolve() == ROOT and (WORK / 'baseline.json').is_file()
mode = sys.argv[1]
assert mode in ('staging', 'issue', 'renew')
if mode == 'staging':
directories = [WORK / 'certbot-staging' / name for name in ('config', 'work', 'logs')]
else:
directories = [WORK / ('certbot-' + name) for name in ('config', 'work', 'logs')]
for path in directories:
path.mkdir(parents=True, exist_ok=True)
os.chmod(path, 0o700)
command = ['docker', 'run', '--rm', '--name', 'ikar_ip_certificate', '--cpus', '0.5',
'--pids-limit', '256', '--cap-drop', 'ALL', '--security-opt', 'no-new-privileges:true']
for source, target in zip(directories, ('/config', '/work', '/logs')):
command += ['--volume', f'{source}:{target}']
command += ['--volume', f'{WORK / "webroot"}:/webroot', IMAGE]
if mode == 'renew':
command += ['renew', '--cert-name', NAME, '--no-random-sleep-on-renew']
else:
command += ['certonly', '--cert-name', NAME, '--preferred-profile', 'shortlived',
'--webroot', '--webroot-path', '/webroot', '--ip-address', IP,
'--agree-tos', '--register-unsafely-without-email']
if mode == 'staging':
command += ['--staging']
command += ['--config-dir', '/config', '--work-dir', '/work', '--logs-dir', '/logs', '--non-interactive']
subprocess.run(command, check=True)
if mode == 'staging':
print(json.dumps({'stagingHttp01Validated': True, 'ip': IP}))
raise SystemExit(0)
issued = directories[0] / 'live' / NAME
certificate = (issued / 'fullchain.pem').read_bytes()
private_key = (issued / 'privkey.pem').read_bytes()
subprocess.run(['openssl', 'x509', '-in', str(issued / 'fullchain.pem'), '-checkip', IP, '-checkend', '86400', '-noout'], check=True)
cert_public = subprocess.check_output(['openssl', 'x509', '-in', str(issued / 'fullchain.pem'), '-pubkey', '-noout'])
key_public = subprocess.check_output(['openssl', 'pkey', '-in', str(issued / 'privkey.pem'), '-pubout'])
assert cert_public == key_public, 'Certificate/key mismatch'
digest = hashlib.sha256(certificate).hexdigest()
group = grp.getgrnam('caddy').gr_gid
tls = WORK / 'tls'
tls.mkdir(exist_ok=True)
os.chmod(tls, 0o750)
os.chown(tls, 0, group)
version = tls / digest[:16]
version.mkdir(exist_ok=True)
os.chmod(version, 0o750)
os.chown(version, 0, group)
for name, data in [('fullchain.pem', certificate), ('privkey.pem', private_key)]:
path = version / name
path.write_bytes(data)
os.chmod(path, 0o640)
os.chown(path, 0, group)
current = tls / 'current'
changed = not current.is_symlink() or current.resolve() != version
if changed:
temporary = tls / 'current.next'
temporary.unlink(missing_ok=True)
temporary.symlink_to(version.name, target_is_directory=True)
os.replace(temporary, current)
site = Path('/etc/caddy/sites/ikar-ip.caddy')
if site.is_file() and f'https://{IP} {{' in site.read_text():
subprocess.run(['caddy', 'validate', '--config', '/etc/caddy/Caddyfile'], check=True)
subprocess.run(['systemctl', 'reload', 'caddy'], check=True)
dates = subprocess.check_output(['openssl', 'x509', '-in', str(current / 'fullchain.pem'), '-dates', '-noout'], text=True).strip()
report = {'ip': IP, 'issuer': 'letsencrypt', 'certificateSha256': digest, 'certificateDates': dates,
'certificateChanged': changed, 'timestamp': datetime.datetime.now(datetime.timezone.utc).isoformat()}
(WORK / 'certificate-status.json').write_text(json.dumps(report, indent=2))
print(json.dumps(report))