execproxy

package
v0.4.0-rc.59 Latest Latest
Warning

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

Go to latest
Published: Jul 25, 2026 License: AGPL-3.0 Imports: 23 Imported by: 0

Documentation

Overview

Package execproxy implements the airlock-side SSH execution path for agentsdk.RegisterExecEndpoint. The dialer opens a session against the operator-configured target, streams stdout/stderr/exit envelopes back over an http.ResponseWriter as NDJSON, and handles host-key TOFU.

Credentials never leave airlock — the private key is decrypted via secrets.Store at dial time and the agent process never sees raw key material. Operational logs record byte counts only, not payloads.

Index

Constants

This section is empty.

Variables

View Source
var (
	// DefaultExecTimeout is the wall-clock cap for a single exec call
	// when the caller doesn't specify ExecCommand.TimeoutMs. SSH dial,
	// auth, command execution, and stream draining all share this
	// budget.
	DefaultExecTimeout = 60 * time.Second

	// MaxExecTimeout is the upper bound — operators can't accidentally
	// allow a single call to hold a session for hours.
	MaxExecTimeout = 10 * time.Minute

	// DialTimeout is how long the TCP+SSH handshake gets before the
	// dialer gives up and returns a transport error.
	DialTimeout = 15 * time.Second

	// StreamChunkBytes is the raw byte count we buffer between reads
	// of session.Stdout/Stderr before base64-encoding and writing one
	// NDJSON envelope. 32 KiB keeps each envelope under the SDK's
	// 256 KiB scanner buffer with a comfortable margin (base64
	// inflates ~33%) while batching enough to avoid envelope-per-byte
	// overhead.
	StreamChunkBytes = 32 * 1024
)

Default ceilings — kept as vars so tests can lower them. Production callers should treat these as constants.

Functions

func HostKeyFingerprint

func HostKeyFingerprint(openssh string) string

HostKeyFingerprint returns the SHA256 fingerprint of a host key OpenSSH-formatted string, for UI display. Empty input → empty output.

func JoinCommand

func JoinCommand(command string, args []string) string

JoinCommand assembles command + args into a single shell command line suitable for SSH exec. Args are shell-quoted; command is not — pipes, redirection, and shell substitution in command pass through to the remote shell unchanged. This is the semantic of `ssh user@host cmd a1 a2`: the SSH client joins everything with spaces and the remote shell does the parsing.

func ShellQuote

func ShellQuote(s string) string

ShellQuote wraps s in single quotes for safe inclusion in a POSIX shell command line. The remote sshd hands our command string to the user's login shell (`$SHELL -c "..."`), so any argument that contains whitespace, glob characters, or shell metacharacters must be quoted to avoid being re-split on the remote side.

Single-quoting handles every metacharacter except `'` itself; we escape an embedded apostrophe by closing the quote, emitting an escaped `'`, and reopening the quote: don't → 'don'\”t'

Types

type ExecRequest

type ExecRequest struct {
	Command   string
	Args      []string
	Stdin     []byte
	TimeoutMs int64
}

ExecRequest is the parsed input from the agent's POST /api/agent/exec/{slug} body. Stdin is the raw bytes (already decoded from base64 on the wire).

type HostKeyMismatchError

type HostKeyMismatchError struct {
	Expected string
	Got      string
}

HostKeyMismatchError is returned when a pinned host key doesn't match what the remote presented during the SSH handshake. Distinct type so the HTTP handler can map it to a specific status / message.

func (*HostKeyMismatchError) Error

func (e *HostKeyMismatchError) Error() string

type Keypair

type Keypair struct {
	PrivatePEM    string
	PublicOpenSSH string
	Comment       string
}

Keypair carries a freshly generated ED25519 keypair plus the OpenSSH public-key line and its dated comment. Airlock stores PrivatePEM encrypted via secrets.Store and persists PublicOpenSSH + Comment in the agent_exec_endpoints row for UI display.

func GenerateED25519

func GenerateED25519(agentSlug, endpointSlug string) (Keypair, error)

GenerateED25519 mints a new ED25519 keypair with a dated, human-grep'able comment. The comment shape lets the operator find old keys in authorized_keys after a rotation:

ssh-ed25519 AAAA… airlock-myagent-ci-runner-2026-05-26

agentSlug + endpointSlug + an ISO date give a stable per-rotation identifier the operator can match exactly.

type PreStreamError

type PreStreamError struct {
	Kind    string // transport | timeout | config | denied
	Status  int    // HTTP status to write
	Message string
}

PreStreamError is returned by Exec when the failure happened before any byte of the streaming response was written. The HTTP handler maps .Status to a status code; the agent SDK uses the status code (not the body) to classify into ExecError.Kind.

func (*PreStreamError) Error

func (e *PreStreamError) Error() string

type SSHDialer

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

SSHDialer is the entry point for the agent-internal exec handler. Build one at server startup and reuse it for every exec call.

func NewSSHDialer

func NewSSHDialer(store secrets.Store, pinner TOFUPinner, logger *zap.Logger) *SSHDialer

NewSSHDialer constructs an SSHDialer with sensible defaults. Call Close at shutdown to stop the cache reaper.

func (*SSHDialer) Close

func (d *SSHDialer) Close()

Close stops the cache reaper. Idempotent.

func (*SSHDialer) EvictCache

func (d *SSHDialer) EvictCache(id uuid.UUID)

EvictCache clears the cached client for endpoint id — called by the admin handlers after any config / keypair / host-key mutation so the next call dials fresh.

func (*SSHDialer) Exec

Exec opens an SSH session, runs the assembled command, and streams stdout/stderr/exit envelopes to w as NDJSON. All errors after the first byte is written go into the stream as a terminal "error" envelope; the function still returns nil to the caller because the HTTP response has already started. Pre-stream errors (dial, auth, host-key mismatch) return without writing anything so the HTTP handler can map them to a status code.

type TOFUPinner

type TOFUPinner interface {
	PinHostKey(ctx context.Context, endpointID uuid.UUID, hostKeyOpenSSH string) error
}

TOFUPinner is the dependency the dialer needs to record a newly-seen host key the first time a TOFU connect succeeds. The HTTP handler implements this against the sqlc-generated SetExecEndpointHostKey.

Jump to

Keyboard shortcuts

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