feat: secure build phase 1 — cosign cell-side image verification (warn default) + Dockerfile validation
Unit Tests / test (push) Successful in 13m28s

- config/cosign/cosign.pub: public verification key committed to repo (safe);
  cosign private key lives in /home/roof/.pic-secrets/ and is NEVER committed
- api/config_manager.py: image_verification config block (modes: off|warn|enforce,
  default: warn) so existing deployments are unaffected until images are signed
- api/service_composer.py: cosign verify before pull/up; enforce aborts the
  operation, warn logs and proceeds, off skips entirely; also fixes the prior
  unsafe proceed-on-pull-failure path
- api/service_store_manager.py: store-image digest requirement (warn default,
  reject under enforce)
- api/Dockerfile: cosign binary copied from the official cosign image
- docker-compose.yml: config/cosign/ bind-mounted into cell-api container
- install.sh: ensure/verify bundled cosign pubkey on new cell installs
- api/manifest_validator.py: validate_build_context() — Dockerfile lint
- tests: full coverage for config modes, composer verify paths, store digest
  guard, and validate_build_context

Verification defaults to warn so nothing breaks in production until images are
signed (phase 2). Private key stored outside git at /home/roof/.pic-secrets/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 03:53:47 -04:00
parent 8d904b1b8f
commit 238db60702
12 changed files with 622 additions and 2 deletions
+38
View File
@@ -47,6 +47,9 @@ logger = logging.getLogger(__name__)
# Valid Python logging levels for the `logging` config section.
_VALID_LOG_LEVELS = ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
# Image signature verification modes (see get/set_image_verification).
_IMAGE_VERIFY_MODES = ('off', 'warn', 'enforce')
# Per-service Python loggers exposed in the verbosity panel.
_LOGGING_PYTHON_SERVICES = (
'network', 'wireguard', 'email', 'calendar',
@@ -1005,6 +1008,41 @@ class ConfigManager:
ident.setdefault('service_ips', {}).pop(service_id, None)
self._save_all_configs()
# ── Image signature verification configuration ────────────────────────
#
# Controls how a cell treats store-service container images at install:
# off — skip cosign verification and the digest-pin requirement
# warn — log a warning on a missing digest / failed signature, proceed
# enforce — refuse to start a service whose image is undigested,
# unsigned, or whose signature does not verify
#
# Default is "warn" until the publish pipeline signs all store images; a
# later phase flips the default to "enforce". The section is backed up and
# restored with the rest of cell_config.json automatically.
def get_image_verification(self) -> Dict[str, Any]:
"""Return the image verification config, e.g. {'mode': 'warn'}."""
cfg = self.configs.get('image_verification')
if not isinstance(cfg, dict) or cfg.get('mode') not in _IMAGE_VERIFY_MODES:
cfg = {'mode': 'warn'}
self.configs['image_verification'] = cfg
return dict(cfg)
def get_image_verification_mode(self) -> str:
"""Return just the verification mode string (off|warn|enforce)."""
return self.get_image_verification()['mode']
def set_image_verification_mode(self, mode: str) -> None:
"""Persist the verification mode. Raises ValueError on an invalid mode."""
mode = (mode or '').lower()
if mode not in _IMAGE_VERIFY_MODES:
raise ValueError(
f"Invalid image verification mode: {mode!r} "
f"(expected one of {sorted(_IMAGE_VERIFY_MODES)})"
)
self.configs['image_verification'] = {'mode': mode}
self._save_all_configs()
# ── Logging verbosity configuration ───────────────────────────────────
def _ensure_logging_config(self) -> None:
"""Ensure a well-formed `logging` section exists, migrating the legacy