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

@@ -26,7 +26,7 @@ Edit the `SETTINGS` block at the top of `openvpncertupdate.py` before first run.
| Flag | Effect |
|---|---|
| `--create CN` | Issue new cert (requires `--email`) |
| `--reissue CN` | Revoke + reissue cert (`--email` optional, falls back to stored) |
| `--reissue CN` | Revoke + regenerate CRL + reissue cert (`--email` optional, falls back to stored) |
| `--revoke CN` | Revoke cert and regenerate CRL |
| `--gen-crl` | Regenerate and copy CRL only |
| `--list` | List recently-expired/soon-to-expire CNs (per `DAYS_PAST`/`DAYS_AHEAD`) with email; read-only, no CA passphrase needed |
@@ -67,8 +67,8 @@ python3 -m pytest tests/test_pki.py::test_sorted_ascending -v # single test
## Re-issue workflow
1. `revoke-issued <CN>` — archives old key + CSR to `pki/revoked/`
2. `build-client-full <CN> --passout=pass:<pw>` — generates new key + cert
3. CRL **not** auto-updated during renewal; use "Regenerate CRL" menu item or `r` hotkey
2. CRL regenerated and copied to `CRL_DEST_PATH` immediately after the revoke succeeds — the old cert is already revoked at this point, so the published CRL would otherwise be stale until a separate manual regen. Not fatal: a failure here is reported but the workflow continues to step 3 (a new cert is more urgent than a fresh CRL, and "Regenerate CRL" / `--gen-crl` remain available to retry)
3. `build-client-full <CN> --passout=pass:<pw>` — generates new key + cert
## TUI key bindings
@@ -88,7 +88,7 @@ python3 -m pytest tests/test_pki.py::test_sorted_ascending -v # single test
- EasyRSA called with `--batch`; `--passin=pass:<passphrase>` omitted when the resolved passphrase is empty
- CA passphrase resolution (`resolve_ca_passphrase()`, run once in `main()` right after arg parsing, before dispatch): `CA_PASSPHRASE=""` → auto-detect via `is_ca_key_encrypted()` (checks `<PKI_DIR>/private/ca.key` PEM header for `ENCRYPTED`) and prompt only if encrypted; `"!empty"` → never check/prompt, passphrase is `""`; `"!ask"` → always prompt, skip detection; any other value → used literally. `--list`/`--list-all` skip this resolution entirely since they only read `index.txt` and never touch the CA
- Cryptgeon: matches the `occulto` browser client — `key=os.urandom(32)` used directly (no derivation) for AES-256-GCM; `contents` = `base64(b"AES-GCM") + "--" + base64(nonce) + "--" + base64(ciphertext)`; `meta` = JSON string `{"type": "text"}`; URL = `<base>/note/<id>#<key.hex()>`
- `copy_crl()` does `chmod 644` after copy
- `copy_crl()` does `chmod 644` after copy, then runs `RESTORECON_BINARY` (default `restorecon`) on the copied file — best-effort like `is_ca_key_encrypted()`: a missing/misconfigured binary is swallowed, not fatal. Set `RESTORECON_BINARY=""` to disable on non-SELinux systems
- Password: pos 1=uppercase, pos 2=lowercase (no j), pos 3-27=alphanumeric, pos 28=lowercase (no j); `oO01lIQ5S2Z8B` banned everywhere
- Inline file path: `<PKI_DIR>/inline/private/<CN>.inline`
- User emails are not stored separately: `build_client_full()` sets `EASYRSA_REQ_EMAIL` whenever an email is known, which EasyRSA embeds as `emailAddress=` in the cert subject — so it round-trips through `<PKI_DIR>/index.txt` itself. `get_email()` reads it back from there; there is no `openvpncertupdate-metadata.json`