Complete guide for storing SSH keys in a TPM2 module with automatic loading via systemd and KWallet integration on KDE Plasma.
This setup provides:
- Hardware-backed SSH keys stored in TPM2 (can't be extracted)
- Automatic unlock at KDE login using KWallet
- PIN protection (not passwordless, but PIN cached per session)
- Works with both SSH and Git commit signing
- TPM 2.0 module (check:
ls /dev/tpm*) - Arch Linux (or similar with systemd)
- KDE Plasma with KWallet
- User in
tssgroup:sudo usermod -aG tss $USER
sudo pacman -S tpm2-tools tpm2-tss tpm2-pkcs11 tpm2-abrmd# Set environment variable
export TPM2_PKCS11_STORE="$HOME/.tpm2_pkcs11"
# Initialize the store
tpm2_ptool init# Create a token (label: "ssh")
# Replace YOUR_SOPIN and YOUR_USERPIN with secure PINs
# SOPIN = recovery/admin PIN
# USERPIN = daily-use PIN
export TPM2_PKCS11_STORE="$HOME/.tpm2_pkcs11"
tpm2_ptool addtoken --pid=1 --label=ssh --sopin="YOUR_SOPIN" --userpin="YOUR_USERPIN"
# Create ECC key (recommended over RSA for TPM)
tpm2_ptool addkey --algorithm=ecc256 --label=ssh --userpin="YOUR_USERPIN"# Extract public key for adding to servers
export TPM2_PKCS11_TCTI=tabrmd:
ssh-keygen -D /usr/lib/libtpm2_pkcs11.so.0 > ~/.ssh/tpm_ecdsa.pub
# View it
cat ~/.ssh/tpm_ecdsa.pubFile: ~/.config/environment.d/ssh_auth_socket.conf
Note: These files should NOT be executable.
# SSH agent socket
SSH_AUTH_SOCK=$XDG_RUNTIME_DIR/ssh-agent.socket
# TPM2 PKCS11 store location
TPM2_PKCS11_STORE=$HOME/.tpm2_pkcs11
# Use tpm2-abrmd daemon for TPM access
TPM2_PKCS11_TCTI=tabrmd:
File: ~/.config/systemd/user/ssh-agent.service.d/tpm-env.conf
Create the directory first:
mkdir -p ~/.config/systemd/user/ssh-agent.service.dThen create the file:
[Service]
# TPM2 environment variables needed by ssh-agent's PKCS#11 helper
# %h expands to the user's home directory
Environment="TPM2_PKCS11_STORE=%h/.tpm2_pkcs11"
Environment="TPM2_PKCS11_TCTI=tabrmd:"
Open KWalletManager:
- Navigate to the
Passwordsfolder - Create new entry named:
tpm2-pkcs11-pin - Set value to your TPM USERPIN
- Save
File: ~/.local/bin/tpm-askpass.sh
#!/bin/bash
# Askpass script for TPM PKCS#11 token
# Retrieves PIN from KWallet for automatic ssh-agent loading
# This is called by ssh-add when it needs the PKCS#11 PIN
# Note: kdewallet is the default wallet name
kwallet-query -r tpm2-pkcs11-pin kdewallet -f Passwords 2>/dev/nullMake it executable:
chmod +x ~/.local/bin/tpm-askpass.shFile: ~/.local/bin/load-tpm-ssh.sh
#!/bin/bash
# Load TPM-backed SSH key into ssh-agent using PIN from KWallet
# This script is called by tpm-ssh-loader.service systemd unit
# Path to tpm2-pkcs11 library
TPM2_PKCS11_SO="/usr/lib/libtpm2_pkcs11.so.0"
# Path to askpass script
ASKPASS_SCRIPT="$HOME/.local/bin/tpm-askpass.sh"
# Check if TPM key is already loaded (avoid duplicate entries)
if ssh-add -l 2>/dev/null | grep -q "tpm2-pkcs11"; then
echo "TPM key already loaded in ssh-agent"
exit 0
fi
# Configure ssh-add to use our askpass script
export SSH_ASKPASS="$ASKPASS_SCRIPT"
export SSH_ASKPASS_REQUIRE=force
export DISPLAY=""
# Load TPM key into ssh-agent
# The </dev/null ensures no TTY is used, forcing askpass
ssh-add -s "$TPM2_PKCS11_SO" </dev/null
if [ $? -eq 0 ]; then
echo "TPM-backed SSH key loaded successfully"
else
echo "Failed to load TPM key"
exit 1
fiMake it executable:
chmod +x ~/.local/bin/load-tpm-ssh.shFile: ~/.config/systemd/user/tpm-ssh-loader.service
[Unit]
Description=Load TPM-backed SSH key into ssh-agent
After=ssh-agent.service dbus.socket graphical-session.target
Wants=ssh-agent.service dbus.socket
# Ensure loader re-runs if ssh-agent restarts
BindsTo=ssh-agent.service
PartOf=graphical-session.target
[Service]
Type=oneshot
# Environment variables
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
Environment=TPM2_PKCS11_STORE=%h/.tpm2_pkcs11
Environment=TPM2_PKCS11_TCTI=tabrmd:
# Display (needed for KWallet communication)
Environment=DISPLAY=:0
# D-Bus session bus address (needed for KWallet)
# %t expands to /run/user/UID
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=%t/bus
# Wait a moment for KWallet and Agent to be fully ready
ExecStartPre=/bin/sleep 5
# Load the TPM key
ExecStart=%h/.local/bin/load-tpm-ssh.sh
# Keep service marked as active
RemainAfterExit=yes
[Install]
WantedBy=graphical-session.target
# Also trigger whenever ssh-agent starts
WantedBy=ssh-agent.serviceNo changes required to ~/.ssh/config for this setup!
The TPM key is loaded into ssh-agent automatically, and SSH will use it via the SSH_AUTH_SOCK environment variable. The agent handles the PKCS#11 communication.
Note: Do NOT add PKCS11Provider to .ssh/config - this would bypass ssh-agent and prompt for PIN on every connection, defeating the purpose of our auto-loading setup.
If you previously added it for testing, remove or comment it out:
# Do NOT use this with our setup:
# PKCS11Provider /usr/lib/libtpm2_pkcs11.so.0
For testing only (not for daily use):
# Direct PKCS#11 access - bypasses agent, prompts for PIN every time
ssh -I /usr/lib/libtpm2_pkcs11.so.0 user@server# Reload systemd user daemon
systemctl --user daemon-reload
# Enable ssh-agent (if not already enabled)
systemctl --user enable --now ssh-agent.service
# Enable TPM loader service
systemctl --user enable --now tpm-ssh-loader.service
# Enable tpm2-abrmd (system-wide)
sudo systemctl enable --now tpm2-abrmd.serviceFile: ~/.ssh/allowed_signers
Add your TPM public key:
YOUR_EMAIL@example.com ecdsa-sha2-nistp256 AAAAE2VjZHNh... (your key from tpm_ecdsa.pub)
Configure Git:
# Use SSH format for signatures
git config --global gpg.format ssh
# Set signing key (path to your public key)
git config --global user.signingkey ~/.ssh/tpm_ecdsa.pub
# Enable signing by default
git config --global commit.gpgsign true
# Set allowed signers file
git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers# Should connect without prompting for PIN (already cached)
ssh user@your-server
# Verify key is loaded
ssh-add -l
# Output: 256 SHA256:... tpm2-pkcs11 (ECDSA)# Make a test commit
git commit -m "Test TPM signed commit"
# Verify signature
git verify-commit HEAD# Check TPM loader service
systemctl --user status tpm-ssh-loader.service
# View logs
journalctl --user -u tpm-ssh-loader.service -n 50
# Check ssh-agent
systemctl --user status ssh-agent.service
### Verify Agent Environment
The most common failure is the `ssh-agent` not having the TPM variables. Verify with:
```bash
# tr replaces null bytes with newlines for readability
sudo tr '\0' '\n' < /proc/$(pgrep -u $USER ssh-agent)/environ | grep -E "TPM|SSH"If you run Emacs (or other apps) as a systemd user service, they might have hardcoded environment variables that conflict with your new setup.
Check your emacs.service (usually in ~/.config/systemd/user/emacs.service):
- Remove:
Environment=SSH_AUTH_SOCK=%t/keyring/ssh(this is often a leftover from GNOME Keyring) - Add:
After=ssh-agent.serviceandWants=ssh-agent.serviceto the[Unit]section.
# Test KWallet access
~/.local/bin/tpm-askpass.sh | wc -c
# Should output number > 0
# Test TPM key listing
export TPM2_PKCS11_TCTI=tabrmd:
ssh-keygen -D /usr/lib/libtpm2_pkcs11.so.0
# Manual load (for debugging)
SSH_ASKPASS=~/.local/bin/tpm-askpass.sh \
SSH_ASKPASS_REQUIRE=force \
ssh-add -s /usr/lib/libtpm2_pkcs11.so.0 </dev/null"Could not add card: agent refused operation"
- Ensure ssh-agent has TPM environment variables (Step 5)
- Check tpm2-abrmd is running:
sudo systemctl status tpm2-abrmd - Verify KWallet entry exists and is accessible
"loaded 0 certificates"
- Check TPM token exists:
tpm2_ptool listtokens --pid=1 - Verify PIN in KWallet matches TPM userpin
- Ensure
TPM2_PKCS11_TCTI=tabrmd:is set
KWallet not accessible
- Make sure
DBUS_SESSION_BUS_ADDRESSis set in the service - Verify
DISPLAYenvironment variable is set - KWallet must be unlocked before the service runs
- The TPM userpin is stored in KWallet (encrypted)
- The sopin should be stored separately as a backup
- Physical access to the running system grants SSH access (key is in agent)
- The TPM key cannot be extracted from the hardware
- If the TPM is cleared or replaced, the keys are lost (unless backed up via export)
~/.config/environment.d/ssh_auth_socket.conf # Environment variables
~/.config/systemd/user/ssh-agent.service.d/tpm-env.conf # ssh-agent TPM config
~/.config/systemd/user/tpm-ssh-loader.service # Auto-loader service
~/.local/bin/tpm-askpass.sh # KWallet PIN retriever
~/.local/bin/load-tpm-ssh.sh # TPM loader script
~/.ssh/config # SSH configuration
~/.ssh/tpm_ecdsa.pub # Public key (extracted)
~/.ssh/allowed_signers # Git signing allowed keys
~/.tpm2_pkcs11/ # TPM database (auto-created)
Based on tpm2-pkcs11 documentation and various Arch Linux TPM guides. The critical fix was ensuring environment variables are properly passed to both ssh-agent and the loader service.
Note: Set secure values for YOUR_SOPIN and YOUR_USERPIN.