Files
node-26/drb-edge-node/app/internal/key_pickup.py
T
Logan CusanoandClaude Opus 5.5 c4b4cb92c9
CI / lint (push) Successful in 8s
CI / test-op25 (push) Successful in 19s
CI / lint (pull_request) Successful in 8s
CI / test-op25 (pull_request) Successful in 18s
CI / test (push) Successful in 55s
CI / test (pull_request) Successful in 51s
Drop last --token hint; note sudo drops DRB_ENROLLMENT_TOKEN (node-26#18)
- key_pickup.py still told operators to re-run install.sh with --token.
- `curl | sudo bash` resets the env, so the documented exported-token
  path needs `sudo --preserve-env=DRB_ENROLLMENT_TOKEN`; say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:17:30 -04:00

74 lines
3.1 KiB
Python

"""
HTTPS api_key pickup — the node's own half of C2's enrollment flow
(Server/drb-c2-core/app/routers/enrollment.py, GET /nodes/{id}/credentials).
Why this exists (node-26#14): key delivery used to be MQTT-only
(nodes/{id}/api_key), and a dynsec broker refuses a node that has no key — or
a stale one — so a locked-out node could never receive the key that would let
it in. install.sh already did this pickup once at install time; doing it from
the running app too means a node approved after install, or whose key was
reissued, recovers on its own with no SSH and no container restart.
Auth is the pickup secret install.sh saved to /configs/pickup_secret at
enrollment (the same file, bind-mounted). Only ever sent to settings.c2_url,
over whatever scheme the operator configured (https in production).
"""
from typing import Optional
import httpx
from app.config import settings
from app.internal import credentials
from app.internal.logger import logger
_TIMEOUT = 15
async def fetch_api_key() -> Optional[str]:
"""Ask C2 for this node's api_key. Returns it, or None when there isn't one
to be had yet (not approved, no pickup secret, C2 unreachable, rejected).
Never raises — the caller retries on its own backoff."""
if not settings.c2_url:
logger.warning("Key pickup skipped: C2_URL is not configured.")
return None
secret = credentials.get_pickup_secret()
if not secret:
logger.warning(
f"Key pickup skipped: no {settings.config_path}/pickup_secret. This node was not enrolled by "
"install.sh on this machine — re-run install.sh, or have an admin Reissue key and write it in."
)
return None
url = f"{settings.c2_url.rstrip('/')}/nodes/{settings.node_id}/credentials"
try:
async with httpx.AsyncClient(timeout=_TIMEOUT) as client:
r = await client.get(url, headers={"X-Pickup-Secret": secret})
except Exception as e:
logger.warning(f"Key pickup: could not reach C2 ({e}).")
return None
if r.status_code == 401:
logger.error(
"Key pickup: C2 rejected the saved pickup secret (401) — it was rotated by a re-enroll elsewhere. "
"Re-run install.sh and give it a fresh enrollment token when it asks (node-26#18)."
)
return None
if r.status_code == 404:
logger.error("Key pickup: C2 does not know this node (404) — it was deleted server-side. Re-run install.sh.")
return None
if r.status_code != 200:
logger.warning(f"Key pickup: unexpected HTTP {r.status_code} from C2.")
return None
try:
body = r.json()
except ValueError:
logger.warning("Key pickup: C2 returned a non-JSON body.")
return None
key = body.get("api_key") if isinstance(body, dict) else None
if not key or not isinstance(key, str):
status = body.get("approval_status") if isinstance(body, dict) else None
logger.info(f"Key pickup: no api_key yet (approval_status={status!r}) — waiting for an admin to approve.")
return None
return key