SSH & GPG Keys
SSH & GPG Keys
Section titled “SSH & GPG Keys”Cryptographic keypairs — public key shared, private key kept secret — are the foundation of secure server access, Git commit signing, and end-to-end email encryption. This page covers both software keys (the file in ~/.ssh/) and hardware-backed keys (where the private key lives on a YubiKey, Nitrokey, or OnlyKey and never touches your computer’s disk).
NEVER share your private keys. A private key is bearer-token authority over every server or signature it’s enrolled with.
SSH vs GPG keys — what each is for
Section titled “SSH vs GPG keys — what each is for”| SSH keys | GPG / OpenPGP keys | |
|---|---|---|
| Primary use | Authenticate to remote systems (SSH login, Git push) | Encrypt, decrypt, and sign email + files |
| Secondary uses | Sign Git commits (since Git 2.34, Aug 2021) | SSH authentication via gpg-agent --enable-ssh-support; signing software releases |
| Typical lifetime | Often per-machine, regenerated freely | Long-lived identity; expires/rotates via subkeys |
| Where stored | ~/.ssh/, an SSH agent, KeePassXC, or a hardware key | ~/.gnupg/, a smartcard, or a hardware key |
They can coexist on the same hardware key — most users with a YubiKey end up using GPG for email/commits and a separate FIDO2 SSH key for servers.
SSH Keys (software)
Section titled “SSH Keys (software)”Key types
Section titled “Key types”| Type | Use case | Notes |
|---|---|---|
ed25519 | Recommended default. Strong, fast, small. | Universally supported in OpenSSH 6.5+ (2014). |
ed25519-sk / ecdsa-sk | FIDO2 hardware-backed (see below) | Requires OpenSSH 8.2+ and a hardware key. |
rsa (4096-bit) | Compatibility with very old servers | Use only if ed25519 is rejected. |
ecdsa (NIST P-256/384/521) | When ed25519 isn’t accepted | Slightly weaker curve trust than ed25519. |
dsa / ssh-dss | Don’t use. | Deprecated, removed from modern OpenSSH defaults. |
Generating an SSH key
Section titled “Generating an SSH key”# Standard ed25519 key:ssh-keygen -t ed25519 -C "you@example.com"
# With a custom path and inline passphrase (for scripted setups):ssh-keygen -t ed25519 -C "you@example.com" -f ~/.ssh/example_ed25519 -N "your_passphrase"
# Compatibility-only RSA fallback:ssh-keygen -t rsa -b 4096 -C "you@example.com"When prompted for a passphrase, use one — it encrypts the key file at rest. An attacker with a copy of an unencrypted private key has full access.
Connecting to a server
Section titled “Connecting to a server”Add your public key (the .pub file) to the server’s ~/.ssh/authorized_keys, then:
# Default identity:ssh you@server.example.com
# Specific key:ssh -i ~/.ssh/example_ed25519 you@server.example.comFor repeated use, an ~/.ssh/config entry avoids retyping. The Host line is the short alias you’ll actually type; Hostname is the real address:
Host gitlab Hostname git.example.com User git IdentityFile ~/.ssh/example_ed25519 PreferredAuthentications publickeyAfter saving that file (make sure permissions are right: chmod 600 ~/.ssh/config), you can now reach the server by the alias alone:
# SSH directly:ssh gitlab
# Run a remote command:ssh gitlab "uptime"
# Copy a file using the alias:scp ./local-file.txt gitlab:~/
# Use the alias as a Git remote (note no @ — the User is set in config):git clone gitlab:username/repo.gitgit remote set-url origin gitlab:username/repo.gitNo more -i ~/.ssh/example_ed25519, no username@host.tld, no -p 2224 — the config handles all of it. To check what SSH thinks it’s about to do for a given alias without actually connecting:
ssh -v gitlab # verbose; shows which key, port, user, identity filessh -G gitlab # prints the fully-resolved config blockYou can add as many Host blocks as you want — one per server or service. Wildcards work too (Host *.internal matches web.internal, db.internal, etc.).
Using SSH keys with Git
Section titled “Using SSH keys with Git”Switch a repo to SSH:
git remote set-url origin git@github.com:username/repo.gitTo sign commits with an SSH key (Git 2.34+):
git config --global gpg.format sshgit config --global user.signingkey ~/.ssh/example_ed25519.pubgit config --global commit.gpgsign trueGitHub, GitLab, Codeberg, Forgejo, and Gitea all verify SSH-signed commits and display a “Verified” badge.
Storing SSH keys
Section titled “Storing SSH keys”ssh-agentholds decrypted keys in memory so you don’t retype the passphrase every connection. Most desktop environments start it automatically; otherwise:eval "$(ssh-agent -s)" && ssh-add ~/.ssh/example_ed25519.- KeePassXC can act as an SSH agent — the encrypted database holds the private key, and KeePassXC exposes it via the SSH agent protocol while unlocked. See KeePassXC SSH agent docs. After enrolling, the private file can be removed from disk; point
IdentityFilein~/.ssh/configat the.pubpublic key. - Apple Keychain (macOS) integrates with
ssh-agentautomatically when youssh-add --apple-use-keychain.
SSH Through a Tunnel or Mesh VPN
Section titled “SSH Through a Tunnel or Mesh VPN”If you don’t want port 22 exposed to the public internet (you generally shouldn’t), there are two well-trodden architectures. Each has a “traditional” variant where SSH auth stays unchanged and a “managed” variant where the overlay network handles auth too.
Decision matrix
Section titled “Decision matrix”| You want… | Pick | Where to set it up |
|---|---|---|
Port 22 hidden, traditional ~/.ssh/id_ed25519 auth, simplest setup | Tailscale as transport — sshd bound to the tailnet IP | Tailscale page |
| Port 22 hidden, no SSH keys to manage, IdP-anchored identity, optional re-auth on sensitive hosts | Tailscale SSH (tailscale up --ssh + tailnet ACLs) | Tailscale page |
| Port 22 hidden, your existing SSH keys, you’re already on Cloudflare for web | Cloudflare Tunnel (traditional SSH) — cloudflared access ssh ProxyCommand | Self-host Cloudflare Tunnels page |
| Port 22 hidden, short-lived SSO-issued certs replace long-lived keys, audit logs for compliance | Cloudflare Access for Infrastructure (GA Oct 2024) | Self-host Cloudflare Tunnels page |
Quick descriptions
Section titled “Quick descriptions”- Tailscale as transport. Install Tailscale on the server and client; SSH normally over the 100.x.x.x CGNAT address or the MagicDNS hostname. Bind sshd to the tailnet interface (
ListenAddress 100.64.x.xin/etc/ssh/sshd_config) so the public internet can’t reach port 22. Authentication is your usual SSH keys. Lowest-friction option if you already have a tailnet. - Tailscale SSH.
tailscale up --sshmakes the Tailscale daemon claim port 22 on the tailnet IP and authenticate connections using tailnet identity (anchored to your IdP — Google, GitHub, Okta, etc.). Noauthorized_keys. Tailnet ACL policy controls who can SSH where, andaction: "check"can require re-auth every N hours for sensitive boxes. - Cloudflare Tunnel (traditional SSH).
cloudflaredrunning on the server forwards SSH from a public Cloudflare hostname to local port 22. Client side: aProxyCommandentry in~/.ssh/config. You authenticate to Cloudflare first (SSO), then SSH does its normal key check on top. Port 22 is closed to the internet — the only path in is through the tunnel. - Cloudflare Access for Infrastructure. Same Cloudflare-protected reachability plus Cloudflare issuing short-lived (~3 min) SSH certificates that replace long-lived keys. Identity comes from your IdP; sshd trusts a Cloudflare CA via
TrustedUserCAKeys. The user runs systemsshwith WARP installed; Cloudflare issues a fresh cert per session. Best fit for organizations that want compliance-grade audit logs and the no-keys-on-laptops model.
GPG (OpenPGP) Keys (software)
Section titled “GPG (OpenPGP) Keys (software)”What GPG does
Section titled “What GPG does”OpenPGP encrypts, decrypts, and signs arbitrary data. Most common uses:
- Sign Git commits and tags (alternative to SSH-key signing, longer-lived identity)
- Sign and encrypt email (Thunderbird, Mutt, Apple Mail + GPG Suite)
- Sign software releases (distro packages, tarballs)
- Decrypt files shared with you
Generating a GPG keypair
Section titled “Generating a GPG keypair”gpg --full-generate-keyChoose ECC (Curve 25519) when offered — small, fast, modern. If you need maximum compatibility with old systems, use RSA 4096. Set a real expiry (1–2 years) — you can extend it later, and an expiring key forces you to think about rotation.
The standard practice is one offline master key that does nothing but certify subkeys, plus separate subkeys for signing, encryption, and authentication. The master stays on an air-gapped storage medium; only the subkeys go on your daily-driver machine (or hardware key).
Signing Git commits with GPG
Section titled “Signing Git commits with GPG”git config --global user.signingkey <YOUR-GPG-KEY-ID>git config --global commit.gpgsign trueFind your key ID with gpg --list-secret-keys --keyid-format=long.
Using GPG to authenticate SSH
Section titled “Using GPG to authenticate SSH”gpg-agent can expose your GPG authentication subkey as an SSH key, so you sign Git commits and SSH-auth with the same identity:
enable-ssh-supportgpg-connect-agent updatestartuptty /byessh-add -L # should now list your GPG auth subkeyAdd this to ~/.bashrc / ~/.zshrc so SSH uses the GPG agent:
export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)Export the SSH public key for authorized_keys:
gpg --export-ssh-key <YOUR-GPG-KEY-ID>Hardware-Backed Keys (YubiKey, Nitrokey, OnlyKey)
Section titled “Hardware-Backed Keys (YubiKey, Nitrokey, OnlyKey)”A hardware security key stores private keys in a tamper-resistant chip so cryptographic operations happen on the device. The key material never enters your computer’s RAM or disk. Even a fully compromised laptop can only ask the key to sign or decrypt while it’s plugged in or tapped via NFC — it can’t steal the key itself.
Why you’d want this
Section titled “Why you’d want this”Common use cases, ranked roughly by how often people start with each:
- Git commit signing — touch the key to sign each push. Phishing-resistant.
- SSH login to servers — same touch model. Unphishable second factor.
- Email signing / encryption — Thunderbird, Mutt, Apple Mail + GPG Suite drive the OpenPGP applet directly.
- Software release signing — distro maintainers, tarball signatures.
- Vault unlock for
pass,gopass, KeePassXC — the key is the master factor. - Identity non-repudiation — “this signature could only have come from this physical device + this person.”
Two capabilities, three SSH paths
Section titled “Two capabilities, three SSH paths”A YubiKey 5 (or Nitrokey 3) exposes two distinct cryptographic stacks:
- OpenPGP smartcard applet — implements the OpenPGP card spec.
gnupgtalks to it viascdaemon. You move existing GPG subkeys onto the card withkeytocard, set a touch policy, and from then on signing/decryption happens on the card. - FIDO2 / WebAuthn — used for browser logins, passkeys, and (since OpenSSH 8.2 in Feb 2020) for SSH via
ed25519-sk/ecdsa-skkey types.
There are three paths to SSH with a hardware key:
| Path | Mechanism | When to pick it |
|---|---|---|
| FIDO2-sk (recommended for new users) | ssh-keygen -t ed25519-sk. Key blob lives in ~/.ssh/, but signing requires the hardware key. No gpg-agent. | You only need SSH. Simplest setup. |
| GPG-on-card auth subkey | gpg-agent --enable-ssh-support exposes the OpenPGP A (authentication) subkey to SSH. | You already use GPG for email/commits and want one key for everything. |
| PIV / PKCS#11 | ykman piv workflow; SSH calls opensc-pkcs11.so. | Enterprise / Windows environments; rarely the right pick on Linux/macOS. |
They can coexist on the same physical key. Most users with a YubiKey 5 end up with FIDO2-sk for new SSH and GPG-on-card for commits + email.
Security tradeoffs vs. software keys
Section titled “Security tradeoffs vs. software keys”| Pro | Con |
|---|---|
| Private key never extractable from the chip | Lose the key → lose access. Backup key is mandatory, not optional. |
| Touch-to-sign defeats malware silently signing without you knowing | A compromised host can still ask the key to sign while it’s plugged in — touch proves presence, not authorization |
| One key works across all your machines | ”Bus factor” risk on shared infrastructure — document revocation procedures |
| FIDO2 ceremony is origin-bound — phishing-resistant by design | Steeper initial setup than ssh-keygen |
Convenience tradeoffs
Section titled “Convenience tradeoffs”- Touch on every
git pushbecomes friction at scale. YubiKey lets you set the touch policy tocachedso one touch unlocks a ~15s window for follow-up signatures. Trade UX for security as needed. - macOS’s bundled OpenSSH historically had FIDO2 disabled — install Homebrew
opensshifssh-keygen -t ed25519-skfails. - Windows native OpenSSH supports
ed25519-skfrom 8.9+;gpg-agentand pinentry are fiddlier. - Webmail can’t drive the card. PGP-with-smartcard requires a desktop IMAP client (Thunderbird, Mutt). Browser-based webmail is a dead path for hardware OpenPGP.
- PIN entry timing — get used to a
pinentrydialog popping up at unexpected moments (every fresh shell, everygit pushafter the cache expires).
Hardware compatibility matrix
Section titled “Hardware compatibility matrix”| Device | OpenPGP applet | FIDO2 SSH (ed25519-sk) | PIV / PKCS#11 | Notes |
|---|---|---|---|---|
| YubiKey 5 (5C NFC, 5C, 5 NFC, 5 Nano) | ✅ RSA + ECC ed25519/cv25519 since fw 5.2.3 | ✅ | ✅ | The “use this for everything” default. ~$58. |
| YubiKey Security Key C NFC (blue) | ❌ | ✅ | ❌ | FIDO2-only. Cheap (~$29) entry-level. |
| YubiKey Bio FIDO Edition | ❌ | ✅ | ❌ | FIDO only — no GPG. |
| YubiKey Bio Multi-protocol | ❌ | ✅ | ✅ | OpenPGP still absent on Bio line as of 2026. |
| Nitrokey 3 (3A Mini, 3C NFC) | ✅ ed25519 supported; secure element since fw 1.7.0 | ✅ | ✅ since fw 1.8.0 | Open-source firmware (Trussed). ~$49–59. |
| OnlyKey | ✅ via OnlyKey agent | ✅ (8.2+ resident keys) | Limited | Indicator-light + button auth. Niche. |
| Token2 PIN+ | ❌ | ✅ | ❌ | FIDO2-only, cheap (~$13–25). Swiss-audited 2024. |
Setup pointers
Section titled “Setup pointers”FIDO2 SSH (simplest path — start here if you only need SSH):
ssh-keygen -t ed25519-sk -O resident -O verify-required -C "you@host"resident→ the key stub is stored on the YubiKey itself. On a new machine, runssh-keygen -Kand the stub reappears. Useful for portable workflows.verify-required→ require PIN + touch for every use. Drop this for touch-only.- See Yubico’s Securing SSH with FIDO2 for the full guide.
GPG on-card (the workflow most YubiKey power users adopt):
- Generate the master key on an air-gapped machine (live USB, Tails) — never on your daily driver.
- Create three subkeys: Sign, Encrypt, Authenticate.
- Move each subkey onto the card with
keytocard. - Back up the master key to encrypted offline storage (printed Paperkey + encrypted USB).
- Set per-operation touch policies:
ykman openpgp keys set-touch SIG on(andDEC,AUT). - Configure
~/.gnupg/gpg-agent.confwithenable-ssh-supportif you want this key to do SSH too.
The de-facto community walkthrough is drduh’s YubiKey-Guide on GitHub — start there. Follow exactly the first time; deviations cause recovery nightmares later.
What Works Where (Services + Apps)
Section titled “What Works Where (Services + Apps)”Git forges — commit signing
Section titled “Git forges — commit signing”| Service | GPG-signed | SSH-signed | ”Verified” badge | Notes |
|---|---|---|---|---|
| GitHub | ✅ | ✅ (Git 2.34+) | ✅ | SSH-signing is the simpler path — reuse your auth key. |
| GitLab (.com + self-hosted) | ✅ | ✅ | ✅ | Self-hosted instances support both. SSH tag signing GA since 19.1. |
| Codeberg / Forgejo | ✅ | ✅ (since Forgejo v12.0, Jul 2025) | ✅ | Strong signing culture. |
| Gitea (self-hosted) | ✅ | ✅ | ✅ | Mirrors the Forgejo model. |
| Bitbucket Cloud | ✅ | ✅ | ⚠️ | CLI-only verification; merges done via UI don’t show as signed. |
| Sourcehut | accepts keys | accepts keys | ❌ no UI badge | Philosophy: “verify locally.” |
| git.irregularchat.com (community GitLab) | ✅ | ✅ | ✅ | Our self-hosted instance. |
Desktop email clients — hardware OpenPGP
Section titled “Desktop email clients — hardware OpenPGP”| Client | Hardware-key OpenPGP | How |
|---|---|---|
| Thunderbird 115+ | ✅ (experimental) | Set mail.openpgp.allow_external_gnupg=true and pair with system gnupg. Native RNP keyring handles public-key operations only. |
| Mutt / NeoMutt | ✅ | Delegates to gpg-agent. Cleanest workflow once configured. |
| Apple Mail + GPG Suite | ✅ | GPG Suite installs gpg-agent and integrates with Mail. Sometimes breaks across macOS major upgrades. |
| Outlook | ❌ (for OpenPGP) | S/MIME only — usable with PIV slot on YubiKey via Windows CSP, but not OpenPGP. |
| ProtonMail Bridge | ❌ | Proton-managed keys only; external smartcards are a long-standing feature request. |
| Mailvelope (browser extension) | ⚠️ | Via local GnuPG bridge, not WebUSB/WebHID directly. |
| ProtonMail web / Tutanota web | ❌ | Own key model; not compatible. |
Mobile
Section titled “Mobile”| Platform | OpenPGP w/ hardware key | SSH w/ hardware key |
|---|---|---|
| Android — OpenKeychain + Thunderbird-Android (K-9) | ✅ USB-C + NFC (YubiKey 5 series, Nitrokey 3). OpenKeychain on F-Droid — slow dev, but works. | ⚠️ via okc-agent bridging OpenKeychain → gpg-agent → ssh. Fragile, reportedly broken on recent GrapheneOS. |
| Android — Termux | ⚠️ same as above (okc-agent fragility) | ✅ Native OpenSSH FIDO2 works with USB-C YubiKey. |
| Android — Termius | n/a | ✅ Native YubiKey FIDO2 SSH support. |
| iOS — software-key OpenPGP | ✅ via iPGMail (~$4, actively maintained — v2025.39.1 Nov 2025). Imports/exports .asc, keys live in iOS Keychain, share-sheet workflow (iOS forbids Mail.app plugins). Or Canary Mail for an OpenPGP-aware mail client (subscription). PGPro is abandoned (last updated Nov 2021, removed from App Store Jan 2025 — older guides still recommend it; don’t). | n/a |
| iOS — OpenPGP smartcard | ❌ No viable path. GnuPG NFC-on-iOS is a wishlist item with no priority. YubiKey 5Ci connects but no iOS app drives the OpenPGP applet. | n/a |
| iOS — Termius + YubiKey 5Ci | n/a | ⚠️ Lightning is dead; USB-C on iPhone 15+ + Termius gives FIDO2 SSH. Some apps support it, many don’t. |
SSH clients (FIDO2 hardware-key support)
Section titled “SSH clients (FIDO2 hardware-key support)”| Client | ed25519-sk / ecdsa-sk |
|---|---|
| OpenSSH 8.2+ (Linux, macOS Homebrew, Windows 10+) | ✅ Native |
| VS Code Remote-SSH | ✅ Passes to OpenSSH; PIN/touch prompt appears in your terminal, not in VS Code |
| WSL | ✅ via Windows OpenSSH bridge or wsl-ssh-agent |
| PuTTY (mainline) | ❌ Still no FIDO2 support — open wishlist item |
| PuTTY-CAC (fork) / SK-SSH-Agent | ✅ Windows workaround |
| Termius (mobile + desktop) | ✅ |
What Recently Changed
Section titled “What Recently Changed”- Sigstore / cosign is displacing GPG release signing. New projects increasingly ship cosign signatures instead of GPG signatures. Cosign is keyless (OIDC-anchored) — much less key management, but a different trust model. Hardware-key GPG remains the standard for distro package signing, mailing-list signatures, and identity non-repudiation.
- OpenKeychain pace is slow. Functional but not actively expanding. The
okc-agentTermux bridge has known breakage on current GrapheneOS — research before relying on it for critical workflows. - PuTTY still has no FIDO2 SSH as of 2026 — five years behind OpenSSH. If you need PuTTY, look at the PuTTY-CAC fork or use Windows OpenSSH instead.
- iOS OpenPGP smartcard path is effectively dead. No app drives the OpenPGP applet on a YubiKey from iOS. (Software-key PGP on iOS is fine via iPGMail — see the mobile table.) For smartcard-backed GPG operations from a phone, that phone is Android.
- PGPro removed from the App Store in January 2025. Older guides still link to it. Don’t recommend; recommend iPGMail instead.
- GPGTools on macOS has had repeated breakage on macOS major upgrades; many users have drifted to plain
gnupgvia Homebrew.
Common Pitfalls
Section titled “Common Pitfalls”- Putting the GPG master key on the daily-driver machine. The whole point of the offline-master / subkeys-on-card model is that the master can’t be stolen along with the laptop. Generate it on an air-gapped machine and never copy it to a network-connected one.
- Forgetting the second key. “I’ll buy the backup later” → eventually loses the first key → locked out. Buy two at the same time.
- Setting touch policy to
offfor convenience. This removes the hardware key’s main protection — malware on the host can sign silently. Ifcached(15s window) is too friction-y, the actual ergonomic answer is to script your workflow, not disable touch. - Using the same GPG key for signing AND encryption AND authentication. Use subkeys. Compromise of one subkey doesn’t burn your whole identity.
- Skipping the revocation certificate. When you generate a GPG master key, immediately export a revocation certificate and store it offline. If your master is ever lost, this is how you tell the world to stop trusting it.
See Also
Section titled “See Also”- MFA Guide → Hardware Security Keys — 2026 buying guide with current prices and the two-key strategy
- GrapheneOS → Hardware Security Keys — what a YubiKey can do on a GrapheneOS phone (and why it can’t unlock the lock screen)
- Mobile Hardening Guide — broader mobile security
- Password Managers Guide — KeePassXC SSH agent integration
- Email Hardening Guide — broader email security
- Digital Force Protection Guide — the umbrella index
References
Section titled “References”Canonical setup guides:
- drduh/YubiKey-Guide — the de-facto community walkthrough for GPG-on-card
- Yubico — PGP developer docs
- Yubico — Securing SSH with FIDO2
- Nitrokey 3 features
- OnlyKey SSH/GPG agent
Standards / release notes:
- OpenSSH 8.2 release notes —
ed25519-skintroduction - OpenPGP smartcard specification
- GitHub — About commit signature verification
- GitLab — Sign commits with SSH
Tools: