feat(swarm): pin worker cert SHA-256 fingerprint per host
AgentClient now verifies the worker's TLS cert fingerprint against SwarmHost.client_cert_fingerprint at __aenter__ time, on top of CA validation. Required before fanning master-orchestrated topology deploys out across multiple swarm hosts: CA pinning alone allows any cert signed by the master CA, which is too coarse once a single deploy can target N hosts. Mismatch raises FingerprintMismatchError so callers can distinguish "wrong worker on the wire" from a transport hiccup.
This commit is contained in:
@@ -99,6 +99,49 @@ async def test_client_health_roundtrip(tmp_path: pathlib.Path) -> None:
|
||||
thread.join(timeout=5)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fingerprint_pin_accepts_matching_cert(tmp_path: pathlib.Path) -> None:
|
||||
"""AgentClient with the correct expected fingerprint connects normally."""
|
||||
port = _free_port()
|
||||
server, thread, master_id = _start_agent(tmp_path, port)
|
||||
try:
|
||||
worker_cert_pem = (tmp_path / "agent" / "worker.crt").read_bytes()
|
||||
expected = pki.fingerprint(worker_cert_pem)
|
||||
host = {
|
||||
"uuid": "h1",
|
||||
"name": "worker-test",
|
||||
"address": "127.0.0.1",
|
||||
"agent_port": port,
|
||||
"client_cert_fingerprint": expected,
|
||||
}
|
||||
async with swarm_client.AgentClient(host=host, identity=master_id) as agent:
|
||||
assert await agent.health() == {"status": "ok"}
|
||||
finally:
|
||||
server.should_exit = True
|
||||
thread.join(timeout=5)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fingerprint_pin_rejects_mismatch(tmp_path: pathlib.Path) -> None:
|
||||
"""A wrong expected fingerprint must raise FingerprintMismatchError."""
|
||||
port = _free_port()
|
||||
server, thread, master_id = _start_agent(tmp_path, port)
|
||||
try:
|
||||
host = {
|
||||
"uuid": "h1",
|
||||
"name": "worker-test",
|
||||
"address": "127.0.0.1",
|
||||
"agent_port": port,
|
||||
"client_cert_fingerprint": "0" * 64,
|
||||
}
|
||||
with pytest.raises(swarm_client.FingerprintMismatchError):
|
||||
async with swarm_client.AgentClient(host=host, identity=master_id):
|
||||
pass
|
||||
finally:
|
||||
server.should_exit = True
|
||||
thread.join(timeout=5)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_impostor_client_cannot_connect(tmp_path: pathlib.Path) -> None:
|
||||
"""A client whose cert was issued by a DIFFERENT CA must be rejected."""
|
||||
|
||||
Reference in New Issue
Block a user