Skip to content

mTLS users signed out on remote connect when companion settings extension activates after Coder #973

@EhabY

Description

@EhabY

Symptoms

mTLS users whose coder.tls* and coder.headerCommand settings are written by a companion extension see, shortly after the remote workspace opens:

  1. "Session expired. You have been signed out." toast.
  2. Header Command setting changed. Reload window to apply.

The workspace itself opens successfully, then both messages appear.

Root cause

The Coder extension activates on onResolveRemoteAuthority:ssh-remote (earliest possible), so it reads the settings that the companion extension persisted on a previous session. setupCoderRemote runs cleanly and watchSettings (src/remote/remote.ts:459) arms with those values as baseline. The damage happens after that, when the companion extension activates and rewrites the same settings.

VS Code's WorkspaceConfiguration.update is per-key and emits a separate onDidChangeConfiguration event for each call. There is no atomic bulk write. So a companion that does:

await config.update('coder.tlsCertFile', newCert, Global);
await config.update('coder.tlsKeyFile',  newKey,  Global);
await config.update('coder.tlsCaFile',   newCa,   Global);
await config.update('coder.headerCommand', newCmd, Global);

leaves the merged config in a partially-updated state between each await, visible to all other extensions. Two consequences cascade:

  • Watcher fires legitimately. When coder.headerCommand changes value, configWatcher.ts:21-40 detects the drift and surfaces the reload prompt. The SSH ProxyCommand already on disk has --header-command "OLD_VALUE" baked in (remote.ts:359), so the prompt is technically correct, but for a companion that is just refreshing settings to the same effective value, it is spurious noise.
  • In-flight requests 401 on the transient state. The request interceptor (src/api/coderApi.ts:505-526) reads TLS settings and coder.headerCommand at request time. A request from Inbox, WorkspaceMonitor, agentMetadata, or workspace polling that lands between two of the companion's update calls sees a mismatched cert/key pair or a cleared coder.headerCommand, the server returns 401, and AuthInterceptor invokes handleAuthFailure (src/extension.ts:104-124), which suspends the session and shows the toast. There is no retry.

DeploymentManager.registerAuthListener (src/deployment/deploymentManager.ts:198-233) only listens to onDidChangeSessionAuth (token changes), not to settings, so once suspended the session stays suspended until the user clicks "Log In", even though the settings stabilise milliseconds later.

Proposed fix

# Change File
1 Single silent retry in AuthInterceptor.handleError when a 401 arrives shortly after a coder.tls* / coder.headerCommand change event. The failing request used pre-change settings; a retry picks up post-change settings. Gated by the existing _retryAttempted flag. src/api/authInterceptor.ts
2 In DeploymentManager, watch coder.tls* / coder.headerCommand. When !isAuthenticated() and a deployment is still set (suspended state), re-attempt setDeploymentIfValid on change, debounced. Restores the session automatically without user action. src/deployment/deploymentManager.ts
3 Coalesce watchSettings events within a short window (e.g. 250 ms). A companion rewriting four keys in sequence should not produce four prompts, and a value that flips empty then back to the same value should be a no-op. src/configWatcher.ts / src/remote/remote.ts:979-1004
4 Drop the needToken(...) half of the pre-check; keep only !context.baseUrl. Covers the separate first-connect-from-fresh case, where a companion that has never persisted settings has not run yet and the current check incorrectly demands a token from an mTLS user. src/remote/remote.ts:174-183

#1 is the load-bearing fix for the toast. #2 is the backstop that restores the session if #1 ever misses. #3 quiets the reload prompt when the companion settles back to the same effective value. #4 is independent of the customer's current symptom but is a real bug on first-connect-from-fresh.

Repro

  1. Coder deployment requiring mTLS.
  2. Companion extension that writes coder.tlsCertFile, coder.tlsKeyFile, coder.tlsCaFile, coder.headerCommand to user scope via sequential update calls, with an activation event later than onResolveRemoteAuthority:ssh-remote.
  3. Open a remote Coder workspace.

Expected: connection establishes silently.
Actual: workspace opens, then "Session expired" toast and "Header Command changed" reload prompt appear.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions