Open-source project
Foxboron/ssh-tpm-agent avatar
Foxboron/ssh-tpm-agent

ssh-tpm-agent: SSH keys sealed inside the TPM

:computer: :key: ssh-agent for TPMs

768 stars42 forksGoMIT

At a glance

What is it?
ssh-tpm-agent is an MIT licensed Go implementation of an ssh-agent compatible service that keeps private keys sealed inside a TPM 2.0 chip, with PIN support, remote key wrapping, session encryption and a proxy mode that falls back to another agent. The judgement is that the win is a native client instead of side-loaded PKCS#11 libraries, and that the swtpm mode is explicitly for testing rather than for use.
Who is it for?
ssh-tpm-agent fits a Linux workstation that has a TPM and an organisation that wants SSH private keys to be hardware bound and non-exportable, because the agent speaks the same protocol as the OpenSSH agent, so ssh, git and every other client work unchanged. It is the wrong choice on a machine without a TPM, or for a user who needs to move a key between machines, since a sealed key is bound to the hardware that produced it.
Can I use it commercially?
Yes. MIT 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 21 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 October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

An agent whose private keys never leave the chip

ssh-tpm-agent is an ssh-agent compatible agent that allows keys to be created by the Trusted Platform Module for authentication towards ssh servers. It is a normal agent from the outside, the same protocol, the same SSH_AUTH_SOCK, the same ssh-add and ssh commands.

The difference is where the private key lives. TPM sealed keys are private keys created inside the Trusted Platform Module and sealed in files with a .tpm suffix. They are bound to the hardware they are produced on and cannot be transferred to other machines. The sealed file on disk is a blob, not a key, and the agent asks the chip to do the signing.

The readme gives the motivation in terms of what you avoid. Instead of having to side load existing PKCS#11 libraries into the ssh-agent and/or the ssh client, you use a native client. That is a real operational difference on a desktop, where the usual approach is a library that has to be present in both the agent and every client that needs it, and where the two sides can disagree about which provider is available.

The key format underneath is the TPM 2.0 Key Files draft, implemented through the go-tpm-keyfiles project by the same author, which is a design that lets the sealed file be a portable artefact even though the key it wraps is not.

PINs, session encryption and a proxy fallback

The feature list is short, and three of the entries are about what you can do with the chip beyond signing.

PIN support comes with a specific property. The readme says the dictionary attack protection from the TPM allows you to use low entropy PINs instead of passphrases, which is the argument that makes a PIN a reasonable choice here. A TPM has a mechanism for counting failed attempts and blocking further ones, so the weakness of a short PIN is mitigated by the hardware rather than by the entropy of the secret itself.

TPM session encryption is the second, which protects the channel between the agent and the chip with a key held in the TPM rather than a key the agent has to store.

The third is proxy support towards other ssh-agent servers, and the readme calls it a fallback. The agent can sit in front of an existing ssh-agent and serve both sets of identities, which means a machine can have a TPM key for the things that matter and ordinary keys for everything else, without changing any client configuration.

There is also support for creating remotely wrapped SSH keys for import, which is the feature that makes a hardware bound key usable in a provisioning flow, and that is covered separately below.

The project also has a software mode. Instead of using the TPM directly you can use --swtpm or set SSH_TPM_AGENT_SWTPM=1 to create an identity backed by swtpm, stored under /var/tmp/ssh-tpm-agent. The readme is clear that swtpm provides no security properties and should only be used for testing.

Installing it and pointing a shell at it

Installation is a single go install, or a download of the pre-built binaries from the releases page:

text
go install github.com/foxboron/ssh-tpm-agent/cmd/...@latest

Using it follows the shape of any agent. You create a key with ssh-tpm-keygen, which generates a sealed public and private ECDSA key pair, writes the sealed key with the .tpm suffix and the public key with the .pub suffix, and prints the fingerprint. You then point the agent at the TPM and export the socket path:

text
$ ssh-tpm-agent -l /var/tmp/tpm.sock
$ export SSH_AUTH_SOCK="$(ssh-tpm-agent --print-socket)"
$ ssh [email protected]

The TPM owner password is the one piece of configuration that needs a decision. The readme says you can specify it with the command line flags -o or --owner-password, which are preferred, and alternatively through the environment variable SSH_TPM_AGENT_OWNER_PASSWORD. The word preferred is doing work in that sentence, and it is best understood before you set it up: a command line flag ends up in the process list and in your shell history, while an environment variable ends up in the environment of the process, and neither is a place a secret belongs in over a long period. The readme does not discuss that trade-off, so it is a decision the adopter has to make.

Importing an existing key so it can live in two places

The import path is the one that makes the hardware binding practical, and it is easy to miss.

You can create an ordinary key with ssh-keygen and then seal it into the TPM with ssh-tpm-keygen --import, which takes the existing public and private pair and writes the sealed .tpm file. The fingerprint stays the same, because it is the same key.

The readme explains the use case: it is useful if you want to back up the key to a remote secure storage while using the key day to day from the TPM. That is the pattern that resolves the usual objection to hardware bound keys, which is that you cannot keep a backup, and it also lets you migrate a key that already exists rather than generating a new one and changing it everywhere.

