feat(device-plane): accept core edge transport ADR

This commit is contained in:
Codex
2026-08-10 16:40:48 +03:00
parent 07224c6f0d
commit bf0bc50abb
27 changed files with 5034 additions and 26 deletions
@@ -0,0 +1,267 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import {
cp,
lstat,
mkdir,
mkdtemp,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const platformRoot = resolve(scriptDir, "../..");
const sourceRoot = resolve(platformRoot, "device-plane");
const artifactDir = resolve(
process.env.NODEDC_DEPLOY_ARTIFACT_DIR
|| resolve(scriptDir, "../deploy-artifacts"),
);
const runtimeCache = resolve(
process.env.NODEDC_DEVICE_EDGE_VPS_RUNTIME_DIR || "/tmp",
);
const [phase, patchId, ...extra] = process.argv.slice(2);
if (
extra.length
|| !["foundation", "backhaul", "relay"].includes(phase)
|| !/^[A-Za-z0-9._-]{1,96}$/.test(patchId || "")
) {
throw new Error(
"usage: build-device-edge-vps-artifact.mjs <foundation|backhaul|relay> <patch-id>",
);
}
const supersededTransportPhases = new Set(["backhaul", "relay"]);
if (
supersededTransportPhases.has(phase)
&& process.env.NODEDC_ALLOW_SUPERSEDED_TRANSPORT !== "test-only"
) {
throw new Error("vps_initiated_transport_frozen:ADR-0001");
}
const nodeArchive = "node-v22.23.2-linux-x64.tar.xz";
const tailscaleArchive = "tailscale_1.102.2_amd64.tgz";
const runtimeDigests = new Map([
[nodeArchive, "d60acfe00a2932254bb0ad20e01b0d74397a0875595de719654b214f4b03f307"],
[tailscaleArchive, "ad2cde12f8de95f7b93a1e0401e652291c603d42b9d60a33fb1741eb38ab04d8"],
]);
const entriesByPhase = {
foundation: [
"vps/config/00-nodedc-b2-vps.conf",
"vps/config/nftables-foundation.conf",
"vps/systemd/nodedc-b2-tailscaled.service",
"deployment/device-edge-vps-foundation-v1.json",
`vendor/${nodeArchive}`,
`vendor/${tailscaleArchive}`,
],
backhaul: [
"vps/config/backhaul_ssh_config",
"vps/systemd/nodedc-b2-backhaul.service",
"deployment/device-edge-vps-backhaul-v1.json",
],
relay: [
"vps/config/nftables-relay.conf",
"vps/systemd/nodedc-b2-relay.service",
"services/device-edge-relay/src",
"deployment/device-edge-vps-relay-v1.json",
],
};
const entries = entriesByPhase[phase];
const ignoredBasenames = new Set([".DS_Store", ".git", "node_modules"]);
const stage = await mkdtemp(join(tmpdir(), `nodedc-device-edge-vps-${phase}-`));
const payload = join(stage, "payload");
const target = join(
artifactDir,
`nodedc-device-edge-vps-${patchId}.tgz`,
);
await assertBoundary();
try {
await mkdir(payload, { recursive: true });
for (const entry of entries) {
if (entry.startsWith("vendor/")) {
const name = basename(entry);
const source = resolve(runtimeCache, name);
const actual = createHash("sha256").update(await readFile(source)).digest("hex");
if (actual !== runtimeDigests.get(name)) {
throw new Error(`runtime_digest_mismatch:${name}:${actual}`);
}
await mkdir(dirname(join(payload, entry)), { recursive: true });
await cp(source, join(payload, entry), { force: true });
continue;
}
await copySafe(resolve(sourceRoot, entry), join(payload, entry));
}
await writeFile(
join(stage, "manifest.env"),
`id=${patchId}\ncomponent=device-edge-vps\ntype=app-overlay\n`,
"utf8",
);
await writeFile(join(stage, "files.txt"), `${entries.join("\n")}\n`, "utf8");
await mkdir(artifactDir, { recursive: true });
const tar = spawnSync(
"python3",
["-c", canonicalTarScript(), target, stage],
{ encoding: "utf8", maxBuffer: 256 * 1024 * 1024 },
);
if (tar.status !== 0) {
throw new Error(`tar_failed:${tar.stderr || tar.stdout}`);
}
const bytes = await readFile(target);
const digest = createHash("sha256").update(bytes).digest("hex");
console.log(JSON.stringify({
ok: true,
patchId,
phase,
artifact: target,
sha256: digest,
size: bytes.length,
component: "device-edge-vps",
entries,
publicIngress: phase === "relay" ? "tcp/9921" : "disabled",
commandTransport: "disabled",
gelios: "untouched",
}, null, 2));
} finally {
await rm(stage, { recursive: true, force: true });
}
async function assertBoundary() {
const descriptorPath = resolve(
sourceRoot,
`deployment/device-edge-vps-${phase}-v1.json`,
);
const descriptor = JSON.parse(await readFile(descriptorPath, "utf8"));
if (
descriptor.component !== "device-edge-vps"
|| descriptor.runtimeHost !== "koffyvngij"
|| descriptor.commandTransport !== "disabled"
|| descriptor.gelios !== "untouched"
|| !String(descriptor.rollback || "").length
) {
throw new Error(`descriptor_boundary_mismatch:${phase}`);
}
const selectedText = await Promise.all(
entries
.filter((entry) => !entry.startsWith("vendor/") && !entry.endsWith("/src"))
.map((entry) => readFile(resolve(sourceRoot, entry), "utf8")),
);
const combined = selectedText.join("\n");
for (const forbidden of [
"PRIVATE KEY",
"AuthKey",
"TS_AUTHKEY",
"PasswordAuthentication yes",
"commandTransport\": \"enabled",
"device.dc.ru",
]) {
if (combined.includes(forbidden)) {
throw new Error(`vps_boundary_violation:${forbidden}`);
}
}
if (phase === "foundation") {
for (const required of [
"PermitRootLogin prohibit-password",
"PasswordAuthentication no",
"AllowTcpForwarding no",
"policy drop",
"tcp dport 22",
"--tun=userspace-networking",
"--socks5-server=127.0.0.1:1055",
]) {
if (!combined.includes(required)) {
throw new Error(`foundation_boundary_missing:${required}`);
}
}
if (combined.includes("tcp dport 9921")) {
throw new Error("foundation_must_not_open_9921");
}
}
if (phase === "backhaul") {
for (const required of [
"\"runtimeUser\": \"nodedc-backhaul\"",
"User=nodedc-backhaul",
"HostName 100.109.216.21",
"Port 2222",
"StrictHostKeyChecking yes",
"LocalForward 127.0.0.1:19921 127.0.0.1:9921",
"ProxyCommand /usr/bin/nc -X 5 -x 127.0.0.1:1055",
"MemoryMax=64M",
]) {
if (!combined.includes(required)) {
throw new Error(`backhaul_boundary_missing:${required}`);
}
}
}
if (phase === "relay") {
for (const required of [
"\"runtimeUser\": \"nodedc-relay\"",
"User=nodedc-relay",
"tcp dport 9921",
"DEVICE_EDGE_RELAY_UPSTREAM_HOST=127.0.0.1",
"DEVICE_EDGE_RELAY_UPSTREAM_PORT=19921",
"DEVICE_EDGE_RELAY_SOURCE_POLICY=public-ipv4-only",
"MemoryMax=192M",
]) {
if (!combined.includes(required)) {
throw new Error(`relay_boundary_missing:${required}`);
}
}
}
}
function canonicalTarScript() {
return [
"import gzip,io,pathlib,sys,tarfile",
"root=pathlib.Path(sys.argv[2])",
"with open(sys.argv[1],'wb') as out:",
" with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:",
" with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:",
" for top in ('manifest.env','files.txt','payload'):",
" p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])",
" for x in paths:",
" info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix())",
" info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644",
" with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info,src if info.isfile() else None)",
].join("\n");
}
async function copySafe(source, destination) {
const sourceStat = await lstat(source);
if (sourceStat.isSymbolicLink()) {
throw new Error(`source_symlink_rejected:${relative(sourceRoot, source)}`);
}
if (sourceStat.isFile()) {
await mkdir(dirname(destination), { recursive: true });
await cp(source, destination, { force: true, verbatimSymlinks: true });
return;
}
if (!sourceStat.isDirectory()) {
throw new Error(`source_type_rejected:${source}`);
}
await mkdir(destination, { recursive: true });
for (const entry of await readdir(source, { withFileTypes: true })) {
if (ignoredBasenames.has(entry.name) || entry.name.startsWith(".env")) {
continue;
}
const childSource = join(source, entry.name);
const childDestination = join(destination, entry.name);
if (entry.isSymbolicLink()) {
throw new Error(`source_symlink_rejected:${relative(sourceRoot, childSource)}`);
}
await copySafe(childSource, childDestination);
}
}
@@ -0,0 +1,160 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import {
cp,
lstat,
mkdir,
mkdtemp,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const platformRoot = resolve(scriptDir, "../..");
const sourceRoot = resolve(platformRoot, "device-plane");
const descriptorRelative =
"deployment/device-plane-backhaul-vps-enrollment-v1.json";
const artifactDir = resolve(
process.env.NODEDC_DEPLOY_ARTIFACT_DIR
|| resolve(scriptDir, "../deploy-artifacts"),
);
const [
patchId = "device-plane-backhaul-vps-enrollment-20260806-001",
...extra
] = process.argv.slice(2);
if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) {
throw new Error(
"usage: build-device-plane-backhaul-vps-enrollment-artifact.mjs [patch-id]",
);
}
if (process.env.NODEDC_ALLOW_SUPERSEDED_TRANSPORT !== "test-only") {
throw new Error("vps_initiated_transport_frozen:ADR-0001");
}
const files = [descriptorRelative];
const stage = await mkdtemp(
join(tmpdir(), "nodedc-device-plane-vps-enrollment-"),
);
const payload = join(stage, "payload");
const target = join(
artifactDir,
`nodedc-device-plane-${patchId}.tgz`,
);
await assertDescriptor();
try {
const source = resolve(sourceRoot, descriptorRelative);
const sourceStat = await lstat(source);
if (!sourceStat.isFile() || sourceStat.isSymbolicLink()) {
throw new Error("device_plane_vps_enrollment_descriptor_unsafe");
}
await mkdir(dirname(join(payload, descriptorRelative)), {
recursive: true,
});
await cp(source, join(payload, descriptorRelative), {
force: true,
verbatimSymlinks: true,
});
await writeFile(
join(stage, "manifest.env"),
`id=${patchId}\ncomponent=device-plane\ntype=app-overlay\n`,
"utf8",
);
await writeFile(join(stage, "files.txt"), `${files.join("\n")}\n`, "utf8");
await mkdir(artifactDir, { recursive: true });
const tar = spawnSync(
"python3",
["-c", canonicalTarScript(), target, stage],
{ encoding: "utf8", maxBuffer: 32 * 1024 * 1024 },
);
if (tar.status !== 0) {
throw new Error(`tar_failed:${tar.stderr || tar.stdout}`);
}
const bytes = await readFile(target);
const sha256 = createHash("sha256").update(bytes).digest("hex");
console.log(JSON.stringify({
ok: true,
patchId,
artifact: target,
sha256,
component: "device-plane",
transition: "rotate-backhaul-client-mini-to-vps",
entries: files,
build: [],
services: ["device-backhaul-target"],
preservedRuntime: [
"device-control-core",
"device-gateway",
"device-postgres",
"nodedc-device-plane-postgres-data",
"Tailscale Serve",
"Gelios",
],
publicIngress: "disabled",
commandTransport: "disabled",
runtimeKeyMaterial: "external-enrollment-only",
rollback: "restore-previous-authorized-key-and-recreate-target",
}, null, 2));
} finally {
await rm(stage, { recursive: true, force: true });
}
async function assertDescriptor() {
const descriptor = JSON.parse(await readFile(
resolve(sourceRoot, descriptorRelative),
"utf8",
));
if (
descriptor.schemaVersion
!== "nodedc.device-plane.backhaul-vps-enrollment.v1"
|| descriptor.mode !== "rotate-backhaul-client-mini-to-vps"
|| descriptor.predecessorPatchId
!== "device-plane-backhaul-target-tailnet-serve-20260804-002"
|| descriptor.predecessorArtifactSha256
!== "219408705dd4d80a962ed00eeb53a69df0b9ab6458443734d5c9cd1d1f795eba"
|| descriptor.nextKeyFingerprint
!== "SHA256:HHTiDYiCRxSiKjBLCip6JMSzGfLGrDz5g8SIkosJcVw"
|| descriptor.commandTransport !== "disabled"
|| descriptor.gelios !== "untouched"
|| descriptor.edgePublicIngress !== "disabled"
) {
throw new Error("device_plane_vps_enrollment_descriptor_mismatch");
}
const text = JSON.stringify(descriptor);
for (const forbidden of [
"PRIVATE KEY",
"authorized_keys",
"TS_AUTHKEY",
"password",
]) {
if (text.includes(forbidden)) {
throw new Error(`device_plane_vps_enrollment_boundary:${forbidden}`);
}
}
}
function canonicalTarScript() {
return [
"import gzip,io,pathlib,sys,tarfile",
"root=pathlib.Path(sys.argv[2])",
"with open(sys.argv[1],'wb') as out:",
" with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:",
" with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:",
" for top in ('manifest.env','files.txt','payload'):",
" p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])",
" for x in paths:",
" info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix())",
" info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644",
" with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info,src if info.isfile() else None)",
].join("\n");
}
File diff suppressed because it is too large Load Diff
+410 -26
View File
@@ -132,6 +132,27 @@ DEVICE_PLANE_BACKHAUL_ENROLLMENT_DIR = DEVICE_PLANE_ROOT / "enrollment"
DEVICE_PLANE_BACKHAUL_ENROLLMENT_PUBLIC_KEY_FILE = (
DEVICE_PLANE_BACKHAUL_ENROLLMENT_DIR / "device-edge-backhaul.pub"
)
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PUBLIC_KEY_FILE = (
DEVICE_PLANE_BACKHAUL_ENROLLMENT_DIR / "device-edge-vps-backhaul.pub"
)
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_REL = (
"deployment/device-plane-backhaul-vps-enrollment-v1.json"
)
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_ENTRIES = (
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_REL,
)
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_PATCH_ID = (
"device-plane-backhaul-target-tailnet-serve-20260804-002"
)
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_ARTIFACT_SHA256 = (
"219408705dd4d80a962ed00eeb53a69df0b9ab6458443734d5c9cd1d1f795eba"
)
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_FINGERPRINT = (
"SHA256:HHTiDYiCRxSiKjBLCip6JMSzGfLGrDz5g8SIkosJcVw"
)
DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_BACKUP = (
"device-plane-backhaul-authorized-keys-before"
)
DEVICE_PLANE_BACKHAUL_SECRET_DIR = DEVICE_PLANE_SECRET_DIR / "backhaul-target"
DEVICE_PLANE_BACKHAUL_HOST_KEY_FILE = (
DEVICE_PLANE_BACKHAUL_SECRET_DIR / "ssh_host_ed25519_key"
@@ -8053,6 +8074,12 @@ def load_artifact(artifact, work_dir):
manifest = parse_manifest(manifest_path)
entries = parse_files_list(files_path)
if is_device_plane_backhaul_vps_enrollment_slice(
manifest["component"],
entries,
):
die("vps_initiated_transport_frozen:ADR-0001")
for rel in entries:
allowed_payload_path(manifest["component"], rel)
if not (payload_dir / rel).exists():
@@ -8093,6 +8120,11 @@ def load_artifact(artifact, work_dir):
entries,
):
validate_device_plane_backhaul_target_payload(payload_dir)
if is_device_plane_backhaul_vps_enrollment_slice(
manifest["component"],
entries,
):
validate_device_plane_backhaul_vps_enrollment_payload(payload_dir)
if manifest["component"] == "n8n-private-extension":
validate_n8n_private_extension_release(payload_dir, entries)
if manifest["component"] == "engine":
@@ -8470,6 +8502,63 @@ def is_device_plane_backhaul_target_slice(component, entries):
)
def is_device_plane_backhaul_vps_enrollment_slice(component, entries):
return (
component == "device-plane"
and entries is not None
and tuple(entries)
== DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_ENTRIES
)
def expected_device_plane_backhaul_vps_enrollment_descriptor():
return {
"schemaVersion": (
"nodedc.device-plane.backhaul-vps-enrollment.v1"
),
"mode": "rotate-backhaul-client-mini-to-vps",
"predecessorPatchId": (
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_PATCH_ID
),
"predecessorArtifactSha256": (
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_ARTIFACT_SHA256
),
"sourceAction": "publish-vps-enrollment-marker-only",
"runtimeAction": (
"rotate-authorized-key-and-recreate-backhaul-target"
),
"selectedServices": [DEVICE_PLANE_BACKHAUL_TARGET_SERVICE],
"preservedServices": list(DEVICE_PLANE_RUNTIME_SERVICES),
"previousEnrollment": "device-edge-backhaul.pub",
"nextEnrollment": "device-edge-vps-backhaul.pub",
"nextKeyFingerprint": (
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_FINGERPRINT
),
"permittedTarget": DEVICE_PLANE_BACKHAUL_PERMITTED_TARGET,
"tailnetAddress": DEVICE_PLANE_BACKHAUL_TAILNET_ADDRESS,
"dockerPortPublication": "disabled",
"routerNatFirewall": "unchanged",
"edgePublicIngress": "disabled",
"funnel": "disabled",
"commandTransport": "disabled",
"gelios": "untouched",
"rollback": (
"restore-previous-authorized-key-and-recreate-target"
),
}
def validate_device_plane_backhaul_vps_enrollment_payload(payload_dir):
descriptor = read_strict_json(
payload_dir / DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_REL,
"Device Plane VPS backhaul enrollment descriptor",
max_bytes=16 * 1024,
)
if descriptor != expected_device_plane_backhaul_vps_enrollment_descriptor():
die("Device Plane VPS backhaul enrollment descriptor mismatch")
return descriptor
def expected_device_plane_backhaul_target_descriptor():
return {
"schemaVersion": (
@@ -8534,14 +8623,13 @@ def validate_device_plane_backhaul_target_payload(payload_dir):
return descriptor
def read_device_plane_backhaul_enrollment_public_key():
path = DEVICE_PLANE_BACKHAUL_ENROLLMENT_PUBLIC_KEY_FILE
def read_device_plane_ed25519_enrollment_public_key(path, comment, label):
try:
path_stat = path.lstat()
text = path.read_text(encoding="ascii")
except (FileNotFoundError, OSError, UnicodeDecodeError):
die(
"Device Plane Edge enrollment public key is missing or unreadable: "
f"{label} is missing or unreadable: "
f"{path}"
)
if (
@@ -8549,28 +8637,56 @@ def read_device_plane_backhaul_enrollment_public_key():
or not stat.S_ISREG(path_stat.st_mode)
or path_stat.st_size > 1024
):
die("Device Plane Edge enrollment public key is unsafe")
die(f"{label} is unsafe")
if text != text.strip() + "\n" or "\n" in text.strip():
die("Device Plane Edge enrollment public key must be one line")
die(f"{label} must be one line")
parts = text.strip().split()
if len(parts) not in (2, 3) or parts[0] != "ssh-ed25519":
die("Device Plane Edge enrollment public key type mismatch")
die(f"{label} type mismatch")
try:
blob = base64.b64decode(parts[1], validate=True)
except Exception:
die("Device Plane Edge enrollment public key encoding mismatch")
die(f"{label} encoding mismatch")
expected_prefix = b"\x00\x00\x00\x0bssh-ed25519\x00\x00\x00\x20"
if len(blob) != len(expected_prefix) + 32 or not blob.startswith(
expected_prefix
):
die("Device Plane Edge enrollment public key shape mismatch")
normalized = f"ssh-ed25519 {parts[1]} nodedc-device-edge-backhaul"
die(f"{label} shape mismatch")
normalized = f"ssh-ed25519 {parts[1]} {comment}"
return {
"line": normalized,
"sha256": hashlib.sha256((normalized + "\n").encode("ascii")).hexdigest(),
"fingerprint": (
"SHA256:"
+ base64.b64encode(hashlib.sha256(blob).digest())
.decode("ascii")
.rstrip("=")
),
}
def read_device_plane_backhaul_enrollment_public_key():
return read_device_plane_ed25519_enrollment_public_key(
DEVICE_PLANE_BACKHAUL_ENROLLMENT_PUBLIC_KEY_FILE,
"nodedc-device-edge-backhaul",
"Device Plane Edge enrollment public key",
)
def read_device_plane_backhaul_vps_enrollment_public_key():
enrollment = read_device_plane_ed25519_enrollment_public_key(
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PUBLIC_KEY_FILE,
"nodedc-device-edge-vps-backhaul",
"Device Plane VPS Edge enrollment public key",
)
if (
enrollment["fingerprint"]
!= DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_FINGERPRINT
):
die("Device Plane VPS Edge enrollment fingerprint mismatch")
return enrollment
def device_plane_tailscale_drop_privileges(uid, gid):
def demote():
os.setgroups([])
@@ -9136,6 +9252,37 @@ def validate_device_plane_backhaul_target_evidence(payload_dir):
}
def validate_device_plane_backhaul_vps_enrollment_evidence(payload_dir):
descriptor = validate_device_plane_backhaul_vps_enrollment_payload(
payload_dir
)
if not state_has_patch_id(
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_PATCH_ID
):
die("Device Plane VPS enrollment predecessor patch is not applied")
if not state_has_sha(
DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_ARTIFACT_SHA256
):
die("Device Plane VPS enrollment predecessor artifact is not applied")
root = component_root("device-plane")
marker = root / DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_REL
if marker.exists() or marker.is_symlink():
die("Device Plane VPS enrollment marker already exists")
runtime = device_plane_runtime_inventory(DEVICE_PLANE_RUNTIME_SERVICES)
validate_device_plane_backhaul_target_runtime(runtime)
previous = read_device_plane_backhaul_enrollment_public_key()
next_enrollment = read_device_plane_backhaul_vps_enrollment_public_key()
if previous["line"] == next_enrollment["line"]:
die("Device Plane VPS enrollment key is not a new identity")
return {
"mode": descriptor["mode"],
"runtime": runtime,
"previousEnrollmentPublicKeySha256": previous["sha256"],
"nextEnrollmentPublicKeySha256": next_enrollment["sha256"],
"nextKeyFingerprint": next_enrollment["fingerprint"],
}
def expected_device_plane_foundation_recovery_descriptor():
return {
"schemaVersion": "nodedc.device-plane.foundation-recovery.v1",
@@ -10713,7 +10860,10 @@ def validate_device_plane_preserved_runtime_unchanged(runtime_before, label):
return current
def validate_device_plane_backhaul_target_runtime(runtime_before):
def validate_device_plane_backhaul_target_runtime(
runtime_before,
expected_enrollment=None,
):
current = validate_device_plane_preserved_runtime_unchanged(
runtime_before,
"Device Plane backhaul",
@@ -10840,7 +10990,11 @@ def validate_device_plane_backhaul_target_runtime(runtime_before):
f"{required}"
)
enrollment = read_device_plane_backhaul_enrollment_public_key()
enrollment = (
expected_enrollment
if expected_enrollment is not None
else read_device_plane_backhaul_enrollment_public_key()
)
authorized = (
'restrict,port-forwarding,permitopen="127.0.0.1:9921" '
f"{enrollment['line']}\n"
@@ -12557,6 +12711,9 @@ def is_platform_provider_catalog_only(entries):
def component_services(component, entries=None):
if is_device_plane_backhaul_vps_enrollment_slice(component, entries):
return (DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,)
if is_device_plane_backhaul_target_slice(component, entries):
return (DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,)
@@ -12931,6 +13088,9 @@ def component_build_args(component, entries=None):
def component_builds(component, entries=None):
if is_device_plane_backhaul_vps_enrollment_slice(component, entries):
return ()
if is_device_plane_backhaul_target_slice(component, entries):
return ((
DEVICE_PLANE_ROOT,
@@ -14940,6 +15100,7 @@ def plan_artifact(artifact):
device_plane_b2_ingress_preflight = None
device_plane_b2_recovery_preflight = None
device_plane_backhaul_preflight = None
device_plane_backhaul_vps_enrollment_preflight = None
device_plane_runtime_before = None
composite_provider_v4_preflight = None
provider_rotating_slot_preflight = None
@@ -15149,6 +15310,15 @@ def plan_artifact(artifact):
device_plane_backhaul_preflight = (
validate_device_plane_backhaul_target_evidence(payload_dir)
)
if is_device_plane_backhaul_vps_enrollment_slice(
manifest["component"],
entries,
):
device_plane_backhaul_vps_enrollment_preflight = (
validate_device_plane_backhaul_vps_enrollment_evidence(
payload_dir
)
)
component = manifest["component"]
root = component_root(component)
@@ -16677,6 +16847,55 @@ def plan_artifact(artifact):
)
print("device_gateway_tcp_9921=disabled:unpublished")
print("device_plane_rollback=marker-only-runtime-unchanged")
if device_plane_backhaul_vps_enrollment_preflight is not None:
print(
"device_plane_transition="
f"{device_plane_backhaul_vps_enrollment_preflight['mode']}"
)
print(
"device_plane_predecessor_patch="
f"{DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_PATCH_ID}"
)
print(
"device_plane_predecessor_artifact_sha256="
f"{DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PREDECESSOR_ARTIFACT_SHA256}"
)
print("device_plane_build=none")
print(
"device_plane_runtime_mutation="
"rotate-authorized-key+recreate:device-backhaul-target"
)
print(
"device_plane_runtime_services="
"preserved:device-control-core,device-gateway,device-postgres"
)
print(
"device_backhaul_previous_enrollment_public_key_sha256="
f"{device_plane_backhaul_vps_enrollment_preflight['previousEnrollmentPublicKeySha256']}"
)
print(
"device_backhaul_next_enrollment_public_key_sha256="
f"{device_plane_backhaul_vps_enrollment_preflight['nextEnrollmentPublicKeySha256']}"
)
print(
"device_backhaul_next_key_fingerprint="
f"{device_plane_backhaul_vps_enrollment_preflight['nextKeyFingerprint']}"
)
print(
"device_backhaul_permitopen="
f"{DEVICE_PLANE_BACKHAUL_PERMITTED_TARGET}"
)
print("device_backhaul_docker_port_publication=disabled")
print("device_backhaul_tailscale_serve=unchanged")
print("device_backhaul_tailscale_funnel=disabled")
print("device_backhaul_router_nat_firewall=unchanged")
print("device_edge_public_ingress=disabled")
print("device_command_transport=disabled")
print("gelios=untouched")
print(
"device_plane_rollback="
"restore-previous-authorized-key+recreate-target"
)
if device_plane_backhaul_preflight is not None:
print(
"device_plane_transition="
@@ -16849,6 +17068,25 @@ def create_backup(root, backup_dir, entries, include_nginx_html):
(backup_dir / "missing-files.txt").write_text("\n".join(missing) + ("\n" if missing else ""), encoding="utf-8")
def backup_device_plane_backhaul_authorized_keys(backup_dir):
source_stat = DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE.lstat()
if (
stat.S_ISLNK(source_stat.st_mode)
or not stat.S_ISREG(source_stat.st_mode)
or source_stat.st_uid != 0
or stat.S_IMODE(source_stat.st_mode) != 0o444
or source_stat.st_size > 2048
):
die("Device Plane backhaul authorized_keys backup source is unsafe")
destination = backup_dir / DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_BACKUP
if destination.exists() or destination.is_symlink():
die("Device Plane backhaul authorized_keys backup collision")
shutil.copy2(DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE, destination)
os.chown(destination, 0, 0)
destination.chmod(0o600)
return sha256_file(destination)
def read_backup_path_list(path):
if not path.is_file():
die(f"deploy backup path list missing: {path}")
@@ -17216,6 +17454,53 @@ def rollback_device_plane_apply(
return f"source+runtime-restored:{restored_count}"
def rollback_device_plane_backhaul_vps_enrollment(
root,
backup_dir,
entries,
current_stamp,
runtime_before,
):
restored_count = restore_platform_overlay(
root,
backup_dir,
entries,
current_stamp,
)
backup = backup_dir / DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_BACKUP
backup_stat = backup.lstat()
previous = read_device_plane_backhaul_enrollment_public_key()
expected = (
'restrict,port-forwarding,permitopen="127.0.0.1:9921" '
f"{previous['line']}\n"
)
if (
stat.S_ISLNK(backup_stat.st_mode)
or not stat.S_ISREG(backup_stat.st_mode)
or backup_stat.st_uid != 0
or stat.S_IMODE(backup_stat.st_mode) != 0o600
or backup_stat.st_size > 2048
or backup.read_text(encoding="ascii") != expected
):
die("Device Plane VPS enrollment rollback backup mismatch")
install_device_plane_backhaul_authorized_key(previous)
run_compose(
"device-plane",
(DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,),
entries,
)
run_healthchecks(
"device-plane",
entries,
(DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,),
)
validate_device_plane_backhaul_target_runtime(
runtime_before,
expected_enrollment=previous,
)
return f"previous-key+target+source-restored:{restored_count}"
def rollback_engine_apply(root, backup_dir, entries, current_stamp, runtime_started, applied_services):
existing = read_backup_path_list(backup_dir / "existing-files.txt")
missing = read_backup_path_list(backup_dir / "missing-files.txt")
@@ -17764,6 +18049,32 @@ def run_engine_node_intelligence_compose(services, entries):
)
def install_device_plane_backhaul_authorized_key(enrollment):
DEVICE_PLANE_BACKHAUL_SECRET_DIR.mkdir(
parents=True,
exist_ok=True,
)
os.chown(DEVICE_PLANE_BACKHAUL_SECRET_DIR, 0, 0)
DEVICE_PLANE_BACKHAUL_SECRET_DIR.chmod(0o700)
authorized = (
'restrict,port-forwarding,permitopen="127.0.0.1:9921" '
f"{enrollment['line']}\n"
)
temporary = DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE.with_suffix(
".installing"
)
if temporary.exists() or temporary.is_symlink():
die("Device Plane backhaul authorized_keys staging path exists")
temporary.write_text(authorized, encoding="ascii")
os.chown(temporary, 0, 0)
temporary.chmod(0o444)
os.replace(temporary, DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE)
expected_sha256 = hashlib.sha256(authorized.encode("ascii")).hexdigest()
if sha256_file(DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE) != expected_sha256:
die("Device Plane backhaul authorized key verification failed")
return expected_sha256
def ensure_device_plane_backhaul_target_state():
enrollment = read_device_plane_backhaul_enrollment_public_key()
DEVICE_PLANE_BACKHAUL_SECRET_DIR.mkdir(
@@ -17818,19 +18129,9 @@ def ensure_device_plane_backhaul_target_state():
os.chown(path, 0, 0)
path.chmod(expected_mode)
authorized = (
'restrict,port-forwarding,permitopen="127.0.0.1:9921" '
f"{enrollment['line']}\n"
expected_authorized_sha256 = install_device_plane_backhaul_authorized_key(
enrollment
)
temporary = DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE.with_suffix(
".installing"
)
if temporary.exists() or temporary.is_symlink():
die("Device Plane backhaul authorized_keys staging path exists")
temporary.write_text(authorized, encoding="ascii")
os.chown(temporary, 0, 0)
temporary.chmod(0o444)
os.replace(temporary, DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE)
DEVICE_PLANE_BACKHAUL_TRUST_DIR.mkdir(parents=True, exist_ok=True)
os.chown(DEVICE_PLANE_BACKHAUL_TRUST_DIR, 0, 0)
@@ -17846,9 +18147,6 @@ def ensure_device_plane_backhaul_target_state():
public_temporary.chmod(0o444)
os.replace(public_temporary, DEVICE_PLANE_BACKHAUL_HOST_PUBLIC_KEY_FILE)
expected_authorized_sha256 = hashlib.sha256(
authorized.encode("ascii")
).hexdigest()
if (
sha256_file(DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE)
!= expected_authorized_sha256
@@ -17929,6 +18227,14 @@ def prepare_component_runtime(component, entries=None):
MAP_GATEWAY_SECRET_RE,
"device plane identifier pepper",
)
if is_device_plane_backhaul_vps_enrollment_slice(
component,
entries,
):
install_device_plane_backhaul_authorized_key(
read_device_plane_backhaul_vps_enrollment_public_key()
)
return
if is_device_plane_backhaul_target_slice(component, entries):
ensure_device_plane_backhaul_target_state()
return
@@ -18685,6 +18991,17 @@ process.stdout.write('engine-l2-closed-loop:0.7.0:cas+safe-profile+external-plan
def run_healthchecks(component, entries=None, services=None):
if is_device_plane_backhaul_vps_enrollment_slice(component, entries):
if tuple(services or ()) != (DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,):
die("Device Plane VPS enrollment service set mismatch")
for service in DEVICE_PLANE_RUNTIME_SERVICES:
healthcheck_compose_service("device-plane", service)
healthcheck_compose_service(
"device-plane",
DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,
)
return
if is_device_plane_backhaul_target_slice(component, entries):
if tuple(services or ()) != (DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,):
die("Device Plane backhaul target service set mismatch")
@@ -19551,6 +19868,7 @@ def apply_artifact(artifact):
node_intelligence_descriptor = None
l2_closed_loop_preflight = None
device_plane_backhaul_preflight = None
device_plane_backhaul_vps_enrollment_preflight = None
node_intelligence_service_stopped = False
apply_started = False
engine_backend_recreated = False
@@ -19628,6 +19946,15 @@ def apply_artifact(artifact):
payload_dir
)
)
if is_device_plane_backhaul_vps_enrollment_slice(
component,
entries,
):
device_plane_backhaul_vps_enrollment_preflight = (
validate_device_plane_backhaul_vps_enrollment_evidence(
payload_dir
)
)
if not root.is_dir():
if bootstrap_root:
root.mkdir(parents=True, exist_ok=True)
@@ -19955,6 +20282,13 @@ def apply_artifact(artifact):
encoding="utf-8",
)
runtime_inventory_path.chmod(0o600)
if (
device_plane_backhaul_vps_enrollment_preflight
is not None
):
backup_device_plane_backhaul_authorized_keys(
backup_dir
)
if device_plane_backhaul_preflight is not None:
tailscale_before_path = (
backup_dir / "tailscale-serve-before.json"
@@ -20067,6 +20401,21 @@ def apply_artifact(artifact):
validate_device_plane_backhaul_target_runtime(
device_plane_runtime_before
)
if is_device_plane_backhaul_vps_enrollment_slice(
component,
entries,
):
if device_plane_runtime_before is None:
die(
"Device Plane VPS enrollment predecessor runtime "
"inventory is missing"
)
validate_device_plane_backhaul_target_runtime(
device_plane_runtime_before,
expected_enrollment=(
read_device_plane_backhaul_vps_enrollment_public_key()
),
)
applied_path = move_artifact(artifact, APPLIED_DIR)
append_jsonl(STATE_FILE, {
@@ -20305,6 +20654,41 @@ def apply_artifact(artifact):
"automatic-rollback=failed",
file=sys.stderr,
)
elif (
is_device_plane_backhaul_vps_enrollment_slice(
component,
entries,
)
and device_plane_runtime_before is not None
):
try:
restored_state = (
rollback_device_plane_backhaul_vps_enrollment(
root,
backup_dir,
entries,
current_stamp,
device_plane_runtime_before,
)
)
rollback_status = (
"ok:device-plane-vps-enrollment:"
f"{restored_state}"
)
print(
"device-plane-vps-enrollment-"
f"automatic-rollback={rollback_status}",
file=sys.stderr,
)
except Exception as rollback_exc:
rollback_status = (
f"failed:{type(rollback_exc).__name__}"
)
print(
"device-plane-vps-enrollment-"
"automatic-rollback=failed",
file=sys.stderr,
)
elif (
component == "device-plane"
and entries is not None
@@ -0,0 +1,349 @@
#!/usr/bin/env python3
import hashlib
import importlib.machinery
import importlib.util
import json
import os
import subprocess
import tarfile
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
SCRIPT_DIR = Path(__file__).resolve().parent
BUILDER = SCRIPT_DIR / "build-device-edge-vps-artifact.mjs"
RUNNER_PATH = SCRIPT_DIR / "nodedc-b2-vps-deploy"
DEFAULT_RUNTIME_CACHE = Path(
os.environ.get("NODEDC_DEVICE_EDGE_VPS_RUNTIME_DIR", "/tmp")
)
def load_runner():
loader = importlib.machinery.SourceFileLoader(
"nodedc_b2_vps_runner_under_test",
str(RUNNER_PATH),
)
spec = importlib.util.spec_from_loader(loader.name, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
return module
RUNNER = load_runner()
class DeviceEdgeVpsArtifactTest(unittest.TestCase):
def build(self, artifact_dir, phase, patch_id, runtime_cache=None):
environment = os.environ.copy()
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
environment["NODEDC_DEVICE_EDGE_VPS_RUNTIME_DIR"] = str(
runtime_cache or DEFAULT_RUNTIME_CACHE
)
if phase in {"backhaul", "relay"}:
environment["NODEDC_ALLOW_SUPERSEDED_TRANSPORT"] = "test-only"
return subprocess.run(
["node", str(BUILDER), phase, patch_id],
check=False,
capture_output=True,
text=True,
env=environment,
)
def test_superseded_transport_builds_fail_closed_by_default(self):
environment = os.environ.copy()
environment.pop("NODEDC_ALLOW_SUPERSEDED_TRANSPORT", None)
with tempfile.TemporaryDirectory(prefix="nodedc-vps-frozen-") as directory:
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = directory
for phase in ("backhaul", "relay"):
with self.subTest(phase=phase):
result = subprocess.run(
[
"node",
str(BUILDER),
phase,
f"device-edge-vps-{phase}-frozen-001",
],
check=False,
capture_output=True,
text=True,
env=environment,
)
self.assertNotEqual(result.returncode, 0)
self.assertIn(
"vps_initiated_transport_frozen:ADR-0001",
result.stderr,
)
def test_runner_rejects_superseded_transport_before_host_preflight(self):
for phase in ("backhaul", "relay"):
with self.subTest(phase=phase), self.assertRaises(RUNNER.DeployError):
RUNNER.preflight({"phase": phase})
def require_runtime_cache(self):
for name, digest in (
(RUNNER.NODE_ARCHIVE, RUNNER.NODE_ARCHIVE_SHA256),
(RUNNER.TAILSCALE_ARCHIVE, RUNNER.TAILSCALE_ARCHIVE_SHA256),
):
path = DEFAULT_RUNTIME_CACHE / name
self.assertTrue(path.is_file(), f"missing runtime fixture: {path}")
self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), digest)
def test_builders_are_deterministic_narrow_and_secret_free(self):
self.require_runtime_cache()
for phase in ("foundation", "backhaul", "relay"):
with self.subTest(phase=phase), tempfile.TemporaryDirectory(
prefix=f"nodedc-vps-{phase}-"
) as directory:
root = Path(directory)
patch_id = f"device-edge-vps-{phase}-unit-001"
first = self.build(root, phase, patch_id)
self.assertEqual(first.returncode, 0, first.stderr)
first_result = json.loads(first.stdout)
first_bytes = Path(first_result["artifact"]).read_bytes()
second = self.build(root, phase, patch_id)
self.assertEqual(second.returncode, 0, second.stderr)
second_result = json.loads(second.stdout)
second_bytes = Path(second_result["artifact"]).read_bytes()
self.assertEqual(first_bytes, second_bytes)
self.assertEqual(first_result["sha256"], second_result["sha256"])
self.assertEqual(
first_result["sha256"],
hashlib.sha256(first_bytes).hexdigest(),
)
self.assertEqual(first_result["entries"], list(RUNNER.PHASE_ENTRIES[phase]))
with tarfile.open(first_result["artifact"], "r:gz") as archive:
members = archive.getmembers()
names = {member.name for member in members}
payload = b"\n".join(
archive.extractfile(member).read()
for member in members
if member.isfile() and member.size < 2 * 1024 * 1024
)
self.assertIn("manifest.env", names)
self.assertIn("files.txt", names)
self.assertFalse(any(
"/secrets/" in name
or "/keys/" in name
or "/trust/" in name
or "/runtime/" in name
or "/node_modules/" in name
or Path(name).name.startswith(".env")
for name in names
))
self.assertNotIn(b"PRIVATE KEY", payload)
self.assertNotIn(b"TS_AUTHKEY", payload)
def test_foundation_builder_rejects_modified_runtime_archive(self):
self.require_runtime_cache()
with tempfile.TemporaryDirectory(prefix="nodedc-vps-corrupt-") as directory:
cache = Path(directory) / "cache"
artifacts = Path(directory) / "artifacts"
cache.mkdir()
for name in (RUNNER.NODE_ARCHIVE, RUNNER.TAILSCALE_ARCHIVE):
(cache / name).write_bytes((DEFAULT_RUNTIME_CACHE / name).read_bytes())
with (cache / RUNNER.NODE_ARCHIVE).open("ab") as handle:
handle.write(b"corrupt")
result = self.build(
artifacts,
"foundation",
"device-edge-vps-foundation-corrupt-001",
runtime_cache=cache,
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("runtime_digest_mismatch", result.stderr)
def test_runner_loads_each_exact_phase(self):
self.require_runtime_cache()
with tempfile.TemporaryDirectory(prefix="nodedc-vps-load-") as directory:
inbox = Path(directory) / "inbox"
inbox.mkdir()
old_inbox = RUNNER.INBOX_ROOT
RUNNER.INBOX_ROOT = inbox
try:
for phase in ("foundation", "backhaul", "relay"):
result = self.build(
inbox,
phase,
f"device-edge-vps-{phase}-load-001",
)
self.assertEqual(result.returncode, 0, result.stderr)
artifact = Path(json.loads(result.stdout)["artifact"])
extraction = Path(directory) / f"extract-{phase}"
extraction.mkdir()
loaded = RUNNER.load_artifact(artifact, extraction)
self.assertEqual(loaded["phase"], phase)
self.assertEqual(loaded["entries"], RUNNER.PHASE_ENTRIES[phase])
self.assertEqual(
loaded["sha256"],
hashlib.sha256(artifact.read_bytes()).hexdigest(),
)
finally:
RUNNER.INBOX_ROOT = old_inbox
def test_plan_is_exact_and_never_claims_dns_or_b2_mutation(self):
self.require_runtime_cache()
with tempfile.TemporaryDirectory(prefix="nodedc-vps-plan-") as directory:
inbox = Path(directory) / "inbox"
inbox.mkdir()
result = self.build(
inbox,
"foundation",
"device-edge-vps-foundation-plan-001",
)
self.assertEqual(result.returncode, 0, result.stderr)
artifact = Path(json.loads(result.stdout)["artifact"])
old_inbox = RUNNER.INBOX_ROOT
RUNNER.INBOX_ROOT = inbox
try:
with patch.object(RUNNER, "assert_root"), patch.object(
RUNNER,
"preflight",
return_value={"predecessor": "unit-predecessor"},
), patch("builtins.print") as output:
RUNNER.plan_artifact(str(artifact))
finally:
RUNNER.INBOX_ROOT = old_inbox
rendered = "\n".join(
" ".join(str(arg) for arg in call.args)
for call in output.call_args_list
)
self.assertIn("phase=foundation", rendered)
self.assertIn("public_b2_ingress=disabled", rendered)
self.assertIn("dns=unchanged", rendered)
self.assertIn("b2_routes=unchanged", rendered)
self.assertIn("command_transport=disabled", rendered)
def test_units_and_firewalls_keep_the_required_boundaries(self):
source_root = SCRIPT_DIR.parent.parent / "device-plane"
foundation = (source_root / "vps/config/nftables-foundation.conf").read_text()
relay = (source_root / "vps/config/nftables-relay.conf").read_text()
sshd = (source_root / "vps/config/00-nodedc-b2-vps.conf").read_text()
backhaul = (source_root / "vps/config/backhaul_ssh_config").read_text()
tailscale_unit = (
source_root / "vps/systemd/nodedc-b2-tailscaled.service"
).read_text()
relay_unit = (source_root / "vps/systemd/nodedc-b2-relay.service").read_text()
backhaul_unit = (
source_root / "vps/systemd/nodedc-b2-backhaul.service"
).read_text()
self.assertIn("policy drop", foundation)
self.assertIn("tcp dport 22", foundation)
self.assertNotIn("tcp dport 9921", foundation)
self.assertIn("tcp dport 9921", relay)
self.assertIn("PasswordAuthentication no", sshd)
self.assertIn("AllowTcpForwarding no", sshd)
self.assertIn("StrictHostKeyChecking yes", backhaul)
self.assertIn("ProxyCommand /usr/bin/nc -X 5 -x 127.0.0.1:1055", backhaul)
self.assertIn("AF_NETLINK", tailscale_unit)
self.assertIn("User=nodedc-edge", tailscale_unit)
self.assertIn("StateDirectoryMode=0700", tailscale_unit)
self.assertIn("User=nodedc-backhaul", backhaul_unit)
self.assertNotIn("User=nodedc-edge", backhaul_unit)
self.assertIn("User=nodedc-relay", relay_unit)
self.assertNotIn("User=nodedc-edge", relay_unit)
self.assertIn("DEVICE_EDGE_RELAY_SOURCE_POLICY=public-ipv4-only", relay_unit)
self.assertIn("MemoryMax=192M", relay_unit)
def test_runner_has_registered_rollback_and_no_generic_latest(self):
source = RUNNER_PATH.read_text(encoding="utf-8")
self.assertIn("def rollback(", source)
self.assertIn('TAILSCALE_REQUIRED_TAG = "tag:device-edge-vps"', source)
self.assertIn("assign_backhaul_trust", source)
self.assertIn("deploy-ok patch=", source)
self.assertNotIn("apply-latest", source)
self.assertNotIn("compose down", source)
self.assertNotIn("docker system prune", source)
def test_executable_preflight_accepts_a_valid_alternatives_symlink(self):
with tempfile.TemporaryDirectory(prefix="nodedc-vps-tool-") as directory:
root = Path(directory)
target = root / "netcat.openbsd"
target.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
target.chmod(0o755)
command = root / "nc"
command.symlink_to(target.name)
self.assertEqual(
RUNNER.assert_executable_command_path(command, "test command"),
target.resolve(),
)
def test_executable_preflight_rejects_a_broken_symlink(self):
with tempfile.TemporaryDirectory(prefix="nodedc-vps-tool-") as directory:
command = Path(directory) / "nc"
command.symlink_to("missing-netcat")
with self.assertRaises(RUNNER.DeployError):
RUNNER.assert_executable_command_path(command, "test command")
def test_backup_restore_preserves_the_exact_relay_partition(self):
with tempfile.TemporaryDirectory(prefix="nodedc-vps-backup-") as directory:
root = Path(directory)
live = root / "live"
backups = root / "backups"
nft = root / "etc/nftables.conf"
relay_unit = root / "etc/nodedc-b2-relay.service"
backups.mkdir()
nft.parent.mkdir(parents=True)
nft.write_text("foundation-firewall\n", encoding="utf-8")
relay_unit.write_text("old-unit\n", encoding="utf-8")
for relative in RUNNER.RELAY_ENTRIES:
target = live / relative
if relative.endswith("/src"):
target.mkdir(parents=True)
(target / "server.mjs").write_text("old-source\n", encoding="utf-8")
else:
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(f"old:{relative}\n", encoding="utf-8")
old_live = RUNNER.LIVE_ROOT
old_backups = RUNNER.BACKUP_ROOT
old_nft = RUNNER.NFTABLES_CONFIG
old_relay_unit = RUNNER.RELAY_UNIT
RUNNER.LIVE_ROOT = live
RUNNER.BACKUP_ROOT = backups
RUNNER.NFTABLES_CONFIG = nft
RUNNER.RELAY_UNIT = relay_unit
completed = subprocess.CompletedProcess([], 0, "table inet old {}\n", "")
try:
with patch.object(RUNNER, "run", return_value=completed), patch.object(
RUNNER,
"service_active",
return_value=False,
), patch.object(
RUNNER,
"systemctl",
return_value=completed,
), patch.object(
RUNNER,
"user_exists",
return_value=False,
):
_backup_id, backup = RUNNER.create_backup("relay-unit", "relay")
nft.write_text("candidate-firewall\n", encoding="utf-8")
relay_unit.write_text("candidate-unit\n", encoding="utf-8")
(live / "services/device-edge-relay/src/server.mjs").write_text(
"candidate-source\n",
encoding="utf-8",
)
RUNNER.restore_backup(backup, "relay")
finally:
RUNNER.LIVE_ROOT = old_live
RUNNER.BACKUP_ROOT = old_backups
RUNNER.NFTABLES_CONFIG = old_nft
RUNNER.RELAY_UNIT = old_relay_unit
self.assertEqual(nft.read_text(), "foundation-firewall\n")
self.assertEqual(relay_unit.read_text(), "old-unit\n")
self.assertEqual(
(live / "services/device-edge-relay/src/server.mjs").read_text(),
"old-source\n",
)
if __name__ == "__main__":
unittest.main(verbosity=2)
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
import base64
import hashlib
import importlib.machinery
import importlib.util
import json
import os
import subprocess
import tarfile
import tempfile
import unittest
from pathlib import Path
from unittest import mock
SCRIPT_DIR = Path(__file__).resolve().parent
BUILDER = (
SCRIPT_DIR / "build-device-plane-backhaul-vps-enrollment-artifact.mjs"
)
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
def load_runner():
loader = importlib.machinery.SourceFileLoader(
"nodedc_device_plane_vps_enrollment_runner_under_test",
str(RUNNER_PATH),
)
spec = importlib.util.spec_from_loader(loader.name, loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
return module
RUNNER = load_runner()
class DevicePlaneBackhaulVpsEnrollmentArtifactTest(unittest.TestCase):
def build(self, artifact_dir, patch_id):
environment = os.environ.copy()
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
environment["NODEDC_ALLOW_SUPERSEDED_TRANSPORT"] = "test-only"
return subprocess.run(
["node", str(BUILDER), patch_id],
check=False,
capture_output=True,
text=True,
env=environment,
)
def test_builder_fails_closed_without_test_only_reconstruction(self):
environment = os.environ.copy()
environment.pop("NODEDC_ALLOW_SUPERSEDED_TRANSPORT", None)
with tempfile.TemporaryDirectory(
prefix="nodedc-vps-enrollment-frozen-",
) as directory:
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = directory
result = subprocess.run(
["node", str(BUILDER), "device-plane-vps-enrollment-frozen-001"],
check=False,
capture_output=True,
text=True,
env=environment,
)
self.assertNotEqual(result.returncode, 0)
self.assertIn(
"vps_initiated_transport_frozen:ADR-0001",
result.stderr,
)
def test_runner_rejects_a_prebuilt_superseded_enrollment_artifact(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-vps-enrollment-frozen-runner-",
) as directory:
root = Path(directory)
result = self.build(
root,
"device-plane-vps-enrollment-frozen-runner-001",
)
self.assertEqual(result.returncode, 0, result.stderr)
artifact = Path(json.loads(result.stdout)["artifact"])
extracted = root / "extracted"
extracted.mkdir()
with self.assertRaises(RUNNER.DeployError) as raised:
RUNNER.load_artifact(artifact, extracted)
self.assertIn(
"vps_initiated_transport_frozen:ADR-0001",
str(raised.exception),
)
def test_artifact_is_deterministic_marker_only_and_secret_free(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-vps-enrollment-artifact-",
) as directory:
root = Path(directory)
first = self.build(root, "device-plane-vps-enrollment-unit-001")
self.assertEqual(first.returncode, 0, first.stderr)
first_result = json.loads(first.stdout)
first_bytes = Path(first_result["artifact"]).read_bytes()
second = self.build(root, "device-plane-vps-enrollment-unit-001")
self.assertEqual(second.returncode, 0, second.stderr)
second_result = json.loads(second.stdout)
second_bytes = Path(second_result["artifact"]).read_bytes()
self.assertEqual(first_bytes, second_bytes)
self.assertEqual(
first_result["sha256"],
hashlib.sha256(first_bytes).hexdigest(),
)
self.assertEqual(
first_result["entries"],
list(RUNNER.DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_ENTRIES),
)
with tarfile.open(first_result["artifact"], "r:gz") as archive:
names = archive.getnames()
payload = b"\n".join(
archive.extractfile(member).read()
for member in archive.getmembers()
if member.isfile()
)
self.assertEqual(
set(names),
{
"manifest.env",
"files.txt",
"payload",
"payload/deployment",
"payload/deployment/device-plane-backhaul-vps-enrollment-v1.json",
},
)
self.assertNotIn(b"PRIVATE KEY", payload)
self.assertNotIn(b"authorized_keys", payload)
def test_registry_selects_only_existing_target_without_build(self):
entries = RUNNER.DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_ENTRIES
self.assertTrue(
RUNNER.is_device_plane_backhaul_vps_enrollment_slice(
"device-plane",
entries,
)
)
self.assertEqual(
RUNNER.component_services("device-plane", entries),
(RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,),
)
self.assertEqual(RUNNER.component_builds("device-plane", entries), ())
def test_vps_public_key_is_pinned_by_computed_fingerprint(self):
blob = (
b"\x00\x00\x00\x0bssh-ed25519\x00\x00\x00\x20"
+ bytes(range(32))
)
key = (
"ssh-ed25519 "
+ base64.b64encode(blob).decode("ascii")
+ " source-comment\n"
)
fingerprint = (
"SHA256:"
+ base64.b64encode(hashlib.sha256(blob).digest())
.decode("ascii")
.rstrip("=")
)
with tempfile.TemporaryDirectory(
prefix="nodedc-vps-enrollment-key-",
) as directory:
path = Path(directory) / "device-edge-vps-backhaul.pub"
path.write_text(key, encoding="ascii")
with (
mock.patch.object(
RUNNER,
"DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_PUBLIC_KEY_FILE",
path,
),
mock.patch.object(
RUNNER,
"DEVICE_PLANE_BACKHAUL_VPS_ENROLLMENT_FINGERPRINT",
fingerprint,
),
):
enrollment = (
RUNNER.read_device_plane_backhaul_vps_enrollment_public_key()
)
self.assertEqual(enrollment["fingerprint"], fingerprint)
self.assertTrue(
enrollment["line"].endswith(
" nodedc-device-edge-vps-backhaul"
)
)
def test_runtime_authorized_key_is_external_and_atomic(self):
blob = (
b"\x00\x00\x00\x0bssh-ed25519\x00\x00\x00\x20"
+ bytes(reversed(range(32)))
)
enrollment = {
"line": (
"ssh-ed25519 "
+ base64.b64encode(blob).decode("ascii")
+ " nodedc-device-edge-vps-backhaul"
)
}
with tempfile.TemporaryDirectory(
prefix="nodedc-vps-enrollment-runtime-",
) as directory:
secret_dir = Path(directory) / "secret"
authorized = secret_dir / "authorized_keys"
with (
mock.patch.object(
RUNNER,
"DEVICE_PLANE_BACKHAUL_SECRET_DIR",
secret_dir,
),
mock.patch.object(
RUNNER,
"DEVICE_PLANE_BACKHAUL_AUTHORIZED_KEYS_FILE",
authorized,
),
mock.patch.object(RUNNER.os, "chown"),
):
digest = RUNNER.install_device_plane_backhaul_authorized_key(
enrollment
)
expected = (
'restrict,port-forwarding,permitopen="127.0.0.1:9921" '
+ enrollment["line"]
+ "\n"
)
self.assertEqual(authorized.read_text(encoding="ascii"), expected)
self.assertEqual(
digest,
hashlib.sha256(expected.encode("ascii")).hexdigest(),
)
self.assertEqual(authorized.stat().st_mode & 0o777, 0o444)
if __name__ == "__main__":
unittest.main(verbosity=2)