Skip to content
cybersecurity Advanced 4-6 hours · 24 min read

Building a Zero-Trust Homelab with YubiKey PIV

Hardware-backed authentication for your homelab using YubiKey PIV smart cards, a private CA, SSH hardening, digital signing, and a Bastion server. No cloud dependencies. No subscriptions. Full control.

Environment: macOS + Linux (Debian/Ubuntu)
ykman step-ca opensc openssh step-cli
Building a Zero-Trust Homelab with YubiKey PIV

Most homelabs authenticate with passwords or SSHA cryptographic protocol for secure remote login, command execution, and file transfer over an unsecured network. Read more → key files sitting on disk. Both are software secrets. If your machine is compromised, those secrets walk out with it. A hardware security tokenA physical device with a tamper-resistant chip that generates and stores cryptographic keys. The private key never leaves the hardware, making it immune to software-based credential theft. Read more → changes the equation: the private key is generated on the chip and never leaves it. Authentication requires physical possession and a PIN. No software exploit can extract what never existed on disk.

Concepts at a Glance

New to hardware tokens or PKIPublic Key Infrastructure - the complete system of policies, roles, hardware, software, and procedures needed to create, manage, distribute, and revoke digital certificates and manage public-key encryption. Read more →? Read this once. It is the “why” behind every command below.

Zero Trust: A security model that says "trust nothing by default, verify everything explicitly." Even if you are inside the network, you still have to prove who you are for every access attempt. Passwords alone do not cut it - you need something you physically have. Full definition
YubiKey / Hardware Security Token: A physical USB device that stores cryptographic keys in a tamper-resistant chip. The key is generated inside the chip and never exported. To use it for authentication, you need the physical token in your hand plus a PIN. Lose one without the other and an attacker cannot get in. Full definition
PIV (Personal Identity Verification): A US government standard (FIPS 201) for smart card authentication. YubiKeys implement PIV, which gives you dedicated key slots for different purposes: login, signing, encryption. Think of it as a standardized key ring on a chip. Full definition
Certificate Authority (CA): A trusted entity that issues digital certificates - cryptographically signed documents that say "this public key belongs to this identity." Your CA is the root of trust for your homelab. Everything trusts it, and it vouches for your YubiKey. Full definition
PKI (Public Key Infrastructure): The overall system of CAs, certificates, and trust relationships. Your private PKI is a chain: a Root CA signs an Intermediate CA, and the Intermediate issues certs to your YubiKeys. Once a cert is issued, the CA can go offline - it is not needed for day-to-day authentication. Full definition
X.509 Certificate: The standard format for digital certificates. It contains a public key, identity info (your name, organization), validity dates, and a signature from the CA. When you authenticate, the other party verifies the CA signed it - proving the key is legitimate. Full definition
PKCS#11: A standard software interface for talking to hardware security devices like YubiKeys. When SSH or a browser needs to use the key on your YubiKey, it calls through PKCS#11. OpenSC is the library that implements this for open-source use on macOS and Linux. Full definition
Bastion Server (Jump Host): A hardened server that sits between your workstation and your internal machines. Instead of SSH-ing directly to every server, you connect to the Bastion first. It is the single door into your lab. One door means one thing to harden, monitor, and audit. Full definition

What You Will Build

By the end of this guide, you will have:

  • A private Certificate AuthorityA trusted entity that issues and signs digital certificates, cryptographically vouching that a public key belongs to a specific identity. The foundation of PKI trust chains. Read more → (CA) issuing X.509 certificates
  • YubiKey PIVPersonal Identity Verification - a US government standard (FIPS 201) for smart card authentication. Defines a set of key slots on a chip for different cryptographic purposes: login, signing, and encryption. Read more → slots programmed with CA-signed certificates
  • macOS smart card login using the YubiKey
  • SSH authentication to all Linux servers using PIV certificates
  • Digital document signing with cryptographic proof
  • A hardened Bastion serverA hardened server that acts as the single, controlled entry point for SSH access to an internal network. All connections to internal hosts must route through the Bastion, creating one audited choke point. Read more → as the single entry point for all SSH access
  • Agent forwarding through the Bastion using the YubiKey
  • Password authentication disabled across all servers
  • A backupA copy of data stored separately from the original, used to restore systems and files after data loss from hardware failure, deletion, ransomware, or disaster. Read more → YubiKey enrolled for redundancy

