The Problem With File-Based Keys
The standard ~/.ssh/id_ed25519 workflow has one fundamental weakness: the private key is a file. It can be copied, backed up to a cloud drive accidentally, exfiltrated by malware, or read from a memory dump after ssh-agent loads it. A strong passphrase slows an attacker down but does not change the nature of the problem. Once an adversary has the file and the passphrase, the key is compromised permanently, on every server that trusts it.
Hardware security keys solve this differently. The private key is generated inside a dedicated piece of hardware and marked non-exportable. Signing operations happen inside the chip; the private key material never appears in system memory, never touches disk in usable form, and cannot be read out even by an attacker with full root access. This is not a property that software can emulate — it requires hardware enforcement.
Most people reach for a YubiKey at this point, and that is a reasonable choice. There is another option sitting inside almost every laptop and desktop shipped in the last decade, though: the TPM 2.0 chip.
What the TPM Actually Provides
A TPM 2.0 chip (specified by the Trusted Computing Group) is a dedicated security processor with its own key hierarchy. At the root is the Storage Root Key (SRK), derived from a device-unique seed that never leaves the chip. New keys are either stored as persistent objects in the TPM’s non-volatile memory (limited to roughly 7–10 slots on most chips) or generated inside the TPM and returned as encrypted blobs wrapped by the SRK.
The blob model is what matters for SSH. The TPM generates an RSA or ECDSA key pair, encrypts the private half under the SRK using AES-256, and hands you back an opaque blob. That blob is useless without the originating TPM; even if an attacker copies both the blob file and associated metadata, they cannot sign anything. The actual signing operation requires the TPM to load the blob, decrypt it internally, perform the cryptographic operation, and return only the signature.
The TPM2 spec enforces this through key object attributes. A key created with fixedTPM=1 and fixedParent=1 cannot be exported, moved, or duplicated. sensitiveDataOrigin=1 means the key was generated inside the chip, not imported. These are hardware-enforced properties, not access control lists that root can override.
Signing with a TPM key takes 20–200ms depending on the algorithm and chip. ECDSA P-256 is on the faster end; RSA-2048 is slower. For interactive SSH sessions, this latency is imperceptible. For automation that opens hundreds of connections in parallel, it is worth keeping in mind.
Two Tools, Two Approaches
There are two practical ways to use TPM keys for SSH on Linux today. They share the same underlying security properties but differ in setup complexity and how they integrate with the rest of your toolchain.
tpm2-pkcs11: The PKCS#11 Path
tpm2-pkcs11 exposes TPM keys through the PKCS#11 standard interface, which OpenSSH has supported since version 5.4. The setup involves more steps but integrates with anything that speaks PKCS#11, including browsers, GPG, and other SSH clients.
The core workflow on Ubuntu 22.04:
sudo apt install tpm2-tools tpm2-pkcs11 libtpm2-pkcs11-1 libtpm2-pkcs11-tools
# Initialize the key hierarchy and create a token
tpm2_ptool init
tpm2_ptool addtoken --pid=1 --sopin=mysopin --userpin=myuserpin --label=ssh
# Generate a key inside the TPM
tpm2_ptool addkey --algorithm=ecc256 --userpin=myuserpin --label=ssh
# Export the public key in OpenSSH format
ssh-keygen -D /usr/lib/x86_64-linux-gnu/libtpm2_pkcs11.so.1
# Connect using the TPM key
ssh -I /usr/lib/x86_64-linux-gnu/libtpm2_pkcs11.so.1 user@host
The tutorial by Remy van Elst at raymii.org covers this stack end-to-end, including adding the key to ~/.ssh/config via the PKCS11Provider directive so you do not pass -I on every invocation.
Behind the scenes, tpm2_ptool maintains a SQLite database at ~/.tpm2_pkcs11/tpm2_pkcs11.sqlite3 that stores token configuration and key blob references. This database is not secret — the blobs it references are encrypted by the TPM — but losing it means losing access to your keys. Back it up alongside your public key.
On Fedora and RHEL 9, system crypto policies may restrict which PKCS#11 providers OpenSSH will load. You may need a targeted policy override:
sudo update-crypto-policies --set DEFAULT:TEST-PKCS11
The library path also differs by distro: /usr/lib64/pkcs11/libtpm2_pkcs11.so on Fedora, /usr/lib/pkcs11/libtpm2_pkcs11.so on Arch. This is the main friction point when following documentation written for a different distribution.
ssh-tpm-agent: The Simpler Path
ssh-tpm-agent takes a different approach. Written in Go, it bypasses PKCS#11 entirely and implements the ssh-agent protocol directly, talking to the TPM via the go-tpm library.
go install github.com/foxboron/ssh-tpm-agent/cmd/ssh-tpm-agent@latest
go install github.com/foxboron/ssh-tpm-agent/cmd/ssh-tpm-keygen@latest
# Generate a TPM-backed key
ssh-tpm-keygen -t ecdsa -C "my workstation key"
# Outputs: ~/.ssh/id_ecdsa_tpm and ~/.ssh/id_ecdsa_tpm.pub
# Start the agent
export SSH_AUTH_SOCK=$(ssh-tpm-agent --print-socket)
ssh-tpm-agent &
# Use SSH normally
ssh user@host
The key file ~/.ssh/id_ecdsa_tpm contains the TPM key blob in a custom ASN.1/PEM format. It is not a private key; it is an encrypted reference to key material inside the TPM. The .pub file is standard OpenSSH format, usable anywhere.
Systemd socket activation is the cleanest production setup:
systemctl --user enable --now ssh-tpm-agent.socket
export SSH_AUTH_SOCK="${XDG_RUNTIME_DIR}/ssh-tpm-agent.sock"
For most users who only need SSH, ssh-tpm-agent is the more straightforward path. The experience is close to the familiar ssh-keygen workflow, and there are no PKCS#11 library paths to manage across distros. If you also need TPM keys accessible from a browser or GPG, tpm2-pkcs11 is worth the added complexity.
How This Compares to FIDO2 Hardware Keys
OpenSSH 8.2 added native support for FIDO2 security keys via the ecdsa-sk and ed25519-sk key types, without requiring PKCS#11:
# With a YubiKey or Titan key plugged in
ssh-keygen -t ed25519-sk -O verify-required -C "yubikey"
The resulting key file contains only the credential ID, not the private key. The private key lives on the hardware device and never leaves it. Physical touch and optionally a PIN are required to sign.
FIDO2 hardware keys and TPM-backed keys share the same fundamental property: non-exportable private keys enforced by hardware. The differences are practical rather than theoretical.
A FIDO2 key is portable — the same YubiKey works on any machine. A TPM key is machine-specific. If your laptop is stolen or replaced, a TPM-backed key is gone; you generate a new one and update authorized_keys. A FIDO2 key travels with you, which has advantages and drawbacks depending on your threat model.
FIDO2 hardware keys also provide explicit user presence confirmation via physical touch, which the TPM does not enforce by default. With tpm2-pkcs11 and ssh-tpm-agent, you can require a PIN per signing operation, but neither requires physical touch. For shared or managed machines, the FIDO2 model is arguably more appropriate.
The cost difference is material. A TPM chip is already in your machine; there is nothing to buy. A YubiKey 5 NFC costs around $50, and owning two (one as a backup) is the standard recommendation. For a personal laptop where portability is not required, the TPM is the zero-cost option with equivalent key protection.
Where the Security Boundary Actually Sits
It is worth being precise about what TPM-backed keys protect against and what they do not.
They protect against: offline key extraction from a stolen or imaged disk, malware that exfiltrates key files, cold boot attacks (the private key is in the TPM, not DRAM), and any scenario where an attacker gains access to the filesystem but not a running session.
They do not protect against: an attacker who has live access to a running session. If someone compromises your system while you are logged in and ssh-tpm-agent is running, they can use the SSH agent socket to sign arbitrary challenges. They cannot extract the private key, but they can authenticate to any server your key is trusted on for as long as they have session access. Agent forwarding (ssh -A) extends this attack surface to remote servers you connect through.
TPM dictionary lockout provides some protection against PIN brute-forcing. The TPM enforces a lockout after a configurable number of failed authentication attempts (the default is typically 5, with a 24-hour lockout period). This meaningfully protects against an attacker who steals your machine and tries to use the key offline. It is worth verifying your current lockout policy:
tpm2_getcap properties-variable | grep -i lockout
The more advanced countermeasure is PCR sealing: binding key access to specific boot measurements, so the key blob can only be unsealed when the system has booted in the expected state. This prevents an attacker from booting from a live USB and attempting signing operations. Neither tpm2-pkcs11 nor ssh-tpm-agent implement PCR sealing by default, though tpm2-tools provides the primitives to do it manually via tpm2_createpolicy and tpm2_create with a PCR policy.
Getting Started
First, confirm your TPM is accessible:
ls /dev/tpm0 /dev/tpmrm0
tpm2_getcap properties-fixed 2>&1 | head -5
If /dev/tpm0 exists but returns permission errors, add yourself to the tss group:
sudo usermod -aG tss $USER
newgrp tss
Then choose your path. For a simple SSH-only setup, ssh-tpm-agent is faster to get running. For broader PKCS#11 integration, follow the tpm2-pkcs11 stack using the official SSH documentation or the Raymii tutorial as your guide.
The setup takes around 20 minutes. The result is SSH authentication backed by hardware that remains secure even if your disk is imaged, your dotfiles are leaked, or your ~/.ssh directory ends up somewhere it should not. For a zero-cost security upgrade, the TPM approach is worth the time.