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 ¶
- type PlainChannel
- type Proxy
- func (p *Proxy) Handle(ctx context.Context, clientConn net.Conn, agentConn net.Conn, sessionID string, ...) (*SessionResult, error)
- func (p *Proxy) HandleDirect(agentConn net.Conn, username string, authMethods []ssh.AuthMethod, ...) (io.ReadWriteCloser, *Recording, error)
- func (p *Proxy) HandlePreAuth(ctx context.Context, clientConn net.Conn, targetSSH ssh.Conn, ...) (*SessionResult, error)
- type Recording
- type RecordingChannel
- type SessionResult
- type SignChannel
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) SendWindowChange ¶ added in v0.3.0
func (pc *PlainChannel) SendWindowChange(cols, rows uint16)
SendWindowChange sends a window-change request on the SSH channel.
type Proxy ¶
type Proxy struct {
// contains filtered or unexported fields
}
Proxy handles SSH-terminating proxy connections for session recording.
func NewProxy ¶
NewProxy creates a new SSH proxy with an ephemeral host key. Prefer NewProxyFromTLSKey for stable host keys across bridge restarts.
func NewProxyFromTLSKey ¶
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 ¶
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) 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) Output ¶
Output records terminal output data. Only stdout from the target should be recorded (not stdin — avoids capturing passwords).
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.
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 ¶
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.