feat: secure build phase 1 — cosign cell-side image verification (warn default) + Dockerfile validation
Unit Tests / test (push) Successful in 13m28s
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user