Self-hosted service
ContainerSSH/ContainerSSH avatar
ContainerSSH/ContainerSSH

ContainerSSH: an SSH server that launches containers per connection

ContainerSSH: Launch containers on demand

3,079 stars113 forksGoApache-2.0

At a glance

What is it?
A Go SSH server that authenticates against a webhook, asks a config service where to run the session, and pipes terminal I/O into a Docker or Kubernetes container.
Who is it for?
The design decision worth taking away is that ContainerSSH splits itself into three decisions you can replace independently: an authentication server, a config server, and a container backend. Nothing about a user session is hardcoded, so the same binary serves an ephemeral teaching lab and a production debugging proxy, and the webhook interface is small enough to implement in an afternoon.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 19 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

An SSH server whose job is to hand the session to a container

The repository describes itself with one line: an SSH server that launches containers in Kubernetes and Docker. That framing matters because it is the inverse of the arrangement most people already have. Normally an SSH server hands a session to a shell on a machine someone administers. Here, the server is a dispatcher. It authenticates the client, works out where the session should run, starts something, and forwards the bytes.

The README lays out the sequence in four steps. The user opens an SSH connection. ContainerSSH calls the authentication server with the user's username and password or public key to check that it is valid. It then calls the config server to obtain the backend location and configuration, if one is configured. Finally it calls the container backend to launch the container with that configuration, sending user input straight through to the backend and container output straight back.

That last clause is the whole streaming story. ContainerSSH is not an orchestrator sitting between the user and a long-lived environment; it is the transport. Which means the interesting engineering is all in the three callbacks rather than in the terminal handling.

The project sits at a healthy middle size: around 3,079 stars and 113 forks, 64 open issues, Apache 2.0 licensed, written in Go, with a last push on 2026-09-17. It has a Slack channel on the CNCF community workspace, which is a reasonable signal of where it sees itself sitting.

The three use cases in the README describe three very different operators

Rather than a feature list, the README organizes itself around three scenarios, and they are worth reading as requirements from three different kinds of operator.

The first is building a lab. The claim is that labs are time-consuming to set up, and ContainerSSH answers with dynamic SSH access exposed through APIs, automatic cleanup on logout using ephemeral containers, and persistent volumes for the data that has to survive. The intended audience is named directly: vendor and student labs. What matters here is that the container is disposable and the volume is not, which is a different split from most session infrastructure.

The second is debugging a production system. The pitch is giving developers production access with their usual tools while logging all changes, authorizing that access, and creating short-lived credentials for the database through simple webhooks, then cleaning up the environment on disconnect. Short-lived credentials issued per session is the part that changes the security conversation, because the credential expires with the session rather than with a password rotation schedule.

The third is running a honeypot. Attackers get dropped into network-isolated containers or even virtual machines, their every move is captured through the audit logging, and a built-in S3 upload keeps that data from being lost along with the host. It is the only one of the three where the hostile party is the user, and it is the one where the audit log stops being a nice-to-have.

Read together, the three make a point about what ContainerSSH is for. It is aimed at situations where the number of environments is driven by user demand rather than by capacity planning, and where somebody has to answer the question of who was allowed in and what they did.

Embedding it means getting a pool, a lifecycle and two objects

You can also use ContainerSSH as a library, which is the route to take if the SSH service needs to live inside something you already run. The README documents it in a short sequence.

First there is a configuration value that needs its defaults populated:

go
cfg := config.AppConfig{}
// Set the default configuration:
cfg.Default()

Then the constructor returns a pool and a lifecycle alongside an error:

go
pool, lifecycle, err := containerssh.New(cfg, loggerFactory)
if err != nil {
    return err
}

The lifecycle owns the run loop, and calling it blocks until the service stops:

go
err := lifecycle.Run()

Hooks have to be registered before that call, which the README is explicit about. A hook receives the service and the lifecycle:

go
lifecycle.OnStarting(
    func(s service.Service, l service.Lifecycle) {
        print("ContainerSSH is starting...")
    },
)

Shutdown is graceful and takes a context used as a timeout, and the pool is what you reach for to rotate log files, which closes and reopens them across all running services.

go
pool.RotateLogs()

The return shape is the thing to internalize. Because there is no global server and no package-level state to configure, the same process can hold more than one instance, and the lifecycle pattern means embedding it does not require adopting its own startup conventions.

A config webhook is one interface and one method

The configuration webhook is where the project's flexibility actually comes from, and the contract you implement is genuinely small. The library ships the HTTP server that serves the requests; you supply the logic.

Adding the dependency is a single module fetch:

bash
go get go.containerssh.io/containerssh

Then you implement the handler interface. The README shows the shape with a package declaration, the config import and the interface itself:

go
package main

import (
    "go.containerssh.io/containerssh/config"
)

type ConfigRequestHandler interface {
	OnConfig(request config.Request) (config.AppConfig, error)
}

One method, one request in, one `AppConfig` out. The request carries the username, which means the decision about which image, which limits and which backend a given person gets is entirely yours to make. Per-user images, a lab environment for students and a production namespace for staff all fall out of branching on that one field.

The README then shows the struct-and-receiver approach it recommends, along with a comment recommending an IDE to discover the options, and starts setting a Docker image based on the username. That is the entire documented tutorial for the webhook. It is short enough that reading the `config` package is a faster path than following the example, and the absence of a more elaborate walkthrough is itself informative: the project is handing you a decision, not a policy.

Persistent mode trades container isolation for session state

The v0.6.0 release, published 2026-03-23, added a persistent Kubernetes execution mode and carried a warning with it that is the most useful thing in the release notes.

