regenerate and copy CRL during reissue; run restorecon after every CRL copy

Reissue internally revokes the old cert (revoke_issued), which makes
the published CRL immediately stale, but neither the CLI --reissue
path nor the TUI renew flow ever regenerated/copied it — only
standalone revoke and "Regenerate CRL" did. A cert revoked as part of
a reissue could keep authenticating against OpenVPN until someone
remembered to regenerate the CRL by hand.

Both reissue paths now call gen_crl()+copy_crl() right after a
successful revoke_issued(), same as standalone revoke. This isn't
gated on the subsequent build_client_full() succeeding, since the CRL
is already stale the moment the old cert is revoked. A CRL-regen
failure here is reported as a warning and the reissue continues to
build the replacement cert — a new cert is more urgent than a fresh
CRL, and "Regenerate CRL"/--gen-crl remain available to retry.

Also add a RESTORECON_BINARY setting (default "restorecon", "" to
disable): copy_crl() now runs it on the destination file after every
copy, everywhere copy_crl() is called. Best-effort like
is_ca_key_encrypted() — a missing/misconfigured binary is swallowed
rather than breaking CRL deployment on non-SELinux systems.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Vlad Doloman
2026-07-09 10:35:17 +03:00
parent bb11702c4f
commit 0477392b5e
4 changed files with 105 additions and 10 deletions

View File

@@ -54,6 +54,7 @@ CONFIG_NAME = "client.ovpn"
VPN_CONFIGS_DIR = "./vpn-configs"
CRL_DEST_PATH = "/etc/openvpn/crl.pem"
RESTORECON_BINARY = "restorecon" # run on CRL_DEST_PATH after each copy; "" disables (non-SELinux systems)
CRYPTGEON_URL = "https://cryptgeon.example.com"
@@ -76,7 +77,7 @@ DAYS_AHEAD = 14
_OVERRIDABLE_SETTINGS = frozenset({
"EASYRSA_DIR", "EASYRSA_PKI_DIR", "CA_PASSPHRASE",
"OVPN_TEMPLATE_PATH", "CONFIG_NAME", "VPN_CONFIGS_DIR",
"CRL_DEST_PATH",
"CRL_DEST_PATH", "RESTORECON_BINARY",
"CRYPTGEON_URL",
"MAIL_FROM", "MAIL_SUBJECT", "EMAIL_TEMPLATE_PATH", "MAIL_BINARY",
"SMTP_HOST", "SMTP_PORT", "SMTP_USER", "SMTP_PASSWORD", "SMTP_TLS",
@@ -307,9 +308,16 @@ def gen_crl(easyrsa_dir: str, pki_dir: str, ca_passphrase: str) -> None:
)
def copy_crl(pki_dir: str, dest_path: str) -> None:
def copy_crl(pki_dir: str, dest_path: str, restorecon_binary: str = "") -> None:
shutil.copy2(f"{pki_dir}/crl.pem", dest_path)
os.chmod(dest_path, 0o644)
if restorecon_binary:
# Best-effort, like is_ca_key_encrypted(): a missing/misconfigured
# restorecon must not break CRL deployment on non-SELinux systems.
try:
subprocess.run([restorecon_binary, dest_path], capture_output=True, text=True)
except OSError:
pass
def is_ca_key_encrypted(pki_dir: str) -> bool:
@@ -1113,6 +1121,20 @@ class CursesApp:
except EasyRSAError as exc:
self._error(stdscr, f"EasyRSA error during revoke:\n{exc}")
return True
# The old cert is now revoked, so the published CRL is stale
# until regenerated — do that now rather than leaving it to a
# separate manual "Regenerate CRL" step. Not fatal: the new
# cert is more urgent than a fresh CRL, so keep going either way.
try:
gen_crl(EASYRSA_DIR, EASYRSA_PKI_DIR, CA_PASSPHRASE)
copy_crl(EASYRSA_PKI_DIR, CRL_DEST_PATH, RESTORECON_BINARY)
except EasyRSAError as exc:
self._error(
stdscr,
f"CRL update failed:\n{exc}\n\n"
f"{final_cn} was revoked but the published CRL is stale.\n"
f"Use \"Regenerate CRL\" once this finishes.",
)
try:
build_client_full(EASYRSA_DIR, EASYRSA_PKI_DIR,
@@ -1189,7 +1211,7 @@ class CursesApp:
try:
revoke_issued(EASYRSA_DIR, EASYRSA_PKI_DIR, cn, CA_PASSPHRASE)
gen_crl(EASYRSA_DIR, EASYRSA_PKI_DIR, CA_PASSPHRASE)
copy_crl(EASYRSA_PKI_DIR, CRL_DEST_PATH)
copy_crl(EASYRSA_PKI_DIR, CRL_DEST_PATH, RESTORECON_BINARY)
except EasyRSAError as exc:
self._error(stdscr, f"Revoke failed:\n{exc}")
return
@@ -1200,7 +1222,7 @@ class CursesApp:
self._msg(stdscr, "Regenerating CRL…")
try:
gen_crl(EASYRSA_DIR, EASYRSA_PKI_DIR, CA_PASSPHRASE)
copy_crl(EASYRSA_PKI_DIR, CRL_DEST_PATH)
copy_crl(EASYRSA_PKI_DIR, CRL_DEST_PATH, RESTORECON_BINARY)
except EasyRSAError as exc:
self._error(stdscr, f"gen-crl failed:\n{exc}")
return
@@ -1312,7 +1334,7 @@ class CliRunner:
try:
revoke_issued(EASYRSA_DIR, EASYRSA_PKI_DIR, cn, CA_PASSPHRASE)
gen_crl(EASYRSA_DIR, EASYRSA_PKI_DIR, CA_PASSPHRASE)
copy_crl(EASYRSA_PKI_DIR, CRL_DEST_PATH)
copy_crl(EASYRSA_PKI_DIR, CRL_DEST_PATH, RESTORECON_BINARY)
except EasyRSAError as exc:
print(f"error: {exc}", file=sys.stderr)
sys.exit(1)
@@ -1322,7 +1344,7 @@ class CliRunner:
print("Regenerating CRL…", file=sys.stderr)
try:
gen_crl(EASYRSA_DIR, EASYRSA_PKI_DIR, CA_PASSPHRASE)
copy_crl(EASYRSA_PKI_DIR, CRL_DEST_PATH)
copy_crl(EASYRSA_PKI_DIR, CRL_DEST_PATH, RESTORECON_BINARY)
except EasyRSAError as exc:
print(f"error: {exc}", file=sys.stderr)
sys.exit(1)
@@ -1344,6 +1366,21 @@ class CliRunner:
except EasyRSAError as exc:
print(f"error during revoke: {exc}", file=sys.stderr)
sys.exit(1)
# The old cert is now revoked, so the published CRL is stale
# until regenerated — do that now rather than leaving it to a
# separate --gen-crl run. Not fatal: the new cert is more
# urgent than a fresh CRL, so keep going either way.
print(f"Regenerating CRL…", file=sys.stderr)
try:
gen_crl(EASYRSA_DIR, EASYRSA_PKI_DIR, CA_PASSPHRASE)
copy_crl(EASYRSA_PKI_DIR, CRL_DEST_PATH, RESTORECON_BINARY)
except EasyRSAError as exc:
print(
f"warning: CRL update failed: {exc}\n"
f"warning: {cn} was revoked but the published CRL is stale; "
f"run --gen-crl once this finishes.",
file=sys.stderr,
)
print(f"Building certificate for {cn}", file=sys.stderr)
try: