feat(device-edge): retire superseded VPS tailnet

This commit is contained in:
Codex
2026-08-12 14:52:36 +03:00
parent e21c188f85
commit 6fd172ecc5
4 changed files with 260 additions and 4 deletions
@@ -0,0 +1,55 @@
{
"schemaVersion": "nodedc.device-edge-vps.tailscale-retirement.v1",
"mode": "retire-superseded-vps-tailnet-after-accepted-core-channel",
"status": "core-channel-only",
"authority": "DCPLATFORM-21/DCPLATFORM-76/ADR-0001",
"component": "device-edge-vps",
"phase": "tailscale-retirement",
"runtimeHost": "koffyvngij",
"predecessorPatch": "device-edge-vps-core-channel-20260812-010",
"predecessorArtifactSha256": "c8ef3c4bb45850cad32e881eba081bc4c891c2886e5500d02cb94616d82353f3",
"runtimeAction": "stop-disable-remove-userspace-tailscale-runtime-state-and-superseded-trust",
"publicIngress": "tcp/443-mtls-only",
"trackerIngress": "disabled",
"rawDeviceTcp9921": "closed",
"commandTransport": "disabled",
"gelios": "untouched",
"preserved": [
"management-ssh-key",
"accepted-node-runtime",
"accepted-core-channel-source-runtime-and-trust",
"core-channel-registration",
"foundation-source-for-audit-and-rollback",
"gelios-production-path"
],
"retired": [
"nodedc-b2-tailscaled.service",
"userspace-socks5-127.0.0.1:1055",
"tailscale-local-state",
"tailscale-runtime-binaries",
"superseded-backhaul-private-key"
],
"forbidden": [
"tailscale-runtime",
"tailnet-address",
"vps-initiated-synology-connection",
"generic-tcp-forwarding",
"tailscale-ssh-backhaul",
"docker",
"public-health",
"tracker-tcp/9921"
],
"acceptance": [
"exact-core-channel-010-predecessor",
"core-channel-remains-accepted",
"tailscale-service-absent-inactive-and-disabled",
"tailscale-userspace-listeners-absent",
"tailscale-local-state-and-runtime-binaries-absent",
"superseded-backhaul-private-key-absent",
"public-443-only-beside-management-ssh",
"tracker-tcp-9921-closed",
"command-transport-disabled"
],
"externalRevocation": "delete-exact-nodedc-b2-vps-machine-in-tailnet-after-deploy-ok",
"rollback": "before-external-tailnet-revocation-restore-backed-up-local-tailscale-runtime-state-unit-and-core-channel-predecessor"
}
@@ -35,11 +35,12 @@ if (
"backhaul",
"relay",
"core-channel",
"tailscale-retirement",
].includes(phase)
|| !/^[A-Za-z0-9._-]{1,96}$/.test(patchId || "")
) {
throw new Error(
"usage: build-device-edge-vps-artifact.mjs <foundation|runtime-reconciliation|backhaul|relay|core-channel> <patch-id>",
"usage: build-device-edge-vps-artifact.mjs <foundation|runtime-reconciliation|backhaul|relay|core-channel|tailscale-retirement> <patch-id>",
);
}
@@ -92,6 +93,9 @@ const entriesByPhase = {
"vps/systemd/nodedc-device-edge-channel.service",
"deployment/device-edge-vps-core-channel-v1.json",
],
"tailscale-retirement": [
"deployment/device-edge-vps-tailscale-retirement-v1.json",
],
};
const entries = entriesByPhase[phase];
const ignoredBasenames = new Set([".DS_Store", ".git", "node_modules"]);
@@ -151,7 +155,7 @@ try {
entries,
publicIngress: phase === "relay"
? "tcp/9921"
: phase === "core-channel"
: ["core-channel", "tailscale-retirement"].includes(phase)
? "tcp/443-mtls-only"
: "disabled",
commandTransport: "disabled",
@@ -289,6 +293,27 @@ async function assertBoundary() {
}
}
}
if (phase === "tailscale-retirement") {
for (const required of [
'"predecessorPatch": "device-edge-vps-core-channel-20260812-010"',
'"runtimeAction": "stop-disable-remove-userspace-tailscale-runtime-state-and-superseded-trust"',
'"trackerIngress": "disabled"',
'"externalRevocation": "delete-exact-nodedc-b2-vps-machine-in-tailnet-after-deploy-ok"',
]) {
if (!combined.includes(required)) {
throw new Error(`tailscale_retirement_boundary_missing:${required}`);
}
}
for (const forbidden of [
"tcp dport 9921",
"LocalForward",
"commandTransport\": \"enabled",
]) {
if (combined.includes(forbidden)) {
throw new Error(`tailscale_retirement_boundary_violation:${forbidden}`);
}
}
}
}
function canonicalTarScript() {
+128 -2
View File
@@ -102,6 +102,10 @@ CHANNEL_CORE_CERTIFICATE = CHANNEL_TRUST_ROOT / "core-certificate.pem"
CHANNEL_RUNTIME_CONFIG = CHANNEL_TRUST_ROOT / "runtime.json"
CHANNEL_HEALTH_PORT = 18222
CHANNEL_PUBLIC_PORT = 443
CORE_CHANNEL_ACCEPTED_PATCH = "device-edge-vps-core-channel-20260812-010"
CORE_CHANNEL_ACCEPTED_SHA256 = (
"c8ef3c4bb45850cad32e881eba081bc4c891c2886e5500d02cb94616d82353f3"
)
FOUNDATION_ENTRIES = (
"vps/config/00-nodedc-b2-vps.conf",
@@ -136,6 +140,9 @@ CORE_CHANNEL_ENTRIES = (
"vps/systemd/nodedc-device-edge-channel.service",
"deployment/device-edge-vps-core-channel-v1.json",
)
TAILSCALE_RETIREMENT_ENTRIES = (
"deployment/device-edge-vps-tailscale-retirement-v1.json",
)
PHASE_ENTRIES = {
"foundation": FOUNDATION_ENTRIES,
@@ -143,6 +150,7 @@ PHASE_ENTRIES = {
"backhaul": BACKHAUL_ENTRIES,
"relay": RELAY_ENTRIES,
"core-channel": CORE_CHANNEL_ENTRIES,
"tailscale-retirement": TAILSCALE_RETIREMENT_ENTRIES,
}
SUPERSEDED_TRANSPORT_PHASES = frozenset({"backhaul", "relay"})
@@ -206,6 +214,10 @@ PHASE_FILE_SHA256 = {
"deployment/device-edge-vps-core-channel-v1.json":
"ecbd66404d197f6d939736bbc1c28cc1e7091781639ffb78d2199a8db2b3ccd2",
},
"tailscale-retirement": {
"deployment/device-edge-vps-tailscale-retirement-v1.json":
"bbe11e8cf4103f44ae7888b4d3f3dde7015eeaef8ea5a7c4c00427a7b85f8e33",
},
}
# Exact immutable baselines from terminally accepted predecessor artifacts.
@@ -622,6 +634,18 @@ def current_phase_preflight(phase: str):
for port in (CHANNEL_HEALTH_PORT, CHANNEL_PUBLIC_PORT, 9921):
assert_port_closed(port)
return {"predecessor": "accepted-foundation-closed-channel"}
if phase == "tailscale-retirement":
core_record = applied_phase_record("core-channel")
if (
core_record.get("patch") != CORE_CHANNEL_ACCEPTED_PATCH
or core_record.get("sha256") != CORE_CHANNEL_ACCEPTED_SHA256
):
die("VPS Tailscale retirement Core channel predecessor mismatch")
source_file_state("core-channel")
validate_core_channel_runtime()
if (LIVE_ROOT / TAILSCALE_RETIREMENT_ENTRIES[0]).exists():
die("VPS Tailscale retirement target path already exists")
return {"predecessor": "accepted-core-channel-010-with-live-tailnet"}
if phase == "backhaul":
for tool in (Path("/usr/bin/ssh"), Path("/usr/bin/nc")):
assert_executable_command_path(
@@ -693,6 +717,13 @@ def backup_targets_for_phase(phase: str):
return common + [BACKHAUL_UNIT, BACKHAUL_KNOWN_HOSTS]
if phase == "core-channel":
return common + [CHANNEL_UNIT, NFTABLES_CONFIG, CHANNEL_TRUST_ROOT]
if phase == "tailscale-retirement":
return common + [
TAILSCALE_UNIT,
TAILSCALE_STATE.parent,
TAILSCALE_BIN.parent,
TRUST_ROOT,
]
return common + [RELAY_UNIT, NFTABLES_CONFIG]
@@ -1141,6 +1172,28 @@ def apply_core_channel(payload: Path):
validate_core_channel_runtime()
def apply_tailscale_retirement(_payload: Path):
# Remote machine deletion is intentionally post-deploy: doing a logout
# inside apply would invalidate the exact local state needed by automatic
# rollback. The accepted local boundary is nevertheless closed completely.
systemctl("disable", "--now", "nodedc-b2-tailscaled.service", check=False)
for path in (
TAILSCALE_UNIT,
TAILSCALE_STATE.parent,
TAILSCALE_BIN.parent,
TRUST_ROOT,
):
if path.is_symlink():
die(f"VPS Tailscale retirement target symlink rejected: {path}")
if path.is_dir():
shutil.rmtree(path)
elif path.exists():
path.unlink()
systemctl("daemon-reload")
systemctl("reset-failed", "nodedc-b2-tailscaled.service", check=False)
validate_tailscale_retirement_runtime()
def sshd_effective():
return run(["/usr/sbin/sshd", "-T"]).stdout.lower()
@@ -1348,6 +1401,67 @@ def validate_core_channel_runtime():
return health
def validate_tailscale_retirement_runtime():
source_file_state("foundation")
source_file_state("core-channel")
source_file_state("tailscale-retirement")
core_record = applied_phase_record("core-channel")
if (
core_record.get("patch") != CORE_CHANNEL_ACCEPTED_PATCH
or core_record.get("sha256") != CORE_CHANNEL_ACCEPTED_SHA256
):
die("retired Tailscale runtime Core channel identity mismatch")
if run([str(NODE_BIN), "--version"]).stdout.strip() != f"v{NODE_VERSION}":
die("retired Tailscale runtime Node version mismatch")
if service_active("nodedc-b2-tailscaled.service"):
die("retired Tailscale service remains active")
if systemctl("is-enabled", "nodedc-b2-tailscaled.service", check=False).returncode == 0:
die("retired Tailscale service remains enabled")
for path in (
TAILSCALE_UNIT,
TAILSCALE_SOCKET,
TAILSCALE_STATE.parent,
TAILSCALE_BIN.parent,
TRUST_ROOT,
):
if path.exists() or path.is_symlink():
die(f"retired Tailscale boundary remains present: {path}")
if run(["/usr/bin/pgrep", "-x", "tailscaled"], check=False).returncode == 0:
die("retired Tailscale process remains running")
assert_management_key()
if not port_is_open(PUBLIC_IPV4, 22, timeout=5):
die("management SSH became unavailable after Tailscale retirement")
if port_is_open("127.0.0.1", 1055):
die("retired Tailscale SOCKS listener remains open")
source_file_state("core-channel")
assert_channel_trust(require_runtime_owner=True)
if not service_active("nodedc-device-edge-channel.service"):
die("Core channel became inactive after Tailscale retirement")
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"retired Tailscale Core channel contract mismatch: {key}")
if not port_is_open(PUBLIC_IPV4, CHANNEL_PUBLIC_PORT, timeout=5):
die("Core channel listener became unavailable after Tailscale retirement")
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 22" not in nft
or "tcp dport 443" not in nft
or "tcp dport 9921" in nft
):
die("retired Tailscale firewall contract mismatch")
return health
def validate_relay_runtime():
validate_backhaul_runtime()
source_file_state("relay")
@@ -1499,7 +1613,7 @@ def plan_artifact(artifact_argument: str):
print("source_admission=public-ipv4-only")
print("services=nodedc-b2-relay")
print(f"relay_runtime_identity={RELAY_USER}:no-credentials")
else:
elif phase == "core-channel":
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")
@@ -1507,6 +1621,16 @@ def plan_artifact(artifact_argument: str):
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")
else:
print("public_core_channel=preserved:155.212.211.15:443/tcp:tls13-mtls-h2")
print(f"health=preserved:127.0.0.1:{CHANNEL_HEALTH_PORT}")
print("tailscale=stop+disable+destroy-local-runtime-state")
print("tailscale_socks_1055=removed")
print("superseded_backhaul_private_key=removed")
print("public_b2_ingress=disabled")
print("tracker_tcp_9921=closed")
print("services=preserved:nodedc-device-edge-channel")
print("external_tailnet_machine_cleanup=required-after-deploy-ok")
print("command_transport=disabled")
print("gelios=untouched")
print("dns=unchanged")
@@ -1540,8 +1664,10 @@ def apply_artifact(artifact_argument: str):
apply_backhaul(loaded["payload"])
elif loaded["phase"] == "relay":
apply_relay(loaded["payload"])
else:
elif loaded["phase"] == "core-channel":
apply_core_channel(loaded["payload"])
else:
apply_tailscale_retirement(loaded["payload"])
archived = archive_artifact(loaded["artifact"], APPLIED_ROOT)
record = {
@@ -98,6 +98,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
"backhaul",
"relay",
"core-channel",
"tailscale-retirement",
):
with self.subTest(phase=phase), tempfile.TemporaryDirectory(
prefix=f"nodedc-vps-{phase}-"
@@ -176,6 +177,7 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
"backhaul",
"relay",
"core-channel",
"tailscale-retirement",
):
result = self.build(
inbox,
@@ -311,6 +313,54 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase):
self.assertIn("public_core_channel=disabled", rendered)
self.assertIn("tracker_tcp_9921=closed", rendered)
def test_tailscale_retirement_plan_preserves_channel_and_opens_no_tracker_port(self):
with tempfile.TemporaryDirectory(prefix="nodedc-vps-retirement-plan-") as directory:
inbox = Path(directory) / "inbox"
inbox.mkdir()
result = self.build(
inbox,
"tailscale-retirement",
"device-edge-vps-tailscale-retirement-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-core-channel-010-with-live-tailnet",
},
), 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=tailscale-retirement", rendered)
self.assertIn(
"predecessor=accepted-core-channel-010-with-live-tailnet",
rendered,
)
self.assertIn(
"public_core_channel=preserved:155.212.211.15:443/tcp:tls13-mtls-h2",
rendered,
)
self.assertIn(
"tailscale=stop+disable+destroy-local-runtime-state",
rendered,
)
self.assertIn("tailscale_socks_1055=removed", rendered)
self.assertIn("superseded_backhaul_private_key=removed", rendered)
self.assertIn("tracker_tcp_9921=closed", rendered)
self.assertIn("external_tailnet_machine_cleanup=required-after-deploy-ok", rendered)
self.assertIn("command_transport=disabled", rendered)
self.assertIn("gelios=untouched", rendered)
def test_publish_payload_preserves_unselected_executable_modes(self):
with tempfile.TemporaryDirectory(prefix="nodedc-vps-publish-scope-") as directory:
root = Path(directory)