"""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))