Skip to content

Commit 069f2ac

Browse files
committed
feat: SSH代理用自己的密钥连容器,不再依赖密码
- proxy.go: 代理连容器时优先用自己的 host key 认证,密码作为备选 - proxy.go: 启动时自动导出公钥到 .pub 文件 - worker.go: 注入 authorized_keys 时始终包含代理的公钥 - ssh_keys.go: 实时同步时也包含代理公钥 代理 host key 持久化在 /var/lib/cloud-cli-proxy/ssh_host_ed25519_key, 其公钥自动注入到所有容器的 authorized_keys 中, 确保代理始终能连接到容器,不受密码状态影响。 Made-with: Cursor
1 parent 39ad89c commit 069f2ac

3 files changed

Lines changed: 66 additions & 17 deletions

File tree

internal/controlplane/http/ssh_keys.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"fmt"
1313
"log/slog"
1414
nethttp "net/http"
15+
"os"
1516
"os/exec"
1617
"strings"
1718
"time"
@@ -281,6 +282,9 @@ func (h *SSHKeyHandler) syncKeysToRunningHosts(userID string) {
281282
func syncInboundKeysToContainer(ctx context.Context, containerName, user string, keys []repository.SSHKey) {
282283
sshDir := "/workspace/.ssh"
283284
var lines []string
285+
if proxyPub := loadProxyPublicKey(); proxyPub != "" {
286+
lines = append(lines, proxyPub)
287+
}
284288
for _, k := range keys {
285289
if k.Purpose == "inbound" && k.PublicKey != "" {
286290
lines = append(lines, strings.TrimSpace(k.PublicKey))
@@ -419,6 +423,19 @@ func readContainerAuthorizedKeyFingerprints(ctx context.Context, containerName s
419423
return result
420424
}
421425

426+
func loadProxyPublicKey() string {
427+
dataDir := os.Getenv("DATA_DIR")
428+
if dataDir == "" {
429+
dataDir = "/var/lib/cloud-cli-proxy"
430+
}
431+
pubKeyPath := dataDir + "/ssh_host_ed25519_key.pub"
432+
data, err := os.ReadFile(pubKeyPath)
433+
if err != nil {
434+
return ""
435+
}
436+
return strings.TrimSpace(string(data))
437+
}
438+
422439
func computeFingerprint(pubKeyStr string) string {
423440
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKeyStr))
424441
if err != nil {

internal/runtime/tasks/worker.go

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -423,27 +423,28 @@ func (w *Worker) injectSSHKeys(ctx context.Context, request agentapi.HostActionR
423423
sshDir := "/workspace/.ssh"
424424

425425
var authorizedKeys []string
426+
if proxyPubKey := loadProxyPublicKey(); proxyPubKey != "" {
427+
authorizedKeys = append(authorizedKeys, proxyPubKey)
428+
}
426429
for _, key := range request.SSHKeys {
427430
if key.Purpose == "inbound" && key.PublicKey != "" {
428431
authorizedKeys = append(authorizedKeys, strings.TrimSpace(key.PublicKey))
429432
}
430433
}
431-
if len(authorizedKeys) > 0 {
432-
content := strings.Join(authorizedKeys, "\n") + "\n"
433-
script := fmt.Sprintf(
434-
"mkdir -p %s && cat > %s/authorized_keys && chmod 600 %s/authorized_keys && chown %s:%s %s/authorized_keys",
435-
sshDir, sshDir, sshDir, user, user, sshDir,
436-
)
437-
cmd := exec.CommandContext(ctx, "docker", "exec", "-i", containerName, "bash", "-c", script)
438-
cmd.Stdin = strings.NewReader(content)
439-
if out, err := cmd.CombinedOutput(); err != nil {
440-
w.repo.RecordEvent(ctx, repository.RecordEventParams{
441-
HostID: &request.HostID,
442-
Level: "warn",
443-
Type: "runtime.ssh_authorized_keys_failed",
444-
Message: fmt.Sprintf("inject authorized_keys failed: %s", strings.TrimSpace(string(out))),
445-
})
446-
}
434+
content := strings.Join(authorizedKeys, "\n") + "\n"
435+
script := fmt.Sprintf(
436+
"mkdir -p %s && cat > %s/authorized_keys && chmod 600 %s/authorized_keys && chown %s:%s %s/authorized_keys",
437+
sshDir, sshDir, sshDir, user, user, sshDir,
438+
)
439+
cmd := exec.CommandContext(ctx, "docker", "exec", "-i", containerName, "bash", "-c", script)
440+
cmd.Stdin = strings.NewReader(content)
441+
if out, err := cmd.CombinedOutput(); err != nil {
442+
w.repo.RecordEvent(ctx, repository.RecordEventParams{
443+
HostID: &request.HostID,
444+
Level: "warn",
445+
Type: "runtime.ssh_authorized_keys_failed",
446+
Message: fmt.Sprintf("inject authorized_keys failed: %s", strings.TrimSpace(string(out))),
447+
})
447448
}
448449

449450
outboundIdx := 0
@@ -597,6 +598,19 @@ func (rv *repoValidator) GetHostWgKeys(ctx context.Context, hostID string) (stri
597598
return rv.repo.GetHostWgKeys(ctx, hostID)
598599
}
599600

601+
func loadProxyPublicKey() string {
602+
dataDir := os.Getenv("DATA_DIR")
603+
if dataDir == "" {
604+
dataDir = "/var/lib/cloud-cli-proxy"
605+
}
606+
pubKeyPath := dataDir + "/ssh_host_ed25519_key.pub"
607+
data, err := os.ReadFile(pubKeyPath)
608+
if err != nil {
609+
return ""
610+
}
611+
return strings.TrimSpace(string(data))
612+
}
613+
600614
func (w *Worker) pullImage(ctx context.Context, imageName string) {
601615
cmd := exec.CommandContext(ctx, "docker", "pull", imageName)
602616
output, err := cmd.CombinedOutput()

internal/sshproxy/proxy.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"net"
1313
"os"
1414
"path/filepath"
15+
"strings"
1516
"sync"
1617
"time"
1718

@@ -66,6 +67,7 @@ func loadOrGenerateHostKey(path string, logger *slog.Logger) (ssh.Signer, error)
6667
return nil, fmt.Errorf("parse host key %s: %w", path, err)
6768
}
6869
logger.Info("SSH proxy loaded persistent host key", "path", path)
70+
exportPublicKey(signer, path, logger)
6971
return signer, nil
7072
}
7173
if !os.IsNotExist(err) {
@@ -98,11 +100,22 @@ func loadOrGenerateHostKey(path string, logger *slog.Logger) (ssh.Signer, error)
98100
} else {
99101
logger.Info("SSH proxy generated and persisted new host key", "path", path)
100102
}
103+
exportPublicKey(signer, path, logger)
101104
}
102105

103106
return signer, nil
104107
}
105108

109+
func exportPublicKey(signer ssh.Signer, privKeyPath string, logger *slog.Logger) {
110+
pubKeyPath := privKeyPath + ".pub"
111+
pubKeyStr := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey()))) + " cloud-cli-proxy-proxy\n"
112+
if err := os.WriteFile(pubKeyPath, []byte(pubKeyStr), 0o644); err != nil {
113+
logger.Warn("cannot export proxy public key", "path", pubKeyPath, "error", err)
114+
} else {
115+
logger.Info("SSH proxy public key exported", "path", pubKeyPath)
116+
}
117+
}
118+
106119
func (s *Server) ListenAndServe(ctx context.Context) error {
107120
config := &ssh.ServerConfig{
108121
MaxAuthTries: 3,
@@ -208,14 +221,19 @@ func (s *Server) handleChannel(newChan ssh.NewChannel, targetAddr, targetUser, t
208221
if user == "" {
209222
user = s.containerUser
210223
}
224+
225+
authMethods := []ssh.AuthMethod{
226+
ssh.PublicKeys(s.hostKey),
227+
}
211228
pass := targetPassword
212229
if pass == "" {
213230
pass = s.containerPassword
214231
}
232+
authMethods = append(authMethods, ssh.Password(pass))
215233

216234
targetConfig := &ssh.ClientConfig{
217235
User: user,
218-
Auth: []ssh.AuthMethod{ssh.Password(pass)},
236+
Auth: authMethods,
219237
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
220238
Timeout: 10 * time.Second,
221239
}

0 commit comments

Comments
 (0)