Internals
Read from the source at commit
ce7d2b6. Every claim here should point at a file and line.
Code map
| Path | Responsibility |
|---|---|
main.go | Real entry point: Main() (main.go:26), argument parsing (getArguments, main.go:145), host-key generation (main.go:238). |
factory.go | Assembles the nested handler stack from the inside out (New, factory.go:22; order at factory.go:54-78). |
internal/sshserver/ | TCP listener, the SSH protocol, and the handler interfaces every layer implements (handler.go). |
internal/backend/ | The bridge layer: loads per-connection config, selects the backend, applies the security overlay (handler.go). |
internal/config/ | Configuration loaders, including the HTTP config webhook client (loader_http.go). |
internal/auth/ | The auth webhook client plus built-in OAuth2 / OIDC and Kerberos (webhook_client_impl.go, oauth2_oidc.go, kerberos.go). |
internal/docker/ | The Docker backend: per-connection container lifecycle and per-channel program execution. |
config/ | The root AppConfig type and its validation (appconfig.go). |
Core data structures
config.AppConfig(config/appconfig.go:11) is the root of all configuration. The config webhook dynamically replaces part of it per connection.sshserver.HandlerandNetworkConnectionHandler(internal/sshserver/handler.go:19,:97) are the interface hierarchy for the connection, authentication, and session-channel stages. Every backend and every integration layer implements this same contract.sshserver.AuthResponse(internal/sshserver/handler.go:35) is a three-valued enum:Success,Failure,Unavailable.Unavailablemeans the auth backend is down, deliberately distinct from a credential failure.backend.networkHandler(internal/backend/handler.go:52) holds the connection and the delegate for the chosen backend (backend sshserver.NetworkConnectionHandler), centralisingOnDisconnectandOnShutdown.docker.channelHandler(internal/docker/handler_channel.go:16) maps one SSH session channel to one program execution inside a container, holding theexec dockerExecutionand thepty/rows/columns/envstate.
A path worth tracing
Follow a Docker backend connection in the default connection execution mode, from handshake to disconnect.
- After the SSH handshake succeeds,
docker.networkHandler.OnHandshakeSuccess(internal/docker/handler_network.go:52) opens aContainerStartcontext with a timeout, prepares the Docker client (setupDockerClient,internal/docker/handler_network.go:153), and pulls the image (pullImage,internal/docker/handler_network.go:144). - It builds the connection labels (
containerssh_connection_id,containerssh_ip,containerssh_username) and, inconnectionmode, creates and starts exactly one container for the connection (internal/docker/handler_network.go:88-95), writing any files frommeta.GetFiles()into it (internal/docker/handler_network.go:97-108). - When the client opens a session channel,
sshConnectionHandler.OnSessionChannel(internal/docker/handler_ssh.go:33) returns achannelHandler. - A
shell,exec, orsubsystemrequest callschannelHandler.OnShell(internal/docker/handler_channel.go:199) orOnExecRequest(internal/docker/handler_channel.go:187), both of which callrun(internal/docker/handler_channel.go:80). runbranches on the execution mode (internal/docker/handler_channel.go:91). Inconnectionmode it callshandleExecModeConnection(internal/docker/handler_channel.go:129), which does acreateExec(the equivalent ofdocker exec) against the already-running container. Insessionmode it callshandleExecModeSession(internal/docker/handler_channel.go:147), which starts a fresh container per session and makes the program that container's main process.c.exec.run(...)(internal/docker/handler_channel.go:108) wires the SSH channel's stdin, stdout, and stderr directly to the container's I/O and returnssession.ExitStatuswhen the program exits.- On disconnect,
networkHandler.OnDisconnect(internal/docker/handler_network.go:164) callscontainer.removeand destroys the container.
OnHandshakeSuccess (create+start container) handler_network.go:52,88-95
OnSessionChannel -> channelHandler handler_ssh.go:33
OnShell/OnExecRequest -> run handler_channel.go:199,187,80
run branches on exec mode handler_channel.go:91
connection: handleExecModeConnection handler_channel.go:129 (docker exec)
session: handleExecModeSession handler_channel.go:147 (new container, PID 1)
c.exec.run wires stdio, returns exit handler_channel.go:108
OnDisconnect (container.remove) handler_network.go:164parseProgram (internal/docker/handler_channel.go:64) decides how to interpret a requested command: if it starts with /, ./, or ../ it is used as an argv directly, otherwise it is wrapped as /bin/sh -c <program>.
The auth webhook client is worth noting alongside this. internal/auth/webhook_client_impl.go builds the endpoints /password (webhook_client_impl.go:68), /pubkey (:92), and /authz (:47), and the password is base64-encoded before being sent (near webhook_client_impl.go:73). This fully separates the credential check from the server, so any IdP, database, or LDAP directory can sit behind the webhook.
Things that surprised me
- A single enum turns ContainerSSH into two different products. The Docker backend has two execution modes (
config.DockerExecutionModeConnectionversusDockerExecutionModeSession,internal/docker/handler_channel.go:91-95). Inconnectionmode there is one container per SSH connection and each channel enters it withdocker exec, so multiple sessions share state and PTY and agent forwarding both work (setupAgent,internal/docker/handler_ssh.go:127). Insessionmode each session channel starts a new container with the user's program as PID 1: isolation is stronger, but port and agent forwarding are explicitly refused insidesetupAgent(internal/docker/handler_ssh.go:185). Honeypot deployments want the strong-isolationsessionmode; lab and debug deployments want the convenientconnectionmode. This one branch is what lets the same server be both. - An unavailable auth backend is not a failed login.
AuthResponsecarries a distinctUnavailablevalue (internal/sshserver/handler.go:35) so that an auth-service outage is handled differently from wrong credentials rather than collapsing into a generic rejection. - Config validation is deliberately skipped when a config server is configured.
AppConfig.Validateskips backend validation whenConfigServer.URL != "" && !dynamic(config/appconfig.go:103), because the backend settings are expected to arrive from the config webhook at connection time, not from the static file. - The server generates its own host key on first run. With no host key configured,
runContainerSSH(main.go:120) generates a temporary host key and writes it back into the configuration file (generateHostKeys,main.go:238).