Skip to content

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 keysGPG / OpenPGP keys
Primary useAuthenticate to remote systems (SSH login, Git push)Encrypt, decrypt, and sign email + files
Secondary usesSign Git commits (since Git 2.34, Aug 2021)SSH authentication via gpg-agent --enable-ssh-support; signing software releases
Typical lifetimeOften per-machine, regenerated freelyLong-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.


TypeUse caseNotes
ed25519Recommended default. Strong, fast, small.Universally supported in OpenSSH 6.5+ (2014).
ed25519-sk / ecdsa-skFIDO2 hardware-backed (see below)Requires OpenSSH 8.2+ and a hardware key.
rsa (4096-bit)Compatibility with very old serversUse only if ed25519 is rejected.
ecdsa (NIST P-256/384/521)When ed25519 isn’t acceptedSlightly weaker curve trust than ed25519.
dsa / ssh-dssDon’t use.Deprecated, removed from modern OpenSSH defaults.
Terminal window
# 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.

Add your public key (the .pub file) to the server’s ~/.ssh/authorized_keys, then:

Terminal window
# Default identity:
ssh you@server.example.com
# Specific key:
ssh -i ~/.ssh/example_ed25519 you@server.example.com

For 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 publickey

After saving that file (make sure permissions are right: chmod 600 ~/.ssh/config), you can now reach the server by the alias alone:

Terminal window
# 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.git
git remote set-url origin gitlab:username/repo.git

No 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:

Terminal window
ssh -v gitlab # verbose; shows which key, port, user, identity file
ssh -G gitlab # prints the fully-resolved config block

You 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.).

Switch a repo to SSH:

Terminal window
git remote set-url origin git@github.com:username/repo.git

To sign commits with an SSH key (Git 2.34+):

Terminal window
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/example_ed25519.pub
git config --global commit.gpgsign true

GitHub, GitLab, Codeberg, Forgejo, and Gitea all verify SSH-signed commits and display a “Verified” badge.

  • ssh-agent holds 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 IdentityFile in ~/.ssh/config at the .pub public key.
  • Apple Keychain (macOS) integrates with ssh-agent automatically when you ssh-add --apple-use-keychain.

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.

You want…PickWhere to set it up
Port 22 hidden, traditional ~/.ssh/id_ed25519 auth, simplest setupTailscale as transport — sshd bound to the tailnet IPTailscale page
Port 22 hidden, no SSH keys to manage, IdP-anchored identity, optional re-auth on sensitive hostsTailscale SSH (tailscale up --ssh + tailnet ACLs)Tailscale page
Port 22 hidden, your existing SSH keys, you’re already on Cloudflare for webCloudflare Tunnel (traditional SSH)cloudflared access ssh ProxyCommandSelf-host Cloudflare Tunnels page
Port 22 hidden, short-lived SSO-issued certs replace long-lived keys, audit logs for complianceCloudflare Access for Infrastructure (GA Oct 2024)Self-host Cloudflare Tunnels page
  • 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.x in /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 --ssh makes the Tailscale daemon claim port 22 on the tailnet IP and authenticate connections using tailnet identity (anchored to your IdP — Google, GitHub, Okta, etc.). No authorized_keys. Tailnet ACL policy controls who can SSH where, and action: "check" can require re-auth every N hours for sensitive boxes.
  • Cloudflare Tunnel (traditional SSH). cloudflared running on the server forwards SSH from a public Cloudflare hostname to local port 22. Client side: a ProxyCommand entry 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 system ssh with 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.

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
Terminal window
gpg --full-generate-key

Choose 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).

Terminal window
git config --global user.signingkey <YOUR-GPG-KEY-ID>
git config --global commit.gpgsign true

Find your key ID with gpg --list-secret-keys --keyid-format=long.

gpg-agent can expose your GPG authentication subkey as an SSH key, so you sign Git commits and SSH-auth with the same identity:

~/.gnupg/gpg-agent.conf
enable-ssh-support
Terminal window
gpg-connect-agent updatestartuptty /bye
ssh-add -L # should now list your GPG auth subkey

Add this to ~/.bashrc / ~/.zshrc so SSH uses the GPG agent:

Terminal window
export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)

Export the SSH public key for authorized_keys:

