A recent post by Christian Bshaatsbergen walks through a technique more Go developers should know: terminating TLS with a private key that exists only inside a TPM and never touches process memory. The post, Signing TLS handshakes inside a TPM, demonstrates the whole chain with a small library, and the mechanics are worth unpacking.
Why a TPM in the TLS path
The classic failure mode for TLS private keys is simple: the key is a file on disk, loaded into process memory, and any memory dump, container escape, or misconfigured backup walks away with it. Hardware security modules solve this, but HSMs are expensive and operationally heavy. A TPM ships in effectively every server-class machine and most laptops already. The trick is making Go’s TLS stack use it.
Go’s crypto/tls package only needs something implementing crypto.Signer to sign the handshake transcript. It does not care where the signing happens. That is the opening. The post uses the go-tpm-tls library, which wraps a TPM-resident key in a crypto.Signer implementation and hands it to a standard tls.Certificate. When the handshake reaches the CertificateVerify step, the signing operation is forwarded to the TPM. The private key material is generated inside the TPM with the fixedtpm and sensitivedataorigin attributes, which means it was created there and cannot be duplicated out.
The performance picture
Hardware signing is slower than software, and the post quantifies it honestly. A persistent TPM key signs in roughly 2.2 milliseconds per handshake against about 0.06 milliseconds for a software key. Transient TPM keys, which are created on demand and evicted after use, run about ten times slower than persistent ones, so persisting the key at a fixed handle is the right default.
That puts a single TPM at a few hundred new TLS connections per second. For an internal mTLS service, a sidecar, or an edge node with connection pooling, that is plenty. For a public endpoint terminating thousands of fresh handshakes per second, it is not, and session resumption becomes essential rather than optional. TLS session resumption and 0-RTT modes skip the expensive CertificateVerify operation for returning clients, so the TPM only signs during the initial handshake. Architecture matters more than raw speed here: put the TPM-backed signer at a layer where connections are long-lived, not one where every request opens a fresh TLS session.
There is also a contention dimension. A TPM is a single hardware resource, and signing operations serialize. Multiple processes hammering the same TPM will queue behind each other, which is another reason the resource manager device matters: it isolates contexts but does not add signing parallelism. If a node runs several services that each need hardware-bound identities, budget for the combined signing rate, not each service in isolation.
Algorithm constraints matter
One finding will surprise people: RSA does not work here for TLS 1.3. Go’s TLS implementation requires RSA-PSS signatures with a specific salt length that TPM 2.0 cannot produce. ECDSA keys on the P-256 or P-384 curves work fine. The practical rule: if you are provisioning TPM-backed TLS keys, generate ECDSA, not RSA.
The post also covers device access details that trip people up in practice. Using /dev/tpmrm0, the kernel’s TPM resource manager, gives each process its own descriptor context and avoids the object eviction races you get when multiple processes share the raw device. Keys should be unrestricted and created without an auth value so the TLS stack can sign without interactive policy sessions.
What it does and does not buy you
The security gain is specific and real: the key is non-exportable, so exfiltration of the key itself is off the table. A memory dump reveals only the handshake transcript, not the signing material. Attestation is a bonus path, since the TPM can prove to a remote party which key signed a given handshake and what firmware booted the machine.
The boundary matters though. A compromised host process can still ask the TPM to sign things. The TPM enforces key residency, not key usage policy. If an attacker gets code execution on the box, they can abuse the signer until you rotate the key. This makes TPM-backed TLS a strong complement to least-privilege service design, not a replacement for it.
Where this fits in 2026
Go 1.24 added post-quantum key exchange via ML-KEM, but the TLS signing API surface is unchanged, which means TPM-backed signing composes with the new key exchange modes without modification. The interest in hardware-bound credentials is also climbing for a separate reason: confidential computing. Confidential VMs on every major cloud expose a vTPM, and binding workload identities to it is one of the few ways to make a workload credential meaningful when you do not trust the host operator.
For teams running sensitive services, the takeaway is concrete. You do not need an HSM procurement cycle to stop storing private keys as files. If your fleet has TPMs, a crypto.Signer shim and a provisioning script get you hardware-bound mTLS identities today, with the caveats above on throughput and algorithms understood up front.