Skip to content

Commit 3dbfe28

Browse files
andystimeclaude
andcommitted
feat: generate dedicated SSH key pair and disable password auth
- Auto-generate Ed25519 SSH key pair in ~/.ggo/ssh/ for TF studio containers - Remove password authentication (PasswordAuthentication no, PermitRootLogin prohibit-password) - Delete default password setup (removed: echo "root:ggo-studio" | chpasswd) - Add IdentityFile to SSH config for seamless key-based authentication - Display private key path in studio creation output - Require SSH key for container access (fail if key generation fails) - Improve security by enforcing key-only authentication Key security improvements: - Dedicated key pair for studio containers (not mixing with user's personal SSH keys) - No password authentication reduces attack surface - Auto-generated on first use, stored securely with 0600 permissions Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent d4d553e commit 3dbfe28

4 files changed

Lines changed: 105 additions & 49 deletions

File tree

cmd/ggo/studio/studio.go

Lines changed: 37 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ var (
3636
command []string
3737
endpoint string
3838
platform string // container platform (e.g., linux/amd64, linux/arm64)
39+
40+
// lastPrivateKeyPath stores the private key path from the most recent buildCreateOptions call
41+
lastPrivateKeyPath string
3942
)
4043

4144
// NewStudioCmd creates the studio command
@@ -212,7 +215,7 @@ Examples:
212215
cmd.Flags().StringVarP(&image, "image", "i", "tensorfusion/studio-torch:latest", "Container image")
213216
cmd.Flags().StringVarP(&shareLink, "share-link", "s", "", "Share link or share code to remote vGPU worker (recommended for GPU access)")
214217
cmd.Flags().StringVar(&serverURL, "server", api.GetDefaultBaseURL(), "Server URL for resolving share links")
215-
cmd.Flags().StringVar(&sshKey, "ssh-key", "", "SSH public key to authorize (auto-detects from ~/.ssh/ if not provided)")
218+
cmd.Flags().StringVar(&sshKey, "ssh-key", "", "SSH public key to authorize (auto-generates dedicated key pair if not provided)")
216219
cmd.Flags().StringArrayVarP(&ports, "port", "p", nil, "Port mappings (host:container)")
217220
cmd.Flags().StringArrayVarP(&volumes, "volume", "v", nil, "Volume mounts (host:container[:ro])")
218221
cmd.Flags().StringArrayVarP(&envVars, "env", "e", nil, "Environment variables (KEY=VALUE)")
@@ -284,13 +287,6 @@ func runCreate(cmd *cobra.Command, args []string) error {
284287
return err
285288
}
286289

287-
// Inform user about SSH authentication method
288-
if !out.IsJSON() && opts.SSHPublicKey != "" {
289-
styles := tui.DefaultStyles()
290-
out.Printf("%s Using SSH key authentication (password: ggo-studio as fallback)\n",
291-
styles.Info.Render("ℹ"))
292-
}
293-
294290
if !out.IsJSON() {
295291
styles := tui.DefaultStyles()
296292
out.Printf("%s Creating studio environment '%s'...\n",
@@ -345,11 +341,12 @@ Original error: %w`, err)
345341
}
346342
}
347343
return out.Render(&createResult{
348-
env: env,
349-
mgr: mgr,
350-
noSSH: noSSH,
351-
backendName: backendName,
352-
socketPath: socketPath,
344+
env: env,
345+
mgr: mgr,
346+
noSSH: noSSH,
347+
backendName: backendName,
348+
socketPath: socketPath,
349+
privateKeyPath: lastPrivateKeyPath,
353350
})
354351
}
355352

@@ -426,15 +423,28 @@ func buildCreateOptions(name string, shareInfo *api.SharePublicInfo) (*studio.Cr
426423
return nil, err
427424
}
428425

429-
// Auto-detect SSH public key if not provided via --ssh-key flag
426+
// Get or create dedicated SSH key pair for TF studio containers
430427
effectiveSSHKey := sshKey
428+
privateKeyPath := ""
431429
if effectiveSSHKey == "" {
432-
if autoKey := studio.GetUserSSHPublicKey(); autoKey != "" {
433-
effectiveSSHKey = autoKey
434-
klog.V(2).Info("Using user's SSH public key for authentication")
430+
pubKey, privPath, err := studio.GetOrCreateStudioSSHKey()
431+
if err != nil {
432+
klog.Warningf("Failed to get/create studio SSH key: %v", err)
433+
} else {
434+
effectiveSSHKey = pubKey
435+
privateKeyPath = privPath
436+
klog.V(2).Infof("Using TF studio SSH key: %s", privPath)
435437
}
436438
}
437439

440+
// Store for use in createResult
441+
lastPrivateKeyPath = privateKeyPath
442+
443+
// Ensure SSH key is available (required for container access)
444+
if effectiveSSHKey == "" {
445+
return nil, fmt.Errorf("SSH public key is required for container access")
446+
}
447+
438448
// Set GPU connection info from share link
439449
gpuWorkerURL := ""
440450
hardwareVendor := ""
@@ -535,11 +545,12 @@ func parseEnvVars(envVars []string) (map[string]string, error) {
535545

536546
// createResult implements Renderable for create command output
537547
type createResult struct {
538-
env *studio.Environment
539-
mgr *studio.Manager
540-
noSSH bool
541-
backendName string
542-
socketPath string
548+
env *studio.Environment
549+
mgr *studio.Manager
550+
noSSH bool
551+
backendName string
552+
socketPath string
553+
privateKeyPath string
543554
}
544555

545556
func (r *createResult) RenderJSON() any {
@@ -583,6 +594,10 @@ func (r *createResult) RenderTUI(out *tui.Output) {
583594
Add("Port", fmt.Sprintf("%d", env.SSHPort)).
584595
Add("User", env.SSHUser)
585596

597+
if r.privateKeyPath != "" {
598+
sshStatus = sshStatus.Add("Private Key", r.privateKeyPath)
599+
}
600+
586601
out.Println(sshStatus.String())
587602

588603
out.Println()

internal/studio/manager.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,13 @@ func (m *Manager) AddSSHConfig(env *Environment) error {
453453
existingConfig = string(data)
454454
}
455455

456+
// Get home directory for private key path
457+
homeDir, err := os.UserHomeDir()
458+
if err != nil {
459+
return errors.Wrap(err, "failed to get user home directory")
460+
}
461+
privateKeyPath := filepath.Join(homeDir, ".ggo", "ssh", "id_ed25519")
462+
456463
// Generate new entry
457464
hostName := fmt.Sprintf("ggo-%s", env.Name)
458465
entry := fmt.Sprintf(`
@@ -461,9 +468,10 @@ Host %s
461468
HostName %s
462469
Port %d
463470
User %s
471+
IdentityFile %s
464472
StrictHostKeyChecking no
465473
UserKnownHostsFile /dev/null
466-
`, env.Name, hostName, env.SSHHost, env.SSHPort, env.SSHUser)
474+
`, env.Name, hostName, env.SSHHost, env.SSHPort, env.SSHUser, privateKeyPath)
467475

468476
// Keep a single entry per studio host by removing any existing one first.
469477
existingConfig = m.removeSSHConfigEntry(existingConfig, hostName)

internal/studio/ssh_key.go

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,79 @@
11
package studio
22

33
import (
4+
"crypto/ed25519"
5+
"crypto/rand"
6+
"encoding/pem"
47
"fmt"
58
"os"
69
"path/filepath"
710
"strings"
811

12+
"golang.org/x/crypto/ssh"
913
"k8s.io/klog/v2"
1014
)
1115

12-
// GetUserSSHPublicKey attempts to read user's SSH public key from standard locations
13-
// Returns the public key content or empty string if not found
14-
func GetUserSSHPublicKey() string {
16+
// GetOrCreateStudioSSHKey gets or creates a dedicated SSH key pair for TF studio containers
17+
// Returns the public key content, private key path, and error if any
18+
func GetOrCreateStudioSSHKey() (publicKey string, privateKeyPath string, err error) {
1519
homeDir, err := os.UserHomeDir()
1620
if err != nil {
17-
klog.V(2).Infof("Failed to get user home directory: %v", err)
18-
return ""
21+
return "", "", fmt.Errorf("failed to get user home directory: %w", err)
1922
}
2023

21-
// Try common SSH public key files in order of preference
22-
keyPaths := []string{
23-
filepath.Join(homeDir, ".ssh", "id_ed25519.pub"),
24-
filepath.Join(homeDir, ".ssh", "id_ecdsa.pub"),
25-
filepath.Join(homeDir, ".ssh", "id_rsa.pub"),
26-
filepath.Join(homeDir, ".ssh", "id_dsa.pub"),
27-
}
24+
// Store dedicated SSH keys in ~/.ggo/ssh/
25+
sshDir := filepath.Join(homeDir, ".ggo", "ssh")
26+
privateKeyPath = filepath.Join(sshDir, "id_ed25519")
27+
publicKeyPath := filepath.Join(sshDir, "id_ed25519.pub")
2828

29-
for _, keyPath := range keyPaths {
30-
if content, err := os.ReadFile(keyPath); err == nil {
31-
publicKey := strings.TrimSpace(string(content))
32-
if publicKey != "" {
33-
klog.V(2).Infof("Found SSH public key: %s", keyPath)
34-
return publicKey
35-
}
29+
// Check if key pair already exists
30+
if pubContent, err := os.ReadFile(publicKeyPath); err == nil {
31+
publicKey = strings.TrimSpace(string(pubContent))
32+
if publicKey != "" {
33+
klog.V(2).Infof("Using existing TF studio SSH key: %s", publicKeyPath)
34+
return publicKey, privateKeyPath, nil
3635
}
3736
}
3837

39-
klog.V(2).Info("No SSH public key found in standard locations")
40-
return ""
38+
// Generate new Ed25519 key pair
39+
klog.Infof("Generating new SSH key pair for TF studio containers...")
40+
if err := os.MkdirAll(sshDir, 0700); err != nil {
41+
return "", "", fmt.Errorf("failed to create SSH directory: %w", err)
42+
}
43+
44+
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
45+
if err != nil {
46+
return "", "", fmt.Errorf("failed to generate SSH key: %w", err)
47+
}
48+
49+
// Convert to SSH format
50+
sshPubKey, err := ssh.NewPublicKey(pubKey)
51+
if err != nil {
52+
return "", "", fmt.Errorf("failed to create SSH public key: %w", err)
53+
}
54+
55+
// Write public key
56+
publicKeyData := ssh.MarshalAuthorizedKey(sshPubKey)
57+
if err := os.WriteFile(publicKeyPath, publicKeyData, 0644); err != nil {
58+
return "", "", fmt.Errorf("failed to write public key: %w", err)
59+
}
60+
61+
// Write private key in PEM format
62+
privKeyBytes, err := ssh.MarshalPrivateKey(privKey, "")
63+
if err != nil {
64+
return "", "", fmt.Errorf("failed to marshal private key: %w", err)
65+
}
66+
67+
privKeyPEM := pem.EncodeToMemory(privKeyBytes)
68+
if err := os.WriteFile(privateKeyPath, privKeyPEM, 0600); err != nil {
69+
return "", "", fmt.Errorf("failed to write private key: %w", err)
70+
}
71+
72+
publicKey = strings.TrimSpace(string(publicKeyData))
73+
klog.Infof("Generated new SSH key pair: %s", publicKeyPath)
74+
klog.Infof("Private key saved to: %s", privateKeyPath)
75+
76+
return publicKey, privateKeyPath, nil
4177
}
4278

4379
// FormatSSHPublicKey formats and validates an SSH public key

internal/studio/ssh_setup.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ HostKey /etc/ssh/ssh_host_ecdsa_key
7070
HostKey /etc/ssh/ssh_host_ed25519_key
7171
7272
# Authentication
73-
PermitRootLogin yes
73+
PermitRootLogin prohibit-password
7474
PubkeyAuthentication yes
7575
AuthorizedKeysFile .ssh/authorized_keys
76-
PasswordAuthentication yes
76+
PasswordAuthentication no
7777
PermitEmptyPasswords no
7878
ChallengeResponseAuthentication no
7979
@@ -94,9 +94,6 @@ UsePAM yes
9494
Subsystem sftp /usr/lib/openssh/sftp-server
9595
SSHD_EOF
9696
97-
# Set a default password for root (can be overridden with SSH key)
98-
echo "root:ggo-studio" | chpasswd
99-
10097
echo "SSH setup completed successfully"
10198
`
10299

0 commit comments

Comments
 (0)