fix(swarm): mTLS client-cert authz on the swarm control plane
The swarm controller (port 8770) exposed 9 routes with zero app-layer auth, and swarmctl --tls defaulted off — anyone able to reach the port could enroll workers (minting CA-signed certs + private keys), deploy, or tear down the fleet. Two fail-closed layers: - require_operator_cert gates every operator route (enroll/deploy/ teardown/hosts/check/deckies). When mTLS is on, the peer cert's CN must be an operator identity (decnet-master/swarmctl); worker and updater@* certs are rejected. Plaintext loopback (single-host master) is accepted as the local operator — the docker.sock boundary. - swarmctl refuses to bind a routable interface without --tls, so a network-exposed plaintext control plane can never start. /heartbeat keeps its worker fingerprint pinning. Closes the two ASVS criticals (control-plane no-auth, unauthenticated cert minting).
This commit is contained in:
@@ -6,8 +6,10 @@ generates a fresh worker keypair + CA-signed cert, and returns the full
|
||||
bundle to the operator. Bundle delivery to the worker (scp/sshpass/etc.)
|
||||
is outside this process's trust boundary.
|
||||
|
||||
Rationale: the worker agent speaks ONLY mTLS; there is no pre-auth
|
||||
bootstrap endpoint, so nothing to attack before the worker is enrolled.
|
||||
Authorization: this mints a CA-signed identity (and its private key), so it
|
||||
is gated by :func:`require_operator_cert` — an operator-CN client cert when
|
||||
the controller runs mTLS, or a local request when it is loopback-bound.
|
||||
A worker's own cert cannot enroll further hosts.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -20,6 +22,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from decnet.swarm import pki
|
||||
from decnet.web.db.repository import BaseRepository
|
||||
from decnet.web.dependencies import get_repo
|
||||
from decnet.web.router.swarm._mtls import PeerCert, require_operator_cert
|
||||
from decnet.web.db.models import SwarmEnrolledBundle, SwarmEnrollRequest, SwarmUpdaterBundle
|
||||
|
||||
router = APIRouter()
|
||||
@@ -39,6 +42,7 @@ router = APIRouter()
|
||||
async def api_enroll_host(
|
||||
req: SwarmEnrollRequest,
|
||||
repo: BaseRepository = Depends(get_repo),
|
||||
_operator: PeerCert = Depends(require_operator_cert),
|
||||
) -> SwarmEnrolledBundle:
|
||||
existing = await repo.get_swarm_host_by_name(req.name)
|
||||
if existing is not None:
|
||||
|
||||
Reference in New Issue
Block a user