Container images with the OpenSSH client: ssh, scp, sftp, ssh-keygen, ssh-agent, ssh-add and ssh-keyscan. OpenSSH is compiled from the signed release tarball on Ubuntu and Alpine, for linux/amd64 and linux/arm64, and the images are rebuilt when OpenSSH publishes a release and when the base image changes. The tools run under any UID, including one that has no entry in /etc/passwd.
This is an unofficial build, not affiliated with or endorsed by the OpenSSH project. Report problems with the image in this repository and OpenSSH bugs upstream.
Run a command on a server, with the key and known_hosts file from your ~/.ssh:
docker run --rm --user "$(id -u):$(id -g)" -v "$HOME/.ssh:/ssh:ro" \
ghcr.io/randomcontainers/openssh-client \
-i /ssh/id_ed25519 -o UserKnownHostsFile=/ssh/known_hosts deploy@example.com uptimeThe entrypoint is ssh. Start the other tools with --entrypoint, here to copy a file from the current directory:
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" -v "$HOME/.ssh:/ssh:ro" \
--entrypoint scp ghcr.io/randomcontainers/openssh-client \
-i /ssh/id_ed25519 -o UserKnownHostsFile=/ssh/known_hosts report.pdf deploy@example.com:/srv/reports/and to create a key pair in it:
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/work" --entrypoint ssh-keygen \
ghcr.io/randomcontainers/openssh-client -t ed25519 -N "" -C deploy -f deploy_keyAdd -it for an interactive login, and whenever ssh has to ask for a password or a passphrase. The OpenSSH manual pages describe every option.
Where ~/.ssh is inside the container depends on the UID (see Running as any UID), so mount your files at a fixed path such as /ssh and name them on the command line: -i /ssh/id_ed25519 -o UserKnownHostsFile=/ssh/known_hosts, or -F /ssh/config with absolute paths in the config file. ssh ignores a private key that other users can read, so keep the key mode 600; with --user "$(id -u):$(id -g)", ssh runs as the key's owner and can read it.
Without a terminal, ssh cannot ask whether to trust a host key it has not seen, so the server must already be in the known_hosts file. ssh-keyscan prints a server's host keys; check their fingerprints (ssh-keygen -l -f known_hosts) against the ones the server's administrator gives you before you rely on them:
docker run --rm --entrypoint ssh-keyscan ghcr.io/randomcontainers/openssh-client example.com >> known_hostsThe image has no /etc/ssh/ssh_config, so ssh starts from its built-in defaults. Mount a file there, for example -v "$PWD/ssh_config:/etc/ssh/ssh_config:ro", to set options for every host.
OpenSSH uses the post-quantum key exchange mlkem768x25519-sha256 by default and prints a warning when a server offers no post-quantum key exchange. -o WarnWeakCrypto=no-pq-kex turns that warning off. DSA keys no longer work: OpenSSH removed them in 10.0.
Mount your agent's socket and point SSH_AUTH_SOCK at it, and ssh uses the keys loaded in the agent on the host:
docker run --rm -it --user "$(id -u):$(id -g)" \
-v "$SSH_AUTH_SOCK:/agent.sock" -e SSH_AUTH_SOCK=/agent.sock \
-v "$HOME/.ssh/known_hosts:/ssh/known_hosts:ro" \
ghcr.io/randomcontainers/openssh-client -o UserKnownHostsFile=/ssh/known_hosts deploy@example.comOn Docker Desktop, the host's agent is at /run/host-services/ssh-auth.sock: mount that path instead of $SSH_AUTH_SOCK.
To run ssh-agent inside the container, give it a socket path with -a, as in ssh-agent -a /tmp/agent.sock. Since OpenSSH 10.1, ssh-agent otherwise puts its socket in ~/.ssh/agent, which does not exist for most UIDs in the image.
OpenSSH refuses to start for a UID that has no entry in /etc/passwd (No user exists for uid 1001), and --user "$(id -u):$(id -g)" usually gives the container such a UID. In this image, ssh, scp, sftp, ssh-keygen, ssh-agent, ssh-add and ssh-keyscan in /usr/local/bin are links to a short shell script, wrapper.sh. When the UID has no entry, the script writes a copy of /etc/passwd with an entry for it to a temporary file, then starts the real program from /usr/local/libexec/openssh/bin with nss_wrapper preloaded, which makes that file the passwd database. Programs that the tool starts, such as the ssh that scp runs, inherit the setting. For a UID that has an entry, such as the image's default UID 1000 or root, the script starts the program directly.
The added entry has the user name ssh and the home directory /home/ssh, which does not exist. For these UIDs:
- ssh logs in to the server as
sshwhen the destination names no user. Always give the remote user:deploy@example.comor-l deploy. ~/.sshis/home/ssh/.ssh. For UID 1000 it is/home/ubuntu/.sshon Ubuntu and/home/app/.sshon Alpine, and for root/root/.ssh. Pass key andknown_hostspaths explicitly, as above.
Each run that needs the entry leaves a small file in $TMPDIR, or /tmp when it is not set. With --read-only, add --tmpfs /tmp.
ssh,scp,sftp,ssh-keygen,ssh-agent,ssh-addandssh-keyscan, linked against the distro's OpenSSL (libcrypto), zlib and libedit, which givessftpcommand-line editing.sftp-server, and the PKCS#11 and security key helpers that the tools start, in/usr/local/libexec/openssh.- nss_wrapper from the distro, for the wrapper described above.
Not included: sshd and ssh-keysign; GSSAPI and Kerberos authentication, which Ubuntu's own OpenSSH package has; built-in FIDO/U2F security key support (libfido2); xauth, which X11 forwarding needs; ssh-copy-id; and the man pages. ssh -V prints the OpenSSH and OpenSSL versions, and the configure flags are in /usr/local/share/randomcontainers/openssh-client/buildinfo.
OpenSSH's default image adds no other tools, so latest and slim are the same image, with the contents listed above. Use latest to run it and the slim tags as a base for your own image. The default images of OpenTofu, Terragrunt and doctl include this build of the OpenSSH client.
<version> is an OpenSSH portable release such as 10.5p1. <major> is its major version, 10, and follows the newest release in that series. Each row lists the default tag and its slim twin, which point to the same image.
| Tags | Base |
|---|---|
latest, slim |
Ubuntu |
<version>, <version>-slim |
Ubuntu |
<major>, <major>-slim |
Ubuntu |
ubuntu, slim-ubuntu |
Ubuntu |
<version>-ubuntu, <version>-slim-ubuntu |
Ubuntu |
<major>-ubuntu, <major>-slim-ubuntu |
Ubuntu |
<version>-ubuntu26.04, <version>-slim-ubuntu26.04 |
Ubuntu 26.04 |
alpine, slim-alpine |
Alpine |
<version>-alpine, <version>-slim-alpine |
Alpine |
<major>-alpine, <major>-slim-alpine |
Alpine |
<version>-alpine3.24, <version>-slim-alpine3.24 |
Alpine 3.24 |
The images are currently built on Ubuntu 26.04 and Alpine 3.24. Tags without a distro version move to the next distro release when the project does; tags ending in ubuntu26.04 or alpine3.24 stay on that release and are no longer rebuilt once the project moves to the next one. Every tag of the current OpenSSH version, including the exact version, is rebuilt in place (see Updates), so pin a digest when you need the same bytes every time.
linux/amd64 and linux/arm64, for both Ubuntu and Alpine. Both are compiled natively on GitHub-hosted runners, without emulation.
The working directory is /work. The image runs as UID 1000, and any other UID works too: HOME is then /, and caches go to /cache, which anyone can write to. How to get output files owned by you depends on how you run containers:
| Runtime | Flag |
|---|---|
| Docker on Linux (rootful), GitHub Actions | --user "$(id -u):$(id -g)" |
| Rootless Podman | --userns=keep-id |
| Rootless Docker | --user 0:0 (root in the container is your user on the host) |
| Docker Desktop on macOS or Windows | none, file ownership is mapped for you |
The same flag lets ssh read keys and config files that only you can read.
Use a slim tag as the base for your own image. slim, slim-ubuntu and slim-alpine move to each new OpenSSH release and are rebuilt when the base image changes. ssh reads /etc/ssh/ssh_config and /etc/ssh/ssh_known_hosts for every user, so an image for a CI job can carry its settings and the host keys it trusts:
FROM ghcr.io/randomcontainers/openssh-client:slim-ubuntu@sha256:...
COPY ssh_config /etc/ssh/ssh_config
COPY known_hosts /etc/ssh/ssh_known_hostsUse slim-alpine for the Alpine image. To install packages, switch to USER root and back to USER 1000:1000 afterwards. The entrypoint is ["tini", "--", "ssh"]; set your own ENTRYPOINT if your image runs something else. To pick up new OpenSSH releases and base image fixes, let Dependabot or Renovate update the digest in your FROM line.
Everything the image adds is under /usr/local. /usr/local/share/randomcontainers/openssh-client/ holds the version, the source URLs, the build options, the license files and runtime-deps, the list of distro packages OpenSSH needs at run time.
Each image has a build provenance attestation from this repository's GitHub Actions run, signed by the shared build workflow in randomcontainers/ci:
gh attestation verify oci://ghcr.io/randomcontainers/openssh-client:latest \
--repo randomcontainers/openssh-client --signer-repo randomcontainers/ciEach platform image also carries an SPDX SBOM that lists every distro package with its version:
docker buildx imagetools inspect ghcr.io/randomcontainers/openssh-client:latest --format '{{ json .SBOM }}'Before compiling, the build checks the tarball against the SHA-256 recorded in package.yml and its signature against the OpenSSH release signing key in keys/openssh-release.gpg (Damien Miller, fingerprint 7168 B983 815A 5EEF 59A4 ADFD 2A3F 414E 7360 60BA).
The project checks the V_<major>_<minor>_P<n> tags of openssh/openssh-portable every 15 minutes. A release is picked up once it is 2 hours old and its tarball and signature are on cdn.openbsd.org; the wait is shorter than the usual 24 hours because most OpenSSH releases fix security issues. The new version and the tarball's SHA-256 are then committed to package.yml and the images are rebuilt. A release signed with a key that is not in keys/openssh-release.gpg fails to build until the key is added. Only the newest release is built; tags of older versions stay as they were last built.
The images of the current version are also rebuilt when the Ubuntu or Alpine base image changes and at least every 7 days, so distro security fixes, including those for OpenSSL, reach the current tags.
docker build -f Dockerfile.ubuntu --target slim \
--build-arg VERSION=<version> \
--build-arg SOURCE_SHA256=<sha256 from package.yml> \
-t openssh-client:local .Use Dockerfile.alpine for the Alpine image. --build-arg JOBS=<n> limits the number of parallel compile jobs.
OpenSSH's LICENCE file lists the terms of its parts, identified together as SSH-OpenSSH. The ML-KEM and ML-DSA code compiled into the tools comes from Cryspen's libcrux, which LICENCE does not cover, and is used under the MIT license, so the image's license label is SSH-OpenSSH AND MIT. LICENCE and the libcrux notice (LICENSE.libcrux) are in /usr/local/share/randomcontainers/openssh-client/licenses/. nss_wrapper, OpenSSL and the other Ubuntu and Alpine packages in the image keep their own licenses.
Every OpenSSH version has a GitHub release in this repository, named v<version>, with the exact openssh-<version>.tar.gz that was compiled and its signature. The build applies no patches. /usr/local/share/randomcontainers/openssh-client/source lists that release and the upstream download URLs.
The files in this repository are available under the MIT license, see LICENSE. The image contains one of them, wrapper.sh, so its licenses/ directory also has a copy of that license as LICENSE.wrapper.
To suggest another tool, use the Request a tool form.