Bring the naitve ssh pam to the browser gateway

This commit is contained in:
Owen
2026-05-22 11:30:21 -07:00
parent e6267cc1fc
commit 1ff26b7acd
7 changed files with 130 additions and 22 deletions
+7 -2
View File
@@ -62,11 +62,16 @@ func (g *Gateway) HandleSSH(w http.ResponseWriter, r *http.Request) {
}
target = net.JoinHostPort(host, port)
} else {
// Native SSH mode: validate against the global gateway token.
// Native SSH mode: validate the gateway token then read the target username.
if subtle.ConstantTimeCompare([]byte(token), []byte(g.authToken)) != 1 {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
username = r.URL.Query().Get("username")
if username == "" {
http.Error(w, "missing username", http.StatusBadRequest)
return
}
}
ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
@@ -81,7 +86,7 @@ func (g *Gateway) HandleSSH(w http.ResponseWriter, r *http.Request) {
defer ws.CloseNow() //nolint:errcheck
if nativeSSH {
if err := serveNativeSSHSession(ctx, ws); err != nil {
if err := serveNativeSSHSession(ctx, ws, username); err != nil {
log.Printf("SSH native session error: %v", err)
}
} else {
+18 -10
View File
@@ -10,13 +10,15 @@ import (
"github.com/fosrl/newt/nativessh"
)
// serveNativeSSHSession handles a WebSocket SSH session by spawning a local
// PTY+shell instead of proxying to an external SSH server. The auth token has
// already been validated at the WebSocket upgrade level, so this function only
// reads (and discards) the initial "auth" frame for protocol compatibility with
// the browser client before starting the shell.
func serveNativeSSHSession(ctx context.Context, ws *websocket.Conn) error {
// Read and discard the auth frame (token already validated at HTTP layer).
// serveNativeSSHSession handles a WebSocket SSH session by authenticating the
// user against the host OS (authorized_keys then PAM password) and then
// spawning a PTY+shell running as that user.
//
// The auth frame from the browser must be a JSON sshClientMsg with type="auth"
// carrying the same password/privateKey fields used by the proxy SSH path.
// The target username is passed in from the HTTP layer (query param).
func serveNativeSSHSession(ctx context.Context, ws *websocket.Conn, username string) error {
// Read the auth frame.
_, authBytes, err := ws.Read(ctx)
if err != nil {
return fmt.Errorf("read auth message: %w", err)
@@ -26,12 +28,18 @@ func serveNativeSSHSession(ctx context.Context, ws *websocket.Conn) error {
return fmt.Errorf("expected auth message, got: %s", authBytes)
}
log.Printf("SSH native: spawning shell")
// Authenticate using host authorized_keys or PAM password.
if err := nativessh.Authenticate(username, authMsg.Password, authMsg.PrivateKey); err != nil {
sendSSHError(ctx, ws, "Authentication failed")
return fmt.Errorf("auth for user %q: %w", username, err)
}
sess, err := nativessh.NewPTYSession()
log.Printf("SSH native: spawning shell as user %q", username)
sess, err := nativessh.NewPTYSessionAs(username)
if err != nil {
sendSSHError(ctx, ws, fmt.Sprintf("Failed to spawn shell: %v", err))
return fmt.Errorf("pty session: %w", err)
return fmt.Errorf("pty session as %q: %w", username, err)
}
defer sess.Close()