This describes how I set up TPM2 hardware-backed SSH keys using tpm2-pkcs11 on NixOS. Keys are generated inside the TPM and cannot be extracted, providing hardware-bound authentication.
- Uses the kernel resource manager (
/dev/tpmrm0) directly—notpm2-abrmddaemon needed - Uses the
esysdbbackend instead of FAPI for simpler configuration - Integrates with
passfor PIN storage (non-interactive operation) - Provides helper scripts for key management
- Sets up a systemd user service for ssh-agent with the TPM2 PKCS#11 provider whitelisted
After enabling the module, these commands are available:
| Command | Description |
|---|---|
ssh-tpm-keygen <label> [algorithm] |
Create a new key in the TPM |
ssh-tpm-list |
Show public keys from the TPM |
ssh-tpm-delkey <label> |
Remove a key from the TPM |
ssh-tpm-load |
Load TPM keys into ssh-agent |
ssh-tpm-unload |
Unload TPM keys from ssh-agent |
Supported algorithms: ecc256 (default), ecc384, rsa2048, rsa3072, rsa4096
pass insert tpm2/ssh-pintpm2_ptool init --path ~/.local/share/tpm2-pkcs11
tpm2_ptool addtoken \
--path ~/.local/share/tpm2-pkcs11 \
--label ssh \
--sopin <your-so-pin> \
--userpin $(pass show tpm2/ssh-pin | head -1)ssh-tpm-keygen my-tpm-key ecc256This outputs the public key in OpenSSH format—add it to your ~/.ssh/authorized_keys or GitHub/GitLab.
ssh-tpm-load
ssh-add -l # verifyThe module sets these automatically:
TPM2_PKCS11_STORE = "~/.local/share/tpm2-pkcs11";
TPM2_PKCS11_BACKEND = "esysdb";
TPM2_PKCS11_LOG_LEVEL = "1";
TSS2_LOG = "fapi+NONE";
TSS2_TCTI = "device:/dev/tpmrm0";# Enable TPM2
security.tpm2.enable = true;
security.tpm2.abrmd.enable = false; # using kernel resource manager directly
# Add user to tss group for /dev/tpmrm0 access
users.users.youruser.extraGroups = [ "tss" ];
# System-wide environment (for ssh-pkcs11-helper)
environment.variables = {
TPM2_PKCS11_BACKEND = "esysdb";
TSS2_LOG = "fapi+NONE";
TSS2_TCTI = "device:/dev/tpmrm0";
};commit 0a13efce428fb3421a6e61e713c4dc5b4f56f5df
Author: Andrew McDermott <aim@frobware.com>
Date: Sat Dec 27 19:57:27 2025 +0000
Add TPM2 PKCS#11 SSH key support
Introduce a home-manager module for TPM2-backed SSH keys using the
tpm2-pkcs11 library. Keys are generated inside the TPM and cannot be
extracted, providing hardware-bound authentication.
Helper scripts:
- ssh-tpm-keygen: create keys in TPM
- ssh-tpm-list: show public keys
- ssh-tpm-delkey: remove keys
- ssh-tpm-load/unload: manage ssh-agent PKCS#11 provider
The module creates a systemd user service running ssh-agent with the
TPM2 PKCS#11 provider whitelisted. PIN is retrieved from pass
(tpm2/ssh-pin) for non-interactive operation.
Uses the kernel resource manager (/dev/tpmrm0) directly, avoiding the
need for tpm2-abrmd. The esysdb backend is used instead of FAPI for
simpler configuration.
commit aba7cc1f1d509ec4721fd3f9a923550b01c9584c
Author: Andrew McDermott <aim@frobware.com>
Date: Sat Dec 27 20:02:54 2025 +0000
Set TPM2 environment variables system-wide
Move TPM2 PKCS#11 configuration to system environment so that
ssh-pkcs11-helper inherits them. This suppresses the FAPI backend
warnings that appeared during SSH operations using PKCS11Provider.
The full module (home/configs/tpm2.nix):
{ config, lib, pkgs, ... }:
# TPM2 ssh-agent configuration with PKCS#11 support
#
# This creates a systemd user service for ssh-agent with the TPM2
# PKCS#11 provider whitelisted via the -P flag.
#
# Requirements:
# - TPM2 must be enabled at system level
# - User must be in the 'tss' group (for /dev/tpmrm0 access)
#
# Uses the kernel resource manager (/dev/tpmrm0) directly, no abrmd needed.
#
# Usage after enabling:
# ssh-add -s ${pkgs.tpm2-pkcs11}/lib/libtpm2_pkcs11.so
# ssh-keygen -D ${pkgs.tpm2-pkcs11}/lib/libtpm2_pkcs11.so # export public key
with lib;
let
cfg = config.frobware.configs.tpm2;
gpgAgentCfg = config.frobware.configs.gpg-agent;
sshAgentSocketDir = "${config.home.homeDirectory}/.ssh";
sshAgentSocket = "${sshAgentSocketDir}/agent.sock";
in
{
options.frobware.configs.tpm2 = {
enable = mkEnableOption "TPM2 PKCS#11 configuration";
enableSshAgent = mkOption {
type = types.bool;
default = true;
description = "Enable ssh-agent with TPM2 PKCS#11 support.";
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = !(cfg.enableSshAgent && gpgAgentCfg.enable && gpgAgentCfg.enableSshSupport);
message = "tpm2.enableSshAgent and gpg-agent.enableSshSupport are mutually exclusive. Both set SSH_AUTH_SOCK.";
}
];
home.packages = [
pkgs.tpm2-pkcs11
pkgs.tpm2-tools
pkgs.yq-go
# Helper script to create a new TPM SSH key
(pkgs.writeShellScriptBin "ssh-tpm-keygen" ''
set -e
LABEL="''${1:-}"
ALGORITHM="''${2:-ecc256}"
if [ -z "$LABEL" ]; then
echo "Usage: ssh-tpm-keygen <label> [algorithm]" >&2
echo "Algorithms: ecc256 (default), ecc384, rsa2048, rsa3072, rsa4096" >&2
exit 1
fi
case "$ALGORITHM" in
ecc256|ecc) ALGORITHM="ecc256" ;;
ecc384) ALGORITHM="ecc384" ;;
rsa2048|rsa) ALGORITHM="rsa2048" ;;
rsa3072) ALGORITHM="rsa3072" ;;
rsa4096) ALGORITHM="rsa4096" ;;
*) echo "Unknown algorithm: $ALGORITHM" >&2; exit 1 ;;
esac
PIN=$(pass show tpm2/ssh-pin 2>/dev/null | head -1)
if [ -z "$PIN" ]; then
echo "Error: Could not retrieve PIN from pass (tpm2/ssh-pin)" >&2
exit 1
fi
tpm2_ptool addkey \
--path "$TPM2_PKCS11_STORE" \
--label ssh \
--userpin "$PIN" \
--key-label "$LABEL" \
--algorithm "$ALGORITHM" >/dev/null
ssh-keygen -D "$TPM2_PKCS11_LIB" 2>/dev/null | grep "$LABEL"
'')
# Helper script to list TPM SSH public keys
(pkgs.writeShellScriptBin "ssh-tpm-list" ''
OBJECTS=$(tpm2_ptool listobjects --path "$TPM2_PKCS11_STORE" --label ssh 2>/dev/null)
if [ -z "$OBJECTS" ]; then
exit 0
fi
askpass=$(mktemp)
trap "rm -f $askpass" EXIT
cat > "$askpass" << 'EOF'
#!/bin/sh
exec pass show tpm2/ssh-pin 2>/dev/null | head -1
EOF
chmod +x "$askpass"
SSH_ASKPASS="$askpass" SSH_ASKPASS_REQUIRE=force \
ssh-keygen -D "$TPM2_PKCS11_LIB" 2>/dev/null
'')
# Helper script to delete a TPM SSH key
(pkgs.writeShellScriptBin "ssh-tpm-delkey" ''
set -e
LABEL="''${1:-}"
if [ -z "$LABEL" ]; then
echo "Usage: ssh-tpm-delkey <label>" >&2
exit 1
fi
OBJECTS=$(tpm2_ptool listobjects --path "$TPM2_PKCS11_STORE" --label ssh 2>/dev/null)
IDS=$(echo "$OBJECTS" | ${pkgs.yq-go}/bin/yq -r ".[] | select(.CKA_LABEL == \"$LABEL\") | .id")
if [ -z "$IDS" ]; then
echo "Key not found: $LABEL" >&2
exit 1
fi
for ID in $IDS; do
tpm2_ptool objdel --path "$TPM2_PKCS11_STORE" "$ID" >/dev/null
done
'')
# Helper script to load TPM SSH keys into agent using PIN from pass
(pkgs.writeShellScriptBin "ssh-tpm-load" ''
OBJECTS=$(tpm2_ptool listobjects --path "$TPM2_PKCS11_STORE" --label ssh 2>/dev/null)
if [ -z "$OBJECTS" ]; then
echo "No keys in TPM token" >&2
exit 1
fi
${pkgs.openssh}/bin/ssh-add -e ${pkgs.tpm2-pkcs11}/lib/libtpm2_pkcs11.so >/dev/null 2>&1 || true
askpass=$(mktemp)
trap "rm -f $askpass" EXIT
cat > "$askpass" << 'EOF'
#!/bin/sh
exec pass show tpm2/ssh-pin 2>/dev/null | head -1
EOF
chmod +x "$askpass"
SSH_ASKPASS="$askpass" SSH_ASKPASS_REQUIRE=force \
${pkgs.openssh}/bin/ssh-add -s ${pkgs.tpm2-pkcs11}/lib/libtpm2_pkcs11.so >/dev/null
'')
# Helper script to unload TPM SSH keys from agent
(pkgs.writeShellScriptBin "ssh-tpm-unload" ''
${pkgs.openssh}/bin/ssh-add -e ${pkgs.tpm2-pkcs11}/lib/libtpm2_pkcs11.so >/dev/null 2>&1 || true
'')
];
# TPM2 environment variables
home.sessionVariables = {
TPM2_PKCS11_STORE = "${config.home.homeDirectory}/.local/share/tpm2-pkcs11";
TPM2_PKCS11_LIB = "${pkgs.tpm2-pkcs11}/lib/libtpm2_pkcs11.so";
TPM2_PKCS11_BACKEND = "esysdb";
TPM2_PKCS11_LOG_LEVEL = "1";
TSS2_LOG = "fapi+NONE";
TSS2_TCTI = "device:/dev/tpmrm0";
} // optionalAttrs cfg.enableSshAgent {
SSH_AUTH_SOCK = sshAgentSocket;
};
systemd.user.services.ssh-agent-tpm2 = mkIf cfg.enableSshAgent {
Unit = {
Description = "SSH Agent with TPM2 PKCS#11 support";
Documentation = "man:ssh-agent(1)";
};
Install = {
WantedBy = [ "default.target" ];
};
Service = {
Type = "simple";
Environment = [
"TPM2_PKCS11_STORE=${config.home.homeDirectory}/.local/share/tpm2-pkcs11"
"TPM2_PKCS11_BACKEND=esysdb"
"TPM2_PKCS11_LOG_LEVEL=1"
"TSS2_LOG=fapi+NONE"
"TSS2_TCTI=device:/dev/tpmrm0"
];
ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${sshAgentSocketDir}";
ExecStart = "${pkgs.openssh}/bin/ssh-agent -D -a ${sshAgentSocket} -P '${pkgs.tpm2-pkcs11}/lib/*.so*'";
Restart = "on-failure";
};
};
};
}{
frobware.configs = {
gpg-agent.enable = true;
gpg-agent.enableSshSupport = false; # mutually exclusive with TPM2 ssh-agent
tpm2.enable = true;
};
}