Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9c85cde

Browse files
committed
fix(agent/agentssh): pin random seed for RSA key generation
Change-Id: I8c7e3070324e5d558374fd6891eea9d48660e1e9 Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent 4449931 commit 9c85cde

File tree

5 files changed

+97
-17
lines changed

5 files changed

+97
-17
lines changed

agent/agent.go

+41-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"hash/fnv"
910
"io"
1011
"net/http"
1112
"net/netip"
@@ -994,7 +995,6 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co
994995
if err := manifestOK.wait(ctx); err != nil {
995996
return xerrors.Errorf("no manifest: %w", err)
996997
}
997-
var err error
998998
defer func() {
999999
networkOK.complete(retErr)
10001000
}()
@@ -1003,9 +1003,20 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co
10031003
network := a.network
10041004
a.closeMutex.Unlock()
10051005
if network == nil {
1006+
keySeed, err := workspaceSeed(manifest.WorkspaceID, manifest.AgentName)
1007+
if err != nil {
1008+
return xerrors.Errorf("generate seed from workspace id: %w", err)
1009+
}
10061010
// use the graceful context here, because creating the tailnet is not itself tied to the
10071011
// agent API.
1008-
network, err = a.createTailnet(a.gracefulCtx, manifest.AgentID, manifest.DERPMap, manifest.DERPForceWebSockets, manifest.DisableDirectConnections)
1012+
network, err = a.createTailnet(
1013+
a.gracefulCtx,
1014+
manifest.AgentID,
1015+
manifest.DERPMap,
1016+
manifest.DERPForceWebSockets,
1017+
manifest.DisableDirectConnections,
1018+
keySeed,
1019+
)
10091020
if err != nil {
10101021
return xerrors.Errorf("create tailnet: %w", err)
10111022
}
@@ -1145,7 +1156,13 @@ func (a *agent) trackGoroutine(fn func()) error {
11451156
return nil
11461157
}
11471158

1148-
func (a *agent) createTailnet(ctx context.Context, agentID uuid.UUID, derpMap *tailcfg.DERPMap, derpForceWebSockets, disableDirectConnections bool) (_ *tailnet.Conn, err error) {
1159+
func (a *agent) createTailnet(
1160+
ctx context.Context,
1161+
agentID uuid.UUID,
1162+
derpMap *tailcfg.DERPMap,
1163+
derpForceWebSockets, disableDirectConnections bool,
1164+
keySeed int64,
1165+
) (_ *tailnet.Conn, err error) {
11491166
// Inject `CODER_AGENT_HEADER` into the DERP header.
11501167
var header http.Header
11511168
if client, ok := a.client.(*agentsdk.Client); ok {
@@ -1172,6 +1189,10 @@ func (a *agent) createTailnet(ctx context.Context, agentID uuid.UUID, derpMap *t
11721189
}
11731190
}()
11741191

1192+
if err := a.sshServer.UpdateHostSigner(keySeed); err != nil {
1193+
return nil, xerrors.Errorf("update host signer: %w", err)
1194+
}
1195+
11751196
sshListener, err := network.Listen("tcp", ":"+strconv.Itoa(workspacesdk.AgentSSHPort))
11761197
if err != nil {
11771198
return nil, xerrors.Errorf("listen on the ssh port: %w", err)
@@ -1849,3 +1870,20 @@ func PrometheusMetricsHandler(prometheusRegistry *prometheus.Registry, logger sl
18491870
}
18501871
})
18511872
}
1873+
1874+
// workspaceSeed converts a WorkspaceID UUID to an int64 hash.
1875+
// This uses the FNV-1a hash algorithm which provides decent distribution and collision
1876+
// resistance for string inputs.
1877+
func workspaceSeed(workspaceID uuid.UUID, agentName string) (int64, error) {
1878+
h := fnv.New64a()
1879+
_, err := h.Write(workspaceID[:])
1880+
if err != nil {
1881+
return 42, err
1882+
}
1883+
_, err = h.Write([]byte(agentName))
1884+
if err != nil {
1885+
return 42, err
1886+
}
1887+
1888+
return int64(h.Sum64()), nil
1889+
}

agent/agentssh/agentssh.go

+44-14
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ package agentssh
33
import (
44
"bufio"
55
"context"
6-
"crypto/rand"
76
"crypto/rsa"
87
"errors"
98
"fmt"
109
"io"
10+
"math/rand"
1111
"net"
1212
"os"
1313
"os/exec"
@@ -128,17 +128,6 @@ type Server struct {
128128
}
129129

130130
func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prometheus.Registry, fs afero.Fs, execer agentexec.Execer, config *Config) (*Server, error) {
131-
// Clients' should ignore the host key when connecting.
132-
// The agent needs to authenticate with coderd to SSH,
133-
// so SSH authentication doesn't improve security.
134-
randomHostKey, err := rsa.GenerateKey(rand.Reader, 2048)
135-
if err != nil {
136-
return nil, err
137-
}
138-
randomSigner, err := gossh.NewSignerFromKey(randomHostKey)
139-
if err != nil {
140-
return nil, err
141-
}
142131
if config == nil {
143132
config = &Config{}
144133
}
@@ -205,8 +194,10 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
205194
slog.F("local_addr", conn.LocalAddr()),
206195
slog.Error(err))
207196
},
208-
Handler: s.sessionHandler,
209-
HostSigners: []ssh.Signer{randomSigner},
197+
Handler: s.sessionHandler,
198+
// HostSigners are intentionally empty, as the host key will
199+
// be set before we start listening.
200+
HostSigners: []ssh.Signer{},
210201
LocalPortForwardingCallback: func(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
211202
// Allow local port forwarding all!
212203
s.logger.Debug(ctx, "local port forward",
@@ -844,7 +835,13 @@ func (s *Server) CreateCommand(ctx context.Context, script string, env []string,
844835
return cmd, nil
845836
}
846837

838+
// Serve starts the server to handle incoming connections on the provided listener.
839+
// It returns an error if no host keys are set or if there is an issue accepting connections.
847840
func (s *Server) Serve(l net.Listener) (retErr error) {
841+
if len(s.srv.HostSigners) == 0 {
842+
return xerrors.New("no host keys set")
843+
}
844+
848845
s.logger.Info(context.Background(), "started serving listener", slog.F("listen_addr", l.Addr()))
849846
defer func() {
850847
s.logger.Info(context.Background(), "stopped serving listener",
@@ -1099,3 +1096,36 @@ func userHomeDir() (string, error) {
10991096
}
11001097
return u.HomeDir, nil
11011098
}
1099+
1100+
// UpdateHostSigner updates the host signer with a new key generated from the provided seed.
1101+
// If an existing host key exists with the same algorithm, it is overwritten
1102+
func (s *Server) UpdateHostSigner(seed int64) error {
1103+
key, err := coderSigner(seed)
1104+
if err != nil {
1105+
return err
1106+
}
1107+
1108+
s.mu.Lock()
1109+
defer s.mu.Unlock()
1110+
1111+
s.srv.AddHostKey(key)
1112+
1113+
return nil
1114+
}
1115+
1116+
// coderSigner generates a deterministic SSH signer based on the provided seed.
1117+
// It uses RSA with a key size of 2048 bits.
1118+
func coderSigner(seed int64) (gossh.Signer, error) {
1119+
// Clients should ignore the host key when connecting.
1120+
// The agent needs to authenticate with coderd to SSH,
1121+
// so SSH authentication doesn't improve security.
1122+
1123+
// nolint: gosec
1124+
deterministicRand := rand.New(rand.NewSource(seed))
1125+
coderHostKey, err := rsa.GenerateKey(deterministicRand, 2048)
1126+
if err != nil {
1127+
return nil, err
1128+
}
1129+
coderSigner, err := gossh.NewSignerFromKey(coderHostKey)
1130+
return coderSigner, err
1131+
}

agent/agentssh/agentssh_internal_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ func Test_sessionStart_orphan(t *testing.T) {
3939
s, err := NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), agentexec.DefaultExecer, nil)
4040
require.NoError(t, err)
4141
defer s.Close()
42+
err = s.UpdateHostSigner(42)
43+
assert.NoError(t, err)
4244

4345
// Here we're going to call the handler directly with a faked SSH session
4446
// that just uses io.Pipes instead of a network socket. There is a large

agent/agentssh/agentssh_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ func TestNewServer_ServeClient(t *testing.T) {
4141
s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), agentexec.DefaultExecer, nil)
4242
require.NoError(t, err)
4343
defer s.Close()
44+
err = s.UpdateHostSigner(42)
45+
assert.NoError(t, err)
4446

4547
ln, err := net.Listen("tcp", "127.0.0.1:0")
4648
require.NoError(t, err)
@@ -146,6 +148,8 @@ func TestNewServer_CloseActiveConnections(t *testing.T) {
146148
s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), agentexec.DefaultExecer, nil)
147149
require.NoError(t, err)
148150
defer s.Close()
151+
err = s.UpdateHostSigner(42)
152+
assert.NoError(t, err)
149153

150154
ln, err := net.Listen("tcp", "127.0.0.1:0")
151155
require.NoError(t, err)
@@ -197,6 +201,8 @@ func TestNewServer_Signal(t *testing.T) {
197201
s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), agentexec.DefaultExecer, nil)
198202
require.NoError(t, err)
199203
defer s.Close()
204+
err = s.UpdateHostSigner(42)
205+
assert.NoError(t, err)
200206

201207
ln, err := net.Listen("tcp", "127.0.0.1:0")
202208
require.NoError(t, err)
@@ -262,6 +268,8 @@ func TestNewServer_Signal(t *testing.T) {
262268
s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), agentexec.DefaultExecer, nil)
263269
require.NoError(t, err)
264270
defer s.Close()
271+
err = s.UpdateHostSigner(42)
272+
assert.NoError(t, err)
265273

266274
ln, err := net.Listen("tcp", "127.0.0.1:0")
267275
require.NoError(t, err)

agent/agentssh/x11_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ func TestServer_X11(t *testing.T) {
3838
s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), fs, agentexec.DefaultExecer, &agentssh.Config{})
3939
require.NoError(t, err)
4040
defer s.Close()
41+
err = s.UpdateHostSigner(42)
42+
assert.NoError(t, err)
4143

4244
ln, err := net.Listen("tcp", "127.0.0.1:0")
4345
require.NoError(t, err)

0 commit comments

Comments
 (0)