sshproxy

package
v0.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 10, 2026 License: MPL-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package sshproxy implements a bridge-side SSH-terminating proxy for session recording. When a tunnel has resource type "ssh-audit", the bridge routes the connection through this proxy instead of the standard byte-splice tunnel.

Architecture:

Client SSH ──▶ Bridge SSHProxy ──▶ Agent ──▶ Target sshd
               (terminates SSH)    (byte pipe)
               (records stdout)

The proxy terminates the client's SSH connection (acts as SSH server), captures terminal output in asciicast v2 format, then opens a new SSH connection to the target through the agent tunnel (acts as SSH client).

Trade-off: ssh-audit sessions cannot survive bridge pod failure because SSH encryption state lives in the bridge process. Regular "ssh" type retains the reliable stream for bridge-death survival.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type PlainChannel added in v0.3.0

type PlainChannel struct {
	// contains filtered or unexported fields
}

PlainChannel wraps an ssh.Channel without recording. Implements io.ReadWriteCloser. Used for non-audit web-ssh sessions.

func (*PlainChannel) Close added in v0.3.0

func (pc *PlainChannel) Close() error

func (*PlainChannel) Read added in v0.3.0

func (pc *PlainChannel) Read(p []byte) (int, error)

func (*PlainChannel) SendWindowChange added in v0.3.0

func (pc *PlainChannel) SendWindowChange(cols, rows uint16)

SendWindowChange sends a window-change request on the SSH channel.

func (*PlainChannel) Write added in v0.3.0

func (pc *PlainChannel) Write(p []byte) (int, error)

type Proxy

type Proxy struct {
	// contains filtered or unexported fields
}

Proxy handles SSH-terminating proxy connections for session recording.

func NewProxy

func NewProxy(logger *slog.Logger) (*Proxy, error)

NewProxy creates a new SSH proxy with an ephemeral host key. Prefer NewProxyFromTLSKey for stable host keys across bridge restarts.

func NewProxyFromTLSKey

func NewProxyFromTLSKey(keyPEM []byte, logger *slog.Logger) (*Proxy, error)

NewProxyFromTLSKey creates an SSH proxy using the bridge's TLS private key as the SSH host key. This ensures the host key is stable across bridge pod restarts (as long as the TLS cert is the same), preventing "REMOTE HOST IDENTIFICATION HAS CHANGED" warnings for users.

func NewProxyWithSigner

func NewProxyWithSigner(signer ssh.Signer, logger *slog.Logger) *Proxy

NewProxyWithSigner creates a proxy with an explicit host signer (for testing).

func (*Proxy) Handle

func (p *Proxy) Handle(ctx context.Context, clientConn net.Conn, agentConn net.Conn, sessionID string, warnCh <-chan string) (*SessionResult, error)

Handle proxies an SSH session between clientConn and agentConn, recording terminal output. The agentConn is a raw TCP pipe to the target sshd (through the agent tunnel).

The proxy: 1. Accepts the client SSH handshake (NoClientAuth — mTLS already authenticated) 2. Opens an SSH connection to the target through agentConn 3. For each client channel request, opens a matching channel on the target 4. Records stdout from the target in asciicast v2 format 5. Returns the recording when the session closes

func (*Proxy) HandleDirect added in v0.3.0

func (p *Proxy) HandleDirect(agentConn net.Conn, username string, authMethods []ssh.AuthMethod, cols, rows int, record bool, logger *slog.Logger) (io.ReadWriteCloser, *Recording, error)

HandleDirect sets up an SSH session to the target and returns the channel as an io.ReadWriteCloser, plus a Recording for session capture. Unlike Handle/HandlePreAuth which manage the full bidirectional relay internally, HandleDirect returns the channel so the caller (webterm.Session) can manage the relay with reconnection support.

When record is true, the returned io.ReadWriteCloser is a RecordingChannel that tees reads into the Recording. When false, no recording is created and the returned Recording is nil. This respects the resource's audit classification — only ssh-audit resources should be recorded.

Used by web-ssh sessions where the bridge receives credentials from the browser (via the API) and authenticates directly to the target. authMethods should contain ssh.PublicKeys(signer) for key auth or ssh.Password(password) for password auth.

func (*Proxy) HandlePreAuth

