Harden Kubernetes lab CA profile [skip ci]
This commit is contained in:
@@ -187,7 +187,9 @@ digest and by the existing GovOPlaN signature policy. It deploys PostgreSQL,
|
||||
Redis, single-node Garage and GreenMail on the state VM. The API, WebUI, workers
|
||||
and scheduler run in K3s from digest-pinned release images. A private lab CA
|
||||
protects both ingress and S3; backend pods receive only the CA Secret and keep
|
||||
TLS verification enabled.
|
||||
TLS verification enabled. The CA profile carries critical `CA:TRUE` and
|
||||
`keyCertSign,cRLSign` constraints. `deploy` and `update` rotate older lab CAs
|
||||
that do not satisfy that profile and reissue the ingress/S3 certificate.
|
||||
|
||||
The final output identifies two local files below `state_directory`:
|
||||
|
||||
@@ -206,7 +208,8 @@ sudo update-ca-certificates
|
||||
```
|
||||
|
||||
Review mappings before adding them to `/etc/hosts`; the lifecycle does not edit
|
||||
the workstation's trust or resolver configuration.
|
||||
the workstation's trust or resolver configuration. Reinstall `pki/ca.crt` in
|
||||
the client trust store after an automatic CA rotation.
|
||||
|
||||
### Enroll the first administrator
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import redirect_stdout
|
||||
from dataclasses import replace
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -24,6 +26,7 @@ from govoplan_lab.lifecycle import ( # noqa: E402
|
||||
LabOperationError,
|
||||
_assert_domain_owned,
|
||||
_domain_description,
|
||||
_ensure_certificates,
|
||||
_render_kubectl_wrapper,
|
||||
destroy,
|
||||
)
|
||||
@@ -234,6 +237,37 @@ class KubernetesLabTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(0o600, stat.S_IMODE(path.stat().st_mode))
|
||||
|
||||
@unittest.skipUnless(shutil.which("openssl"), "openssl is required")
|
||||
def test_generated_lab_ca_passes_strict_chain_validation(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-lab-pki-") as directory:
|
||||
config = replace(
|
||||
load_config(REHEARSAL_CONFIG),
|
||||
state_directory=Path(directory),
|
||||
)
|
||||
_ensure_certificates(config, CommandRunner(config))
|
||||
ca_certificate = config.state_directory / "pki" / "ca.crt"
|
||||
server_certificate = config.state_directory / "pki" / "server.crt"
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"openssl",
|
||||
"verify",
|
||||
"-x509_strict",
|
||||
"-CAfile",
|
||||
str(ca_certificate),
|
||||
str(server_certificate),
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
0,
|
||||
result.returncode,
|
||||
(result.stdout + result.stderr).decode(errors="replace"),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -857,6 +857,7 @@ def _ensure_certificates(config: LabConfig, runner: CommandRunner) -> None:
|
||||
ca_valid = (
|
||||
ca_key.is_file()
|
||||
and ca_cert.is_file()
|
||||
and _ca_key_usage_is_valid(runner, ca_cert)
|
||||
and runner.run(
|
||||
[
|
||||
"openssl",
|
||||
@@ -900,6 +901,10 @@ def _ensure_certificates(config: LabConfig, runner: CommandRunner) -> None:
|
||||
str(ca_key),
|
||||
"-subj",
|
||||
f"/CN={config.name} private lab CA",
|
||||
"-addext",
|
||||
"basicConstraints=critical,CA:TRUE",
|
||||
"-addext",
|
||||
"keyUsage=critical,keyCertSign,cRLSign",
|
||||
"-out",
|
||||
str(ca_cert),
|
||||
],
|
||||
@@ -988,6 +993,28 @@ def _ensure_certificates(config: LabConfig, runner: CommandRunner) -> None:
|
||||
write_private(config.state_directory / "hosts", render_hosts(config))
|
||||
|
||||
|
||||
def _ca_key_usage_is_valid(runner: CommandRunner, certificate: Path) -> bool:
|
||||
result = runner.run(
|
||||
[
|
||||
"openssl",
|
||||
"x509",
|
||||
"-in",
|
||||
str(certificate),
|
||||
"-noout",
|
||||
"-ext",
|
||||
"keyUsage",
|
||||
],
|
||||
capture=True,
|
||||
check=False,
|
||||
)
|
||||
output = result.stdout.decode("utf-8", errors="replace")
|
||||
return (
|
||||
result.returncode == 0
|
||||
and "Certificate Sign" in output
|
||||
and "CRL Sign" in output
|
||||
)
|
||||
|
||||
|
||||
def _deploy_state_services(
|
||||
config: LabConfig,
|
||||
runner: CommandRunner,
|
||||
|
||||
Reference in New Issue
Block a user