Skip to content

feat: add configurable SSH host key algorithm support #18866

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ type Options struct {
IgnorePorts map[int]string
PortCacheDuration time.Duration
SSHMaxTimeout time.Duration
SSHHostKeyAlgorithm string
TailnetListenPort uint16
Subsystems []codersdk.AgentSubsystem
PrometheusRegistry *prometheus.Registry
Expand Down Expand Up @@ -186,6 +187,7 @@ func New(options Options) Agent {
reportMetadataInterval: options.ReportMetadataInterval,
announcementBannersRefreshInterval: options.ServiceBannerRefreshInterval,
sshMaxTimeout: options.SSHMaxTimeout,
sshHostKeyAlgorithm: options.SSHHostKeyAlgorithm,
subsystems: options.Subsystems,
logSender: agentsdk.NewLogSender(options.Logger),
blockFileTransfer: options.BlockFileTransfer,
Expand Down Expand Up @@ -257,6 +259,7 @@ type agent struct {
sessionToken atomic.Pointer[string]
sshServer *agentssh.Server
sshMaxTimeout time.Duration
sshHostKeyAlgorithm string
blockFileTransfer bool

lifecycleUpdate chan struct{}
Expand Down Expand Up @@ -292,6 +295,7 @@ func (a *agent) init() {
// pass the "hard" context because we explicitly close the SSH server as part of graceful shutdown.
sshSrv, err := agentssh.NewServer(a.hardCtx, a.logger.Named("ssh-server"), a.prometheusRegistry, a.filesystem, a.execer, &agentssh.Config{
MaxTimeout: a.sshMaxTimeout,
HostKeyAlgorithm: a.sshHostKeyAlgorithm,
MOTDFile: func() string { return a.manifest.Load().MOTDFile },
AnnouncementBanners: func() *[]codersdk.BannerConfig { return a.announcementBanners.Load() },
UpdateEnv: a.updateCommandEnv,
Expand Down
39 changes: 32 additions & 7 deletions agent/agentssh/agentssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package agentssh
import (
"bufio"
"context"
"crypto/ed25519"
"errors"
"fmt"
"io"
"math/rand"
"net"
"os"
"os/exec"
Expand Down Expand Up @@ -113,6 +115,9 @@ type Config struct {
BlockFileTransfer bool
// ReportConnection.
ReportConnection reportConnectionFunc
// HostKeyAlgorithm specifies the SSH host key algorithm to use.
// Valid values: "rsa", "ed25519". Default: "rsa" for backward compatibility.
HostKeyAlgorithm string
// Experimental: allow connecting to running containers via Docker exec.
// Note that this is different from the devcontainers feature, which uses
// subagents.
Expand Down Expand Up @@ -1312,7 +1317,11 @@ func userHomeDir() (string, error) {
// UpdateHostSigner updates the host signer with a new key generated from the provided seed.
// If an existing host key exists with the same algorithm, it is overwritten
func (s *Server) UpdateHostSigner(seed int64) error {
key, err := CoderSigner(seed)
algorithm := s.config.HostKeyAlgorithm
if algorithm == "" {
algorithm = "rsa" // Default to RSA for backward compatibility
}
key, err := CoderSigner(seed, algorithm)
if err != nil {
return err
}
Expand All @@ -1325,14 +1334,30 @@ func (s *Server) UpdateHostSigner(seed int64) error {
return nil
}

// CoderSigner generates a deterministic SSH signer based on the provided seed.
// It uses RSA with a key size of 2048 bits.
func CoderSigner(seed int64) (gossh.Signer, error) {
// CoderSigner generates a deterministic SSH signer based on the provided seed and algorithm.
// Supported algorithms: "rsa" (2048 bits), "ed25519".
func CoderSigner(seed int64, algorithm string) (gossh.Signer, error) {
// Clients should ignore the host key when connecting.
// The agent needs to authenticate with coderd to SSH,
// so SSH authentication doesn't improve security.
coderHostKey := agentrsa.GenerateDeterministicKey(seed)

coderSigner, err := gossh.NewSignerFromKey(coderHostKey)
return coderSigner, err
switch algorithm {
case "ed25519":
// Generate deterministic Ed25519 key
// nolint: gosec
rng := rand.New(rand.NewSource(seed))
_, privateKey, err := ed25519.GenerateKey(rng)
if err != nil {
return nil, err
}
return gossh.NewSignerFromKey(privateKey)

case "rsa", "":
// Default to RSA for backward compatibility
coderHostKey := agentrsa.GenerateDeterministicKey(seed)
return gossh.NewSignerFromKey(coderHostKey)

default:
return nil, xerrors.Errorf("unsupported host key algorithm: %s", algorithm)
}
}
9 changes: 9 additions & 0 deletions cli/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
pprofAddress string
noReap bool
sshMaxTimeout time.Duration
sshHostKeyAlgorithm string
tailnetListenPort int64
prometheusAddress string
debugAddress string
Expand Down Expand Up @@ -356,6 +357,7 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
EnvironmentVariables: environmentVariables,
IgnorePorts: ignorePorts,
SSHMaxTimeout: sshMaxTimeout,
SSHHostKeyAlgorithm: sshHostKeyAlgorithm,
Subsystems: subsystems,

PrometheusRegistry: prometheusRegistry,
Expand Down Expand Up @@ -451,6 +453,13 @@ func (r *RootCmd) workspaceAgent() *serpent.Command {
Description: "Specify the max timeout for a SSH connection, it is advisable to set it to a minimum of 60s, but no more than 72h.",
Value: serpent.DurationOf(&sshMaxTimeout),
},
{
Flag: "ssh-host-key-algorithm",
Default: "rsa",
Env: "CODER_AGENT_SSH_HOST_KEY_ALGORITHM",
Description: "Specify the SSH host key algorithm to use. Valid values: rsa, ed25519.",
Value: serpent.StringOf(&sshHostKeyAlgorithm),
},
{
Flag: "tailnet-listen-port",
Default: "0",
Expand Down
2 changes: 1 addition & 1 deletion cli/ssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ func TestSSH(t *testing.T) {
keySeed, err := agent.SSHKeySeed(user.Username, workspace.Name, "dev")
assert.NoError(t, err)

signer, err := agentssh.CoderSigner(keySeed)
signer, err := agentssh.CoderSigner(keySeed, "rsa")
assert.NoError(t, err)

conn, channels, requests, err := ssh.NewClientConn(&testutil.ReaderWriterConn{
Expand Down
3 changes: 3 additions & 0 deletions cli/testdata/coder_agent_--help.golden
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ OPTIONS:
--script-data-dir string, $CODER_AGENT_SCRIPT_DATA_DIR (default: /tmp)
Specify the location for storing script data.

--ssh-host-key-algorithm string, $CODER_AGENT_SSH_HOST_KEY_ALGORITHM (default: rsa)
Specify the SSH host key algorithm to use. Valid values: rsa, ed25519.

--ssh-max-timeout duration, $CODER_AGENT_SSH_MAX_TIMEOUT (default: 72h)
Specify the max timeout for a SSH connection, it is advisable to set
it to a minimum of 60s, but no more than 72h.
Expand Down
Loading