The existing connection mode creates a new pod for each SSH connection and terminates that pod when the session ends. The new persistent mode execs into an already existing pod, with options to create the pod if it is missing or to refuse entry if it is not. The release notes give the motivation directly: use cases where stateful sessions are needed, including but not limited to managing long-running interactive processes and resource sharing.

The warning says plainly that ContainerSSH cannot guarantee user isolation in this mode, and expects that users of the feature will apply their own configuration. That is not a caveat about a rough edge. It is the consequence of the feature. Attaching to a pre-existing pod means the isolation ContainerSSH could previously guarantee, one client per fresh environment, is now something the surrounding configuration has to provide. If two developers land in the same pod they can see each other's files and processes, and ContainerSSH has no way to prevent that.

So the honest reading is that connection mode and persistent mode are different tools with different threat models, and the choice belongs to whoever writes the config webhook. The same release also added extra scope support for OIDC authentication, fixed Kerberos authentication bugs, and fixed Docker image pulling. v0.5.2 from 2025-01-19 moved all library code out of the separate libcontainerssh repository so the import path is now `go.containerssh.io/containerssh`, and moved handshake success processing outside the callbacks in response to crypto/ssh CVE-2024-45337, noting that the previous handling did not allow any additional access but was changed to make the code clearer.

Releases you can verify, and a dependency list that shows the scope

ContainerSSH publishes SLSA provenance with each release, as a `multiple.intoto.jsonl` file, intended to be checked with slsa-verifier to confirm artifacts come from the project. The README gives the invocation:

sh
slsa-verifier verify-artifact <artifact-to-verify> \
--provenance-path <path-to-your-provenance> \
--source-uri github.com/containerssh/containerssh

Successful verification prints a pass line for the artifact and a second line confirming verified SLSA provenance. For a project whose entire value proposition is running someone else's commands with real credentials, build provenance is not decoration, and having it documented in the README rather than only in a wiki is a point in the project's favour.

The dependency list in `go.mod` tells you the same story from a different angle. The module is `go.containerssh.io/containerssh` built against Go 1.25.3, and it requires the Docker client at v28.5.2, `k8s.io/api`, `k8s.io/apimachinery` and `k8s.io/client-go` all at v0.35.3, `sigs.k8s.io/kind` for testing against real clusters, `github.com/containerssh/gokrb5/v8` for Kerberos, `github.com/aws/aws-sdk-go` for the S3 audit upload, `github.com/oschwald/geoip2-golang` and `github.com/gorilla/schema` for webhook request decoding. A Go Report Card badge sits in the README, and there is a `golicense.json` alongside `go-license-detector` for dependency license scanning.

The tree explains the rest. `auth/`, `config/`, `agentprotocol/`, `message/`, `metadata/`, `auditlog/`, `http/`, `log/`, `cmd/`, `internal/` and `service/` map almost one to one onto the pipeline described in the architecture section. `config.example.yaml` is the reference configuration, `containerssh.service` is a systemd unit for running the daemon, `.goreleaser.yaml` handles packaging, `swagger_gen.go` is generated from the webhook API definition, and `e2e_backend_test.go` with `e2e_framework_test.go` sit at the root as end to end coverage across backends.

Editorial conclusion

The design decision worth taking away is that ContainerSSH splits itself into three decisions you can replace independently: an authentication server, a config server, and a container backend. Nothing about a user session is hardcoded, so the same binary serves an ephemeral teaching lab and a production debugging proxy, and the webhook interface is small enough to implement in an afternoon. The boundary the repository itself draws is the isolation warning attached to persistent Kubernetes mode, where attaching to an existing pod means ContainerSSH can no longer promise one client per environment. What the README does not settle is the operational side: the concrete YAML for a real backend, the audit log format, and the S3 upload configuration all live in the documentation site. The place to start is the quickstart for a single Docker backend, then the embedding section if you would rather not run the binary at all.

Frequently asked questions

What does ContainerSSH actually do when someone connects?

It calls the authentication server with the user's username and password or public key, then calls the config server to get the backend location and configuration, then asks the container backend to launch a container. User input is forwarded to the backend and container output is streamed back to the user.

How is ContainerSSH different from running sshd inside a container?

ContainerSSH is the SSH server itself, and it launches the container as part of handling the connection rather than expecting a container that already has an sshd inside it. Because it owns the session lifecycle, it can create the container on demand, tear it down on logout, and audit everything that happened during it.

Can I use ContainerSSH as a Go library instead of a service?

Yes. Calling containerssh.New with a config and a logger factory returns a service pool and a lifecycle. You register hooks with lifecycle.OnStarting before calling lifecycle.Run, shut down with lifecycle.Stop using a context as the timeout, and rotate logs through the pool.

What is the persistent Kubernetes execution mode and what is the risk?

Unlike connection mode, which creates a pod per SSH connection and deletes it when the session ends, persistent mode execs into an existing pod, optionally creating it first. The v0.6.0 release notes warn that ContainerSSH cannot guarantee user isolation in this mode, so isolation has to come from the configuration you supply.

How do I verify that a ContainerSSH release is authentic?

Each release ships a SLSA provenance file named multiple.intoto.jsonl. Pass it to slsa-verifier with verify-artifact, the provenance path and --source-uri github.com/containerssh/containerssh, and a successful run reports the artifact as verified.

Official sources

  1. ContainerSSH/ContainerSSH on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/containerssh-containerssh.svg)](https://hysenlabs.com/projects/containerssh-containerssh)