Fleet P1: BYO Tailscale + network metrics, Tailscale SSH, exec hardening
Security hardening: - The gateway no longer sends arbitrary shell to nodes. The WSS exec op is replaced by a typed `verify` op the daemon runs itself (fixed host+docker check); future container ops are typed too. cm-api NodeHub.verify() + the daemon's handle_command only dispatches vetted ops. BYO Tailscale: - migrations/0019_workspace_tailscale.sql + cm-db fleet_tailscale repo (store the user's Tailscale API key + tailnet, server-side only). - cm-api routes/tailscale.rs: POST/GET/DELETE /api/fleet/tailscale + GET /api/fleet/tailscale/devices (proxies api.tailscale.com device list). - Daemon: --tailscale-authkey → `tailscale up --authkey … --ssh` (enables Tailscale SSH for keyless user access); else `tailscale set --ssh=true`. Reports its tailscale IP (already). UI: - Fleet overview gains a Tailscale section: connect (key+tailnet) + live tailnet device status (online/last-seen/IP/os). Node cards show a copyable Tailscale SSH target (ssh <ip>). Remaining: P2 — RemoteDriver + placement (run agents on nodes) and the in-UI remote terminal (PTY proxied over the WSS channel). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2bdd0a23e8
commit
7332d69f8a
@@ -0,0 +1,49 @@
|
||||
//! Per-workspace Tailscale connection (BYO tailnet): the API key + tailnet we
|
||||
//! use to read fleet network metrics. Used server-side only.
|
||||
|
||||
use cm_domain::WorkspaceId;
|
||||
use sqlx::{PgPool, Row};
|
||||
|
||||
use crate::DbError;
|
||||
|
||||
/// Store (or replace) a workspace's Tailscale API key + tailnet.
|
||||
pub async fn set(
|
||||
pool: &PgPool,
|
||||
workspace_id: WorkspaceId,
|
||||
api_key: &str,
|
||||
tailnet: &str,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query(
|
||||
"INSERT INTO workspace_tailscale (workspace_id, api_key, tailnet, connected_at)
|
||||
VALUES ($1, $2, $3, now())
|
||||
ON CONFLICT (workspace_id) DO UPDATE SET
|
||||
api_key = excluded.api_key, tailnet = excluded.tailnet, connected_at = now()",
|
||||
)
|
||||
.bind(workspace_id.as_uuid())
|
||||
.bind(api_key)
|
||||
.bind(tailnet)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a workspace's stored (api_key, tailnet), if connected.
|
||||
pub async fn get(
|
||||
pool: &PgPool,
|
||||
workspace_id: WorkspaceId,
|
||||
) -> Result<Option<(String, String)>, DbError> {
|
||||
let row = sqlx::query("SELECT api_key, tailnet FROM workspace_tailscale WHERE workspace_id = $1")
|
||||
.bind(workspace_id.as_uuid())
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(row.map(|r| (r.get("api_key"), r.get("tailnet"))))
|
||||
}
|
||||
|
||||
/// Disconnect a workspace's Tailscale.
|
||||
pub async fn delete(pool: &PgPool, workspace_id: WorkspaceId) -> Result<(), DbError> {
|
||||
sqlx::query("DELETE FROM workspace_tailscale WHERE workspace_id = $1")
|
||||
.bind(workspace_id.as_uuid())
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -6,6 +6,7 @@ pub mod companies;
|
||||
pub mod connections;
|
||||
pub mod credits;
|
||||
pub mod files;
|
||||
pub mod fleet_tailscale;
|
||||
pub mod messages;
|
||||
pub mod nodes;
|
||||
pub mod orgs;
|
||||
|
||||
Reference in New Issue
Block a user