Architecture
Big picture
ContainerSSH is a single Go binary. Its entry point cmd/containerssh/main.go calls containerssh.Main(), whose real body is Main() at the repository root (main.go:26). The interesting part is how one SSH connection is served. The server is built as a stack of handlers wrapped around each other like nesting dolls. New() (factory.go:22) assembles them from the inside out, and each layer implements the same sshserver.Handler contract while wrapping the layer inside it.
The assembly order is fixed (factory.go:54-78), innermost first:
createBackend(factory.go:54,backend.New): the innermost layer that picks the container backend (docker, kubernetes, or sshproxy).createAuthHandler(factory.go:59,authintegration.New): the layer that calls the authentication webhook.createAuditLogHandler(factory.go:64,auditlogintegration.New): audit logging and GeoIP.createMetricsBackend(factory.go:69,metricsintegration.NewHandler): Prometheus metrics.createSSHServer(factory.go:74,sshserver.New): the outermost layer that actually listens for TCP and speaks SSH.
A request travels from the outside (SSH) inward (backend). When the SSH server accepts a connection, Handler.OnNetworkConnection (internal/sshserver/handler.go:30) fires, and each layer returns a NetworkConnectionHandler (internal/sshserver/handler.go:97) that chains to the next.
Components
SSH server layer
sshserver (internal/sshserver/) is the outermost layer. It owns the TCP listener and the SSH protocol, and it defines the handler interfaces every other layer implements: Handler, NetworkConnectionHandler, and the session-channel handler (internal/sshserver/handler.go:19, :97). Everything below it is expressed as an implementation of these interfaces.
Authentication layer
The auth integration layer (internal/auth/) sits between the SSH server and the backend. On a login attempt it POSTs to an external webhook and translates the reply into a three-valued verdict (see below). Built-in OAuth2 / OIDC and Kerberos implementations also live here (internal/auth/oauth2_oidc.go, internal/auth/kerberos.go).
Backend layer
The backend layer (internal/backend/) is the innermost handler. It holds the connection and delegates to a concrete container backend. backend.networkHandler (internal/backend/handler.go:52) keeps the connection and its chosen backend (backend sshserver.NetworkConnectionHandler), and centralises OnDisconnect and OnShutdown. The concrete backends are internal/docker/, internal/kubernetes/, and internal/sshproxy/.
Configuration
config.AppConfig (config/appconfig.go:11) is the root of all configuration. It bundles SSH, ConfigServer, Auth, Audit, Security, Backend, Docker, Kubernetes, and SSHProxy. Some of it is what the per-connection config webhook overrides at runtime.
How a request flows
Trace one connection into a Docker container.
- The SSH handshake completes and control reaches the backend layer's
OnHandshakeSuccess(internal/backend/handler.go:96). This is where the connection's real configuration is decided. loadConnectionSpecificConfig(internal/backend/handler.go:179) calls the config webhook and merges its reply onto the base configuration. The HTTP call itself ishttpLoader.LoadConnection(internal/config/loader_http.go:37), which does aclient.Getand layers the returnedAppConfigwithstructutils.Merge(internal/config/loader_http.go:42-49).getConfiguredBackend(internal/backend/handler.go:139) reads the resultingappConfig.Backendstring (docker,kubernetes, orsshproxy) and constructs the matching backend.security.New(internal/backend/handler.go:130) always wraps a security overlay around that backend before returning it.- For the Docker backend,
OnHandshakeSuccess(internal/docker/handler_network.go:52) sets up the Docker client, pulls the image, and (in connection mode) creates and starts one container for the connection (internal/docker/handler_network.go:88-95). - When the client opens a session channel, the Docker backend returns a channel handler (
internal/docker/handler_ssh.go:33) and ashell/execrequest runs a program inside the container (see Internals). - On disconnect,
networkHandler.OnDisconnect(internal/docker/handler_network.go:164) removes the container. That is the mechanism behind "logging out destroys everything."
Key design decisions
- Authentication and per-connection configuration are HTTP webhooks, not config. The server has no user database and no per-user config file. It POSTs to your auth webhook and to your config webhook, so a single deployment can route different users to different backends and images with all the logic living in your service. The config merge happens in
loader_http.go:42-49. - A three-valued auth verdict.
sshserver.AuthResponse(internal/sshserver/handler.go:35) isSuccess,Failure, orUnavailable.Unavailablemeans the auth backend is down, distinct from wrong credentials, so an outage does not look like a rejected password. - A security overlay is always applied.
security.Newwraps every backend (internal/backend/handler.go:130) rather than leaving hardening optional. - Config validation is skipped when a config server is in use.
AppConfig.Validate(dynamic bool)(config/appconfig.go:92) skips backend validation whenConfigServer.URL != "" && !dynamic(config/appconfig.go:103), because the backend settings are expected to arrive from the config server rather than the static file.
Extension points
- Authentication webhook: implement an HTTP service that answers
/password,/pubkey, and/authz(see Internals); the auth layer calls it per login. - Configuration webhook: implement an HTTP service that returns a partial
AppConfigper connection; it is merged onto the base config (internal/config/loader_http.go:37). - Container backend:
docker,kubernetes, andsshproxyare selected by theBackendstring; each is a package underinternal/. - Audit log sink: the audit layer can upload the binary log to S3-compatible storage, and metrics are exposed for Prometheus.