feat(device-plane): activate canonical outbound edge channel
This commit is contained in:
@@ -4,13 +4,16 @@
|
|||||||
"action": "upgrade",
|
"action": "upgrade",
|
||||||
"predecessor": {
|
"predecessor": {
|
||||||
"kind": "release",
|
"kind": "release",
|
||||||
"patchId": "device-manager-release-20260811-009",
|
"patchId": "device-manager-release-20260811-010",
|
||||||
"artifactSha256": "e0cc696e9387d26f005d342529f0e7c08be54113b9725271aecd9baf58b3726d"
|
"artifactSha256": "d4132993216eb674967dc6fc65d9670cfc2a9efdf46186ca019030f259de2d0e"
|
||||||
},
|
},
|
||||||
"service": "device-manager",
|
"service": "device-manager",
|
||||||
"publicIngress": "reverse-proxy-only",
|
"publicIngress": "reverse-proxy-only",
|
||||||
"deviceCoreManagementApi": "file-token-authenticated",
|
"deviceCoreManagementApi": "file-token-authenticated",
|
||||||
"launcherTrust": "file-token-scoped-to-device-core-handoff",
|
"launcherTrust": "file-token-scoped-to-device-core-handoff",
|
||||||
|
"edgeChannel": "core-initiated-pinned-mtls-enabled-zero-or-more-registered-edges",
|
||||||
|
"edgeChannelIdentity": "runner-managed-host-local-private-key-public-certificate-export",
|
||||||
|
"edgeChannelEgress": "dedicated-core-only-bridge-no-host-ingress-public-ipv4-tcp-8443-registration-policy",
|
||||||
"healthGate": "bounded-container-grace+core-contract",
|
"healthGate": "bounded-container-grace+core-contract",
|
||||||
"commandTransport": "disabled",
|
"commandTransport": "disabled",
|
||||||
"gelios": "untouched",
|
"gelios": "untouched",
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
DEVICE_MANAGEMENT_API_ENABLED: "true"
|
DEVICE_MANAGEMENT_API_ENABLED: "true"
|
||||||
DEVICE_MANAGEMENT_CORE_TOKEN_FILE: /run/nodedc-secrets/management-core-token
|
DEVICE_MANAGEMENT_CORE_TOKEN_FILE: /run/nodedc-secrets/management-core-token
|
||||||
|
DEVICE_EDGE_CHANNEL_ENABLED: "true"
|
||||||
|
DEVICE_EDGE_CHANNEL_CORE_KEY_FILE: /run/nodedc-secrets/device-edge-channel/core-private-key.pem
|
||||||
|
DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE: /run/nodedc-secrets/device-edge-channel/core-certificate.pem
|
||||||
|
DEVICE_EDGE_CHANNEL_TRUST_ROOT: /run/nodedc-secrets/device-edge-channel/peers
|
||||||
|
DEVICE_EDGE_CHANNEL_MAX_EDGES: "32"
|
||||||
|
DEVICE_EDGE_CHANNEL_RECONCILE_INTERVAL_MS: "15000"
|
||||||
volumes:
|
volumes:
|
||||||
- type: bind
|
- type: bind
|
||||||
source: /volume1/docker/nodedc-device-plane/secrets/management-core-token
|
source: /volume1/docker/nodedc-device-plane/secrets/management-core-token
|
||||||
@@ -10,6 +16,26 @@ services:
|
|||||||
read_only: true
|
read_only: true
|
||||||
bind:
|
bind:
|
||||||
create_host_path: false
|
create_host_path: false
|
||||||
|
- type: bind
|
||||||
|
source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/core-private-key.pem
|
||||||
|
target: /run/nodedc-secrets/device-edge-channel/core-private-key.pem
|
||||||
|
read_only: true
|
||||||
|
bind:
|
||||||
|
create_host_path: false
|
||||||
|
- type: bind
|
||||||
|
source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/core-certificate.pem
|
||||||
|
target: /run/nodedc-secrets/device-edge-channel/core-certificate.pem
|
||||||
|
read_only: true
|
||||||
|
bind:
|
||||||
|
create_host_path: false
|
||||||
|
- type: bind
|
||||||
|
source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/peers
|
||||||
|
target: /run/nodedc-secrets/device-edge-channel/peers
|
||||||
|
read_only: true
|
||||||
|
bind:
|
||||||
|
create_host_path: false
|
||||||
|
networks:
|
||||||
|
- device-plane-egress
|
||||||
|
|
||||||
device-manager:
|
device-manager:
|
||||||
image: nodedc/device-manager:local
|
image: nodedc/device-manager:local
|
||||||
@@ -71,6 +97,10 @@ services:
|
|||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
device-plane-egress:
|
||||||
|
name: nodedc-device-plane-egress
|
||||||
|
driver: bridge
|
||||||
|
internal: false
|
||||||
platform-edge:
|
platform-edge:
|
||||||
external: true
|
external: true
|
||||||
name: nodedc-platform_edge
|
name: nodedc-platform_edge
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const devicePlaneRoot = resolve(platformRoot, "device-plane");
|
|||||||
const designRoot = resolve(process.env.NODEDC_DEVICE_MANAGER_SOURCE_ROOT || resolve(platformRoot, "../NODEDC_DESIGN_GUIDELINE"));
|
const designRoot = resolve(process.env.NODEDC_DEVICE_MANAGER_SOURCE_ROOT || resolve(platformRoot, "../NODEDC_DESIGN_GUIDELINE"));
|
||||||
const managerRoot = resolve(designRoot, "apps/device-manager");
|
const managerRoot = resolve(designRoot, "apps/device-manager");
|
||||||
const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts"));
|
const artifactDir = resolve(process.env.NODEDC_DEPLOY_ARTIFACT_DIR || resolve(scriptDir, "../deploy-artifacts"));
|
||||||
const [patchId = "device-manager-release-20260811-010", ...extra] = process.argv.slice(2);
|
const [patchId = "device-manager-release-20260811-011", ...extra] = process.argv.slice(2);
|
||||||
if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) throw new Error("usage: build-device-manager-control-plane-artifact.mjs [patch-id]");
|
if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) throw new Error("usage: build-device-manager-control-plane-artifact.mjs [patch-id]");
|
||||||
|
|
||||||
const entries = [
|
const entries = [
|
||||||
@@ -88,11 +88,20 @@ try {
|
|||||||
"DEVICE_MANAGEMENT_API_ENABLED: \"true\"",
|
"DEVICE_MANAGEMENT_API_ENABLED: \"true\"",
|
||||||
"NODEDC_LAUNCHER_INTERNAL_TOKEN_FILE: /run/nodedc-secrets/device-core-internal-token",
|
"NODEDC_LAUNCHER_INTERNAL_TOKEN_FILE: /run/nodedc-secrets/device-core-internal-token",
|
||||||
"NODEDC_DEVICE_CORE_TOKEN_FILE: /run/nodedc-secrets/management-core-token",
|
"NODEDC_DEVICE_CORE_TOKEN_FILE: /run/nodedc-secrets/management-core-token",
|
||||||
|
"DEVICE_EDGE_CHANNEL_ENABLED: \"true\"",
|
||||||
|
"DEVICE_EDGE_CHANNEL_CORE_KEY_FILE: /run/nodedc-secrets/device-edge-channel/core-private-key.pem",
|
||||||
|
"DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE: /run/nodedc-secrets/device-edge-channel/core-certificate.pem",
|
||||||
|
"DEVICE_EDGE_CHANNEL_TRUST_ROOT: /run/nodedc-secrets/device-edge-channel/peers",
|
||||||
|
"source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/core-private-key.pem",
|
||||||
|
"source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/core-certificate.pem",
|
||||||
|
"source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/peers",
|
||||||
|
"name: nodedc-device-plane-egress",
|
||||||
"name: nodedc-platform_edge",
|
"name: nodedc-platform_edge",
|
||||||
]) if (!compose.includes(required)) throw new Error(`device_manager_compose_contract_missing:${required}`);
|
]) if (!compose.includes(required)) throw new Error(`device_manager_compose_contract_missing:${required}`);
|
||||||
for (const forbidden of [
|
for (const forbidden of [
|
||||||
"NODEDC_INTERNAL_ACCESS_TOKEN:",
|
"NODEDC_INTERNAL_ACCESS_TOKEN:",
|
||||||
"NODEDC_PLATFORM_SERVICE_TOKEN:",
|
"NODEDC_PLATFORM_SERVICE_TOKEN:",
|
||||||
|
"PRIVATE KEY",
|
||||||
"0.0.0.0:18122",
|
"0.0.0.0:18122",
|
||||||
"0.0.0.0:9921:9921",
|
"0.0.0.0:9921:9921",
|
||||||
"- \"9921:9921\"",
|
"- \"9921:9921\"",
|
||||||
@@ -113,6 +122,9 @@ try {
|
|||||||
|| !/^[A-Za-z0-9._-]{1,96}$/.test(predecessor.patchId || "")
|
|| !/^[A-Za-z0-9._-]{1,96}$/.test(predecessor.patchId || "")
|
||||||
|| !/^[a-f0-9]{64}$/.test(predecessor.artifactSha256 || "")
|
|| !/^[a-f0-9]{64}$/.test(predecessor.artifactSha256 || "")
|
||||||
|| (descriptor.action === "activate") !== (predecessor.kind === "reconciliation")
|
|| (descriptor.action === "activate") !== (predecessor.kind === "reconciliation")
|
||||||
|
|| descriptor.edgeChannel !== "core-initiated-pinned-mtls-enabled-zero-or-more-registered-edges"
|
||||||
|
|| descriptor.edgeChannelIdentity !== "runner-managed-host-local-private-key-public-certificate-export"
|
||||||
|
|| descriptor.edgeChannelEgress !== "dedicated-core-only-bridge-no-host-ingress-public-ipv4-tcp-8443-registration-policy"
|
||||||
|| descriptor.healthGate !== "bounded-container-grace+core-contract"
|
|| descriptor.healthGate !== "bounded-container-grace+core-contract"
|
||||||
|| descriptor.rollback !== "restore-preapply-snapshot"
|
|| descriptor.rollback !== "restore-preapply-snapshot"
|
||||||
) throw new Error("device_manager_activation_successor_contract_mismatch");
|
) throw new Error("device_manager_activation_successor_contract_mismatch");
|
||||||
|
|||||||
@@ -72,6 +72,27 @@ DEVICE_PLANE_IDENTIFIER_PEPPER_FILE = DEVICE_PLANE_SECRET_DIR / "identifier-pepp
|
|||||||
DEVICE_PLANE_MANAGEMENT_CORE_TOKEN_FILE = (
|
DEVICE_PLANE_MANAGEMENT_CORE_TOKEN_FILE = (
|
||||||
DEVICE_PLANE_SECRET_DIR / "management-core-token"
|
DEVICE_PLANE_SECRET_DIR / "management-core-token"
|
||||||
)
|
)
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_SECRET_DIR = (
|
||||||
|
DEVICE_PLANE_SECRET_DIR / "device-edge-channel"
|
||||||
|
)
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_CORE_PRIVATE_KEY_FILE = (
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_SECRET_DIR / "core-private-key.pem"
|
||||||
|
)
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE = (
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_SECRET_DIR / "core-certificate.pem"
|
||||||
|
)
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_PEER_TRUST_DIR = (
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_SECRET_DIR / "peers"
|
||||||
|
)
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORT_DIR = (
|
||||||
|
DEVICE_PLANE_ROOT / "enrollment" / "device-edge-channel"
|
||||||
|
)
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_CERTIFICATE_FILE = (
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORT_DIR / "core-certificate.pem"
|
||||||
|
)
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_FINGERPRINT_FILE = (
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORT_DIR / "core-certificate.sha256"
|
||||||
|
)
|
||||||
DEVICE_PLANE_CONTROL_CORE_IMAGE = "nodedc/device-control-core:local"
|
DEVICE_PLANE_CONTROL_CORE_IMAGE = "nodedc/device-control-core:local"
|
||||||
DEVICE_PLANE_GATEWAY_IMAGE = "nodedc/device-gateway:local"
|
DEVICE_PLANE_GATEWAY_IMAGE = "nodedc/device-gateway:local"
|
||||||
DEVICE_PLANE_MANAGER_IMAGE = "nodedc/device-manager:local"
|
DEVICE_PLANE_MANAGER_IMAGE = "nodedc/device-manager:local"
|
||||||
@@ -221,7 +242,7 @@ DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL = (
|
|||||||
)
|
)
|
||||||
DEVICE_PLANE_MANAGER_COMPOSE_REL = "docker-compose.device-manager.yml"
|
DEVICE_PLANE_MANAGER_COMPOSE_REL = "docker-compose.device-manager.yml"
|
||||||
DEVICE_PLANE_MANAGER_COMPOSE_SHA256 = (
|
DEVICE_PLANE_MANAGER_COMPOSE_SHA256 = (
|
||||||
"4954120aaddc999798b64c304d8cf692b79714feb727d873117bd1f3434e865e"
|
"369a2acf9c1a1030b9e1c6c366144b1eaf8900aef0ee59bb6bf23250b7b371b9"
|
||||||
)
|
)
|
||||||
DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_ENTRIES = (
|
DEVICE_PLANE_MANAGER_FAILED_CONTROL_PLANE_ENTRIES = (
|
||||||
".dockerignore",
|
".dockerignore",
|
||||||
@@ -556,6 +577,7 @@ DEVICE_PLANE_FOUNDATION_NETWORK_PUBLICATION_COMPOSE_SHA256 = (
|
|||||||
)
|
)
|
||||||
DEVICE_PLANE_PRIVATE_NETWORK = "nodedc-device-plane-private"
|
DEVICE_PLANE_PRIVATE_NETWORK = "nodedc-device-plane-private"
|
||||||
DEVICE_PLANE_CONTROL_NETWORK = "nodedc-device-plane-control"
|
DEVICE_PLANE_CONTROL_NETWORK = "nodedc-device-plane-control"
|
||||||
|
DEVICE_PLANE_EGRESS_NETWORK = "nodedc-device-plane-egress"
|
||||||
DEVICE_PLANE_FOUNDATION_PREDECESSOR_CONTAINER_IDS = {
|
DEVICE_PLANE_FOUNDATION_PREDECESSOR_CONTAINER_IDS = {
|
||||||
"device-control-core": (
|
"device-control-core": (
|
||||||
"ccdaeee71472a557ccecb485e81b31f5a4ee37b1b3f3cb4d5edbcf12d02bb08b"
|
"ccdaeee71472a557ccecb485e81b31f5a4ee37b1b3f3cb4d5edbcf12d02bb08b"
|
||||||
@@ -2065,7 +2087,7 @@ def ensure_external_data_plane_provisioner_secret():
|
|||||||
def resolve_openssl_binary():
|
def resolve_openssl_binary():
|
||||||
candidate = shutil.which("openssl")
|
candidate = shutil.which("openssl")
|
||||||
if not candidate:
|
if not candidate:
|
||||||
die("openssl is required to provision the Engine credential issuer")
|
die("openssl is required to provision runtime identities")
|
||||||
path = Path(candidate).resolve()
|
path = Path(candidate).resolve()
|
||||||
try:
|
try:
|
||||||
path_stat = path.lstat()
|
path_stat = path.lstat()
|
||||||
@@ -2091,6 +2113,214 @@ def run_openssl(arguments, label):
|
|||||||
die(f"openssl {label} failed")
|
die(f"openssl {label} failed")
|
||||||
|
|
||||||
|
|
||||||
|
def capture_openssl(arguments, label):
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(resolve_openssl_binary()), *arguments],
|
||||||
|
check=False,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
die(f"openssl {label} failed")
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_safe_runtime_directory(path, mode, label):
|
||||||
|
try:
|
||||||
|
path_stat = path.lstat()
|
||||||
|
except FileNotFoundError:
|
||||||
|
path.mkdir(parents=True, exist_ok=False)
|
||||||
|
path_stat = path.lstat()
|
||||||
|
if stat.S_ISLNK(path_stat.st_mode) or not stat.S_ISDIR(path_stat.st_mode):
|
||||||
|
die(f"{label} directory is unsafe: {path}")
|
||||||
|
os.chown(path, 0, MAP_GATEWAY_RUNTIME_GID)
|
||||||
|
path.chmod(mode)
|
||||||
|
|
||||||
|
|
||||||
|
def device_edge_channel_certificate_fingerprint(path):
|
||||||
|
raw = capture_openssl(
|
||||||
|
["x509", "-in", str(path), "-noout", "-fingerprint", "-sha256"],
|
||||||
|
"Device Edge channel certificate fingerprint",
|
||||||
|
).decode("ascii", errors="strict").strip()
|
||||||
|
match = re.fullmatch(
|
||||||
|
r"SHA256 Fingerprint=((?:[A-F0-9]{2}:){31}[A-F0-9]{2})",
|
||||||
|
raw,
|
||||||
|
flags=re.IGNORECASE,
|
||||||
|
)
|
||||||
|
if not match:
|
||||||
|
die("Device Edge channel certificate fingerprint is invalid")
|
||||||
|
return match.group(1).upper()
|
||||||
|
|
||||||
|
|
||||||
|
def install_public_runtime_export(source, destination, mode, label):
|
||||||
|
source_bytes = source.read_bytes()
|
||||||
|
if b"PRIVATE KEY" in source_bytes:
|
||||||
|
die(f"{label} contains private key material")
|
||||||
|
try:
|
||||||
|
destination_stat = destination.lstat()
|
||||||
|
except FileNotFoundError:
|
||||||
|
destination_stat = None
|
||||||
|
if destination_stat is not None and (
|
||||||
|
stat.S_ISLNK(destination_stat.st_mode)
|
||||||
|
or not stat.S_ISREG(destination_stat.st_mode)
|
||||||
|
):
|
||||||
|
die(f"{label} destination is unsafe: {destination}")
|
||||||
|
temporary = destination.parent / (
|
||||||
|
f".{destination.name}.{os.getpid()}.{time.time_ns()}.tmp"
|
||||||
|
)
|
||||||
|
descriptor = None
|
||||||
|
try:
|
||||||
|
descriptor = os.open(
|
||||||
|
str(temporary),
|
||||||
|
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
|
||||||
|
mode,
|
||||||
|
)
|
||||||
|
os.write(descriptor, source_bytes)
|
||||||
|
os.fsync(descriptor)
|
||||||
|
os.fchown(descriptor, 0, MAP_GATEWAY_RUNTIME_GID)
|
||||||
|
os.fchmod(descriptor, mode)
|
||||||
|
os.close(descriptor)
|
||||||
|
descriptor = None
|
||||||
|
os.replace(temporary, destination)
|
||||||
|
fsync_directory(destination.parent)
|
||||||
|
finally:
|
||||||
|
if descriptor is not None:
|
||||||
|
os.close(descriptor)
|
||||||
|
if temporary.exists():
|
||||||
|
temporary.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_device_edge_channel_core_identity():
|
||||||
|
# The Core private key is born on Synology and never enters a deployment
|
||||||
|
# artifact, Compose environment, Ops, or runner output. Only the matching
|
||||||
|
# public certificate and fingerprint are exported for the explicit VPS
|
||||||
|
# trust handoff.
|
||||||
|
ensure_safe_runtime_directory(
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_SECRET_DIR,
|
||||||
|
0o710,
|
||||||
|
"Device Edge channel identity",
|
||||||
|
)
|
||||||
|
ensure_safe_runtime_directory(
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_PEER_TRUST_DIR,
|
||||||
|
0o710,
|
||||||
|
"Device Edge channel peer trust",
|
||||||
|
)
|
||||||
|
private_key = DEVICE_PLANE_EDGE_CHANNEL_CORE_PRIVATE_KEY_FILE
|
||||||
|
certificate = DEVICE_PLANE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE
|
||||||
|
private_exists = private_key.exists() or private_key.is_symlink()
|
||||||
|
certificate_exists = certificate.exists() or certificate.is_symlink()
|
||||||
|
if private_exists != certificate_exists:
|
||||||
|
die("Device Edge channel Core identity is incomplete")
|
||||||
|
|
||||||
|
created = False
|
||||||
|
if not private_exists:
|
||||||
|
private_tmp = private_key.with_name(
|
||||||
|
f".{private_key.name}.{os.getpid()}.{time.time_ns()}.tmp"
|
||||||
|
)
|
||||||
|
certificate_tmp = certificate.with_name(
|
||||||
|
f".{certificate.name}.{os.getpid()}.{time.time_ns()}.tmp"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
run_openssl([
|
||||||
|
"req", "-x509", "-newkey", "ed25519", "-nodes",
|
||||||
|
"-days", "3650",
|
||||||
|
"-subj", "/CN=nodedc-device-control-core",
|
||||||
|
"-addext", "basicConstraints=critical,CA:FALSE",
|
||||||
|
"-addext", "keyUsage=critical,digitalSignature",
|
||||||
|
"-addext", "extendedKeyUsage=clientAuth",
|
||||||
|
"-keyout", str(private_tmp),
|
||||||
|
"-out", str(certificate_tmp),
|
||||||
|
], "Device Edge channel Core identity generation")
|
||||||
|
os.chown(private_tmp, 0, MAP_GATEWAY_RUNTIME_GID)
|
||||||
|
private_tmp.chmod(0o640)
|
||||||
|
os.chown(certificate_tmp, 0, MAP_GATEWAY_RUNTIME_GID)
|
||||||
|
certificate_tmp.chmod(0o640)
|
||||||
|
fsync_file(private_tmp)
|
||||||
|
fsync_file(certificate_tmp)
|
||||||
|
os.replace(private_tmp, private_key)
|
||||||
|
os.replace(certificate_tmp, certificate)
|
||||||
|
fsync_directory(DEVICE_PLANE_EDGE_CHANNEL_SECRET_DIR)
|
||||||
|
created = True
|
||||||
|
finally:
|
||||||
|
if private_tmp.exists():
|
||||||
|
private_tmp.unlink()
|
||||||
|
if certificate_tmp.exists():
|
||||||
|
certificate_tmp.unlink()
|
||||||
|
|
||||||
|
for path, label in (
|
||||||
|
(private_key, "private key"),
|
||||||
|
(certificate, "certificate"),
|
||||||
|
):
|
||||||
|
path_stat = path.lstat()
|
||||||
|
if (
|
||||||
|
stat.S_ISLNK(path_stat.st_mode)
|
||||||
|
or not stat.S_ISREG(path_stat.st_mode)
|
||||||
|
or path_stat.st_uid != 0
|
||||||
|
or path_stat.st_gid != MAP_GATEWAY_RUNTIME_GID
|
||||||
|
or stat.S_IMODE(path_stat.st_mode) != 0o640
|
||||||
|
or path_stat.st_size < 64
|
||||||
|
or path_stat.st_size > 32 * 1024
|
||||||
|
):
|
||||||
|
die(f"Device Edge channel Core {label} boundary mismatch")
|
||||||
|
|
||||||
|
private_text = private_key.read_text(encoding="ascii")
|
||||||
|
certificate_text = certificate.read_text(encoding="ascii")
|
||||||
|
if "PRIVATE KEY" not in private_text or "PRIVATE KEY" in certificate_text:
|
||||||
|
die("Device Edge channel Core private/public boundary mismatch")
|
||||||
|
if certificate_text.count("-----BEGIN CERTIFICATE-----") != 1:
|
||||||
|
die("Device Edge channel Core certificate cardinality mismatch")
|
||||||
|
run_openssl(["pkey", "-in", str(private_key), "-check", "-noout"],
|
||||||
|
"Device Edge channel Core private key validation")
|
||||||
|
run_openssl(["x509", "-in", str(certificate), "-noout", "-checkend", "604800"],
|
||||||
|
"Device Edge channel Core certificate lifetime validation")
|
||||||
|
run_openssl([
|
||||||
|
"verify", "-purpose", "sslclient", "-CAfile", str(certificate),
|
||||||
|
str(certificate),
|
||||||
|
], "Device Edge channel Core certificate purpose validation")
|
||||||
|
certificate_public = capture_openssl(
|
||||||
|
["x509", "-in", str(certificate), "-pubkey", "-noout"],
|
||||||
|
"Device Edge channel Core certificate public key",
|
||||||
|
)
|
||||||
|
private_public = capture_openssl(
|
||||||
|
["pkey", "-in", str(private_key), "-pubout"],
|
||||||
|
"Device Edge channel Core private key public derivation",
|
||||||
|
)
|
||||||
|
if certificate_public != private_public:
|
||||||
|
die("Device Edge channel Core certificate/private key mismatch")
|
||||||
|
|
||||||
|
ensure_safe_runtime_directory(
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORT_DIR,
|
||||||
|
0o755,
|
||||||
|
"Device Edge channel public export",
|
||||||
|
)
|
||||||
|
install_public_runtime_export(
|
||||||
|
certificate,
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_CERTIFICATE_FILE,
|
||||||
|
0o444,
|
||||||
|
"Device Edge channel Core certificate export",
|
||||||
|
)
|
||||||
|
fingerprint = device_edge_channel_certificate_fingerprint(certificate)
|
||||||
|
fingerprint_tmp = DEVICE_PLANE_EDGE_CHANNEL_EXPORT_DIR / (
|
||||||
|
f".core-certificate.sha256.{os.getpid()}.{time.time_ns()}.tmp"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
fingerprint_tmp.write_text(f"SHA256={fingerprint}\n", encoding="ascii")
|
||||||
|
os.chown(fingerprint_tmp, 0, MAP_GATEWAY_RUNTIME_GID)
|
||||||
|
fingerprint_tmp.chmod(0o444)
|
||||||
|
fsync_file(fingerprint_tmp)
|
||||||
|
os.replace(
|
||||||
|
fingerprint_tmp,
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_FINGERPRINT_FILE,
|
||||||
|
)
|
||||||
|
fsync_directory(DEVICE_PLANE_EDGE_CHANNEL_EXPORT_DIR)
|
||||||
|
finally:
|
||||||
|
if fingerprint_tmp.exists():
|
||||||
|
fingerprint_tmp.unlink()
|
||||||
|
return "created" if created else "reused"
|
||||||
|
|
||||||
|
|
||||||
def validate_ed25519_public_key_file(path):
|
def validate_ed25519_public_key_file(path):
|
||||||
try:
|
try:
|
||||||
value = path.read_text(encoding="ascii")
|
value = path.read_text(encoding="ascii")
|
||||||
@@ -8859,6 +9089,16 @@ def expected_device_plane_manager_release_boundaries():
|
|||||||
"publicIngress": "reverse-proxy-only",
|
"publicIngress": "reverse-proxy-only",
|
||||||
"deviceCoreManagementApi": "file-token-authenticated",
|
"deviceCoreManagementApi": "file-token-authenticated",
|
||||||
"launcherTrust": "file-token-scoped-to-device-core-handoff",
|
"launcherTrust": "file-token-scoped-to-device-core-handoff",
|
||||||
|
"edgeChannel": (
|
||||||
|
"core-initiated-pinned-mtls-enabled-zero-or-more-registered-edges"
|
||||||
|
),
|
||||||
|
"edgeChannelIdentity": (
|
||||||
|
"runner-managed-host-local-private-key-public-certificate-export"
|
||||||
|
),
|
||||||
|
"edgeChannelEgress": (
|
||||||
|
"dedicated-core-only-bridge-no-host-ingress-"
|
||||||
|
"public-ipv4-tcp-8443-registration-policy"
|
||||||
|
),
|
||||||
"healthGate": "bounded-container-grace+core-contract",
|
"healthGate": "bounded-container-grace+core-contract",
|
||||||
"commandTransport": "disabled",
|
"commandTransport": "disabled",
|
||||||
"gelios": "untouched",
|
"gelios": "untouched",
|
||||||
@@ -9062,6 +9302,20 @@ def validate_device_plane_manager_control_plane_payload(
|
|||||||
"/run/nodedc-secrets/device-core-internal-token",
|
"/run/nodedc-secrets/device-core-internal-token",
|
||||||
"NODEDC_DEVICE_CORE_TOKEN_FILE: "
|
"NODEDC_DEVICE_CORE_TOKEN_FILE: "
|
||||||
"/run/nodedc-secrets/management-core-token",
|
"/run/nodedc-secrets/management-core-token",
|
||||||
|
'DEVICE_EDGE_CHANNEL_ENABLED: "true"',
|
||||||
|
"DEVICE_EDGE_CHANNEL_CORE_KEY_FILE: "
|
||||||
|
"/run/nodedc-secrets/device-edge-channel/core-private-key.pem",
|
||||||
|
"DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE: "
|
||||||
|
"/run/nodedc-secrets/device-edge-channel/core-certificate.pem",
|
||||||
|
"DEVICE_EDGE_CHANNEL_TRUST_ROOT: "
|
||||||
|
"/run/nodedc-secrets/device-edge-channel/peers",
|
||||||
|
"source: /volume1/docker/nodedc-device-plane/secrets/"
|
||||||
|
"device-edge-channel/core-private-key.pem",
|
||||||
|
"source: /volume1/docker/nodedc-device-plane/secrets/"
|
||||||
|
"device-edge-channel/core-certificate.pem",
|
||||||
|
"source: /volume1/docker/nodedc-device-plane/secrets/"
|
||||||
|
"device-edge-channel/peers",
|
||||||
|
"name: nodedc-device-plane-egress",
|
||||||
"name: nodedc-platform_edge",
|
"name: nodedc-platform_edge",
|
||||||
):
|
):
|
||||||
if required not in compose:
|
if required not in compose:
|
||||||
@@ -12317,10 +12571,27 @@ def validate_device_manager_control_plane_runtime():
|
|||||||
manager = inspect_device_plane_container(manager_ids[0])
|
manager = inspect_device_plane_container(manager_ids[0])
|
||||||
core_environment = container_environment(core, "Device Control Core")
|
core_environment = container_environment(core, "Device Control Core")
|
||||||
manager_environment = container_environment(manager, "Device Manager")
|
manager_environment = container_environment(manager, "Device Manager")
|
||||||
if (
|
expected_core_environment = {
|
||||||
core_environment.get("DEVICE_MANAGEMENT_API_ENABLED") != "true"
|
"DEVICE_MANAGEMENT_API_ENABLED": "true",
|
||||||
or core_environment.get("DEVICE_MANAGEMENT_CORE_TOKEN_FILE")
|
"DEVICE_MANAGEMENT_CORE_TOKEN_FILE": (
|
||||||
!= "/run/nodedc-secrets/management-core-token"
|
"/run/nodedc-secrets/management-core-token"
|
||||||
|
),
|
||||||
|
"DEVICE_EDGE_CHANNEL_ENABLED": "true",
|
||||||
|
"DEVICE_EDGE_CHANNEL_CORE_KEY_FILE": (
|
||||||
|
"/run/nodedc-secrets/device-edge-channel/core-private-key.pem"
|
||||||
|
),
|
||||||
|
"DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE": (
|
||||||
|
"/run/nodedc-secrets/device-edge-channel/core-certificate.pem"
|
||||||
|
),
|
||||||
|
"DEVICE_EDGE_CHANNEL_TRUST_ROOT": (
|
||||||
|
"/run/nodedc-secrets/device-edge-channel/peers"
|
||||||
|
),
|
||||||
|
"DEVICE_EDGE_CHANNEL_MAX_EDGES": "32",
|
||||||
|
"DEVICE_EDGE_CHANNEL_RECONCILE_INTERVAL_MS": "15000",
|
||||||
|
}
|
||||||
|
if any(
|
||||||
|
core_environment.get(key) != value
|
||||||
|
for key, value in expected_core_environment.items()
|
||||||
):
|
):
|
||||||
die("Device Control Core management runtime mismatch")
|
die("Device Control Core management runtime mismatch")
|
||||||
expected_manager_environment = {
|
expected_manager_environment = {
|
||||||
@@ -12371,6 +12642,35 @@ def validate_device_manager_control_plane_runtime():
|
|||||||
or core_management_mount.get("RW") is not False
|
or core_management_mount.get("RW") is not False
|
||||||
):
|
):
|
||||||
die("Device Control Core management secret mount mismatch")
|
die("Device Control Core management secret mount mismatch")
|
||||||
|
expected_core_channel_mounts = {
|
||||||
|
"/run/nodedc-secrets/device-edge-channel/core-private-key.pem": (
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_CORE_PRIVATE_KEY_FILE
|
||||||
|
),
|
||||||
|
"/run/nodedc-secrets/device-edge-channel/core-certificate.pem": (
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE
|
||||||
|
),
|
||||||
|
"/run/nodedc-secrets/device-edge-channel/peers": (
|
||||||
|
DEVICE_PLANE_EDGE_CHANNEL_PEER_TRUST_DIR
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for destination, source in expected_core_channel_mounts.items():
|
||||||
|
mount = core_mounts.get(destination)
|
||||||
|
if (
|
||||||
|
mount is None
|
||||||
|
or mount.get("Type") != "bind"
|
||||||
|
or mount.get("Source") != str(source)
|
||||||
|
or mount.get("RW") is not False
|
||||||
|
):
|
||||||
|
die("Device Control Core Edge channel mount mismatch")
|
||||||
|
core_networks = set(
|
||||||
|
((core.get("NetworkSettings") or {}).get("Networks") or {}).keys()
|
||||||
|
)
|
||||||
|
if core_networks != {
|
||||||
|
DEVICE_PLANE_PRIVATE_NETWORK,
|
||||||
|
DEVICE_PLANE_CONTROL_NETWORK,
|
||||||
|
DEVICE_PLANE_EGRESS_NETWORK,
|
||||||
|
}:
|
||||||
|
die("Device Control Core Edge channel network boundary mismatch")
|
||||||
manager_mounts = {
|
manager_mounts = {
|
||||||
mount.get("Destination"): mount
|
mount.get("Destination"): mount
|
||||||
for mount in manager.get("Mounts") or []
|
for mount in manager.get("Mounts") or []
|
||||||
@@ -12405,6 +12705,7 @@ def validate_device_manager_control_plane_runtime():
|
|||||||
}:
|
}:
|
||||||
die("Device Manager network boundary mismatch")
|
die("Device Manager network boundary mismatch")
|
||||||
validate_device_plane_runtime_secret_metadata(include_management=True)
|
validate_device_plane_runtime_secret_metadata(include_management=True)
|
||||||
|
ensure_device_edge_channel_core_identity()
|
||||||
ensure_platform_runtime_secret(
|
ensure_platform_runtime_secret(
|
||||||
PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE,
|
PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE,
|
||||||
MAP_GATEWAY_SECRET_RE,
|
MAP_GATEWAY_SECRET_RE,
|
||||||
@@ -18147,6 +18448,14 @@ def plan_artifact(artifact):
|
|||||||
"runtime_secret=runner-managed:"
|
"runtime_secret=runner-managed:"
|
||||||
f"{PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE}"
|
f"{PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE}"
|
||||||
)
|
)
|
||||||
|
print(
|
||||||
|
"runtime_private_key=runner-managed-host-local:"
|
||||||
|
f"{DEVICE_PLANE_EDGE_CHANNEL_CORE_PRIVATE_KEY_FILE}"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"runtime_public_certificate_export=runner-managed:"
|
||||||
|
f"{DEVICE_PLANE_EDGE_CHANNEL_EXPORTED_CORE_CERTIFICATE_FILE}"
|
||||||
|
)
|
||||||
print(
|
print(
|
||||||
"device_postgres="
|
"device_postgres="
|
||||||
f"{device_plane_postgres_plan_selection(device_plane_postgres_preflight)}"
|
f"{device_plane_postgres_plan_selection(device_plane_postgres_preflight)}"
|
||||||
@@ -18327,6 +18636,20 @@ def plan_artifact(artifact):
|
|||||||
"device-backhaul-target"
|
"device-backhaul-target"
|
||||||
)
|
)
|
||||||
print("device_manager_health_gate=bounded-grace+contract")
|
print("device_manager_health_gate=bounded-grace+contract")
|
||||||
|
print(
|
||||||
|
"device_edge_channel="
|
||||||
|
"enabled:core-initiated:pinned-mtls:registered-edges-only"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"device_edge_channel_egress="
|
||||||
|
"dedicated-core-only-bridge:no-host-ingress"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"device_edge_channel_identity="
|
||||||
|
"host-local-private-key:public-certificate-export-only"
|
||||||
|
)
|
||||||
|
print("device_edge_channel_registrations=preserved")
|
||||||
|
print("device_edge_channel_commands=disabled")
|
||||||
print("device_manager_public_route=unchanged:absent")
|
print("device_manager_public_route=unchanged:absent")
|
||||||
print("device_gateway_tcp_9921=preserved:loopback-only")
|
print("device_gateway_tcp_9921=preserved:loopback-only")
|
||||||
print(
|
print(
|
||||||
@@ -19818,6 +20141,7 @@ def prepare_component_runtime(component, entries=None):
|
|||||||
MAP_GATEWAY_SECRET_RE,
|
MAP_GATEWAY_SECRET_RE,
|
||||||
"Device Core Hub handoff",
|
"Device Core Hub handoff",
|
||||||
)
|
)
|
||||||
|
ensure_device_edge_channel_core_identity()
|
||||||
if is_device_plane_backhaul_vps_enrollment_slice(
|
if is_device_plane_backhaul_vps_enrollment_slice(
|
||||||
component,
|
component,
|
||||||
entries,
|
entries,
|
||||||
|
|||||||
@@ -189,6 +189,18 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
|
|||||||
self.assertNotIn("device-postgres", result["services"])
|
self.assertNotIn("device-postgres", result["services"])
|
||||||
self.assertIn("docker-compose.device-manager.yml", entries)
|
self.assertIn("docker-compose.device-manager.yml", entries)
|
||||||
self.assertNotIn("docker-compose.device-plane.yml", entries)
|
self.assertNotIn("docker-compose.device-plane.yml", entries)
|
||||||
|
compose = (
|
||||||
|
PLATFORM_ROOT / "device-plane/docker-compose.device-manager.yml"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
for required in (
|
||||||
|
'DEVICE_EDGE_CHANNEL_ENABLED: "true"',
|
||||||
|
"DEVICE_EDGE_CHANNEL_CORE_KEY_FILE: ",
|
||||||
|
"DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE: ",
|
||||||
|
"DEVICE_EDGE_CHANNEL_TRUST_ROOT: ",
|
||||||
|
"name: nodedc-device-plane-egress",
|
||||||
|
):
|
||||||
|
self.assertIn(required, compose)
|
||||||
|
self.assertNotIn("PRIVATE KEY", compose)
|
||||||
checks = RUNNER.component_healthchecks("device-plane", entries, tuple(result["services"]))
|
checks = RUNNER.component_healthchecks("device-plane", entries, tuple(result["services"]))
|
||||||
self.assertEqual(checks[0]["expected_json"]["managementApi"], "enabled")
|
self.assertEqual(checks[0]["expected_json"]["managementApi"], "enabled")
|
||||||
self.assertEqual(checks[0]["expected_json"]["discoveryIngest"], "enabled")
|
self.assertEqual(checks[0]["expected_json"]["discoveryIngest"], "enabled")
|
||||||
@@ -215,7 +227,16 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
|
|||||||
[RUNNER.PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE],
|
[RUNNER.PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE],
|
||||||
)
|
)
|
||||||
|
|
||||||
with mock.patch.object(RUNNER, "ensure_platform_runtime_secret") as ensure:
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"ensure_platform_runtime_secret",
|
||||||
|
) as ensure,
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"ensure_device_edge_channel_core_identity",
|
||||||
|
) as ensure_edge_identity,
|
||||||
|
):
|
||||||
RUNNER.prepare_component_runtime(
|
RUNNER.prepare_component_runtime(
|
||||||
"device-plane",
|
"device-plane",
|
||||||
RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES,
|
RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES,
|
||||||
@@ -230,6 +251,7 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
|
|||||||
RUNNER.PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE,
|
RUNNER.PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
ensure_edge_identity.assert_called_once_with()
|
||||||
|
|
||||||
def test_apply_gate_checks_exact_services_core_contract_and_runtime_boundary(self):
|
def test_apply_gate_checks_exact_services_core_contract_and_runtime_boundary(self):
|
||||||
entries = RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES
|
entries = RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES
|
||||||
|
|||||||
Reference in New Issue
Block a user