feat(device-edge): add canonical core channel deployment

This commit is contained in:
Codex
2026-08-11 20:00:58 +03:00
parent 1124c15216
commit 6461e7fca8
8 changed files with 877 additions and 14 deletions
@@ -0,0 +1,54 @@
{
"schemaVersion": "nodedc.device-edge-vps.core-channel.v1",
"mode": "provider-neutral-core-initiated-mtls-http2",
"status": "closed-tracker-ingress",
"authority": "DCPLATFORM-21/DCPLATFORM-76/ADR-0001",
"component": "device-edge-vps",
"phase": "core-channel",
"runtimeHost": "koffyvngij",
"runtimeUser": "nodedc-channel",
"runtimeService": "nodedc-device-edge-channel.service",
"runtime": "accepted-node-v22.23.2-no-docker",
"publicIngress": "tcp/8443-mtls-only",
"health": "127.0.0.1:18222",
"trackerIngress": "disabled",
"rawDeviceTcp9921": "closed",
"commandTransport": "disabled",
"gelios": "untouched",
"privateKeyBoundary": "runner-managed-host-local-only",
"peerTrustPrerequisite": "exact-pinned-self-signed-core-certificate-and-fingerprint",
"tls": "TLSv1.3+h2+mutual-authentication",
"resourceCeilings": {
"memory": "128M",
"swap": "0",
"cpu": "50%",
"tasks": 64,
"openFiles": 1024
},
"preserved": [
"management-ssh-key",
"accepted-node-runtime",
"foundation-source",
"gelios-production-path"
],
"forbidden": [
"vps-initiated-synology-connection",
"generic-tcp-forwarding",
"tailscale-ssh-backhaul",
"docker",
"public-health",
"tracker-tcp/9921"
],
"acceptance": [
"exact-non-root-runtime-identity",
"tls13-h2-mutual-authentication",
"edge-server-and-core-client-self-signed-identities-mutually-pinned",
"core-initiated-channel-accepted",
"unknown-core-certificate-rejected",
"public-8443-only-beside-management-ssh",
"tracker-tcp-9921-closed",
"loopback-health-contract",
"resource-ceilings-present"
],
"rollback": "close-8443-stop-channel-restore-exact-accepted-foundation-without-backhaul-or-relay"
}
@@ -0,0 +1,203 @@
import { lstat, readFile } from "node:fs/promises";
import { createServer } from "node:http";
import { pathToFileURL } from "node:url";
import { createDeviceEdgeChannelServer } from "./runtime.mjs";
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
await main();
}
async function main() {
const runtime = await readRuntimeConfiguration(process.env);
const channel = createDeviceEdgeChannelServer(runtime.channel);
const health = createHealthServer(channel, runtime.health);
let stopping = false;
await channel.start();
await listen(health, runtime.health.port, runtime.health.host);
console.log(JSON.stringify({
event: "device_edge_channel_started",
host: runtime.channel.host,
port: runtime.channel.port,
healthHost: runtime.health.host,
healthPort: runtime.health.port,
edgeRegistrationId: runtime.channel.edgeRegistrationId,
channelGeneration: runtime.channel.channelGeneration,
trustGeneration: runtime.channel.trustGeneration,
trackerIngress: "disabled",
commandTransport: "disabled",
}));
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
async function shutdown() {
if (stopping) return;
stopping = true;
await Promise.allSettled([
channel.stop(),
closeServer(health),
]);
process.exit(0);
}
}
export async function readRuntimeConfiguration(environment = {}) {
const configPath = requiredPath(
environment.DEVICE_EDGE_CHANNEL_CONFIG_FILE,
"device_edge_channel_config_file_required",
);
const keyPath = requiredPath(
environment.DEVICE_EDGE_CHANNEL_KEY_FILE,
"device_edge_channel_key_file_required",
);
const certificatePath = requiredPath(
environment.DEVICE_EDGE_CHANNEL_CERTIFICATE_FILE,
"device_edge_channel_certificate_file_required",
);
const coreTrustPath = requiredPath(
environment.DEVICE_EDGE_CHANNEL_CORE_TRUST_FILE,
"device_edge_channel_core_trust_file_required",
);
const config = normalizeRuntimeDocument(JSON.parse(
await readBoundedRegularFile(configPath, 32 * 1024, "utf8"),
));
const [key, cert, ca] = await Promise.all([
readBoundedRegularFile(keyPath, 32 * 1024),
readBoundedRegularFile(certificatePath, 32 * 1024),
readBoundedRegularFile(coreTrustPath, 64 * 1024),
]);
return Object.freeze({
channel: Object.freeze({
edgeRegistrationId: config.edgeRegistrationId,
channelGeneration: config.channelGeneration,
trustGeneration: config.trustGeneration,
host: normalizeHost(environment.DEVICE_EDGE_CHANNEL_HOST ?? "0.0.0.0"),
port: normalizePort(environment.DEVICE_EDGE_CHANNEL_PORT, 8443),
tls: Object.freeze({
key,
cert,
ca,
allowedCoreFingerprints: config.allowedCoreFingerprints,
}),
}),
health: Object.freeze({
host: normalizeHost(
environment.DEVICE_EDGE_CHANNEL_HEALTH_HOST ?? "127.0.0.1",
),
port: normalizePort(environment.DEVICE_EDGE_CHANNEL_HEALTH_PORT, 18222),
}),
});
}
export function normalizeRuntimeDocument(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new TypeError("device_edge_channel_runtime_config_invalid");
}
const allowedKeys = new Set([
"schemaVersion",
"edgeRegistrationId",
"channelGeneration",
"trustGeneration",
"allowedCoreFingerprints",
]);
if (Object.keys(value).some((key) => !allowedKeys.has(key))) {
throw new TypeError("device_edge_channel_runtime_config_key_invalid");
}
if (value.schemaVersion !== "nodedc.device-edge.channel-runtime.v1") {
throw new TypeError("device_edge_channel_runtime_schema_invalid");
}
const fingerprints = value.allowedCoreFingerprints;
if (!Array.isArray(fingerprints) || fingerprints.length < 1 || fingerprints.length > 2) {
throw new TypeError("device_edge_channel_runtime_core_identity_invalid");
}
if (new Set(fingerprints).size !== fingerprints.length) {
throw new TypeError("device_edge_channel_runtime_core_identity_invalid");
}
return Object.freeze({
schemaVersion: value.schemaVersion,
edgeRegistrationId: normalizeRef(value.edgeRegistrationId, "edge_registration"),
channelGeneration: normalizeRef(value.channelGeneration, "channel_generation"),
trustGeneration: normalizeRef(value.trustGeneration, "trust_generation"),
allowedCoreFingerprints: Object.freeze(fingerprints.map((fingerprint) => {
if (typeof fingerprint !== "string" || !/^([A-F0-9]{2}:){31}[A-F0-9]{2}$/.test(fingerprint)) {
throw new TypeError("device_edge_channel_runtime_core_identity_invalid");
}
return fingerprint;
})),
});
}
function createHealthServer(channel, healthConfig) {
return createServer((request, response) => {
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.setHeader("Cache-Control", "no-store");
response.setHeader("X-Content-Type-Options", "nosniff");
if (request.method !== "GET" || request.url !== "/healthz") {
response.statusCode = 404;
response.end(JSON.stringify({ ok: false, error: "not_found" }));
return;
}
const status = channel.status();
response.statusCode = 200;
response.end(JSON.stringify({
ok: true,
service: "nodedc-device-edge-channel",
health: `${healthConfig.host}:${healthConfig.port}`,
...status,
}));
});
}
async function readBoundedRegularFile(path, maximumBytes, encoding = null) {
const state = await lstat(path);
if (!state.isFile() || state.isSymbolicLink() || state.size < 1 || state.size > maximumBytes) {
throw new Error("device_edge_channel_runtime_file_invalid");
}
return readFile(path, encoding ?? undefined);
}
function requiredPath(value, code) {
if (typeof value !== "string" || value.trim() === "" || !value.startsWith("/")) {
throw new Error(code);
}
return value;
}
function normalizeRef(value, field) {
if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
throw new TypeError(`device_edge_channel_runtime_${field}_invalid`);
}
return value;
}
function normalizeHost(value) {
if (typeof value !== "string" || value.length < 1 || value.length > 253) {
throw new TypeError("device_edge_channel_runtime_host_invalid");
}
return value;
}
function normalizePort(value, fallback) {
const number = Number(value ?? fallback);
if (!Number.isSafeInteger(number) || number < 1 || number > 65_535) {
throw new TypeError("device_edge_channel_runtime_port_invalid");
}
return number;
}
function listen(server, port, host) {
return new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(port, host, () => {
server.off("error", reject);
resolve();
});
});
}
function closeServer(server) {
return new Promise((resolve) => server.close(() => resolve()));
}
@@ -0,0 +1,46 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { normalizeRuntimeDocument } from "../src/server.mjs";
const fingerprint = Array.from({ length: 32 }, () => "AB").join(":");
test("runtime document accepts one generation-bound Core identity", () => {
const result = normalizeRuntimeDocument({
schemaVersion: "nodedc.device-edge.channel-runtime.v1",
edgeRegistrationId: "edge:moscow-vps-1",
channelGeneration: "channel:1",
trustGeneration: "trust:1",
allowedCoreFingerprints: [fingerprint],
});
assert.equal(result.edgeRegistrationId, "edge:moscow-vps-1");
assert.deepEqual(result.allowedCoreFingerprints, [fingerprint]);
});
test("runtime document rejects hidden authority and missing identity", () => {
assert.throws(() => normalizeRuntimeDocument({
schemaVersion: "nodedc.device-edge.channel-runtime.v1",
edgeRegistrationId: "edge:moscow-vps-1",
channelGeneration: "channel:1",
trustGeneration: "trust:1",
allowedCoreFingerprints: [fingerprint],
endpoint: "https://attacker.invalid",
}), /runtime_config_key_invalid/);
assert.throws(() => normalizeRuntimeDocument({
schemaVersion: "nodedc.device-edge.channel-runtime.v1",
edgeRegistrationId: "edge:moscow-vps-1",
channelGeneration: "channel:1",
trustGeneration: "trust:1",
allowedCoreFingerprints: [],
}), /runtime_core_identity_invalid/);
assert.throws(() => normalizeRuntimeDocument({
schemaVersion: "nodedc.device-edge.channel-runtime.v1",
edgeRegistrationId: "edge:moscow-vps-1",
channelGeneration: "channel:1",
trustGeneration: "trust:1",
allowedCoreFingerprints: [fingerprint, fingerprint],
}), /runtime_core_identity_invalid/);
});
@@ -0,0 +1,26 @@
#!/usr/sbin/nft -f
flush ruleset
table inet nodedc_b2_vps {
chain input {
type filter hook input priority -10; policy drop;
iifname "lo" accept
ct state invalid drop
ct state established,related accept
ip protocol icmp accept
ip6 nexthdr ipv6-icmp accept
tcp dport 22 ct state new limit rate 30/minute burst 60 packets accept
tcp dport 8443 ct state new limit rate 120/minute burst 120 packets accept
}
chain forward {
type filter hook forward priority -10; policy drop;
}
chain output {
type filter hook output priority -10; policy accept;
}
}
@@ -0,0 +1,50 @@
[Unit]
Description=NODE.DC provider-neutral Device Edge Core channel
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=nodedc-channel
Group=nodedc-channel
ExecStart=/opt/nodedc-b2-vps/runtime/node/bin/node --jitless /opt/nodedc-b2-vps/services/device-edge-channel/src/server.mjs
Environment=DEVICE_EDGE_CHANNEL_HOST=0.0.0.0
Environment=DEVICE_EDGE_CHANNEL_PORT=8443
Environment=DEVICE_EDGE_CHANNEL_HEALTH_HOST=127.0.0.1
Environment=DEVICE_EDGE_CHANNEL_HEALTH_PORT=18222
Environment=DEVICE_EDGE_CHANNEL_CONFIG_FILE=/var/lib/nodedc-b2-vps/channel-trust/runtime.json
Environment=DEVICE_EDGE_CHANNEL_KEY_FILE=/var/lib/nodedc-b2-vps/channel-trust/edge-private-key.pem
Environment=DEVICE_EDGE_CHANNEL_CERTIFICATE_FILE=/var/lib/nodedc-b2-vps/channel-trust/edge-certificate.pem
Environment=DEVICE_EDGE_CHANNEL_CORE_TRUST_FILE=/var/lib/nodedc-b2-vps/channel-trust/core-certificate.pem
Restart=always
RestartSec=2
TimeoutStartSec=20
TimeoutStopSec=15
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
RestrictAddressFamilies=AF_INET AF_INET6
CapabilityBoundingSet=
AmbientCapabilities=
UMask=0077
MemoryMax=128M
MemorySwapMax=0
CPUQuota=50%
TasksMax=64
LimitNOFILE=1024
[Install]
WantedBy=multi-user.target
@@ -29,11 +29,11 @@ const runtimeCache = resolve(
const [phase, patchId, ...extra] = process.argv.slice(2);
if (
extra.length
|| !["foundation", "backhaul", "relay"].includes(phase)
|| !["foundation", "backhaul", "relay", "core-channel"].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>",
"usage: build-device-edge-vps-artifact.mjs <foundation|backhaul|relay|core-channel> <patch-id>",
);
}
@@ -72,6 +72,17 @@ const entriesByPhase = {
"services/device-edge-relay/src",
"deployment/device-edge-vps-relay-v1.json",
],
"core-channel": [
"packages/device-protocol-contract/package.json",
"packages/device-protocol-contract/src",
"packages/device-edge-channel-contract/package.json",
"packages/device-edge-channel-contract/src",
"services/device-edge-channel/package.json",
"services/device-edge-channel/src",
"vps/config/nftables-core-channel.conf",
"vps/systemd/nodedc-device-edge-channel.service",
"deployment/device-edge-vps-core-channel-v1.json",
],
};
const entries = entriesByPhase[phase];
const ignoredBasenames = new Set([".DS_Store", ".git", "node_modules"]);
@@ -129,7 +140,11 @@ try {
size: bytes.length,
component: "device-edge-vps",
entries,
publicIngress: phase === "relay" ? "tcp/9921" : "disabled",
publicIngress: phase === "relay"
? "tcp/9921"
: phase === "core-channel"
? "tcp/8443-mtls-only"
: "disabled",
commandTransport: "disabled",
gelios: "untouched",
}, null, 2));
@@ -221,6 +236,34 @@ async function assertBoundary() {
}
}
}
if (phase === "core-channel") {
for (const required of [
"\"runtimeUser\": \"nodedc-channel\"",
"\"trackerIngress\": \"disabled\"",
"User=nodedc-channel",
"node --jitless",
"tcp dport 8443",
"MemoryMax=128M",
"MemorySwapMax=0",
"CPUQuota=50%",
"TasksMax=64",
"LimitNOFILE=1024",
]) {
if (!combined.includes(required)) {
throw new Error(`core_channel_boundary_missing:${required}`);
}
}
for (const forbidden of [
"tcp dport 9921",
"LocalForward",
"tailscale-userspace",
"DEVICE_EDGE_RELAY_UPSTREAM",
]) {
if (combined.includes(forbidden)) {
throw new Error(`core_channel_boundary_violation:${forbidden}`);
}
}
}
}
function canonicalTarScript() {
+315 -9
View File
@@ -44,6 +44,8 @@ BACKHAUL_USER = "nodedc-backhaul"
BACKHAUL_GROUP = "nodedc-backhaul"
RELAY_USER = "nodedc-relay"
RELAY_GROUP = "nodedc-relay"
CHANNEL_USER = "nodedc-channel"
CHANNEL_GROUP = "nodedc-channel"
TAILSCALE_REQUIRED_TAG = "tag:device-edge-vps"
MANAGEMENT_KEY_FINGERPRINT = (
"SHA256:DYYy1E3DaxIQGC0jnsW6SP7gXdBHUy3A1zn4pvgVUEw"
@@ -89,6 +91,14 @@ NFTABLES_CONFIG = Path("/etc/nftables.conf")
TAILSCALE_UNIT = Path("/etc/systemd/system/nodedc-b2-tailscaled.service")
BACKHAUL_UNIT = Path("/etc/systemd/system/nodedc-b2-backhaul.service")
RELAY_UNIT = Path("/etc/systemd/system/nodedc-b2-relay.service")
CHANNEL_UNIT = Path("/etc/systemd/system/nodedc-device-edge-channel.service")
CHANNEL_TRUST_ROOT = Path("/var/lib/nodedc-b2-vps/channel-trust")
CHANNEL_PRIVATE_KEY = CHANNEL_TRUST_ROOT / "edge-private-key.pem"
CHANNEL_CERTIFICATE = CHANNEL_TRUST_ROOT / "edge-certificate.pem"
CHANNEL_CORE_CERTIFICATE = CHANNEL_TRUST_ROOT / "core-certificate.pem"
CHANNEL_RUNTIME_CONFIG = CHANNEL_TRUST_ROOT / "runtime.json"
CHANNEL_HEALTH_PORT = 18222
CHANNEL_PUBLIC_PORT = 8443
FOUNDATION_ENTRIES = (
"vps/config/00-nodedc-b2-vps.conf",
@@ -109,11 +119,23 @@ RELAY_ENTRIES = (
"services/device-edge-relay/src",
"deployment/device-edge-vps-relay-v1.json",
)
CORE_CHANNEL_ENTRIES = (
"packages/device-protocol-contract/package.json",
"packages/device-protocol-contract/src",
"packages/device-edge-channel-contract/package.json",
"packages/device-edge-channel-contract/src",
"services/device-edge-channel/package.json",
"services/device-edge-channel/src",
"vps/config/nftables-core-channel.conf",
"vps/systemd/nodedc-device-edge-channel.service",
"deployment/device-edge-vps-core-channel-v1.json",
)
PHASE_ENTRIES = {
"foundation": FOUNDATION_ENTRIES,
"backhaul": BACKHAUL_ENTRIES,
"relay": RELAY_ENTRIES,
"core-channel": CORE_CHANNEL_ENTRIES,
}
SUPERSEDED_TRANSPORT_PHASES = frozenset({"backhaul", "relay"})
@@ -121,19 +143,19 @@ SUPERSEDED_TRANSPORT_PHASES = frozenset({"backhaul", "relay"})
PHASE_FILE_SHA256 = {
"foundation": {
"vps/config/00-nodedc-b2-vps.conf":
"cc94d0579f85d0af9746b9ce760bc72980f4a22fb59027e1f5f9c7bf3aaebd64",
"2079748f48b2297ecb067e16ae46248e3a981b26331aa0998dc14fdf7454cd4a",
"vps/config/nftables-foundation.conf":
"4d44f902d8d98d1aa8506fca9d9582e700f6424def2b1d667ab2cd5a5ee84934",
"bce5b8c6e2226d47d8d322f8ee9e7158f2d7a553c5e2c4bc2a722d6067f46e28",
"vps/systemd/nodedc-b2-tailscaled.service":
"de147d29bc1759f56533d31058993df55e1200973f2899903bdbf3d13ff579da",
"deployment/device-edge-vps-foundation-v1.json":
"317c98b42520fff3238275908482de7aa611b4ee41c6b1f8062abd2730ab072a",
"5ae007195b17d6cfaeb564abbed6dc9234eb36bf62db23bf2b9ace539c398898",
f"vendor/{NODE_ARCHIVE}": NODE_ARCHIVE_SHA256,
f"vendor/{TAILSCALE_ARCHIVE}": TAILSCALE_ARCHIVE_SHA256,
},
"backhaul": {
"vps/config/backhaul_ssh_config":
"ff1a3575b5a55a56b08a8642aa820d1f8e5f07e0e7698fdd0e01a142452674d4",
"d0df8b70dda025b7c1c3fecd2dafffe60a5bb753650d3bc37db65db626cfc1af",
"vps/systemd/nodedc-b2-backhaul.service":
"64c26cad21cc17675c67ae4a57fc43b129065a8d22fda894648340b310d3aa8c",
"deployment/device-edge-vps-backhaul-v1.json":
@@ -141,7 +163,7 @@ PHASE_FILE_SHA256 = {
},
"relay": {
"vps/config/nftables-relay.conf":
"d99290bf825a3aff2ad8f9dbc6502ba6c32295f0cabd3f71c3d9b94c06373259",
"497485a2fb1b79faa95eb67ca30a89a3b65c3d4d8ddcb14cd1dc9acfeb2bb6f2",
"vps/systemd/nodedc-b2-relay.service":
"12a927a4cb42016229ac438f6e75969e1bf015037f2b5b0a60ab0591bdf424d2",
"services/device-edge-relay/src/runtime.mjs":
@@ -151,6 +173,28 @@ PHASE_FILE_SHA256 = {
"deployment/device-edge-vps-relay-v1.json":
"cb3c2fff4878021783efef4f4a4d1ec31c8e3d8e6325fe33657fec22bec20165",
},
"core-channel": {
"packages/device-protocol-contract/package.json":
"19d0d07da0341e8c2e8d3485400767566b18f6270245f0e9a9681c95013ba3b7",
"packages/device-protocol-contract/src/index.mjs":
"21a8b2b85a807899f946387c7976eaffcdc438b7db5e3d41fc3930f4437f0ec7",
"packages/device-edge-channel-contract/package.json":
"57d5349b5dcef2cacd4f3e4fad010359a65d59f5f903eff07d89f67c497f97c0",
"packages/device-edge-channel-contract/src/index.mjs":
"09d45e6104779212605fed650544794aafe1c2b1ab66f33f07bef4d92f430c80",
"services/device-edge-channel/package.json":
"bdf502be43b62bdd6db05b022a532d93ba954277ac5143d6058d2f27f6a2e9d2",
"services/device-edge-channel/src/runtime.mjs":
"cbb07f7e644a68e9c8c36c1c2ecf0224c06c46c08c49339d62637b10a1495501",
"services/device-edge-channel/src/server.mjs":
"ea891634a18efb9eb44f17b56c95ba97527215c4d0a6147cc2b7bad1d7356e36",
"vps/config/nftables-core-channel.conf":
"1a04a5450042e80b8a20da3c6634dd6bc68693f191a463ba9a62984279d81d0c",
"vps/systemd/nodedc-device-edge-channel.service":
"57c6c0c5eb952e1e196f50c410a8537468af145941b21dd4aad6ff0e8ca56249",
"deployment/device-edge-vps-core-channel-v1.json":
"938f6f7959b78e6be3a6e91a54dca2922fbd813f1a33dca5cebe0dc256a83a14",
},
}
@@ -452,7 +496,7 @@ def current_phase_preflight(phase: str):
die("VPS foundation live/runtime root already exists")
if any(path.exists() for path in (SSHD_DROPIN, TAILSCALE_UNIT, BACKHAUL_UNIT, RELAY_UNIT)):
die("VPS foundation system path already exists")
for port in (1055, 18221, 19921, 9921):
for port in (1055, 18221, CHANNEL_HEALTH_PORT, 19921, 9921, CHANNEL_PUBLIC_PORT):
assert_port_closed(port)
return {"predecessor": "clean-ubuntu-24.04.4"}
@@ -462,6 +506,19 @@ def current_phase_preflight(phase: str):
require_running_tailnet=phase in {"backhaul", "relay"},
expected_key_user=BACKHAUL_USER if phase == "relay" else SERVICE_USER,
)
if phase == "core-channel":
if service_active("nodedc-b2-backhaul.service"):
die("frozen VPS backhaul service must remain inactive")
if service_active("nodedc-b2-relay.service"):
die("frozen VPS relay service must remain inactive")
if CHANNEL_UNIT.exists() or (LIVE_ROOT / CORE_CHANNEL_ENTRIES[-1]).exists():
die("VPS Core channel target path already exists")
if user_exists(CHANNEL_USER):
die("VPS Core channel runtime user already exists")
assert_channel_trust(require_runtime_owner=False)
for port in (CHANNEL_HEALTH_PORT, CHANNEL_PUBLIC_PORT, 9921):
assert_port_closed(port)
return {"predecessor": "accepted-foundation-closed-channel"}
if phase == "backhaul":
for tool in (Path("/usr/bin/ssh"), Path("/usr/bin/nc")):
assert_executable_command_path(
@@ -529,6 +586,8 @@ def backup_targets_for_phase(phase: str):
return common + [SSHD_DROPIN, NFTABLES_CONFIG, TAILSCALE_UNIT]
if phase == "backhaul":
return common + [BACKHAUL_UNIT, BACKHAUL_KNOWN_HOSTS]
if phase == "core-channel":
return common + [CHANNEL_UNIT, NFTABLES_CONFIG, CHANNEL_TRUST_ROOT]
return common + [RELAY_UNIT, NFTABLES_CONFIG]
@@ -568,7 +627,7 @@ def create_backup(patch_id: str, phase: str):
"serviceUserExisted": user_exists(),
"serviceUsersExisted": {
name: user_exists(name)
for name in (SERVICE_USER, BACKHAUL_USER, RELAY_USER)
for name in (SERVICE_USER, BACKHAUL_USER, RELAY_USER, CHANNEL_USER)
},
"services": {
name: {
@@ -582,6 +641,7 @@ def create_backup(patch_id: str, phase: str):
"nodedc-b2-tailscaled.service",
"nodedc-b2-backhaul.service",
"nodedc-b2-relay.service",
"nodedc-device-edge-channel.service",
)
},
}
@@ -740,6 +800,146 @@ def assign_backhaul_trust(account):
os.chmod(BACKHAUL_KNOWN_HOSTS, 0o444)
def certificate_fingerprint(path: Path):
output = run([
"/usr/bin/openssl",
"x509",
"-in",
str(path),
"-noout",
"-fingerprint",
"-sha256",
]).stdout.strip()
prefix = "sha256 Fingerprint="
if not output.lower().startswith(prefix.lower()):
die("VPS certificate fingerprint output is invalid")
fingerprint = output.split("=", 1)[1].upper()
if not re.fullmatch(r"(?:[A-F0-9]{2}:){31}[A-F0-9]{2}", fingerprint):
die("VPS certificate fingerprint is invalid")
return fingerprint
def assert_channel_trust(*, require_runtime_owner: bool):
directory = assert_directory_nonsymlink(
CHANNEL_TRUST_ROOT,
"Core channel trust root",
)
expected_uid = 0
expected_gid = 0
if require_runtime_owner:
account = pwd.getpwnam(CHANNEL_USER)
expected_uid = account.pw_uid
expected_gid = account.pw_gid
if (
directory.st_uid != expected_uid
or directory.st_gid != expected_gid
or (directory.st_mode & 0o777) != 0o700
):
die("Core channel trust root ownership/mode mismatch")
for path, maximum, mode in (
(CHANNEL_PRIVATE_KEY, 32 * 1024, 0o400),
(CHANNEL_CERTIFICATE, 32 * 1024, 0o444),
(CHANNEL_CORE_CERTIFICATE, 32 * 1024, 0o444),
(CHANNEL_RUNTIME_CONFIG, 32 * 1024, 0o444),
):
state = assert_regular_nonsymlink(path, f"Core channel trust {path.name}")
if state.st_size < 1 or state.st_size > maximum:
die(f"Core channel trust file size mismatch: {path.name}")
if (
state.st_uid != expected_uid
or state.st_gid != expected_gid
or (state.st_mode & 0o777) != mode
):
die(f"Core channel trust file ownership/mode mismatch: {path.name}")
private_text = CHANNEL_PRIVATE_KEY.read_text(encoding="ascii")
public_text = (
CHANNEL_CERTIFICATE.read_text(encoding="ascii")
+ CHANNEL_CORE_CERTIFICATE.read_text(encoding="ascii")
)
if "PRIVATE KEY" not in private_text or "PRIVATE KEY" in public_text:
die("Core channel private/public trust boundary mismatch")
for path in (
CHANNEL_CERTIFICATE,
CHANNEL_CORE_CERTIFICATE,
):
if path.read_text(encoding="ascii").count("-----BEGIN CERTIFICATE-----") != 1:
die(f"Core channel certificate cardinality mismatch: {path.name}")
run(["/usr/bin/openssl", "pkey", "-in", str(CHANNEL_PRIVATE_KEY), "-check", "-noout"])
run(["/usr/bin/openssl", "x509", "-in", str(CHANNEL_CERTIFICATE), "-noout"])
run(["/usr/bin/openssl", "x509", "-in", str(CHANNEL_CORE_CERTIFICATE), "-noout"])
run([
"/usr/bin/openssl", "verify", "-purpose", "sslserver",
"-CAfile", str(CHANNEL_CERTIFICATE), str(CHANNEL_CERTIFICATE),
])
run([
"/usr/bin/openssl", "verify", "-purpose", "sslclient",
"-CAfile", str(CHANNEL_CORE_CERTIFICATE), str(CHANNEL_CORE_CERTIFICATE),
])
run([
"/usr/bin/openssl", "x509", "-in", str(CHANNEL_CERTIFICATE),
"-noout", "-checkip", PUBLIC_IPV4,
])
certificate_key = run([
"/usr/bin/openssl", "x509", "-in", str(CHANNEL_CERTIFICATE), "-pubkey", "-noout",
]).stdout.strip()
private_key = run([
"/usr/bin/openssl", "pkey", "-in", str(CHANNEL_PRIVATE_KEY), "-pubout",
]).stdout.strip()
if certificate_key != private_key:
die("Core channel Edge certificate/private key mismatch")
try:
document = json.loads(CHANNEL_RUNTIME_CONFIG.read_text(encoding="utf-8"))
except json.JSONDecodeError:
die("Core channel runtime configuration is invalid JSON")
if set(document) != {
"schemaVersion",
"edgeRegistrationId",
"channelGeneration",
"trustGeneration",
"allowedCoreFingerprints",
}:
die("Core channel runtime configuration key set mismatch")
if document.get("schemaVersion") != "nodedc.device-edge.channel-runtime.v1":
die("Core channel runtime configuration schema mismatch")
for key in ("edgeRegistrationId", "channelGeneration", "trustGeneration"):
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}", str(document.get(key, ""))):
die(f"Core channel runtime configuration ref mismatch: {key}")
fingerprints = document.get("allowedCoreFingerprints")
if (
not isinstance(fingerprints, list)
or not 1 <= len(fingerprints) <= 2
or len(fingerprints) != len(set(fingerprints))
or any(
not isinstance(value, str)
or not re.fullmatch(r"(?:[A-F0-9]{2}:){31}[A-F0-9]{2}", value)
for value in fingerprints
)
):
die("Core channel Core identity allowlist mismatch")
if certificate_fingerprint(CHANNEL_CORE_CERTIFICATE) not in fingerprints:
die("Core channel Core trust fingerprint mismatch")
return document
def assign_channel_trust(account):
assert_channel_trust(require_runtime_owner=False)
os.chown(CHANNEL_TRUST_ROOT, account.pw_uid, account.pw_gid)
os.chmod(CHANNEL_TRUST_ROOT, 0o700)
for path, mode in (
(CHANNEL_PRIVATE_KEY, 0o400),
(CHANNEL_CERTIFICATE, 0o444),
(CHANNEL_CORE_CERTIFICATE, 0o444),
(CHANNEL_RUNTIME_CONFIG, 0o444),
):
os.chown(path, account.pw_uid, account.pw_gid)
os.chmod(path, mode)
return assert_channel_trust(require_runtime_owner=True)
def apply_nftables(source: Path):
install_file(source, NFTABLES_CONFIG, 0o644)
run(["/usr/sbin/nft", "-c", "-f", str(NFTABLES_CONFIG)])
@@ -813,6 +1013,19 @@ def apply_relay(payload: Path):
validate_relay_runtime()
def apply_core_channel(payload: Path):
account = ensure_service_user(
CHANNEL_USER,
"/var/lib/nodedc-b2-vps/channel-runtime",
)
assign_channel_trust(account)
install_file(LIVE_ROOT / CORE_CHANNEL_ENTRIES[7], CHANNEL_UNIT, 0o644)
apply_nftables(LIVE_ROOT / CORE_CHANNEL_ENTRIES[6])
systemctl("daemon-reload")
systemctl("enable", "--now", "nodedc-device-edge-channel.service")
validate_core_channel_runtime()
def sshd_effective():
return run(["/usr/sbin/sshd", "-T"]).stdout.lower()
@@ -919,6 +1132,85 @@ def relay_health():
die(f"VPS relay health timeout: {last_error}")
def core_channel_health(*, require_accepted: bool):
last_error = None
for _attempt in range(60):
try:
with urllib.request.urlopen(
f"http://127.0.0.1:{CHANNEL_HEALTH_PORT}/healthz",
timeout=3,
) as response:
payload = json.loads(response.read(65537).decode("utf-8"))
if (
response.status == 200
and payload.get("ok") is True
and (
not require_accepted
or payload.get("channel") == "accepted"
)
):
return payload
last_error = f"channel={payload.get('channel')}"
except Exception as error:
last_error = str(error)
time.sleep(2)
die(f"VPS Core channel health timeout: {last_error}")
def validate_core_channel_runtime():
validate_foundation_runtime(
require_running_tailnet=False,
expected_key_user=SERVICE_USER,
)
source_file_state("core-channel")
assert_channel_trust(require_runtime_owner=True)
if service_active("nodedc-b2-backhaul.service"):
die("frozen VPS backhaul service became active")
if service_active("nodedc-b2-relay.service"):
die("frozen VPS relay service became active")
if not service_active("nodedc-device-edge-channel.service"):
die("VPS Core channel service is not active")
health = core_channel_health(require_accepted=True)
expected = {
"ok": True,
"service": "nodedc-device-edge-channel",
"channel": "accepted",
"trackerIngress": "disabled",
"commandTransport": "disabled",
}
for key, value in expected.items():
if health.get(key) != value:
die(f"VPS Core channel health contract mismatch: {key}")
if not port_is_open(PUBLIC_IPV4, CHANNEL_PUBLIC_PORT, timeout=5):
die("VPS public Core channel listener is unavailable")
assert_port_closed(9921)
nft = run(["/usr/sbin/nft", "list", "table", "inet", "nodedc_b2_vps"]).stdout
if (
"policy drop" not in nft
or "tcp dport 8443" not in nft
or "tcp dport 9921" in nft
):
die("VPS Core channel firewall contract mismatch")
unit = run([
"/usr/bin/systemctl",
"show",
"nodedc-device-edge-channel.service",
"--property=User,Group,NoNewPrivileges,MemoryMax,MemorySwapMax,CPUQuotaPerSecUSec,TasksMax,LimitNOFILE",
]).stdout
for required in (
"User=nodedc-channel",
"Group=nodedc-channel",
"NoNewPrivileges=yes",
"MemoryMax=134217728",
"MemorySwapMax=0",
"TasksMax=64",
"LimitNOFILE=1024",
):
if required not in unit:
die(f"VPS Core channel resource boundary mismatch: {required}")
return health
def validate_relay_runtime():
validate_backhaul_runtime()
source_file_state("relay")
@@ -979,6 +1271,7 @@ def rollback(backup: Path, phase: str):
for service in (
"nodedc-b2-relay.service",
"nodedc-b2-backhaul.service",
"nodedc-device-edge-channel.service",
"nodedc-b2-tailscaled.service",
):
if phase == "foundation" or service != "nodedc-b2-tailscaled.service":
@@ -1004,6 +1297,9 @@ def rollback(backup: Path, phase: str):
if phase == "relay":
if not users_before.get(RELAY_USER, False) and user_exists(RELAY_USER):
run(["/usr/sbin/userdel", RELAY_USER], check=False)
if phase == "core-channel":
if not users_before.get(CHANNEL_USER, False) and user_exists(CHANNEL_USER):
run(["/usr/sbin/userdel", CHANNEL_USER], check=False)
if phase == "foundation" and not metadata.get("serviceUserExisted"):
runtime_state_root = Path("/var/lib/nodedc-b2-vps")
if LIVE_ROOT.exists() and not LIVE_ROOT.is_symlink():
@@ -1052,13 +1348,21 @@ def plan_artifact(artifact_argument: str):
print("public_b2_ingress=disabled")
print("services=nodedc-b2-backhaul")
print(f"backhaul_runtime_identity={BACKHAUL_USER}:private-key-owner")
else:
elif phase == "relay":
print("public_b2_ingress=155.212.211.15:9921/tcp")
print("health=127.0.0.1:18221")
print("private_upstream=127.0.0.1:19921")
print("source_admission=public-ipv4-only")
print("services=nodedc-b2-relay")
print(f"relay_runtime_identity={RELAY_USER}:no-credentials")
else:
print("public_core_channel=155.212.211.15:8443/tcp:tls13-mtls-h2")
print(f"health=127.0.0.1:{CHANNEL_HEALTH_PORT}")
print("public_b2_ingress=disabled")
print("tracker_tcp_9921=closed")
print("services=nodedc-device-edge-channel")
print(f"channel_runtime_identity={CHANNEL_USER}:host-local-private-key")
print("peer_trust=preprovisioned-pinned-self-signed-core-certificate+fingerprint")
print("command_transport=disabled")
print("gelios=untouched")
print("dns=unchanged")
@@ -1088,8 +1392,10 @@ def apply_artifact(artifact_argument: str):
apply_foundation(loaded["payload"])
elif loaded["phase"] == "backhaul":
apply_backhaul(loaded["payload"])
else:
elif loaded["phase"] == "relay":
apply_relay(loaded["payload"])
else:
apply_core_channel(loaded["payload"])
archived = archive_artifact(loaded["artifact"], APPLIED_ROOT)
record = {
@@ -92,7 +92,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
def test_builders_are_deterministic_narrow_and_secret_free(self):
self.require_runtime_cache()
for phase in ("foundation", "backhaul", "relay"):
for phase in ("foundation", "backhaul", "relay", "core-channel"):
with self.subTest(phase=phase), tempfile.TemporaryDirectory(
prefix=f"nodedc-vps-{phase}-"
) as directory:
@@ -164,7 +164,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
old_inbox = RUNNER.INBOX_ROOT
RUNNER.INBOX_ROOT = inbox
try:
for phase in ("foundation", "backhaul", "relay"):
for phase in ("foundation", "backhaul", "relay", "core-channel"):
result = self.build(
inbox,
phase,
@@ -217,10 +217,55 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
self.assertIn("b2_routes=unchanged", rendered)
self.assertIn("command_transport=disabled", rendered)
def test_core_channel_plan_is_exact_and_keeps_tracker_ingress_closed(self):
with tempfile.TemporaryDirectory(prefix="nodedc-vps-channel-plan-") as directory:
inbox = Path(directory) / "inbox"
inbox.mkdir()
result = self.build(
inbox,
"core-channel",
"device-edge-vps-core-channel-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": "accepted-foundation-closed-channel"},
), 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=core-channel", rendered)
self.assertIn(
"predecessor=accepted-foundation-closed-channel",
rendered,
)
self.assertIn(
"public_core_channel=155.212.211.15:8443/tcp:tls13-mtls-h2",
rendered,
)
self.assertIn("tracker_tcp_9921=closed", rendered)
self.assertIn("public_b2_ingress=disabled", rendered)
self.assertIn(
"peer_trust=preprovisioned-pinned-self-signed-core-certificate+fingerprint",
rendered,
)
self.assertIn("command_transport=disabled", rendered)
self.assertIn("gelios=untouched", 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()
channel = (source_root / "vps/config/nftables-core-channel.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 = (
@@ -230,11 +275,16 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
backhaul_unit = (
source_root / "vps/systemd/nodedc-b2-backhaul.service"
).read_text()
channel_unit = (
source_root / "vps/systemd/nodedc-device-edge-channel.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("tcp dport 8443", channel)
self.assertNotIn("tcp dport 9921", channel)
self.assertIn("PasswordAuthentication no", sshd)
self.assertIn("AllowTcpForwarding no", sshd)
self.assertIn("StrictHostKeyChecking yes", backhaul)
@@ -248,6 +298,15 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
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)
self.assertIn("User=nodedc-channel", channel_unit)
self.assertIn("node --jitless", channel_unit)
self.assertIn("MemoryMax=128M", channel_unit)
self.assertIn("MemorySwapMax=0", channel_unit)
self.assertIn("CPUQuota=50%", channel_unit)
self.assertIn("TasksMax=64", channel_unit)
self.assertIn("LimitNOFILE=1024", channel_unit)
self.assertNotIn("LocalForward", channel_unit)
self.assertNotIn("DEVICE_EDGE_RELAY_UPSTREAM", channel_unit)
def test_runner_has_registered_rollback_and_no_generic_latest(self):
source = RUNNER_PATH.read_text(encoding="utf-8")
@@ -344,6 +403,82 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
"old-source\n",
)
def test_backup_restore_preserves_core_channel_source_trust_and_firewall(self):
with tempfile.TemporaryDirectory(prefix="nodedc-vps-channel-backup-") as directory:
root = Path(directory)
live = root / "live"
backups = root / "backups"
nft = root / "etc/nftables.conf"
channel_unit = root / "etc/nodedc-device-edge-channel.service"
trust = root / "state/channel-trust"
backups.mkdir()
nft.parent.mkdir(parents=True)
trust.mkdir(parents=True)
nft.write_text("foundation-firewall\n", encoding="utf-8")
channel_unit.parent.mkdir(parents=True, exist_ok=True)
channel_unit.write_text("old-channel-unit\n", encoding="utf-8")
(trust / "runtime.json").write_text("old-runtime\n", encoding="utf-8")
for relative in RUNNER.CORE_CHANNEL_ENTRIES:
target = live / relative
if relative.endswith("/src"):
target.mkdir(parents=True)
(target / "server.mjs").write_text("old-channel-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_unit = RUNNER.CHANNEL_UNIT
old_trust = RUNNER.CHANNEL_TRUST_ROOT
RUNNER.LIVE_ROOT = live
RUNNER.BACKUP_ROOT = backups
RUNNER.NFTABLES_CONFIG = nft
RUNNER.CHANNEL_UNIT = channel_unit
RUNNER.CHANNEL_TRUST_ROOT = trust
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(
"channel-unit",
"core-channel",
)
nft.write_text("candidate-firewall\n", encoding="utf-8")
channel_unit.write_text("candidate-channel-unit\n", encoding="utf-8")
(trust / "runtime.json").write_text("candidate-runtime\n", encoding="utf-8")
(live / "services/device-edge-channel/src/server.mjs").write_text(
"candidate-channel-source\n",
encoding="utf-8",
)
RUNNER.restore_backup(backup, "core-channel")
finally:
RUNNER.LIVE_ROOT = old_live
RUNNER.BACKUP_ROOT = old_backups
RUNNER.NFTABLES_CONFIG = old_nft
RUNNER.CHANNEL_UNIT = old_unit
RUNNER.CHANNEL_TRUST_ROOT = old_trust
self.assertEqual(nft.read_text(), "foundation-firewall\n")
self.assertEqual(channel_unit.read_text(), "old-channel-unit\n")
self.assertEqual((trust / "runtime.json").read_text(), "old-runtime\n")
self.assertEqual(
(live / "services/device-edge-channel/src/server.mjs").read_text(),
"old-channel-source\n",
)
if __name__ == "__main__":
unittest.main(verbosity=2)