Prerequisites

Hardware:

  • Two YubiKey 5 series tokens (one primary, one backup). USB-C recommended for modern MacBooks.
  • A server or VMA software-based emulation of a complete computer that runs its own operating system and applications, isolated from the host hardware. Read more → to run the CA (any Linux host with DockerA platform that packages applications into containers, providing a standardized way to build, ship, and run software consistently across any environment. Read more → or bare metal)
  • A Bastion/jump server (can be a lightweight VM or container)

Software (macOS):

  • Homebrew
  • ykman (YubiKey Manager CLI): brew install ykman
  • Yubico Authenticator (GUI): brew install --cask yubico-authenticator
  • step CLI (Smallstep): brew install step
  • OpenSC: brew install opensc
  • Homebrew OpenSSH: brew install openssh (required for PKCS#11 agent forwarding)
  • NSS tools: brew install nss (for document signing with LibreOffice)

Network:

  • A dedicated VLAN for the CA (recommended, not required)
  • Firewall rules to restrict access to the CA and Bastion

Required knowledge:

  • Basic Linux command line (navigating directories, running commands, editing files with nano/vim)
  • What SSH is and how to connect to a remote server
  • Basic networking concepts (IP addresses, VLANs, ports)
YubiKey 5C NFC Two-factor authentication · FIPS 140-2 certified
Buy on Amazon

Affiliate link. BytesNation earns a small commission at no extra cost to you.

Part 1: Building the Certificate Authority

Your CA is a certificate vending machine. You bring it a public key, it stamps a certificate on it saying “I vouch for this key.” Once the certificate is loaded onto your YubiKey, the CA can sit offline. It is not in the authentication path at all; it only needs to come online when you issue, renew, or revoke certificates. No third-party dependency. No subscription. No certificate chain touching the internet. You control who gets issued, for how long, and you can revoke instantly.

Installing step-ca

Smallstep’s step-ca is a lightweight, open source CA that runs as a single binary. It handles all the complexity of running a two-tier PKI in a simple CLI.

Add the Smallstep apt repository on your CA server (Debian/Ubuntu):

apt-get update && apt-get install -y --no-install-recommends curl gpg ca-certificates

curl -fsSL https://packages.smallstep.com/keys/apt/repo-signing-key.gpg -o \
  /etc/apt/keyrings/smallstep.asc

cat << 'EOF' > /etc/apt/sources.list.d/smallstep.sources
Types: deb
URIs: https://packages.smallstep.com/stable/debian
Suites: debs
Components: main
Signed-By: /etc/apt/keyrings/smallstep.asc
EOF

apt-get update && apt-get -y install step-cli step-ca

Verify the installation:

step version && step-ca version

Initializing the CA

step ca init

You will be prompted for:

  • Deployment Type: Standalone
  • PKI Name: Choose a name (e.g., “HomeLab CA”)
  • DNS names or IP addresses: The CA server’s hostname or IP
  • Bind address: :443
  • Provisioner name: Your email or admin identity
  • Password: This protects your CA keys. Store it securely offline.
What just happened:

This created a two-tier PKI. A Root CA and an Intermediate CA were generated. The Root CA signed the Intermediate CA’s certificate, and the Intermediate is what will actually sign your YubiKey certificates. Why two tiers? The Root CA stays offline after this - only the Intermediate is exposed. If the Intermediate is ever compromised, you revoke it and issue a new one from the Root. Your Root is never at risk.

Root CAself-signed trust anchorOFFLINEsignsIntermediate CAissues end-entity certsONLINEsignssignsSlot 9aAuthentication CertSSH · macOS login · VPNSlot 9cDigital Signature CertDocuments · Code · EmailPrivate keys generated on-chip · never exported · never on disk
Two-tier PKI: Root CA stays offline. Intermediate CA issues certs to YubiKey slots. Once certs are issued, the CA can be powered off.

Save the root fingerprint printed at the end - you will need it when you set up client machines to trust your CA.

Extending Certificate Lifetimes

The default certificate lifetime is 24 hours. That is designed for short-lived automated certificates, not hardware tokens you want to use for a year. Update the provisioner:

step ca provisioner update your-provisioner@email.com \
  --x509-max-dur=8760h \
  --x509-default-dur=8760h

8760 hours is one year. Adjust to your preference.

Running as a Service

Save the CA password for unattended startup:

echo 'YOUR_PASSWORD' > /path/to/step/secrets/password.txt
chmod 600 /path/to/step/secrets/password.txt

Create a systemd unit so the CA starts automatically on reboot:

cat << 'EOF' > /etc/systemd/system/step-ca.service
[Unit]
Description=Smallstep CA Server
After=network.target

[Service]
Type=simple
User=root
ExecStart=/usr/bin/step-ca /path/to/step/config/ca.json \
  --password-file /path/to/step/secrets/password.txt
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable step-ca
systemctl start step-ca

Verify it started cleanly:

systemctl status step-ca
step ca health

Bootstrapping Clients

“Bootstrapping” means teaching your Mac to trust your private CA. Without this step, your Mac will see certificates from your CA and reject them as untrusted.

On your workstation (macOS), run:

step ca bootstrap \
  --ca-url https://YOUR_CA_IP \
  --fingerprint YOUR_ROOT_FINGERPRINT

Then install the root cert into the macOS system trust store:

step certificate install $(step path)/certs/root_ca.crt

Your Mac now trusts any certificate issued by your CA. This is the same process companies use to deploy internal CAs to corporate laptops - you are doing the same thing for your homelab.

Part 2: Programming the YubiKey

The YubiKey has several applications on it (OTP, FIDO2, OpenPGP, PIV). This guide uses the PIV application. PIV gives you dedicated key slots, each designed for a specific purpose.

Understanding PIV Slots

9aAuthenticationPIN POLICYOnce / sessionTOUCHAlwaysSSH · Login · VPN9cDigital SignaturePIN POLICYEvery useTOUCHAlwaysDocuments · Code · Email signing9dKey ManagementPIN POLICYOnce / sessionTOUCHConfigurableEncryption · Key unwrapping9eCard AuthPIN POLICYNeverTOUCHNeverPhysical access · Badge tapPrivate key generated on-chip per slot · never exported
YubiKey PIV slot layout. Each slot has independent key material and its own PIN and touch policies.
Why multiple slots?:

Different operations have different security requirements. Logging in (9a) is something you do frequently, so you PIN once per session. Signing a document (9c) is a deliberate act, so it asks for a PIN every single time. The slot design enforces these policies at the hardware level.

SlotPurposePIN PolicyUse Case
9aAuthenticationOnce per sessionSystem login, SSH, VPN
9cDigital SignatureEvery useSigning documents, code, emails
9dKey ManagementConfigurableEncryption/decryption
9eCard AuthenticationNeverPhysical access, badge tap

The private key is generated on the YubiKey’s chip and never leaves it.

Initial YubiKey Setup

Change the default PIN (123456) and PUK (12345678). The PUK is a backup code used to unblock a locked PIN - treat it like a recovery key:

ykman piv access change-pin
ykman piv access change-puk

Generate the CHUID and CCC objects (metadata structures macOS requires to recognize the card as a smart card):

ykman piv objects generate chuid
ykman piv objects generate ccc

Secure the management key by setting it to PIN-protected mode:

ykman piv access change-management-key --protect
What is the management key?:

The management key controls administrative operations on the PIV application (like importing certs or generating keys). By running this command, a random AES key is generated and stored on the YubiKey itself, locked behind your PIN. You never need to know or remember it - the YubiKey handles it. This prevents someone who finds your YubiKey from reprogramming its slots without your PIN.

Programming Slot 9a (Authentication)

Generate a key pair directly on the YubiKey chip:

ykman piv keys generate \
  --algorithm ECCP256 \
  --pin-policy ONCE \
  --touch-policy ALWAYS \
  9a ~/yubikey-pub.pem
  • ECCP256: Elliptic Curve P-256. A modern, efficient algorithm. Equivalent security to a 3072-bit RSA key but much faster.
  • --pin-policy ONCE: You enter your PIN once when you plug in the YubiKey. Subsequent operations in that session do not re-prompt.
  • --touch-policy ALWAYS: You must physically touch the gold disc on the YubiKey for every authentication. This is the anti-remote-exploit protection.

The command exports the public key only to ~/yubikey-pub.pem. The private key was generated inside the chip and has never been on your filesystem.

Create a Certificate Signing Request (CSR) - essentially an application to your CA asking it to certify this public key:

ykman piv certificates request \
  --subject "CN=Your Name,O=Your Org" \
  9a ~/yubikey-pub.pem ~/yubikey.csr

Sign it with your CA (this is the CA doing its job - stamping the cert):

step ca sign --not-after=8760h ~/yubikey.csr ~/yubikey.crt

Import the signed certificate back onto the YubiKey:

ykman piv certificates import 9a ~/yubikey.crt

Verify it looks correct:

ykman piv certificates export 9a - | step certificate inspect

Clean up - these files are no longer needed. The private key never left the chip:

rm ~/yubikey-pub.pem ~/yubikey.csr ~/yubikey.crt
1. Key GeneratedOn YubiKey chipPrivate keynever leaves2. CSR CreatedPublic key + identityykman pivcertificates request3. CA Signsstep-ca issues certstep ca sign--not-after=8760h4. Cert ImportedBack onto YubiKeyykman pivcertificates import🔒 key never leaves chipcert lives on YubiKey · private key unchanged
Certificate signing flow: key generation, CSR creation, CA signing, and import back to the YubiKey. The private key is never involved in steps 2-4.

Programming Slot 9c (Digital Signature)

Same workflow, different PIN policy:

ykman piv keys generate \
  --algorithm ECCP256 \
  --pin-policy ALWAYS \
  --touch-policy ALWAYS \
  9c ~/yubikey-9c-pub.pem

ykman piv certificates request \
  --subject "CN=Your Name,O=Your Org" \
  9c ~/yubikey-9c-pub.pem ~/yubikey-9c.csr

step ca sign --not-after=8760h ~/yubikey-9c.csr ~/yubikey-9c.crt

ykman piv certificates import 9c ~/yubikey-9c.crt

rm ~/yubikey-9c-pub.pem ~/yubikey-9c.csr ~/yubikey-9c.crt
Note:

Note --pin-policy ALWAYS for slot 9c. Every signature requires explicit PIN entry because each signature is a deliberate act: you are cryptographically attesting “I signed this.”

Part 3: macOS Smart Card Login

macOS has built-in support for PIV smart cards through a framework called CryptoTokenKit. When you plug in a YubiKey, macOS can use its certificates for system login - the same mechanism large enterprises use for CAC/PIV card login on government machines. You are building the same thing for your homelab.

Pairing the YubiKey

Verify macOS detects the YubiKey:

system_profiler SPSmartCardsDataType

You should see the YubiKey listed as a reader with the PIV token driver loaded.

List the identities (certificates) macOS sees on the card:

sc_auth identities

If the cert does not appear, unplug and replug the YubiKey to force CryptoTokenKit to re-enumerate.

Pair the authentication cert to your macOS user account. The cert hash is the hex string shown by sc_auth identities:

sudo sc_auth pair -u YOUR_USERNAME -h YOUR_CERT_HASH

Touch the YubiKey when prompted. Verify the pairing succeeded:

sc_auth list -u YOUR_USERNAME

Setting the Auth Policy

Enable smart card authentication while keeping password/biometric as a fallback:

sudo defaults write /Library/Preferences/com.apple.security.smartcard \
  allowSmartCard -bool true
sudo defaults write /Library/Preferences/com.apple.security.smartcard \
  allowUnmappedUsers -int 0
Danger:

Do not enforce smart card only until you have a second YubiKey enrolled as a backup. If you enforce with one key and lose it, you are locked out of your own machine.

Testing

Lock your screen (Ctrl+Cmd+Q). With the YubiKey plugged in, you should see a PIN prompt. Enter your PIV PIN, touch the YubiKey. You are in.

Pull the YubiKey out. The login screen reverts to password/Touch ID. This confirms your fallback works and you are not yet locked out without the key.

Part 4: SSH Authentication

Now you use the same key on slot 9a to authenticate SSH sessions. Instead of a key file on disk, your Mac asks the YubiKey to sign the SSH challenge. The private key never leaves the chip.

WorkstationmacOSSSH Servertarget hostYubiKeyon your desktouch requiredphysical press · gold discssh serverTCP connectchallenge (random nonce)prove who you aresign(challenge)via PKCS#11 / OpenSCsigned responseprivate key never left chipsigned response✓ Authenticatedsession openPrivate key never leaves the YubiKey chip at any point in this flow
SSH authentication with YubiKey: the server challenges, PKCS#11 routes the request to the chip, touch required, signed response proves identity.

Extracting the SSH Public Key

OpenSC exposes the YubiKey through a PKCS#11 module. SSH can ask that module for the public key:

ssh-keygen -D /opt/homebrew/lib/pkcs11/opensc-pkcs11.so

This outputs the public keys from all populated PIV slots. The 9a key is labeled PIV AUTH pubkey. The “failed to fetch key” messages for empty slots are normal - just ignore them.

Copy the full line starting with ecdsa-sha2-nistp256.

Deploying to Servers

On each target Linux server, add the public key to the authorized keys file:

mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo 'YOUR_ECDSA_PUBLIC_KEY PIV AUTH pubkey' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
This is just like normal SSH key auth:

You are doing the same thing you would with a regular SSH key - putting the public key in authorized_keys. The difference is the corresponding private key is on a hardware chip, not a file. The server does not care; it just verifies the signature.

SSH Config

Tell SSH to use the PKCS#11 provider for specific hosts. Add to your ~/.ssh/config:

Host myserver
    HostName x.x.x.x
    User myuser
    PKCS11Provider /opt/homebrew/lib/pkcs11/opensc-pkcs11.so

Test the connection:

ssh myserver

You will be prompted for your PIV PIN and then a physical touch on the YubiKey. Verify on the server that YubiKey auth was used (not a password or key file):

journalctl -u ssh --no-pager -n 5

Look for Accepted publickey with ECDSA to confirm YubiKey auth was used.

SSH Agent Conflicts

If SSH uses your ed25519 key on disk instead of the YubiKey, that key is cached in the ssh-agent and being offered first. Remove it:

ssh-add -d ~/.ssh/id_ed25519

For hosts where you want YubiKey-only auth, do not include IdentityFile or IdentitiesOnly in the SSH config block for those hosts.

Part 5: Digital Document Signing

Slot 9c exists for signing. When you sign a document with it, the signature proves two things: this document came from the holder of this private key, and it has not been modified since signing. The YubiKey provides cryptographic non-repudiation - you cannot later claim you did not sign it.

Command Line Signing

Sign a file:

pkcs11-tool \
  --module /opt/homebrew/lib/pkcs11/opensc-pkcs11.so \
  --sign --slot 0 --id 02 \
  --mechanism ECDSA-SHA256 \
  --input-file ~/document.txt \
  --output-file ~/document.sig \
  --login

Verify the signature (anyone with your public certificate can do this):

pkcs11-tool \
  --module /opt/homebrew/lib/pkcs11/opensc-pkcs11.so \
  --verify --slot 0 --id 02 \
  --mechanism ECDSA-SHA256 \
  --input-file ~/document.txt \
  --signature-file ~/document.sig

PDF Signing with LibreOffice

LibreOffice uses Firefox’s NSS certificate database. Register the PKCS#11 module:

modutil \
  -add "OpenSC" \
  -libfile /opt/homebrew/lib/pkcs11/opensc-pkcs11.so \
  -dbdir "YOUR_FIREFOX_PROFILE_PATH"

Import your CA root cert to eliminate trust warnings:

certutil -A \
  -n "Your CA Name" \
  -t "CT,C,C" \
  -i /path/to/root_ca.crt \
  -d "YOUR_FIREFOX_PROFILE_PATH"

Then in LibreOffice: File > Digital Signatures > Digital Signatures > Sign Document. Select your cert, enter PIN, touch the YubiKey. The signature is embedded in PAdES format directly in the PDF.

Part 6: The Bastion Server

Why a Bastion

Right now, your workstation can SSH directly to every server. That means every server is exposed to your workstation, and if your workstation is compromised, an attacker can move laterally to every machine they can reach from it.

A Bastion (jump host) creates a single choke point. Your workstation can only SSH to the Bastion. Internal servers only accept SSH from the Bastion. To reach any internal machine, you have to deliberately enter the Bastion first - that is the “intent” in intent-based access. One hop, one audit point, one thing to harden.

Hardening the Bastion

Create a hardened sshd config at /etc/ssh/sshd_config.d/99-bastion.conf:

# Authentication
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey

# Disable legacy auth methods
ChallengeResponseAuthentication no
KerberosAuthentication no
GSSAPIAuthentication no

# Reduce attack surface
X11Forwarding no
PermitTunnel no
AllowAgentForwarding yes

# Session hygiene
ClientAliveInterval 300
ClientAliveCountMax 2
LoginGraceTime 30
MaxAuthTries 3
MaxSessions 2

# Logging
LogLevel VERBOSE
Warning:

AllowAgentForwarding yes is critical. Without it, your YubiKey cannot authenticate the second hop from the Bastion to an internal server. The agent forwarding is what carries the signing request back to your physical key.

The Dual-Agent Problem (macOS)

This is the trickiest part of the whole guide. Here is what is happening:

When you SSH from the Bastion to an internal server, the internal server sends a signing challenge back through the tunnel to the Bastion, which forwards it to your workstation’s SSH agent, which asks the YubiKey to sign it. This is agent forwarding - your key never leaves your desk, but requests travel to it through the tunnel.

The problem: Apple ships its own ssh-agent which, for security reasons, refuses to load third-party PKCS#11 providers like OpenSC. So Apple’s agent cannot load the YubiKey for forwarding. But you cannot just disable Apple’s agent - VS Code Remote SSH, Claude Code, git, and macOS Keychain all depend on it.

The solution: keep Apple’s agent for everyday use. Spin up Homebrew’s ssh-agent temporarily only when you need the Bastion.

The bastion() Function

Add this to your ~/.zshrc:

function bastion() {
  export ORIG_SSH_AUTH_SOCK="$SSH_AUTH_SOCK"
  eval $(/opt/homebrew/bin/ssh-agent -s -P \
    '/opt/homebrew/lib/pkcs11/*,/opt/homebrew/Cellar/opensc/*/lib/*.so')
  ssh-add -s /opt/homebrew/lib/pkcs11/opensc-pkcs11.so
  ssh -A jump
  export SSH_AUTH_SOCK="$ORIG_SSH_AUTH_SOCK"
}

What each line does:

  1. Save Apple’s agent socket so you can restore it when you exit
  2. Start Homebrew’s ssh-agent, whitelisting the OpenSC PKCS#11 provider
  3. Load the YubiKey into Homebrew’s agent (prompts for PIV PIN)
  4. SSH to the Bastion with agent forwarding (-A)
  5. When you type exit, restore Apple’s agent

Daily workflow: type bastion, enter PIN, touch YubiKey, you are on the Bastion. From there, ssh server-a, touch YubiKey for each hop. Type exit when done; Apple’s agent is restored automatically.

Agent Whitelist

The -P flag tells Homebrew’s ssh-agent which PKCS#11 providers are allowed. You need both:

  • /opt/homebrew/lib/pkcs11/* (the symlink path)
  • /opt/homebrew/Cellar/opensc/*/lib/*.so (the resolved real path)

Without both, the agent resolves the symlink and rejects the real path as “provider not allowed.” This trips up almost everyone the first time.

Bastion SSH Config

On the Bastion, create ~/.ssh/config for easy access to internal hosts:

Host server-a
    HostName 10.x.x.x
    User myuser

Host server-b
    HostName 10.x.x.x
    User myuser

From the Bastion: ssh server-a, touch YubiKey, you are in.

Authentication Flow

YubiKeyon your deskWorkstationmacOSBastionjump hostServerinternal hostHOP 1 — Workstation → Bastionbastion() · ssh -A jumpchallenge 1sign via PKCS#11touch · PIN enteredsigned response → connectedHOP 2 — Bastion → Server (via agent forwarding)ssh server-achallenge 2 routes back through tunneltouch · PIN cachedsigned response → authenticatedYubiKey never moves · signing requests travel to it · no keys on any server
Bastion dual-hop: Hop 1 authenticates your workstation to the Bastion. Hop 2 shows how the signing challenge routes backwards through the agent forwarding tunnel to reach the YubiKey at your desk.

This is what actually happens when you type bastion and then ssh server-a:

First hop (workstation to Bastion):

  1. bastion starts Homebrew’s agent and loads the YubiKey (PIN entered)
  2. SSH connects to the Bastion with agent forwarding active
  3. Bastion sends a signing challenge; YubiKey prompts for touch
  4. You touch the YubiKey; the signed response goes to the Bastion; session opens

Second hop (Bastion to internal host):

  1. ssh server-a on the Bastion
  2. server-a sends a signing challenge to the Bastion
  3. The Bastion forwards it through the tunnel to your workstation’s agent
  4. Your agent sends it to the YubiKey (PIN is cached from step 1)
  5. YubiKey prompts for touch only; you touch it at your desk
  6. Signed response travels back through the tunnel; access granted

Your key never moves. The requests travel to it.

Firewall Rules

For full enforcement: allow SSH (port 22) to internal hosts only from the Bastion’s IP. Block all other SSH sources, including your own workstation. The only path in is through the Bastion.

Part 7: Enrolling a Backup YubiKey

Danger:

Never enforce hardware-only auth with a single key. If you lose it or it fails, you are locked out of every machine. Always enroll a backup before enabling enforcement.

When programming a second key, use the --device SERIAL flag so ykman targets the right token. Get the serial by running ykman list:

ykman --device SERIAL piv keys generate \
  --algorithm ECCP256 \
  --pin-policy ONCE \
  --touch-policy ALWAYS \
  9a ~/yubikey2-pub.pem

ykman --device SERIAL piv certificates request \
  --subject "CN=Your Name,O=Your Org" \
  9a ~/yubikey2-pub.pem ~/yubikey2.csr

step ca sign --not-after=8760h ~/yubikey2.csr ~/yubikey2.crt

ykman --device SERIAL piv certificates import 9a ~/yubikey2.crt

Repeat for slot 9c. Issue a separate cert from the same CA - same identity, different key material. Pair it with macOS. Add its SSH public key to every server. Verify both keys can authenticate everywhere before continuing.

Warning:

Never plug both YubiKeys in simultaneously. The OTP applet on both fires and floods your terminal with garbage characters.

Store the backup in a fireproof safe, ideally offsite. The primary is your daily driver. The backup only comes out if the primary is lost or damaged.

Part 8: Enforcement

You have two enrolled keys, you have tested both, and you have a recovery path. Now you lock down.

Disable Password Auth on All Servers

On each Linux host, remove password authentication from SSH:

sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart ssh    # Ubuntu
sudo systemctl restart sshd   # Debian
Tip:

Test with a second terminal before closing your current session. Verify you can reconnect with your YubiKey, then close the original session.

Recovery Path

If both YubiKeys are lost or damaged:

  1. Access your VMs through the hypervisor console (e.g., the Proxmox web UI console). This bypasses SSH entirely.
  2. From the console, re-enable password auth, set a strong temporary password, and reconnect via SSH.
  3. Once you have a new YubiKey set up, disable passwords again.

This is your break-glass procedure. Document it somewhere offline.

PKI Server Hardening

Your CA server holds the most sensitive keys in the whole setup. Disable root SSH login and create a standard user with sudo:

adduser myuser
apt install -y sudo
usermod -aG sudo myuser
cp /root/.ssh/authorized_keys /home/myuser/.ssh/authorized_keys
chown -R myuser:myuser /home/myuser/.ssh
sed -i 's/#PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
systemctl restart sshd

CA keys are now only accessible by logging in as myuser and using sudo. Root direct access is gone.

Part 9: Certificate Renewal

Certs expire. Set a calendar reminder 30 days before the expiration date you chose (one year from issuance if you followed this guide).

Export the current public key from the YubiKey:

ykman piv keys export 9a ~/yubikey-pub.pem

Generate a new CSR with the same key:

ykman piv certificates request \
  --subject "CN=Your Name,O=Your Org" \
  9a ~/yubikey-pub.pem ~/yubikey-renewal.csr

Sign it with your CA:

step ca sign --not-after=8760h ~/yubikey-renewal.csr ~/yubikey-renewal.crt

Import the new cert - this overwrites the old one. The key pair stays the same, only the cert changes:

ykman piv certificates import 9a ~/yubikey-renewal.crt

Clean up, then repeat for slot 9c and for the backup key.

Tip:

If the cert expires before you renew, password/biometric fallback (if still enabled) lets you log in and fix it. If the CA is down, restore from backup - all existing certs stay valid because the trust chain is unchanged. The CA going offline does not invalidate anything it has already issued.

Security Model: Why This Works

Three attack vectors, all neutralized by the hardware touch requirement:

Remote workstation compromise: An attacker gains a shell on your machine. They can run commands, but they cannot physically touch the YubiKey sitting on your desk. Every signing operation requires that touch. The attack stops there.

Bastion socket hijack: An attacker compromises the Bastion and tries to hijack the forwarded agent socket. The signing request routes back to the YubiKey, which waits for a touch that never comes. The attack stops there.

Lateral movement from a compromised server: No keys are on disk, no agent socket is present, no credentials are stored anywhere. The server is an island. The attack stops there.

The physical touch requirement is the kill switch for all three. No amount of software compromise, no remote exploit, no stolen session can bypass a hardware button that a human has to physically press.

CA Backup Strategy

Your CA keys are the root of trust for the entire infrastructure. If they are lost, you rebuild from scratch. Treat them accordingly:

  1. Back up the root and intermediate CA keys to an encrypted USB drive immediately after setup
  2. Store the USB in a fireproof safe, separate from your YubiKey backup
  3. If the CA server is destroyed: rebuild the server, restore keys from backup, bring it back up. All existing certs remain valid.
  4. If both CA and backup are lost: rebuild the entire PKI from scratch, new root, new intermediate, new certs, re-trust everywhere. This is painful. This is why you make the backup.

The CA is not in the authentication path. It is a vending machine that can be unplugged after issuing certs. Nothing stops working while it is offline.

What You Have Now

  • A private PKI issuing certificates under your full control
  • Two YubiKeys with CA-signed PIV certificates for authentication and signing
  • macOS smart card login backed by hardware
  • SSH authentication to all servers using hardware-backed keys, no key files on disk
  • Digital document signing with cryptographic proof of identity
  • A hardened Bastion server as the single entry point
  • Agent forwarding that keeps your YubiKey at your desk while authenticating through tunnels
  • Password authentication disabled across all servers
  • A backup key and a documented recovery path

No cloud dependencies. No subscription fees. No software secrets on disk. Physical possession of the YubiKey is the only way in.

Next Steps

  • Deploy an Identity Provider (Authentik or Keycloak) for web app SSO with YubiKey as the MFA factor
  • Configure FreeRADIUS for 802.1X cert-based network authentication (Wi-Fi and wired)
  • Set up git commit signing with slot 9c
  • Explore Windows smart card login for RDP access
  • Configure S/MIME email signing
yubikey piv ssh pki zero-trust homelab certificate-authority bastion smart-card