Add signed runtime distribution pipeline
This commit is contained in:
@@ -22,6 +22,15 @@ from .bundle import (
|
||||
render_compose,
|
||||
service_names,
|
||||
)
|
||||
from .distribution import (
|
||||
MAX_KEYRING_BYTES,
|
||||
MAX_MANIFEST_BYTES,
|
||||
DistributionError,
|
||||
file_sha256,
|
||||
load_bounded_json,
|
||||
verify_manifest,
|
||||
verify_manifest_binding,
|
||||
)
|
||||
from .model import (
|
||||
InstallationSpec,
|
||||
image_is_digest_pinned,
|
||||
@@ -212,40 +221,7 @@ def static_checks(spec: InstallationSpec, paths: BundlePaths) -> tuple[Check, ..
|
||||
)
|
||||
)
|
||||
|
||||
if spec.release.manifest_url and spec.release.manifest_sha256:
|
||||
checks.append(
|
||||
Check(
|
||||
"release.manifest",
|
||||
"ok",
|
||||
"A distribution manifest URL and expected digest are recorded.",
|
||||
)
|
||||
)
|
||||
else:
|
||||
checks.append(
|
||||
Check(
|
||||
"release.manifest",
|
||||
"error" if spec.profile == "self-hosted" else "warning",
|
||||
"No verified distribution manifest is recorded.",
|
||||
"Use a published signed distribution manifest for self-hosted apply.",
|
||||
)
|
||||
)
|
||||
if spec.profile == "self-hosted":
|
||||
checks.append(
|
||||
Check(
|
||||
"release.signature_verification",
|
||||
"error",
|
||||
"Signed distribution-manifest verification is not implemented in the deployer yet.",
|
||||
"Use the published verifier/bootstrap slice before a production apply.",
|
||||
)
|
||||
)
|
||||
checks.append(
|
||||
Check(
|
||||
"modules.image_composition",
|
||||
"error" if spec.enabled_modules else "warning",
|
||||
"The selected module set is not yet verified against image package contents.",
|
||||
"Use the signed distribution composition evidence before production apply.",
|
||||
)
|
||||
)
|
||||
checks.extend(_distribution_checks(spec, paths))
|
||||
|
||||
values = read_env(paths.env)
|
||||
required = {"MASTER_KEY_B64", "DATABASE_URL"}
|
||||
@@ -364,6 +340,130 @@ def static_checks(spec: InstallationSpec, paths: BundlePaths) -> tuple[Check, ..
|
||||
return tuple(checks)
|
||||
|
||||
|
||||
def _distribution_checks(
|
||||
spec: InstallationSpec,
|
||||
paths: BundlePaths,
|
||||
) -> tuple[Check, ...]:
|
||||
blocking_level = "error" if spec.profile == "self-hosted" else "warning"
|
||||
if not (
|
||||
spec.release.manifest_sha256
|
||||
and spec.release.manifest_keyring_sha256
|
||||
and spec.release.manifest_signature_key_id
|
||||
and spec.release.composition_sha256
|
||||
and paths.manifest.exists()
|
||||
and paths.keyring.exists()
|
||||
):
|
||||
return (
|
||||
Check(
|
||||
"release.manifest",
|
||||
blocking_level,
|
||||
"No locally verified runtime distribution is recorded.",
|
||||
"Run govoplan-deploy verify-release --adopt with an independently trusted keyring.",
|
||||
),
|
||||
Check(
|
||||
"release.signature_verification",
|
||||
blocking_level,
|
||||
"Runtime distribution signature evidence is unavailable.",
|
||||
"Install and verify the signed distribution before apply.",
|
||||
),
|
||||
Check(
|
||||
"modules.image_composition",
|
||||
blocking_level,
|
||||
"Enabled modules are not bound to image composition evidence.",
|
||||
"Adopt a distribution whose composition contains every enabled module.",
|
||||
),
|
||||
)
|
||||
try:
|
||||
manifest_digest = file_sha256(
|
||||
paths.manifest,
|
||||
maximum_bytes=MAX_MANIFEST_BYTES,
|
||||
)
|
||||
if manifest_digest != spec.release.manifest_sha256:
|
||||
raise DistributionError("stored manifest digest does not match installation")
|
||||
keyring_digest = file_sha256(
|
||||
paths.keyring,
|
||||
maximum_bytes=MAX_KEYRING_BYTES,
|
||||
)
|
||||
if keyring_digest != spec.release.manifest_keyring_sha256:
|
||||
raise DistributionError("stored keyring digest does not match installation")
|
||||
manifest = load_bounded_json(
|
||||
paths.manifest,
|
||||
maximum_bytes=MAX_MANIFEST_BYTES,
|
||||
)
|
||||
keyring = load_bounded_json(
|
||||
paths.keyring,
|
||||
maximum_bytes=MAX_KEYRING_BYTES,
|
||||
)
|
||||
key_id = verify_manifest(
|
||||
manifest,
|
||||
keyring,
|
||||
expected_channel=spec.release.channel,
|
||||
)
|
||||
if key_id != spec.release.manifest_signature_key_id:
|
||||
raise DistributionError("verified signature key does not match installation")
|
||||
verify_manifest_binding(
|
||||
manifest,
|
||||
channel=spec.release.channel,
|
||||
version=spec.release.version,
|
||||
api_image=spec.release.api_image,
|
||||
web_image=spec.release.web_image,
|
||||
enabled_modules=spec.enabled_modules,
|
||||
composition_sha256=spec.release.composition_sha256,
|
||||
dependencies=_selected_dependency_images(spec),
|
||||
)
|
||||
except (DistributionError, OSError) as exc:
|
||||
return (
|
||||
Check(
|
||||
"release.manifest",
|
||||
blocking_level,
|
||||
f"Runtime distribution verification failed: {exc}",
|
||||
"Re-adopt an unexpired, non-revoked manifest from a trusted release key.",
|
||||
),
|
||||
Check(
|
||||
"release.signature_verification",
|
||||
blocking_level,
|
||||
"Runtime distribution signature is not trusted.",
|
||||
"Correct the manifest/keyring binding before apply.",
|
||||
),
|
||||
Check(
|
||||
"modules.image_composition",
|
||||
blocking_level,
|
||||
"Runtime image composition is not trusted.",
|
||||
"Correct the signed composition binding before apply.",
|
||||
),
|
||||
)
|
||||
return (
|
||||
Check(
|
||||
"release.manifest",
|
||||
"ok",
|
||||
"Stored runtime distribution matches its independently pinned digest.",
|
||||
),
|
||||
Check(
|
||||
"release.signature_verification",
|
||||
"ok",
|
||||
f"Runtime distribution is signed by trusted key {key_id}.",
|
||||
),
|
||||
Check(
|
||||
"modules.image_composition",
|
||||
"ok",
|
||||
"Every enabled module is present in signed image composition evidence.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _selected_dependency_images(spec: InstallationSpec) -> dict[str, str]:
|
||||
values = {"load_balancer": spec.components.load_balancer.image}
|
||||
if spec.components.postgres.mode == "managed":
|
||||
values["postgres"] = spec.components.postgres.image
|
||||
if spec.components.redis.mode == "managed":
|
||||
values["redis"] = spec.components.redis.image
|
||||
if spec.components.mail.mode == "test-mail":
|
||||
values["test_mail"] = spec.components.mail.image
|
||||
if spec.components.storage.mode == "garage":
|
||||
values["garage"] = spec.components.storage.image
|
||||
return values
|
||||
|
||||
|
||||
def host_checks(
|
||||
spec: InstallationSpec,
|
||||
paths: BundlePaths,
|
||||
|
||||
Reference in New Issue
Block a user