Terminal window
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.

Common use cases, ranked roughly by how often people start with each:

  1. Git commit signing — touch the key to sign each push. Phishing-resistant.
  2. SSH login to servers — same touch model. Unphishable second factor.
  3. Email signing / encryption — Thunderbird, Mutt, Apple Mail + GPG Suite drive the OpenPGP applet directly.
  4. Software release signing — distro maintainers, tarball signatures.
  5. Vault unlock for pass, gopass, KeePassXC — the key is the master factor.
  6. Identity non-repudiation — “this signature could only have come from this physical device + this person.”

A YubiKey 5 (or Nitrokey 3) exposes two distinct cryptographic stacks:

  • OpenPGP smartcard applet — implements the OpenPGP card spec. gnupg talks to it via scdaemon. You move existing GPG subkeys onto the card with keytocard, 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-sk key types.

There are three paths to SSH with a hardware key:

PathMechanismWhen 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 subkeygpg-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#11ykman 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.

ProCon
Private key never extractable from the chipLose the key → lose access. Backup key is mandatory, not optional.
Touch-to-sign defeats malware silently signing without you knowingA 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 designSteeper initial setup than ssh-keygen
  • Touch on every git push becomes friction at scale. YubiKey lets you set the touch policy to cached so 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 openssh if ssh-keygen -t ed25519-sk fails.
  • Windows native OpenSSH supports ed25519-sk from 8.9+; gpg-agent and 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 pinentry dialog popping up at unexpected moments (every fresh shell, every git push after the cache expires).
DeviceOpenPGP appletFIDO2 SSH (ed25519-sk)PIV / PKCS#11Notes
YubiKey 5 (5C NFC, 5C, 5 NFC, 5 Nano)✅ RSA + ECC ed25519/cv25519 since fw 5.2.3The “use this for everything” default. ~$58.
YubiKey Security Key C NFC (blue)FIDO2-only. Cheap (~$29) entry-level.
YubiKey Bio FIDO EditionFIDO only — no GPG.
YubiKey Bio Multi-protocolOpenPGP 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.0Open-source firmware (Trussed). ~$49–59.
OnlyKey✅ via OnlyKey agent✅ (8.2+ resident keys)LimitedIndicator-light + button auth. Niche.
Token2 PIN+FIDO2-only, cheap (~$13–25). Swiss-audited 2024.

FIDO2 SSH (simplest path — start here if you only need SSH):

Terminal window
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, run ssh-keygen -K and 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):

  1. Generate the master key on an air-gapped machine (live USB, Tails) — never on your daily driver.
  2. Create three subkeys: Sign, Encrypt, Authenticate.
  3. Move each subkey onto the card with keytocard.
  4. Back up the master key to encrypted offline storage (printed Paperkey + encrypted USB).
  5. Set per-operation touch policies: ykman openpgp keys set-touch SIG on (and DEC, AUT).
  6. Configure ~/.gnupg/gpg-agent.conf with enable-ssh-support if 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.


ServiceGPG-signedSSH-signed”Verified” badgeNotes
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.
Sourcehutaccepts keysaccepts keys❌ no UI badgePhilosophy: “verify locally.”
git.irregularchat.com (community GitLab)Our self-hosted instance.

Desktop email clients — hardware OpenPGP

Section titled “Desktop email clients — hardware OpenPGP”
ClientHardware-key OpenPGPHow
Thunderbird 115+✅ (experimental)Set mail.openpgp.allow_external_gnupg=true and pair with system gnupg. Native RNP keyring handles public-key operations only.
Mutt / NeoMuttDelegates to gpg-agent. Cleanest workflow once configured.
Apple Mail + GPG SuiteGPG 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 BridgeProton-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 webOwn key model; not compatible.
PlatformOpenPGP w/ hardware keySSH 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 — Termiusn/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 5Cin/a⚠️ Lightning is dead; USB-C on iPhone 15+ + Termius gives FIDO2 SSH. Some apps support it, many don’t.
Cliented25519-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)

  • 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-agent Termux 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 gnupg via Homebrew.

  • 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 off for convenience. This removes the hardware key’s main protection — malware on the host can sign silently. If cached (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.


Canonical setup guides:

Standards / release notes:

Tools: