From c2d06bddc7a8e12fbc97f81b7ebbebf8740e752b Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 12 Aug 2026 01:43:04 +0300 Subject: [PATCH] feat(device-edge): standardize Core channel on mTLS 443 --- .../device-edge-core-channel-v1.json | 4 ++-- .../device-edge-vps-core-channel-v1.json | 7 +++--- .../ADR_0001_CORE_INITIATED_EDGE_CHANNEL.md | 4 ++-- .../src/edge-channel-supervisor.mjs | 2 +- .../src/infrastructure-management.mjs | 2 +- .../test/edge-channel-supervisor.test.mjs | 2 +- .../test/infrastructure-management.test.mjs | 11 +++++----- .../test/infrastructure-repository.test.mjs | 4 ++-- .../device-edge-channel/src/runtime.mjs | 2 +- .../device-edge-channel/src/server.mjs | 2 +- .../vps/config/nftables-core-channel.conf | 2 +- .../nodedc-device-edge-channel.service | 6 ++--- .../build-device-edge-vps-artifact.mjs | 6 +++-- infra/deploy-runner/nodedc-b2-vps-deploy | 22 ++++++++++--------- .../test_device_edge_vps_artifact.py | 12 ++++++++-- 15 files changed, 51 insertions(+), 37 deletions(-) diff --git a/device-plane/deployment/device-edge-core-channel-v1.json b/device-plane/deployment/device-edge-core-channel-v1.json index e8fef6c..56323ba 100644 --- a/device-plane/deployment/device-edge-core-channel-v1.json +++ b/device-plane/deployment/device-edge-core-channel-v1.json @@ -6,7 +6,7 @@ "transport": { "protocol": "http2-bidirectional-stream", "tls": "TLSv1.3-mutual-authentication", - "edgeListen": "0.0.0.0:8443", + "edgeListen": "0.0.0.0:443", "endpointSource": "device-control-core.edge-registration", "browserAccess": "forbidden", "bearerOnlyAuthentication": "forbidden", @@ -30,7 +30,7 @@ "allowedEdgeListeners": [ "management-ssh", "raw-device-tcp/9921", - "core-channel-mtls/8443" + "core-channel-mtls/443" ] }, "messageContract": { diff --git a/device-plane/deployment/device-edge-vps-core-channel-v1.json b/device-plane/deployment/device-edge-vps-core-channel-v1.json index 4e6e8cc..dedc68c 100644 --- a/device-plane/deployment/device-edge-vps-core-channel-v1.json +++ b/device-plane/deployment/device-edge-vps-core-channel-v1.json @@ -9,7 +9,7 @@ "runtimeUser": "nodedc-channel", "runtimeService": "nodedc-device-edge-channel.service", "runtime": "accepted-node-v22.23.2-no-docker", - "publicIngress": "tcp/8443-mtls-only", + "publicIngress": "tcp/443-mtls-only", "health": "127.0.0.1:18222", "trackerIngress": "disabled", "rawDeviceTcp9921": "closed", @@ -18,6 +18,7 @@ "privateKeyBoundary": "runner-managed-host-local-only", "peerTrustPrerequisite": "exact-pinned-self-signed-core-certificate-and-fingerprint", "tls": "TLSv1.3+h2+mutual-authentication", + "networkPrivilege": "CAP_NET_BIND_SERVICE-only-for-non-root-tcp-443", "resourceCeilings": { "memory": "128M", "swap": "0", @@ -45,10 +46,10 @@ "edge-server-and-core-client-self-signed-identities-mutually-pinned", "core-initiated-channel-accepted", "unknown-core-certificate-rejected", - "public-8443-only-beside-management-ssh", + "public-443-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" + "rollback": "close-443-stop-channel-restore-exact-accepted-foundation-without-backhaul-or-relay" } diff --git a/device-plane/docs/ADR_0001_CORE_INITIATED_EDGE_CHANNEL.md b/device-plane/docs/ADR_0001_CORE_INITIATED_EDGE_CHANNEL.md index 0da8113..4be99d6 100644 --- a/device-plane/docs/ADR_0001_CORE_INITIATED_EDGE_CHANNEL.md +++ b/device-plane/docs/ADR_0001_CORE_INITIATED_EDGE_CHANNEL.md @@ -21,7 +21,7 @@ HTTP/2 bidirectional session: ARUSNAVI B2 -> raw TCP/9921 -> Device Gateway Edge on the VPS - -> accepted Core-owned mTLS session on TCP/8443 + -> accepted Core-owned mTLS session on standard HTTPS TCP/443 -> Device Gateway Core on Synology -> Device Control Core ``` @@ -220,7 +220,7 @@ The target Edge exposes only: - management SSH according to the separately accepted management policy; - raw tracker ingress TCP/9921; -- Core channel TCP/8443 with mandatory mTLS. +- Core channel TCP/443 with mandatory mTLS; this is not a browser or bearer-token endpoint. Synology exposes no public device or backhaul port. The Edge receives no subnet route, exit-node capability, Tailscale SSH, Docker socket or generic proxy. The diff --git a/device-plane/services/device-control-core/src/edge-channel-supervisor.mjs b/device-plane/services/device-control-core/src/edge-channel-supervisor.mjs index a7bddec..b528ab4 100644 --- a/device-plane/services/device-control-core/src/edge-channel-supervisor.mjs +++ b/device-plane/services/device-control-core/src/edge-channel-supervisor.mjs @@ -266,7 +266,7 @@ function normalizeRegistration(value) { || endpoint.pathname !== "/" || endpoint.search || endpoint.hash - || endpoint.port !== "8443" + || endpoint.port !== "" || endpoint.hostname !== String(value.servername || "").toLowerCase() || !isPublicIpv4(endpoint.hostname) ) { diff --git a/device-plane/services/device-control-core/src/infrastructure-management.mjs b/device-plane/services/device-control-core/src/infrastructure-management.mjs index 6ec8069..8310f2d 100644 --- a/device-plane/services/device-control-core/src/infrastructure-management.mjs +++ b/device-plane/services/device-control-core/src/infrastructure-management.mjs @@ -418,7 +418,7 @@ function normalizeEdgeEndpoint(value) { || endpoint.pathname !== "/" || endpoint.search || endpoint.hash - || endpoint.port !== "8443" + || endpoint.port !== "" || !isPublicIpv4(endpoint.hostname) ) { throw new TypeError("device_edge_channel_endpoint_invalid"); diff --git a/device-plane/services/device-control-core/test/edge-channel-supervisor.test.mjs b/device-plane/services/device-control-core/test/edge-channel-supervisor.test.mjs index 112fea5..e2abfab 100644 --- a/device-plane/services/device-control-core/test/edge-channel-supervisor.test.mjs +++ b/device-plane/services/device-control-core/test/edge-channel-supervisor.test.mjs @@ -101,7 +101,7 @@ test("supervisor keeps a failed trust enrollment isolated from other Edges", asy function registration(channelGeneration, edgeRegistrationId = "edge:pilot") { return { edgeRegistrationId, - endpoint: "https://155.212.211.15:8443/", + endpoint: "https://155.212.211.15/", servername: "155.212.211.15", channelGeneration, trustBundleRef: "edge-trust:moscow-edge", diff --git a/device-plane/services/device-control-core/test/infrastructure-management.test.mjs b/device-plane/services/device-control-core/test/infrastructure-management.test.mjs index 8102672..8b12b60 100644 --- a/device-plane/services/device-control-core/test/infrastructure-management.test.mjs +++ b/device-plane/services/device-control-core/test/infrastructure-management.test.mjs @@ -150,7 +150,7 @@ test("normalizes only a pinned Core-initiated public Edge channel", () => { deploymentRef: "deployment:device-edge/moscow-1", lifecycleState: "active", channel: { - endpoint: "https://155.212.211.15:8443/", + endpoint: "https://155.212.211.15/", servername: "155.212.211.15", generationRef: "channel-generation:1", trustBundleRef: "edge-trust:moscow-edge", @@ -163,14 +163,15 @@ test("normalizes only a pinned Core-initiated public Edge channel", () => { }, }); - assert.equal(command.channel.endpoint, "https://155.212.211.15:8443/"); + assert.equal(command.channel.endpoint, "https://155.212.211.15/"); assert.equal(command.channel.lifecycleState, "active"); assert.equal(command.channel.certificateIdentities.length, 1); for (const endpoint of [ - "https://127.0.0.1:8443/", - "https://192.168.1.1:8443/", + "https://127.0.0.1/", + "https://192.168.1.1/", + "https://155.212.211.15:8443/", "https://155.212.211.15:9921/", - "http://155.212.211.15:8443/", + "http://155.212.211.15/", ]) { assert.throws( () => normalizeInfrastructureManagementCommand("edge.ensure", { diff --git a/device-plane/services/device-control-core/test/infrastructure-repository.test.mjs b/device-plane/services/device-control-core/test/infrastructure-repository.test.mjs index 8159d54..ba28534 100644 --- a/device-plane/services/device-control-core/test/infrastructure-repository.test.mjs +++ b/device-plane/services/device-control-core/test/infrastructure-repository.test.mjs @@ -198,7 +198,7 @@ test("lists only bounded active Edge channel registrations without secrets", asy step("from device_edges", { rows: [{ id: edgeId, - channel_endpoint: "https://155.212.211.15:8443/", + channel_endpoint: "https://155.212.211.15/", channel_servername: "155.212.211.15", channel_generation_ref: "channel-generation:1", channel_trust_bundle_ref: "edge-trust:moscow-edge", @@ -218,7 +218,7 @@ test("lists only bounded active Edge channel registrations without secrets", asy assert.deepEqual(registrations[0], { edgeRegistrationId: `edge:${edgeId}`, - endpoint: "https://155.212.211.15:8443/", + endpoint: "https://155.212.211.15/", servername: "155.212.211.15", channelGeneration: "channel-generation:1", trustBundleRef: "edge-trust:moscow-edge", diff --git a/device-plane/services/device-edge-channel/src/runtime.mjs b/device-plane/services/device-edge-channel/src/runtime.mjs index a75fef4..d5859f4 100644 --- a/device-plane/services/device-edge-channel/src/runtime.mjs +++ b/device-plane/services/device-edge-channel/src/runtime.mjs @@ -412,7 +412,7 @@ function normalizeConfig(options) { channelGeneration, trust, host: normalizeHost(options.host ?? "127.0.0.1"), - port: normalizePort(options.port ?? 8443), + port: normalizePort(options.port ?? 443), keepaliveMs, deadPeerMs, acceptanceTimeoutMs: normalizeDuration( diff --git a/device-plane/services/device-edge-channel/src/server.mjs b/device-plane/services/device-edge-channel/src/server.mjs index 555ff3e..c02882e 100644 --- a/device-plane/services/device-edge-channel/src/server.mjs +++ b/device-plane/services/device-edge-channel/src/server.mjs @@ -75,7 +75,7 @@ export async function readRuntimeConfiguration(environment = {}) { 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), + port: normalizePort(environment.DEVICE_EDGE_CHANNEL_PORT, 443), tls: Object.freeze({ key, cert, diff --git a/device-plane/vps/config/nftables-core-channel.conf b/device-plane/vps/config/nftables-core-channel.conf index 40b45f5..070ae9c 100644 --- a/device-plane/vps/config/nftables-core-channel.conf +++ b/device-plane/vps/config/nftables-core-channel.conf @@ -13,7 +13,7 @@ table inet nodedc_b2_vps { 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 + tcp dport 443 ct state new limit rate 120/minute burst 120 packets accept } chain forward { diff --git a/device-plane/vps/systemd/nodedc-device-edge-channel.service b/device-plane/vps/systemd/nodedc-device-edge-channel.service index a76f5fb..8f73a48 100644 --- a/device-plane/vps/systemd/nodedc-device-edge-channel.service +++ b/device-plane/vps/systemd/nodedc-device-edge-channel.service @@ -9,7 +9,7 @@ User=nodedc-channel Group=nodedc-channel ExecStart=/opt/nodedc-b2-vps/runtime/node/bin/node /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_PORT=443 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 @@ -37,8 +37,8 @@ LockPersonality=yes MemoryDenyWriteExecute=no SystemCallArchitectures=native RestrictAddressFamilies=AF_INET AF_INET6 -CapabilityBoundingSet= -AmbientCapabilities= +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE UMask=0077 MemoryMax=128M MemorySwapMax=0 diff --git a/infra/deploy-runner/build-device-edge-vps-artifact.mjs b/infra/deploy-runner/build-device-edge-vps-artifact.mjs index 156cacb..ec52355 100644 --- a/infra/deploy-runner/build-device-edge-vps-artifact.mjs +++ b/infra/deploy-runner/build-device-edge-vps-artifact.mjs @@ -152,7 +152,7 @@ try { publicIngress: phase === "relay" ? "tcp/9921" : phase === "core-channel" - ? "tcp/8443-mtls-only" + ? "tcp/443-mtls-only" : "disabled", commandTransport: "disabled", gelios: "untouched", @@ -264,7 +264,9 @@ async function assertBoundary() { "User=nodedc-channel", "ExecStart=/opt/nodedc-b2-vps/runtime/node/bin/node /opt/nodedc-b2-vps/services/device-edge-channel/src/server.mjs", "MemoryDenyWriteExecute=no", - "tcp dport 8443", + "CapabilityBoundingSet=CAP_NET_BIND_SERVICE", + "AmbientCapabilities=CAP_NET_BIND_SERVICE", + "tcp dport 443", "MemoryMax=128M", "MemorySwapMax=0", "CPUQuota=50%", diff --git a/infra/deploy-runner/nodedc-b2-vps-deploy b/infra/deploy-runner/nodedc-b2-vps-deploy index a47ac15..8cbe4ea 100755 --- a/infra/deploy-runner/nodedc-b2-vps-deploy +++ b/infra/deploy-runner/nodedc-b2-vps-deploy @@ -101,7 +101,7 @@ 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 +CHANNEL_PUBLIC_PORT = 443 FOUNDATION_ENTRIES = ( "vps/config/00-nodedc-b2-vps.conf", @@ -196,15 +196,15 @@ PHASE_FILE_SHA256 = { "services/device-edge-channel/package.json": "bdf502be43b62bdd6db05b022a532d93ba954277ac5143d6058d2f27f6a2e9d2", "services/device-edge-channel/src/runtime.mjs": - "cbb07f7e644a68e9c8c36c1c2ecf0224c06c46c08c49339d62637b10a1495501", + "74f8f8dbaf144fed8443e83af1ae116f1465e5378f839e92d8b25f55655893ec", "services/device-edge-channel/src/server.mjs": - "ea891634a18efb9eb44f17b56c95ba97527215c4d0a6147cc2b7bad1d7356e36", + "a82057218bb368ab926404f90a19cc17c0359b57dc890f38a1324ab8c497c17b", "vps/config/nftables-core-channel.conf": - "1a04a5450042e80b8a20da3c6634dd6bc68693f191a463ba9a62984279d81d0c", + "075b47ad2972161cac4d91cf77afb82eb078067ef50a5f9a933a13159af8bf48", "vps/systemd/nodedc-device-edge-channel.service": - "afb2842db18d1863b2a089b4e89693f1ec17c78921457774ac9652f26b804856", + "a677598a1d1fa66cdc577ef8bb862756abd7725e62b662b2106f01780f59a767", "deployment/device-edge-vps-core-channel-v1.json": - "938f6f7959b78e6be3a6e91a54dca2922fbd813f1a33dca5cebe0dc256a83a14", + "ecbd66404d197f6d939736bbc1c28cc1e7091781639ffb78d2199a8db2b3ccd2", }, } @@ -559,7 +559,7 @@ def assert_runtime_reconciliation_predecessor(foundation_record): if ( "policy drop" not in nft or "tcp dport 22" not in nft - or "tcp dport 8443" in nft + or "tcp dport 443" in nft or "tcp dport 9921" in nft ): die("runtime reconciliation firewall predecessor mismatch") @@ -1322,7 +1322,7 @@ def validate_core_channel_runtime(): 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 443" not in nft or "tcp dport 9921" in nft ): die("VPS Core channel firewall contract mismatch") @@ -1330,12 +1330,14 @@ def validate_core_channel_runtime(): "/usr/bin/systemctl", "show", "nodedc-device-edge-channel.service", - "--property=User,Group,NoNewPrivileges,MemoryMax,MemorySwapMax,CPUQuotaPerSecUSec,TasksMax,LimitNOFILE", + "--property=User,Group,NoNewPrivileges,CapabilityBoundingSet,AmbientCapabilities,MemoryMax,MemorySwapMax,CPUQuotaPerSecUSec,TasksMax,LimitNOFILE", ]).stdout for required in ( "User=nodedc-channel", "Group=nodedc-channel", "NoNewPrivileges=yes", + "CapabilityBoundingSet=cap_net_bind_service", + "AmbientCapabilities=cap_net_bind_service", "MemoryMax=134217728", "MemorySwapMax=0", "TasksMax=64", @@ -1498,7 +1500,7 @@ def plan_artifact(artifact_argument: str): 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("public_core_channel=155.212.211.15:443/tcp:tls13-mtls-h2") print(f"health=127.0.0.1:{CHANNEL_HEALTH_PORT}") print("public_b2_ingress=disabled") print("tracker_tcp_9921=closed") diff --git a/infra/deploy-runner/test_device_edge_vps_artifact.py b/infra/deploy-runner/test_device_edge_vps_artifact.py index 9f5cb6d..a493427 100644 --- a/infra/deploy-runner/test_device_edge_vps_artifact.py +++ b/infra/deploy-runner/test_device_edge_vps_artifact.py @@ -261,7 +261,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase): rendered, ) self.assertIn( - "public_core_channel=155.212.211.15:8443/tcp:tls13-mtls-h2", + "public_core_channel=155.212.211.15:443/tcp:tls13-mtls-h2", rendered, ) self.assertIn("tracker_tcp_9921=closed", rendered) @@ -402,7 +402,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase): 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.assertIn("tcp dport 443", channel) self.assertNotIn("tcp dport 9921", channel) self.assertIn("PasswordAuthentication no", sshd) self.assertIn("AllowTcpForwarding no", sshd) @@ -424,6 +424,14 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase): channel_unit, ) self.assertIn("MemoryDenyWriteExecute=no", channel_unit) + self.assertIn( + "CapabilityBoundingSet=CAP_NET_BIND_SERVICE", + channel_unit, + ) + self.assertIn( + "AmbientCapabilities=CAP_NET_BIND_SERVICE", + channel_unit, + ) self.assertNotIn("--jitless", channel_unit) self.assertIn("MemoryMax=128M", channel_unit) self.assertIn("MemorySwapMax=0", channel_unit)