feat(device-edge): standardize Core channel on mTLS 443

This commit is contained in:
Codex
2026-08-12 01:43:04 +03:00
parent 2eb6116880
commit c2d06bddc7
15 changed files with 51 additions and 37 deletions
@@ -6,7 +6,7 @@
"transport": { "transport": {
"protocol": "http2-bidirectional-stream", "protocol": "http2-bidirectional-stream",
"tls": "TLSv1.3-mutual-authentication", "tls": "TLSv1.3-mutual-authentication",
"edgeListen": "0.0.0.0:8443", "edgeListen": "0.0.0.0:443",
"endpointSource": "device-control-core.edge-registration", "endpointSource": "device-control-core.edge-registration",
"browserAccess": "forbidden", "browserAccess": "forbidden",
"bearerOnlyAuthentication": "forbidden", "bearerOnlyAuthentication": "forbidden",
@@ -30,7 +30,7 @@
"allowedEdgeListeners": [ "allowedEdgeListeners": [
"management-ssh", "management-ssh",
"raw-device-tcp/9921", "raw-device-tcp/9921",
"core-channel-mtls/8443" "core-channel-mtls/443"
] ]
}, },
"messageContract": { "messageContract": {
@@ -9,7 +9,7 @@
"runtimeUser": "nodedc-channel", "runtimeUser": "nodedc-channel",
"runtimeService": "nodedc-device-edge-channel.service", "runtimeService": "nodedc-device-edge-channel.service",
"runtime": "accepted-node-v22.23.2-no-docker", "runtime": "accepted-node-v22.23.2-no-docker",
"publicIngress": "tcp/8443-mtls-only", "publicIngress": "tcp/443-mtls-only",
"health": "127.0.0.1:18222", "health": "127.0.0.1:18222",
"trackerIngress": "disabled", "trackerIngress": "disabled",
"rawDeviceTcp9921": "closed", "rawDeviceTcp9921": "closed",
@@ -18,6 +18,7 @@
"privateKeyBoundary": "runner-managed-host-local-only", "privateKeyBoundary": "runner-managed-host-local-only",
"peerTrustPrerequisite": "exact-pinned-self-signed-core-certificate-and-fingerprint", "peerTrustPrerequisite": "exact-pinned-self-signed-core-certificate-and-fingerprint",
"tls": "TLSv1.3+h2+mutual-authentication", "tls": "TLSv1.3+h2+mutual-authentication",
"networkPrivilege": "CAP_NET_BIND_SERVICE-only-for-non-root-tcp-443",
"resourceCeilings": { "resourceCeilings": {
"memory": "128M", "memory": "128M",
"swap": "0", "swap": "0",
@@ -45,10 +46,10 @@
"edge-server-and-core-client-self-signed-identities-mutually-pinned", "edge-server-and-core-client-self-signed-identities-mutually-pinned",
"core-initiated-channel-accepted", "core-initiated-channel-accepted",
"unknown-core-certificate-rejected", "unknown-core-certificate-rejected",
"public-8443-only-beside-management-ssh", "public-443-only-beside-management-ssh",
"tracker-tcp-9921-closed", "tracker-tcp-9921-closed",
"loopback-health-contract", "loopback-health-contract",
"resource-ceilings-present" "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"
} }
@@ -21,7 +21,7 @@ HTTP/2 bidirectional session:
ARUSNAVI B2 ARUSNAVI B2
-> raw TCP/9921 -> raw TCP/9921
-> Device Gateway Edge on the VPS -> 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 Gateway Core on Synology
-> Device Control Core -> Device Control Core
``` ```
@@ -220,7 +220,7 @@ The target Edge exposes only:
- management SSH according to the separately accepted management policy; - management SSH according to the separately accepted management policy;
- raw tracker ingress TCP/9921; - 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 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 route, exit-node capability, Tailscale SSH, Docker socket or generic proxy. The
@@ -266,7 +266,7 @@ function normalizeRegistration(value) {
|| endpoint.pathname !== "/" || endpoint.pathname !== "/"
|| endpoint.search || endpoint.search
|| endpoint.hash || endpoint.hash
|| endpoint.port !== "8443" || endpoint.port !== ""
|| endpoint.hostname !== String(value.servername || "").toLowerCase() || endpoint.hostname !== String(value.servername || "").toLowerCase()
|| !isPublicIpv4(endpoint.hostname) || !isPublicIpv4(endpoint.hostname)
) { ) {
@@ -418,7 +418,7 @@ function normalizeEdgeEndpoint(value) {
|| endpoint.pathname !== "/" || endpoint.pathname !== "/"
|| endpoint.search || endpoint.search
|| endpoint.hash || endpoint.hash
|| endpoint.port !== "8443" || endpoint.port !== ""
|| !isPublicIpv4(endpoint.hostname) || !isPublicIpv4(endpoint.hostname)
) { ) {
throw new TypeError("device_edge_channel_endpoint_invalid"); throw new TypeError("device_edge_channel_endpoint_invalid");
@@ -101,7 +101,7 @@ test("supervisor keeps a failed trust enrollment isolated from other Edges", asy
function registration(channelGeneration, edgeRegistrationId = "edge:pilot") { function registration(channelGeneration, edgeRegistrationId = "edge:pilot") {
return { return {
edgeRegistrationId, edgeRegistrationId,
endpoint: "https://155.212.211.15:8443/", endpoint: "https://155.212.211.15/",
servername: "155.212.211.15", servername: "155.212.211.15",
channelGeneration, channelGeneration,
trustBundleRef: "edge-trust:moscow-edge", trustBundleRef: "edge-trust:moscow-edge",
@@ -150,7 +150,7 @@ test("normalizes only a pinned Core-initiated public Edge channel", () => {
deploymentRef: "deployment:device-edge/moscow-1", deploymentRef: "deployment:device-edge/moscow-1",
lifecycleState: "active", lifecycleState: "active",
channel: { channel: {
endpoint: "https://155.212.211.15:8443/", endpoint: "https://155.212.211.15/",
servername: "155.212.211.15", servername: "155.212.211.15",
generationRef: "channel-generation:1", generationRef: "channel-generation:1",
trustBundleRef: "edge-trust:moscow-edge", 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.lifecycleState, "active");
assert.equal(command.channel.certificateIdentities.length, 1); assert.equal(command.channel.certificateIdentities.length, 1);
for (const endpoint of [ for (const endpoint of [
"https://127.0.0.1:8443/", "https://127.0.0.1/",
"https://192.168.1.1:8443/", "https://192.168.1.1/",
"https://155.212.211.15:8443/",
"https://155.212.211.15:9921/", "https://155.212.211.15:9921/",
"http://155.212.211.15:8443/", "http://155.212.211.15/",
]) { ]) {
assert.throws( assert.throws(
() => normalizeInfrastructureManagementCommand("edge.ensure", { () => normalizeInfrastructureManagementCommand("edge.ensure", {
@@ -198,7 +198,7 @@ test("lists only bounded active Edge channel registrations without secrets", asy
step("from device_edges", { step("from device_edges", {
rows: [{ rows: [{
id: edgeId, id: edgeId,
channel_endpoint: "https://155.212.211.15:8443/", channel_endpoint: "https://155.212.211.15/",
channel_servername: "155.212.211.15", channel_servername: "155.212.211.15",
channel_generation_ref: "channel-generation:1", channel_generation_ref: "channel-generation:1",
channel_trust_bundle_ref: "edge-trust:moscow-edge", 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], { assert.deepEqual(registrations[0], {
edgeRegistrationId: `edge:${edgeId}`, edgeRegistrationId: `edge:${edgeId}`,
endpoint: "https://155.212.211.15:8443/", endpoint: "https://155.212.211.15/",
servername: "155.212.211.15", servername: "155.212.211.15",
channelGeneration: "channel-generation:1", channelGeneration: "channel-generation:1",
trustBundleRef: "edge-trust:moscow-edge", trustBundleRef: "edge-trust:moscow-edge",
@@ -412,7 +412,7 @@ function normalizeConfig(options) {
channelGeneration, channelGeneration,
trust, trust,
host: normalizeHost(options.host ?? "127.0.0.1"), host: normalizeHost(options.host ?? "127.0.0.1"),
port: normalizePort(options.port ?? 8443), port: normalizePort(options.port ?? 443),
keepaliveMs, keepaliveMs,
deadPeerMs, deadPeerMs,
acceptanceTimeoutMs: normalizeDuration( acceptanceTimeoutMs: normalizeDuration(
@@ -75,7 +75,7 @@ export async function readRuntimeConfiguration(environment = {}) {
channelGeneration: config.channelGeneration, channelGeneration: config.channelGeneration,
trustGeneration: config.trustGeneration, trustGeneration: config.trustGeneration,
host: normalizeHost(environment.DEVICE_EDGE_CHANNEL_HOST ?? "0.0.0.0"), 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({ tls: Object.freeze({
key, key,
cert, cert,
@@ -13,7 +13,7 @@ table inet nodedc_b2_vps {
ip6 nexthdr ipv6-icmp accept ip6 nexthdr ipv6-icmp accept
tcp dport 22 ct state new limit rate 30/minute burst 60 packets 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 { chain forward {
@@ -9,7 +9,7 @@ User=nodedc-channel
Group=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 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_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_HOST=127.0.0.1
Environment=DEVICE_EDGE_CHANNEL_HEALTH_PORT=18222 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_CONFIG_FILE=/var/lib/nodedc-b2-vps/channel-trust/runtime.json
@@ -37,8 +37,8 @@ LockPersonality=yes
MemoryDenyWriteExecute=no MemoryDenyWriteExecute=no
SystemCallArchitectures=native SystemCallArchitectures=native
RestrictAddressFamilies=AF_INET AF_INET6 RestrictAddressFamilies=AF_INET AF_INET6
CapabilityBoundingSet= CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities= AmbientCapabilities=CAP_NET_BIND_SERVICE
UMask=0077 UMask=0077
MemoryMax=128M MemoryMax=128M
MemorySwapMax=0 MemorySwapMax=0
@@ -152,7 +152,7 @@ try {
publicIngress: phase === "relay" publicIngress: phase === "relay"
? "tcp/9921" ? "tcp/9921"
: phase === "core-channel" : phase === "core-channel"
? "tcp/8443-mtls-only" ? "tcp/443-mtls-only"
: "disabled", : "disabled",
commandTransport: "disabled", commandTransport: "disabled",
gelios: "untouched", gelios: "untouched",
@@ -264,7 +264,9 @@ async function assertBoundary() {
"User=nodedc-channel", "User=nodedc-channel",
"ExecStart=/opt/nodedc-b2-vps/runtime/node/bin/node /opt/nodedc-b2-vps/services/device-edge-channel/src/server.mjs", "ExecStart=/opt/nodedc-b2-vps/runtime/node/bin/node /opt/nodedc-b2-vps/services/device-edge-channel/src/server.mjs",
"MemoryDenyWriteExecute=no", "MemoryDenyWriteExecute=no",
"tcp dport 8443", "CapabilityBoundingSet=CAP_NET_BIND_SERVICE",
"AmbientCapabilities=CAP_NET_BIND_SERVICE",
"tcp dport 443",
"MemoryMax=128M", "MemoryMax=128M",
"MemorySwapMax=0", "MemorySwapMax=0",
"CPUQuota=50%", "CPUQuota=50%",
+12 -10
View File
@@ -101,7 +101,7 @@ CHANNEL_CERTIFICATE = CHANNEL_TRUST_ROOT / "edge-certificate.pem"
CHANNEL_CORE_CERTIFICATE = CHANNEL_TRUST_ROOT / "core-certificate.pem" CHANNEL_CORE_CERTIFICATE = CHANNEL_TRUST_ROOT / "core-certificate.pem"
CHANNEL_RUNTIME_CONFIG = CHANNEL_TRUST_ROOT / "runtime.json" CHANNEL_RUNTIME_CONFIG = CHANNEL_TRUST_ROOT / "runtime.json"
CHANNEL_HEALTH_PORT = 18222 CHANNEL_HEALTH_PORT = 18222
CHANNEL_PUBLIC_PORT = 8443 CHANNEL_PUBLIC_PORT = 443
FOUNDATION_ENTRIES = ( FOUNDATION_ENTRIES = (
"vps/config/00-nodedc-b2-vps.conf", "vps/config/00-nodedc-b2-vps.conf",
@@ -196,15 +196,15 @@ PHASE_FILE_SHA256 = {
"services/device-edge-channel/package.json": "services/device-edge-channel/package.json":
"bdf502be43b62bdd6db05b022a532d93ba954277ac5143d6058d2f27f6a2e9d2", "bdf502be43b62bdd6db05b022a532d93ba954277ac5143d6058d2f27f6a2e9d2",
"services/device-edge-channel/src/runtime.mjs": "services/device-edge-channel/src/runtime.mjs":
"cbb07f7e644a68e9c8c36c1c2ecf0224c06c46c08c49339d62637b10a1495501", "74f8f8dbaf144fed8443e83af1ae116f1465e5378f839e92d8b25f55655893ec",
"services/device-edge-channel/src/server.mjs": "services/device-edge-channel/src/server.mjs":
"ea891634a18efb9eb44f17b56c95ba97527215c4d0a6147cc2b7bad1d7356e36", "a82057218bb368ab926404f90a19cc17c0359b57dc890f38a1324ab8c497c17b",
"vps/config/nftables-core-channel.conf": "vps/config/nftables-core-channel.conf":
"1a04a5450042e80b8a20da3c6634dd6bc68693f191a463ba9a62984279d81d0c", "075b47ad2972161cac4d91cf77afb82eb078067ef50a5f9a933a13159af8bf48",
"vps/systemd/nodedc-device-edge-channel.service": "vps/systemd/nodedc-device-edge-channel.service":
"afb2842db18d1863b2a089b4e89693f1ec17c78921457774ac9652f26b804856", "a677598a1d1fa66cdc577ef8bb862756abd7725e62b662b2106f01780f59a767",
"deployment/device-edge-vps-core-channel-v1.json": "deployment/device-edge-vps-core-channel-v1.json":
"938f6f7959b78e6be3a6e91a54dca2922fbd813f1a33dca5cebe0dc256a83a14", "ecbd66404d197f6d939736bbc1c28cc1e7091781639ffb78d2199a8db2b3ccd2",
}, },
} }
@@ -559,7 +559,7 @@ def assert_runtime_reconciliation_predecessor(foundation_record):
if ( if (
"policy drop" not in nft "policy drop" not in nft
or "tcp dport 22" 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 or "tcp dport 9921" in nft
): ):
die("runtime reconciliation firewall predecessor mismatch") 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 nft = run(["/usr/sbin/nft", "list", "table", "inet", "nodedc_b2_vps"]).stdout
if ( if (
"policy drop" not in nft "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 or "tcp dport 9921" in nft
): ):
die("VPS Core channel firewall contract mismatch") die("VPS Core channel firewall contract mismatch")
@@ -1330,12 +1330,14 @@ def validate_core_channel_runtime():
"/usr/bin/systemctl", "/usr/bin/systemctl",
"show", "show",
"nodedc-device-edge-channel.service", "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 ]).stdout
for required in ( for required in (
"User=nodedc-channel", "User=nodedc-channel",
"Group=nodedc-channel", "Group=nodedc-channel",
"NoNewPrivileges=yes", "NoNewPrivileges=yes",
"CapabilityBoundingSet=cap_net_bind_service",
"AmbientCapabilities=cap_net_bind_service",
"MemoryMax=134217728", "MemoryMax=134217728",
"MemorySwapMax=0", "MemorySwapMax=0",
"TasksMax=64", "TasksMax=64",
@@ -1498,7 +1500,7 @@ def plan_artifact(artifact_argument: str):
print("services=nodedc-b2-relay") print("services=nodedc-b2-relay")
print(f"relay_runtime_identity={RELAY_USER}:no-credentials") print(f"relay_runtime_identity={RELAY_USER}:no-credentials")
else: 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(f"health=127.0.0.1:{CHANNEL_HEALTH_PORT}")
print("public_b2_ingress=disabled") print("public_b2_ingress=disabled")
print("tracker_tcp_9921=closed") print("tracker_tcp_9921=closed")
@@ -261,7 +261,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
rendered, rendered,
) )
self.assertIn( 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, rendered,
) )
self.assertIn("tracker_tcp_9921=closed", rendered) self.assertIn("tracker_tcp_9921=closed", rendered)
@@ -402,7 +402,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
self.assertIn("tcp dport 22", foundation) self.assertIn("tcp dport 22", foundation)
self.assertNotIn("tcp dport 9921", foundation) self.assertNotIn("tcp dport 9921", foundation)
self.assertIn("tcp dport 9921", relay) 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.assertNotIn("tcp dport 9921", channel)
self.assertIn("PasswordAuthentication no", sshd) self.assertIn("PasswordAuthentication no", sshd)
self.assertIn("AllowTcpForwarding no", sshd) self.assertIn("AllowTcpForwarding no", sshd)
@@ -424,6 +424,14 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
channel_unit, channel_unit,
) )
self.assertIn("MemoryDenyWriteExecute=no", 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.assertNotIn("--jitless", channel_unit)
self.assertIn("MemoryMax=128M", channel_unit) self.assertIn("MemoryMax=128M", channel_unit)
self.assertIn("MemorySwapMax=0", channel_unit) self.assertIn("MemorySwapMax=0", channel_unit)