Skip to content
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
12 changes: 11 additions & 1 deletion experimental/ssh/internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,8 +580,18 @@ func spawnSSHClient(ctx context.Context, userName, privateKeyPath string, server
if opts.UserKnownHostsFile != "" {
sshArgs = append(sshArgs, "-o", "UserKnownHostsFile="+opts.UserKnownHostsFile)
}
// When no remote command is specified, explicitly start bash as a login shell.
// The default login shell on Databricks compute images is often /bin/sh.
// The -t flag forces PTY allocation, which is required when specifying a remote command.
if len(opts.AdditionalArgs) == 0 {
sshArgs = append(sshArgs, "-t")
}
sshArgs = append(sshArgs, hostName)
sshArgs = append(sshArgs, opts.AdditionalArgs...)
if len(opts.AdditionalArgs) == 0 {
sshArgs = append(sshArgs, "/bin/bash", "-l")
} else {
sshArgs = append(sshArgs, opts.AdditionalArgs...)
}

log.Debugf(ctx, "Launching SSH client: ssh %s", strings.Join(sshArgs, " "))
sshCmd := exec.CommandContext(ctx, "ssh", sshArgs...)
Expand Down
2 changes: 2 additions & 0 deletions experimental/ssh/internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ func Run(ctx context.Context, client *databricks.WorkspaceClient, opts ServerOpt
return fmt.Errorf("failed to save metadata to the workspace: %w", err)
}

ensureBashLoginShell(ctx)

sshdConfigPath, err := prepareSSHDConfig(ctx, client, opts)
if err != nil {
return fmt.Errorf("failed to setup SSH configuration: %w", err)
Expand Down
66 changes: 66 additions & 0 deletions experimental/ssh/internal/server/sshd.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package server

import (
"bufio"
"context"
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"strings"
Expand All @@ -17,6 +19,70 @@ import (
"github.com/databricks/databricks-sdk-go"
)

const bashPath = "/bin/bash"

// ensureBashLoginShell attempts to set bash as the login shell for the current user
// by editing /etc/passwd directly. This ensures interactive SSH sessions use bash
// instead of sh without depending on external tools like usermod.
func ensureBashLoginShell(ctx context.Context) {
if _, err := os.Stat(bashPath); err != nil {
log.Warnf(ctx, "bash not found at %s, keeping default login shell", bashPath)
return
}

currentUser, err := user.Current()
if err != nil {
log.Warnf(ctx, "Failed to get current user for shell setup: %v", err)
return
}

err = setLoginShellInPasswd(currentUser.Username, bashPath)
if err != nil {
log.Warnf(ctx, "Failed to set bash as login shell for user %s: %v", currentUser.Username, err)
} else {
log.Infof(ctx, "Set login shell to %s for user %s", bashPath, currentUser.Username)
}
}

// setLoginShellInPasswd updates the login shell for the given user in /etc/passwd.
// Each line in /etc/passwd has 7 colon-delimited fields; the last field is the login shell.
func setLoginShellInPasswd(username, shell string) error {
const passwdPath = "/etc/passwd"

data, err := os.ReadFile(passwdPath)
if err != nil {
return fmt.Errorf("failed to read %s: %w", passwdPath, err)
}

prefix := username + ":"
var result []string
found := false

scanner := bufio.NewScanner(strings.NewReader(string(data)))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, prefix) {
fields := strings.Split(line, ":")
if len(fields) == 7 {
if fields[6] == shell {
// Already set to the desired shell.
return nil
}
fields[6] = shell
line = strings.Join(fields, ":")
found = true
}
}
result = append(result, line)
}

if !found {
return fmt.Errorf("user %s not found in %s", username, passwdPath)
}

return os.WriteFile(passwdPath, []byte(strings.Join(result, "\n")+"\n"), 0o644)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will always fail. The repl user is not root.

It might be possible with usermod if it is setuid but I'm not sure about that.

}

func prepareSSHDConfig(ctx context.Context, client *databricks.WorkspaceClient, opts ServerOptions) (string, error) {
clientPublicKey, err := keys.GetSecret(ctx, client, opts.SecretScopeName, opts.AuthorizedKeySecretName)
if err != nil {
Expand Down
Loading