Skip to content

Add Separated-Key Architecture#146

Open
dvicory wants to merge 5 commits into
ibizaman:mainfrom
dvicory:separated-key
Open

Add Separated-Key Architecture#146
dvicory wants to merge 5 commits into
ibizaman:mainfrom
dvicory:separated-key

Conversation

@dvicory

@dvicory dvicory commented Oct 7, 2025

Copy link
Copy Markdown
Contributor

Overview

Closes #132.

This PR introduces a separated-key architecture that protects SOPS-encrypted secrets (including ZFS passphrase, passwords, API keys) from physical access attacks. By separating the boot SSH key from the runtime SSH key used for SOPS decryption, an attacker with physical access to the unencrypted /boot partition can no longer decrypt your secrets.

New hosts created with gen-new-host now use separated-key mode by default. Migration tools are provided for upgrading existing single-key hosts.

Security Model

The Problem: Single-Key Vulnerability

In traditional single-key mode, one SSH key (/boot/host_key) serves two purposes:

  1. Initrd SSH: SSH into initrd to decrypt ZFS root pool
  2. SOPS decryption: Decrypt secrets for system operation

Since /boot must be unencrypted, this creates a vulnerability:

  • 🔓 Physical access to drive → Can read /boot/host_key
  • 🔓 With boot key → Can decrypt all SOPS secrets (ZFS passphrase, passwords, API keys)
  • 🔓 With secrets → Full system compromise, even while powered off

The Solution: Separated-Key Architecture

Boot key (/boot/host_key):

  • Purpose: SSH host key for initrd environment only
  • Location: Unencrypted /boot partition (required for remote decrypt)
  • Security: Physical access = SSH MITM possible, but data remains safe at rest

Runtime key (/persist/etc/ssh/ssh_host_ed25519_key):

  • Purpose: SSH host key for normal system + SOPS secrets decryption
  • Location: Encrypted on ZFS pool in /persist
  • Security: Physical access = cannot decrypt secrets without boot unlock

Configuration Modes

Single-key (legacy, discouraged):

sops.age.sshKeyPaths = ["/boot/host_key"];

Separated-key (default, recommended):

sops.age.sshKeyPaths = ["/persist/etc/ssh/ssh_host_ed25519_key"];

Core Implementation

Automatic Detection

Skarabox automatically detects separated-key mode when the runtime key path (/persist/etc/ssh/ssh_host_ed25519_key) is configured in sops.age.sshKeyPaths.

New Configuration Options

  • skarabox.runtimeHostKeyPath - Path to runtime private key (default: "${hostDir}/runtime_host_key")
  • skarabox.runtimeHostKeyPub - Runtime public key content

Key Installation

During system installation (install-on-beacon), the runtime key is:

  1. Passed via --disk-encryption-keys to disko
  2. Installed to /persist/etc/ssh/ssh_host_ed25519_key in disko's postMountHook
  3. Configured for OpenSSH via services.openssh.hostKeys

Boot Dependency

/persist is marked as neededForBoot to ensure SOPS can access the runtime key early in boot process.

Breaking Changes

Default to Separated-Key Mode

gen-new-host now creates separated-key hosts by default. Use --single-key flag for legacy single-key mode.

Backward compatibility: Existing single-key hosts remain fully functional.

Migration Tools for Existing Hosts

Three new tools help migrate existing single-key hosts to separated-key architecture:

  • enable-key-separation - Generates runtime keys and updates .sops.yaml configuration
  • install-runtime-key - Deploys runtime key to /persist/etc/ssh/ on target host
  • rotate-boot-key - Securely replaces boot key via partition wipe

Complete migration workflow is documented in docs/normal-operations.md.

Testing

  • ✅ Fresh separated-key hosts created with gen-new-host
  • ✅ Single-key hosts migrating to separated-key mode
  • ✅ Boot key rotation with rotate-boot-key

Introduce two-key security model protecting against physical access:

Boot key (/boot/host_key):
  - SSH host key for initrd environment
  - Stored unencrypted (necessary for remote unlock)
  - Physical access = SSH MITM, but data safe at rest

Runtime key (/persist/etc/ssh/ssh_host_ed25519_key):
  - SSH host key for normal system operation
  - Used for SOPS secrets decryption
  - Stored encrypted on ZFS pool
  - Physical access = cannot decrypt secrets

Security improvement: Physical /boot access no longer compromises
SOPS-encrypted secrets (ZFS passphrase, passwords, API keys).

Core changes:
- Add runtimeHostKeyPath and runtimeHostKeyPub options
- Auto-detect separated-key mode from sops.age.sshKeyPaths
- Install runtime key during disko (postMountHook)
- Configure OpenSSH to use runtime key for post-boot
- Mark /persist as neededForBoot for early SOPS access
- Improves known_hosts generation
- Pass runtime key via --disk-encryption-keys during install

Templates default to separated-key mode.
Single-key mode remains functional (backward compatible).
Provide scripts to assist with migration (some manual steps
still required, to be documented).

1. enable-key-separation: Prepare migration
   - Generate runtime SSH host key
   - Derive age key from runtime key for SOPS
   - Update .sops.yaml with runtime key, renames boot key

2. install-runtime-key: Deploy to target
   - Copy runtime key to /persist/etc/ssh/
   - Required before deploying separated-key configuration

3. rotate-boot-key: Rotate to prevent future compromise
   - Back up /boot to tmpfs
   - Securely wipe boot partition (dd + TRIM)
   - Recreate filesystem and reinstall bootloader
   - Handle mirrored boot partitions automatically
New hosts now use separated-key architecture by default for better
security. Add --single-key flag for legacy mode (discouraged).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security implications of single SSH host key architecture

1 participant