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:
@@ -14,6 +14,8 @@ def _patch_issue(monkeypatch, ovpn_path="/out/cn_2026-01-01/client.ovpn",
|
||||
one_time_url="https://cg.example.com/#/note/abc/xyz"):
|
||||
"""Patch all side-effectful callables used by CliRunner._issue."""
|
||||
monkeypatch.setattr("openvpncertupdate.revoke_issued", MagicMock())
|
||||
monkeypatch.setattr("openvpncertupdate.gen_crl", MagicMock())
|
||||
monkeypatch.setattr("openvpncertupdate.copy_crl", MagicMock())
|
||||
monkeypatch.setattr("openvpncertupdate.build_client_full", MagicMock())
|
||||
monkeypatch.setattr("openvpncertupdate.build_ovpn", MagicMock(return_value=ovpn_path))
|
||||
monkeypatch.setattr("openvpncertupdate.create_note", MagicMock(return_value=one_time_url))
|
||||
@@ -21,6 +23,8 @@ def _patch_issue(monkeypatch, ovpn_path="/out/cn_2026-01-01/client.ovpn",
|
||||
monkeypatch.setattr("openvpncertupdate.generate_password", MagicMock(return_value="Testpass1234567890abcdefgh"))
|
||||
return {
|
||||
"revoke_issued": sys.modules["openvpncertupdate"].revoke_issued,
|
||||
"gen_crl": sys.modules["openvpncertupdate"].gen_crl,
|
||||
"copy_crl": sys.modules["openvpncertupdate"].copy_crl,
|
||||
"build_client_full": sys.modules["openvpncertupdate"].build_client_full,
|
||||
"build_ovpn": sys.modules["openvpncertupdate"].build_ovpn,
|
||||
"create_note": sys.modules["openvpncertupdate"].create_note,
|
||||
@@ -87,6 +91,33 @@ def test_reissue_calls_revoke_then_build(monkeypatch, capsys):
|
||||
assert mocks["build_client_full"].call_args.args[2] == "bob"
|
||||
|
||||
|
||||
def test_reissue_regenerates_and_copies_crl(monkeypatch, capsys):
|
||||
# The old cert is revoked as part of reissue, so the published CRL is
|
||||
# stale until regenerated — reissue must do that itself now.
|
||||
mocks = _patch_issue(monkeypatch)
|
||||
CliRunner().reissue("bob", "bob@example.com")
|
||||
mocks["gen_crl"].assert_called_once()
|
||||
mocks["copy_crl"].assert_called_once()
|
||||
|
||||
|
||||
def test_create_does_not_touch_crl(monkeypatch, capsys):
|
||||
# --create never revokes anything, so there's nothing stale to fix.
|
||||
mocks = _patch_issue(monkeypatch)
|
||||
CliRunner().create("alice", "alice@example.com")
|
||||
mocks["gen_crl"].assert_not_called()
|
||||
mocks["copy_crl"].assert_not_called()
|
||||
|
||||
|
||||
def test_reissue_continues_building_when_crl_regen_fails(monkeypatch, capsys):
|
||||
import openvpncertupdate
|
||||
mocks = _patch_issue(monkeypatch)
|
||||
monkeypatch.setattr("openvpncertupdate.gen_crl",
|
||||
MagicMock(side_effect=openvpncertupdate.EasyRSAError("gen-crl failed")))
|
||||
CliRunner().reissue("bob", "bob@example.com")
|
||||
mocks["build_client_full"].assert_called_once()
|
||||
assert "CRL update failed" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_reissue_falls_back_to_index_txt_email(monkeypatch, capsys):
|
||||
# get_email() reads straight from index.txt; this stubs that lookup
|
||||
# rather than the file itself (index.txt parsing is covered in test_pki.py).
|
||||
|
||||
@@ -68,6 +68,33 @@ def test_copy_crl_copies_and_chmods(mock_chmod, mock_copy):
|
||||
mock_chmod.assert_called_once_with("/etc/openvpn/crl.pem", 0o644)
|
||||
|
||||
|
||||
@patch("openvpncertupdate.subprocess.run")
|
||||
@patch("openvpncertupdate.shutil.copy2")
|
||||
@patch("openvpncertupdate.os.chmod")
|
||||
def test_copy_crl_skips_restorecon_when_binary_empty(mock_chmod, mock_copy, mock_run):
|
||||
copy_crl("/pki", "/etc/openvpn/crl.pem", restorecon_binary="")
|
||||
mock_run.assert_not_called()
|
||||
|
||||
|
||||
@patch("openvpncertupdate.subprocess.run")
|
||||
@patch("openvpncertupdate.shutil.copy2")
|
||||
@patch("openvpncertupdate.os.chmod")
|
||||
def test_copy_crl_runs_restorecon_when_binary_set(mock_chmod, mock_copy, mock_run):
|
||||
copy_crl("/pki", "/etc/openvpn/crl.pem", restorecon_binary="restorecon")
|
||||
mock_run.assert_called_once_with(
|
||||
["restorecon", "/etc/openvpn/crl.pem"], capture_output=True, text=True)
|
||||
|
||||
|
||||
@patch("openvpncertupdate.subprocess.run", side_effect=FileNotFoundError("no restorecon"))
|
||||
@patch("openvpncertupdate.shutil.copy2")
|
||||
@patch("openvpncertupdate.os.chmod")
|
||||
def test_copy_crl_restorecon_failure_is_non_fatal(mock_chmod, mock_copy, mock_run):
|
||||
# Missing/misconfigured restorecon must not break CRL deployment.
|
||||
copy_crl("/pki", "/etc/openvpn/crl.pem", restorecon_binary="restorecon") # must not raise
|
||||
mock_copy.assert_called_once()
|
||||
mock_chmod.assert_called_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# is_ca_key_encrypted
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user