func (p *Proxy) HandlePreAuth(ctx context.Context, clientConn net.Conn, targetSSH ssh.Conn, targetChans <-chan ssh.NewChannel, targetReqs <-chan *ssh.Request, sessionID string, warnCh <-chan string) (*SessionResult, error)

HandlePreAuth proxies an SSH session where the target connection is already authenticated. Used when remote signing pre-authenticates the bridge→target SSH connection during the pre-flight phase (before client SSH data flows).

The proxy only handles the client SSH handshake (NoClientAuth — mTLS already authenticated the user) and channel forwarding with recording.

type Recording

type Recording struct {
	// contains filtered or unexported fields
}

Recording captures terminal I/O in asciicast v2 format. Thread-safe: multiple goroutines may call Output/Resize concurrently.

Format spec: https://docs.asciinema.org/manual/asciicast/v2/

func NewRecording

func NewRecording() *Recording

NewRecording creates a new recording. Call Start() to write the header.

func (*Recording) Bytes

func (r *Recording) Bytes() []byte

Bytes returns the complete recording data.

func (*Recording) EnsureStarted

func (r *Recording) EnsureStarted()

EnsureStarted starts the recording if it hasn't been started yet. Called on shell/exec to capture output even without a PTY (e.g., `ssh user@host "command"`). Uses default dimensions since there's no terminal. This prevents users from bypassing recording by avoiding PTY.

func (*Recording) Len

func (r *Recording) Len() int

Len returns the current size of the recording in bytes.

func (*Recording) Output

func (r *Recording) Output(data []byte)

Output records terminal output data. Only stdout from the target should be recorded (not stdin — avoids capturing passwords).

func (*Recording) Resize

func (r *Recording) Resize(width, height int)

Resize records a terminal resize event.

func (*Recording) Start

func (r *Recording) Start(width, height int, env map[string]string)

Start writes the asciicast header and marks the recording start time. Must be called before Output/Resize. Width and height are initial terminal dimensions from the pty-req.

type RecordingChannel added in v0.3.0

type RecordingChannel struct {
	// contains filtered or unexported fields
}

RecordingChannel wraps an ssh.Channel to record stdout on Read and support window-change requests. Implements io.ReadWriteCloser.

func (*RecordingChannel) Close added in v0.3.0

func (rc *RecordingChannel) Close() error

func (*RecordingChannel) Read added in v0.3.0

func (rc *RecordingChannel) Read(p []byte) (int, error)

func (*RecordingChannel) SendWindowChange added in v0.3.0

func (rc *RecordingChannel) SendWindowChange(cols, rows uint16)

SendWindowChange sends a window-change request on the SSH channel.

func (*RecordingChannel) Write added in v0.3.0

func (rc *RecordingChannel) Write(p []byte) (int, error)

type SessionResult

type SessionResult struct {
	Recording []byte // asciicast v2 data
}

SessionResult contains the recording data after a session completes.

type SignChannel

type SignChannel struct {
	// contains filtered or unexported fields
}

SignChannel handles the pre-flight signing protocol between the bridge and CLI. It reads public keys from the CLI, then provides a signFunc that sends sign requests and reads responses. Thread-safe: the signFunc serializes requests since the text protocol is line-oriented.

func NewSignChannel

func NewSignChannel(reader *bufio.Reader, writer io.Writer, logger *slog.Logger) *SignChannel

NewSignChannel creates a sign channel from a buffered reader and writer (typically the client connection). Call ReadPublicKeys() first, then use Signers() to get SSH signers for the target connection.

func (*SignChannel) HasKeys

func (sc *SignChannel) HasKeys() bool

HasKeys returns true if the CLI sent at least one public key.

func (*SignChannel) ReadPublicKeys

func (sc *SignChannel) ReadPublicKeys() error

ReadPublicKeys reads pubkey lines from the CLI until "pubkeys-done". Format: "pubkey:{base64-wire-format-public-key}\n" per key.

func (*SignChannel) SendReady

func (sc *SignChannel) SendReady() error

SendReady tells the CLI that the pre-flight phase is complete and SSH data can start flowing.

func (*SignChannel) Signers

func (sc *SignChannel) Signers() []ssh.Signer

Signers returns SSH signers backed by the remote CLI's SSH agent. Each signer sends a sign request over the text channel when Sign() is called.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL