mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-25 01:37:05 +00:00
fix(mesh): trust central for cross-node dial auth and regenerate overrides at boot (#1014)
Two bugs in the same Phase D follow-up surface, fixed together because they both block declaring B-verify complete on the production fleet. Cross-node mesh dials returned `denied` at the agent. The pilot's `tcp_open` handler in `agent.ts::resolveMeshTarget` consulted the local SQLite `mesh_stacks` table, which is no longer written to under the post-Phase D control plane (state lives only on central). Drop the check. The pilot tunnel JWT (scope `pilot_tunnel`, signed with central's `auth_jwt_secret`) authenticates the caller; the same trust model already applies to filesystem ops, exec, and container control over the same tunnel. Threat-model trade-off: a leaked `pilot_tunnel` JWT or compromised central can now dial any compose-managed service on the pilot. Containers without `com.docker.compose.project` + `com.docker.compose.service` labels remain unreachable via this path. `MeshService.start()` did not regenerate compose override files at boot. After a Sencho restart with missing overrides on disk, meshed user containers had no `extra_hosts` / `networks: [sencho_mesh]` injection until each stack was opted out and back in. Add `regenerateAllOverrides()` that walks every `mesh_stacks` row and re-pushes via `pushOverrideToNode`. Best-effort: per-stack failures log to the mesh activity buffer; `MeshService.start()` is fire-and-forget at startup so a slow remote node does not delay boot. Tests: - `pilot-agent-mesh-resolve.test.ts` (new): mocks dockerode and proves `resolveMeshTarget` no longer returns `denied` with an empty `mesh_stacks` table. - `mesh-service.test.ts`: three new cases for `regenerateAllOverrides` - fan-out across the fleet, skip when `senchoIp` is null, log per-stack warning on push failure without throwing.
This commit is contained in:
@@ -194,6 +194,7 @@ export class MeshService extends EventEmitter implements MeshForwarderHost {
|
||||
await this.setupMeshNetwork();
|
||||
await this.refreshAliasCache();
|
||||
await this.syncForwarderListeners();
|
||||
await this.regenerateAllOverrides();
|
||||
this.aliasRefreshTimer = setInterval(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
@@ -646,6 +647,38 @@ export class MeshService extends EventEmitter implements MeshForwarderHost {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk every `mesh_stacks` row across the fleet and re-push each override
|
||||
* to its owning node. Called once at boot so on-disk override files
|
||||
* survive a Sencho restart even if they were lost (image rebuild, volume
|
||||
* reset, manual cleanup). Best-effort: failures are logged per-stack and
|
||||
* other nodes still get regenerated. An offline remote node leaves stale
|
||||
* overrides until the next opt-in / opt-out on that node.
|
||||
*/
|
||||
private async regenerateAllOverrides(): Promise<void> {
|
||||
if (!this.senchoIp) return;
|
||||
const db = DatabaseService.getInstance();
|
||||
const stacks = db.listMeshStacks();
|
||||
await Promise.allSettled(
|
||||
stacks.map(async (s) => {
|
||||
try {
|
||||
await this.pushOverrideToNode(s.node_id, s.stack_name);
|
||||
} catch (err) {
|
||||
this.logActivity({
|
||||
source: 'mesh', level: 'warn', type: 'forwarder.error',
|
||||
nodeId: s.node_id,
|
||||
message: `boot override regen failed for ${s.stack_name}: ${sanitizeForLog((err as Error).message)}`,
|
||||
details: { stackName: s.stack_name },
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
this.logActivity({
|
||||
source: 'mesh', level: 'info', type: 'mesh.enable',
|
||||
message: `boot regenerated ${stacks.length} override(s)`,
|
||||
});
|
||||
}
|
||||
|
||||
// --- Alias aggregation ---
|
||||
|
||||
public async refreshAliasCache(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user