The reverse direction also exists, and it is what the wrapping feature is for. On the client side you create a primary key under a hierarchy, the example uses the owner hierarchy with a storage root key, and the resulting srk.pem file is transferred to the remote end. That end then creates a p256 SSH key with no password, either with ssh-keygen or with openssl, and wraps it with ssh-tpm-keygen --wrap-with using the srk.pem from the client side.

The point of the arrangement is that the private key is generated on the machine that will use it, and the client holds something that can unseal it, so the key never travels in the clear and never sits on the server.

Socket activation and the proxy mode for mixed setups

Two operational details are documented in detail and both matter for a desktop rollout.

The first is systemd socket activation. The agent installs user units for itself:

text
$ ssh-tpm-agent --install-user-units
Installed /home/fox/.config/systemd/user/ssh-tpm-agent.socket
Installed /home/fox/.config/systemd/user/ssh-tpm-agent.service
Enable with: systemctl --user enable --now ssh-tpm-agent.socket

So the agent only runs when something asks for the socket, which is the right behaviour for a program that holds access to a signing key. The makefile installs both user and system units, and the system unit installation is done by the ssh-tpm-hostkeys binary rather than by the agent.

The second is proxy support. You start the usual agent, add a strong RSA key to it, and then start ssh-tpm-agent with -A pointing at the existing socket. The agent then serves identities from both sources, and ssh-add -L lists them together, the RSA key from the proxied agent and the ECDSA key from the TPM.

There is a related helper, ssh-tpm-add, which adds a sealed .tpm file to a running agent, so that an identity can be loaded the way ssh-add loads an ordinary key. The agent can also be started with --no-load, so it starts with no identities and the user adds them afterwards.

Go 1.25, a Landlock dependency and a signed release tarball

The repository is a Go project with a Makefile that is more conventional than the modern ones, and a few details in it deserve attention.

The module is github.com/foxboron/ssh-tpm-agent and it requires Go 1.25.0. The dependencies are the interesting part: go-tpm and go-tpm-tools from Google for the TPM layer, the author's own go-tpm-keyfiles for the key file format, awnumar/memcall, which is used to keep secrets out of ordinary memory, landlock-lsm/go-landlock for restricting what the process can do, and the usual golang.org/x packages for crypto, syscalls, terminal handling and text.

The Makefile installs to /usr/local by default, builds the binaries with -buildmode=pie and -trimpath, and ships four man pages, for ssh-tpm-agent, ssh-tpm-keygen, ssh-tpm-add and ssh-tpm-hostkeys. The lint target runs go vet and staticcheck, and the test target runs go test with verbose output. The version is taken from the file .tarball-version-git when the source was exported rather than cloned, which is how the tarball builds know their own version.

The release target signs the artefacts with gpg, downloading the release with gh release download, building a tarball with git archive and signing the linux-amd64, linux-arm64 and linux-arm binaries as well as the source tarball.

The release history is v0.7.0 on 2024-10-18, v0.8.0 on 2025-03-26 and v0.9.0 on 2026-05-04, with the last push to the repository on 2026-10-02.

Editorial conclusion

ssh-tpm-agent fits a Linux workstation that has a TPM and an organisation that wants SSH private keys to be hardware bound and non-exportable, because the agent speaks the same protocol as the OpenSSH agent, so ssh, git and every other client work unchanged. It is the wrong choice on a machine without a TPM, or for a user who needs to move a key between machines, since a sealed key is bound to the hardware that produced it. It is also the wrong choice if you plan to use the swtpm mode for anything real, which the readme says provides no security properties and is for testing only. Before you roll it out, decide where the TPM owner password comes from, because the readme prefers the command line flag or the environment variable over an interactive prompt, and read the install target first, since the makefile installs both user and system systemd units.

Frequently asked questions

What does ssh-tpm-agent do differently from the OpenSSH agent?

It keeps private keys sealed inside a TPM 2.0 chip, in files with a .tpm suffix that are bound to the hardware that produced them and cannot be transferred. It speaks the same agent protocol, so ssh, git and other clients work unchanged, and it avoids side-loading PKCS#11 libraries into the agent and the client.

Can I still use a key that I already have?

Yes. ssh-tpm-keygen --import seals an existing public and private ECDSA pair into the TPM, keeping the same fingerprint, which is useful if you want to back the key up to remote storage while using it from the chip day to day.

Is the swtpm mode safe to use for real keys?

No. The readme states that swtpm provides no security properties and should only be used for testing. You enable it with --swtpm or by setting SSH_TPM_AGENT_SWTPM=1, and the identity is stored under /var/tmp/ssh-tpm-agent.

How is the TPM owner password supplied?

With the -o or --owner-password command line flags, which the readme says are preferred, or alternatively with the SSH_TPM_AGENT_OWNER_PASSWORD environment variable. PIN support also relies on the dictionary attack protection built into the TPM, which allows low entropy PINs in place of passphrases.

Official sources

  1. Foxboron/ssh-tpm-agent on GitHub
  2. Issues
  3. License: MIT
  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/foxboron-ssh-tpm-agent.svg)](https://hysenlabs.com/projects/foxboron-ssh-tpm-agent)

Was this useful?