Skip to content

Instantly share code, notes, and snippets.

@Kaylebor
Last active March 4, 2026 10:34
Show Gist options
  • Select an option

  • Save Kaylebor/22fc953c8a161aeda97dddd07fdc5626 to your computer and use it in GitHub Desktop.

Select an option

Save Kaylebor/22fc953c8a161aeda97dddd07fdc5626 to your computer and use it in GitHub Desktop.
TPM2-Based SSH Key Setup with Automatic Unlock

TPM2-Based SSH Key Setup with Automatic Unlock

Complete guide for storing SSH keys in a TPM2 module with automatic loading via systemd and KWallet integration on KDE Plasma.

Overview

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

Prerequisites

  • TPM 2.0 module (check: ls /dev/tpm*)
  • Arch Linux (or similar with systemd)
  • KDE Plasma with KWallet
  • User in tss group: sudo usermod -aG tss $USER

Packages Required

sudo pacman -S tpm2-tools tpm2-tss tpm2-pkcs11 tpm2-abrmd

Step-by-Step Setup

1. Initialize TPM2 PKCS#11 Store

# Set environment variable
export TPM2_PKCS11_STORE="$HOME/.tpm2_pkcs11"

# Initialize the store
tpm2_ptool init

2. Create Token and Key

# 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"

3. Extract Public Key

# 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.pub

4. Configure Environment Variables

File: ~/.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:

5. Configure ssh-agent with TPM Support

File: ~/.config/systemd/user/ssh-agent.service.d/tpm-env.conf

Create the directory first:

mkdir -p ~/.config/systemd/user/ssh-agent.service.d

Then 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:"

6. Create KWallet Entry for PIN

Open KWalletManager:

  1. Navigate to the Passwords folder
  2. Create new entry named: tpm2-pkcs11-pin
  3. Set value to your TPM USERPIN
  4. Save

7. Create Askpass Script

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/null

Make it executable:

chmod +x ~/.local/bin/tpm-askpass.sh

8. Create TPM Loader Script

File: ~/.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
fi

Make it executable:

chmod +x ~/.local/bin/load-tpm-ssh.sh

9. Create Systemd Service for Auto-Loading

File: ~/.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.service

10. Configure SSH (Optional)

No 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

11. Enable and Start Services

# 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.service

12. Configure Git Commit Signing (Optional)

File: ~/.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

Verification

Test SSH Connection

# 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)

Test Git Commit Signing

# Make a test commit
git commit -m "Test TPM signed commit"

# Verify signature
git verify-commit HEAD

Troubleshooting

Check Service Status

# 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"

Emacs/Systemd Service Environment

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.service and Wants=ssh-agent.service to the [Unit] section.

Manual Testing

# 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

Common Issues

"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_ADDRESS is set in the service
  • Verify DISPLAY environment variable is set
  • KWallet must be unlocked before the service runs

Security Notes

  • 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)

File Locations Summary

~/.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)

Credits

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment