Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions apps/ingest/internal/handlers/terminal_ws.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,7 @@ func (h *TerminalWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
authMsg.Password = ""
if err != nil {
slog.Warn("terminal ws: SSH connection failed", "session_id", sessionID, "host_id", info.HostID, "username", info.Username, "err", err)
reason := "ssh authentication failed"
message := "SSH authentication failed"
reason, message := terminalSSHFailureDetails(err)
if errors.Is(err, queries.ErrSSHHostKeyNotTrusted) || errors.Is(err, queries.ErrSSHHostKeyMismatch) {
reason = "ssh host key verification failed"
message = "SSH host key verification failed"
Expand Down Expand Up @@ -375,6 +374,13 @@ func isSSHAuthenticationFailure(err error) bool {
return errors.As(err, &authErr)
}

func terminalSSHFailureDetails(err error) (reason string, message string) {
if isSSHAuthenticationFailure(err) {
return "ssh authentication failed", "SSH authentication failed"
}
return "ssh connection failed", "SSH connection failed"
}

func writeWS(ctx context.Context, conn *websocket.Conn, msg wsMessage) error {
raw, err := json.Marshal(msg)
if err != nil {
Expand Down
12 changes: 12 additions & 0 deletions apps/ingest/internal/handlers/terminal_ws_security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ func TestIsSSHAuthenticationFailure(t *testing.T) {
}
}

func TestTerminalSSHFailureDetails(t *testing.T) {
reason, message := terminalSSHFailureDetails(&ssh.ServerAuthError{})
if reason != "ssh authentication failed" || message != "SSH authentication failed" {
t.Fatalf("terminalSSHFailureDetails(auth) = %q, %q", reason, message)
}

reason, message = terminalSSHFailureDetails(errors.New("dial tcp: lookup host.example.test: no such host"))
if reason != "ssh connection failed" || message != "SSH connection failed" {
t.Fatalf("terminalSSHFailureDetails(network) = %q, %q", reason, message)
}
}

func TestInsecureSkipVerifyTrueAllowlist(t *testing.T) {
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
Expand Down
23 changes: 23 additions & 0 deletions apps/web/lib/actions/agents-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import {
revokedCertificates,
hosts,
hostDockerStatus,
dockerContainers,
dockerContainerLifecycleEvents,
dockerContainerMetrics,
dockerTelemetryBatches,
hostMetrics,
hostGroupMembers,
checks,
Expand Down Expand Up @@ -1231,6 +1235,25 @@ export async function deleteHost(
.delete(hostMetrics)
.where(and(eq(hostMetrics.hostId, hostId), eq(hostMetrics.instanceId, instanceId)))

// 12a. Docker inventory/telemetry rows reference both the host and, for
// per-container data, docker_containers rows. Delete child tables
// first so hosts with Docker telemetry can still be removed.
await tx
.delete(dockerContainerMetrics)
.where(and(eq(dockerContainerMetrics.hostId, hostId), eq(dockerContainerMetrics.instanceId, instanceId)))
await tx
.delete(dockerContainerLifecycleEvents)
.where(and(eq(dockerContainerLifecycleEvents.hostId, hostId), eq(dockerContainerLifecycleEvents.instanceId, instanceId)))
await tx
.delete(dockerContainers)
.where(and(eq(dockerContainers.hostId, hostId), eq(dockerContainers.instanceId, instanceId)))
await tx
.delete(dockerTelemetryBatches)
.where(and(eq(dockerTelemetryBatches.hostId, hostId), eq(dockerTelemetryBatches.instanceId, instanceId)))
await tx
.delete(hostDockerStatus)
.where(and(eq(hostDockerStatus.hostId, hostId), eq(hostDockerStatus.instanceId, instanceId)))

// 13. Resource tags
await tx
.delete(resourceTags)
Expand Down
14 changes: 14 additions & 0 deletions apps/web/lib/actions/agents-delete-hard.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,18 @@ test('deleteHost performs a hard delete and clears agent dependants before delet
pendingDelete < agentDelete,
'pending CSR rows must be deleted before the agent row to avoid FK rollback',
)

for (const table of [
'dockerContainerMetrics',
'dockerContainerLifecycleEvents',
'dockerContainers',
'dockerTelemetryBatches',
'hostDockerStatus',
]) {
assert.match(
segment,
new RegExp(`\\.delete\\(${table}\\)`),
`deleteHost must delete ${table} rows before deleting the host`,
)
}
})
Loading