feat: support --hostname-suffix flag on coder ssh (#17279)

Adds `hostname-suffix` flag to `coder ssh` command for use in SSH Config ProxyCommands.

Also enforces that Coder server doesn't start the suffix with a dot.

part of: #16828
This commit is contained in:
Spike Curtis
2025-04-07 21:33:33 +04:00
committed by GitHub
parent aa0a63a295
commit d312e82a51
5 changed files with 137 additions and 61 deletions

View File

@ -620,6 +620,15 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
return xerrors.Errorf("parse ssh config options %q: %w", vals.SSHConfig.SSHConfigOptions.String(), err)
}
// The workspace hostname suffix is always interpreted as implicitly beginning with a single dot, so it is
// a config error to explicitly include the dot. This ensures that we always interpret the suffix as a
// separate DNS label, and not just an ordinary string suffix. E.g. a suffix of 'coder' will match
// 'en.coder' but not 'encoder'.
if strings.HasPrefix(vals.WorkspaceHostnameSuffix.String(), ".") {
return xerrors.Errorf("you must omit any leading . in workspace hostname suffix: %s",
vals.WorkspaceHostnameSuffix.String())
}
options := &coderd.Options{
AccessURL: vals.AccessURL.Value(),
AppHostname: appHostname,

View File

@ -65,6 +65,7 @@ func (r *RootCmd) ssh() *serpent.Command {
var (
stdio bool
hostPrefix string
hostnameSuffix string
forwardAgent bool
forwardGPG bool
identityAgent string
@ -202,10 +203,14 @@ func (r *RootCmd) ssh() *serpent.Command {
parsedEnv = append(parsedEnv, [2]string{k, v})
}
workspaceInput := strings.TrimPrefix(inv.Args[0], hostPrefix)
// convert workspace name format into owner/workspace.agent
namedWorkspace := normalizeWorkspaceInput(workspaceInput)
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, namedWorkspace)
deploymentSSHConfig := codersdk.SSHConfigResponse{
HostnamePrefix: hostPrefix,
HostnameSuffix: hostnameSuffix,
}
workspace, workspaceAgent, err := findWorkspaceAndAgentByHostname(
ctx, inv, client,
inv.Args[0], deploymentSSHConfig, disableAutostart)
if err != nil {
return err
}
@ -564,6 +569,12 @@ func (r *RootCmd) ssh() *serpent.Command {
Description: "Strip this prefix from the provided hostname to determine the workspace name. This is useful when used as part of an OpenSSH proxy command.",
Value: serpent.StringOf(&hostPrefix),
},
{
Flag: "hostname-suffix",
Env: "CODER_SSH_HOSTNAME_SUFFIX",
Description: "Strip this suffix from the provided hostname to determine the workspace name. This is useful when used as part of an OpenSSH proxy command. The suffix must be specified without a leading . character.",
Value: serpent.StringOf(&hostnameSuffix),
},
{
Flag: "forward-agent",
FlagShorthand: "A",
@ -656,6 +667,30 @@ func (r *RootCmd) ssh() *serpent.Command {
return cmd
}
// findWorkspaceAndAgentByHostname parses the hostname from the commandline and finds the workspace and agent it
// corresponds to, taking into account any name prefixes or suffixes configured (e.g. myworkspace.coder, or
// vscode-coder--myusername--myworkspace).
func findWorkspaceAndAgentByHostname(
ctx context.Context, inv *serpent.Invocation, client *codersdk.Client,
hostname string, config codersdk.SSHConfigResponse, disableAutostart bool,
) (
codersdk.Workspace, codersdk.WorkspaceAgent, error,
) {
// for suffixes, we don't explicitly get the . and must add it. This is to ensure that the suffix is always
// interpreted as a dotted label in DNS names, not just any string suffix. That is, a suffix of 'coder' will
// match a hostname like 'en.coder', but not 'encoder'.
qualifiedSuffix := "." + config.HostnameSuffix
switch {
case config.HostnamePrefix != "" && strings.HasPrefix(hostname, config.HostnamePrefix):
hostname = strings.TrimPrefix(hostname, config.HostnamePrefix)
case config.HostnameSuffix != "" && strings.HasSuffix(hostname, qualifiedSuffix):
hostname = strings.TrimSuffix(hostname, qualifiedSuffix)
}
hostname = normalizeWorkspaceInput(hostname)
return getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, hostname)
}
// watchAndClose ensures closer is called if the context is canceled or
// the workspace reaches the stopped state.
//

View File

@ -1690,8 +1690,21 @@ func TestSSH(t *testing.T) {
}
})
t.Run("SSHHostPrefix", func(t *testing.T) {
t.Run("SSHHost", func(t *testing.T) {
t.Parallel()
testCases := []struct {
name, hostnameFormat string
flags []string
}{
{"Prefix", "coder.dummy.com--%s--%s", []string{"--ssh-host-prefix", "coder.dummy.com--"}},
{"Suffix", "%s--%s.coder", []string{"--hostname-suffix", "coder"}},
{"Both", "%s--%s.coder", []string{"--hostname-suffix", "coder", "--ssh-host-prefix", "coder.dummy.com--"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, workspace, agentToken := setupWorkspaceForAgent(t)
_, _ = tGoContext(t, func(ctx context.Context) {
// Run this async so the SSH command has to wait for
@ -1714,7 +1727,10 @@ func TestSSH(t *testing.T) {
user, err := client.User(ctx, codersdk.Me)
require.NoError(t, err)
inv, root := clitest.New(t, "ssh", "--stdio", "--ssh-host-prefix", "coder.dummy.com--", fmt.Sprintf("coder.dummy.com--%s--%s", user.Username, workspace.Name))
args := []string{"ssh", "--stdio"}
args = append(args, tc.flags...)
args = append(args, fmt.Sprintf(tc.hostnameFormat, user.Username, workspace.Name))
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, client, root)
inv.Stdin = clientOutput
inv.Stdout = serverInput
@ -1753,6 +1769,8 @@ func TestSSH(t *testing.T) {
<-cmdDone
})
}
})
}
//nolint:paralleltest // This test uses t.Setenv, parent test MUST NOT be parallel.
func TestSSH_ForwardGPG(t *testing.T) {

View File

@ -23,6 +23,11 @@ OPTIONS:
locally and will not be started for you. If a GPG agent is already
running in the workspace, it will be attempted to be killed.
--hostname-suffix string, $CODER_SSH_HOSTNAME_SUFFIX
Strip this suffix from the provided hostname to determine the
workspace name. This is useful when used as part of an OpenSSH proxy
command. The suffix must be specified without a leading . character.
--identity-agent string, $CODER_SSH_IDENTITY_AGENT
Specifies which identity agent to use (overrides $SSH_AUTH_SOCK),
forward agent must also be enabled.

View File

@ -29,6 +29,15 @@ Specifies whether to emit SSH output over stdin/stdout.
Strip this prefix from the provided hostname to determine the workspace name. This is useful when used as part of an OpenSSH proxy command.
### --hostname-suffix
| | |
|-------------|-----------------------------------------|
| Type | <code>string</code> |
| Environment | <code>$CODER_SSH_HOSTNAME_SUFFIX</code> |
Strip this suffix from the provided hostname to determine the workspace name. This is useful when used as part of an OpenSSH proxy command. The suffix must be specified without a leading . character.
### -A, --forward-agent
| | |