feat(swarmctl): --tls with auto-issued or BYOC server cert

swarmctl CLI gains --tls/--cert/--key/--client-ca flags. With --tls the
controller runs uvicorn under HTTPS + mTLS (CERT_REQUIRED) so worker
heartbeats can reach it cross-host. Default is still 127.0.0.1 plaintext
for backwards compat with the master-CLI enrollment flow.

Auto-issue path (no --cert/--key given): a server cert signed by the
existing DECNET CA is issued once and parked under ~/.decnet/swarmctl/.
Workers already ship that CA's ca.crt from the enroll bundle, so they
verify the endpoint with no extra trust config. BYOC via --cert/--key
when the operator wants a publicly-trusted or externally-managed cert.
The auto-cert path is idempotent across restarts to keep a stable
fingerprint for any long-lived mTLS sessions.
This commit is contained in:
2026-04-19 21:46:32 -04:00
parent e411063075
commit 62f7c88b90
3 changed files with 109 additions and 2 deletions

View File

@@ -52,6 +52,38 @@ def test_load_worker_bundle_returns_none_if_missing(tmp_path: pathlib.Path) -> N
assert pki.load_worker_bundle(tmp_path / "empty") is None
def test_ensure_swarmctl_cert_issues_from_same_ca(tmp_path: pathlib.Path) -> None:
ca_dir = tmp_path / "ca"
swarmctl_dir = tmp_path / "swarmctl"
cert_path, key_path, ca_path = pki.ensure_swarmctl_cert(
"0.0.0.0", ca_dir=ca_dir, swarmctl_dir=swarmctl_dir
)
assert cert_path.exists() and key_path.exists() and ca_path.exists()
# Server cert is signed by the same CA that workers will ship — that's
# the whole point of the auto-issue path.
cert = x509.load_pem_x509_certificate(cert_path.read_bytes())
ca_cert = x509.load_pem_x509_certificate(ca_path.read_bytes())
assert cert.issuer == ca_cert.subject
san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
ips = {str(v) for v in san.get_values_for_type(x509.IPAddress)}
dns = set(san.get_values_for_type(x509.DNSName))
assert "0.0.0.0" in ips
assert "localhost" in dns
# Key perm is the same 0600 we enforce on worker.key.
assert (key_path.stat().st_mode & 0o777) == 0o600
def test_ensure_swarmctl_cert_is_idempotent(tmp_path: pathlib.Path) -> None:
# Second call must NOT re-issue — otherwise a restart of swarmctl
# would rotate the server cert and break any worker mid-TLS-session.
ca_dir = tmp_path / "ca"
swarmctl_dir = tmp_path / "swarmctl"
first = pki.ensure_swarmctl_cert("0.0.0.0", ca_dir=ca_dir, swarmctl_dir=swarmctl_dir)
first_pem = first[0].read_bytes()
second = pki.ensure_swarmctl_cert("0.0.0.0", ca_dir=ca_dir, swarmctl_dir=swarmctl_dir)
assert second[0].read_bytes() == first_pem
def test_fingerprint_stable_across_calls(tmp_path: pathlib.Path) -> None:
ca = pki.ensure_ca(tmp_path / "ca")
issued = pki.issue_worker_cert(ca, "worker-03", ["127.0.0.1"])