Files
DECNET/decnet/web/router/swarm/_mtls.py
anti a4193d7022 fix(updater): enforce master CN gate and mandatory tarball checksum
The worker-side updater extracted + pip-installed + re-exec'd any tarball
from any caller holding a CA-signed cert; the documented updater@* CN
gating was never implemented. Now:

- require_master_cert gates /update, /update-self, /rollback, /releases:
  the client cert CN must be decnet-master (the identity UpdaterClient
  presents). A worker/agent cert can no longer push code to a peer.
- sha256 is mandatory on /update and /update-self (400 otherwise), so the
  integrity check always runs before extract/install. UpdaterClient
  already sends it; this just hardens the contract.

The transport peer-identity primitives move to decnet/web/_mtls.py (a
light namespace module) so the minimal updater reuses them without
importing the API router tree; router/swarm/_mtls.py re-exports them and
keeps the operator gate. Closes the updater-RCE critical.
2026-05-30 17:22:12 -04:00

76 lines
2.9 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""Operator authorization for the swarm control plane.
The transport peer-identity primitives live in :mod:`decnet.web._mtls` so the
minimal worker-side updater can reuse them without importing the API router
tree. This module adds the swarm-controller-specific operator gate on top and
re-exports the primitives for existing importers.
Role distinction is by CN, which the PKI assigns per identity
(``decnet/swarm/pki.py:issue_worker_cert``):
decnet-master master / operator client
swarmctl operator CLI server identity
{agent_name} worker agent
updater@{agent_name} per-worker updater
"""
from __future__ import annotations
from fastapi import HTTPException, Request
from decnet.logging import get_logger
from decnet.web._mtls import ( # re-exported for existing importers
LOOPBACK_HOSTS,
PeerCert,
client_is_loopback,
extract_peer_cert,
extract_peer_fingerprint,
)
__all__ = [
"LOOPBACK_HOSTS",
"PeerCert",
"client_is_loopback",
"extract_peer_cert",
"extract_peer_fingerprint",
"OPERATOR_CNS",
"require_operator_cert",
]
log = get_logger("swarm.mtls")
# Operator identities permitted to drive the control plane (enroll / deploy /
# teardown / host management). Worker and updater certs are intentionally
# excluded — a worker's still-valid cert must not be able to enroll new hosts
# or tear the fleet down.
OPERATOR_CNS = frozenset({"decnet-master", "swarmctl"})
def require_operator_cert(request: Request) -> PeerCert:
"""FastAPI dependency authorizing a swarm control-plane operation.
Two accepted paths, matching the deployment posture:
* **mTLS on** (any routable bind — enforced by the swarmctl startup guard):
a peer cert is present. Transport already proved it is CA-signed; we
additionally require its CN to be in :data:`OPERATOR_CNS`. Worker and
``updater@*`` certs are rejected — a worker's still-valid cert must never
drive enroll/deploy/teardown.
* **Loopback plaintext** (single-host master, the shipping default): no peer
cert, but the request came from ``127.0.0.1``/``::1``. Accepted as the
local operator — the same trust boundary as ``docker.sock``.
A certless request from any non-loopback client is refused (fail-closed);
in practice the startup guard prevents that combination from arising.
"""
peer = extract_peer_cert(request.scope)
if peer is not None:
if peer.cn not in OPERATOR_CNS:
log.warning("rejected non-operator cert on control plane: cn=%r", peer.cn)
raise HTTPException(status_code=403, detail="operator certificate required")
return peer
if client_is_loopback(request):
# Local operator on the master box; no client cert over plaintext loopback.
return PeerCert(sha256="", cn=None)
raise HTTPException(status_code=403, detail="operator certificate required")