Automate full disk encryption (LUKS) on Hetzner Cloud with remote SSH unlock capability.
Hetzner Cloud provides excellent value for cloud infrastructure, but does not offer platform-managed disk encryption with customer-controlled keys. Their own data privacy FAQ states that customers are responsible for encrypting data on rented servers.
This toolkit solves that problem by automating:
- Full disk encryption using LUKS2 + LVM on the root disk
- Remote unlock via SSH using
dracut-sshd(no console access needed) - Post-boot hardening and optional tooling (Docker, NetBird, Cloudflare WARP)
It targets RHEL-family distributions (Rocky Linux, AlmaLinux, CentOS Stream) on Hetzner Cloud.
The hz tool is the primary way to interact with your encrypted Hetzner VMs after provisioning. It wraps SSH/SCP with automatic LUKS unlock support.
Add hz to your PATH:
ln -s "$(pwd)/hz" ~/bin/hz
# or
cp hz /usr/local/bin/hz# SSH into a Hetzner VM (by name or ID)
hz ssh myuser@hetzner-vm
# Run a remote command
hz ssh hetzner-vm 'uptime'
# Copy files to/from the VM
hz scp localfile.txt myuser@hetzner-vm:/tmp/
hz scp myuser@hetzner-vm:/etc/hosts ./hosts-backup
hz scp -r myuser@hetzner-vm:/var/log/ ./logs/If the server is waiting at the LUKS unlock prompt (in the dracut-sshd initramfs), hz will:
- Detect the LUKS unlock prompt
- Prompt for the passphrase (or fetch from 1Password via
op://reference) - Send the passphrase to unlock the disk
- Wait for boot to complete, then connect
This means you can reboot an encrypted server and simply run hz ssh myserver - the unlock happens automatically.
Store your LUKS passphrases in 1Password and reference them:
# Full reference
export HZ_LUKS_UNLOCK="op://Private/my-server/luks-password"
# Short form (expands to op://<vault>/<server-name>/luks-password)
export HZ_LUKS_UNLOCK="op://Private"
hz ssh my-server| Variable | Default | Description |
|---|---|---|
HZ_LUKS_UNLOCK |
- | LUKS passphrase or op:// 1Password reference |
HZ_OP_ACCOUNT |
- | 1Password account (if you have multiple) |
HZ_SSH_STRICT |
y |
y = normal host key checking; n = disable |
HZ_RDNS |
n |
y = use rDNS hostname for SSH config matching |
HZ_CONNECT_WAIT |
120 |
Seconds to wait for TCP port 22 |
HZ_BOOT_WAIT |
600 |
Max seconds to wait for unlock + boot |
HZ_RESET_ON_CRYPTFAIL |
- | y = auto-reset on wrong password; n = fail; unset = prompt |
HZ_RESET_MAX |
1 |
Max automatic resets per run |
HZ_SSH_CMD |
ssh |
Override SSH binary |
HZ_SCP_CMD |
scp |
Override SCP binary |
- Hetzner Cloud API token
hcloudCLI installed and configuredjq,ssh,opensslavailable locally- An existing Hetzner Cloud server to provision (the script will wipe it)
export HCLOUD_TOKEN="your-token"
./provision.sh <server-name-or-id>You'll be prompted for:
- Username to create
- SSH key(s) to use
- Optional features (sudo NOPASSWD, WARP for IPv6-only, NetBird)
The script generates and displays the LUKS and user passwords - save these.
export HCLOUD_TOKEN="your-token"
export PROVISION_USER="your-username"
export USER_SSH_KEY="ssh-ed25519 AAAA..." # Can be multiple newline-separated keys
export UNLOCK_SSH_KEY="$USER_SSH_KEY" # Optional: separate key for initramfs
# Optional configuration
export LUKS_PASSWORD="your-shared-luks-pass" # Optional: override generated password
export ROOT_PASSWORD="your-shared-root-pass" # Optional: override generated password
export USER_PASSWORD="your-shared-user-pass" # Optional: override generated password
export NETBIRD_SETUP_KEY="your-setup-key" # Install and connect NetBird
export WARP_TUNNEL=y # Enable IPv4 tunneling on IPv6-only servers
export WARP_TUNNEL_MODE=proxy # proxy (TCP-only) or warp (UDP-capable)
export SUDO_NOPASSWD=n # Keep passwordless sudo? (y/n)
# Optional: save credentials to 1Password (requires `op`)
# Interactive mode: prompts for vault ref
# Non-interactive mode: set these explicitly
export OP_VAULT_REF="op://Servers" # Target vault (enables saving)
export OP_OVERWRITE_EXISTING=n # Overwrite existing item? (y/n)
# For automation/CI, prefer service accounts:
# export OP_SERVICE_ACCOUNT_TOKEN="ops_..."
export FORCE_IMAGE="Rocky-10-latest-amd64-base.tar.gz" # Specific image
./provision.sh <server-name-or-id>- Validates local tools and
HCLOUD_TOKEN - Switches the server into Hetzner rescue mode
- Installs a RHEL-family OS with LUKS encryption via
installimage - Configures
dracut-sshdfor SSH access during boot - Boots and automatically unlocks (handles SELinux relabel reboot)
- Finalizes with networking, hardening, and optional tools
The finalization phase installs a developer workstation baseline:
- System:
git,ripgrep,make,gcc,clang - Python:
python3-devel,uv - Node.js:
fnm+ Node.js 24 - Tools: GitHub CLI (
gh), 1Password CLI (op), OpenCode
For automation or testing the full flow:
export HCLOUD_TOKEN="your-token"
export PROVISION_USER="your-username"
export USER_SSH_KEY="ssh-ed25519 AAAA..."
export UNLOCK_SSH_KEY="$USER_SSH_KEY"
./test-provision-flow.sh <server-name-or-id>Warning: This is destructive and will wipe the target server.
On first boot, SELinux relabeling may trigger an automatic reboot:
- LUKS unlock
- System reboots for SELinux relabel
- LUKS unlock again
- Normal boot completes
The provisioning scripts handle this automatically. If using hz manually, just run the command again.
If you need to unlock manually without hz:
# SSH into initramfs (IPv4)
ssh root@<server-ip>
# SSH into initramfs (IPv6 - use -6 flag, no brackets)
ssh -6 root@<ipv6-address>
# Find and unlock
socket=$(for s in /run/systemd/ask-password/sck.*; do [ -S "$s" ] && echo "$s" && break; done)
printf '%s' 'YOUR-LUKS-PASSWORD' | /usr/lib/systemd/systemd-reply-password 1 "$socket"Hetzner Cloud does not provide NAT64/DNS64, so IPv6-only servers cannot reach IPv4-only hosts by default.
Enable Cloudflare WARP for IPv4 tunneling:
export WARP_TUNNEL=y
export WARP_TUNNEL_MODE=proxy # or 'warp' for UDP support (needed for NetBird)If SSH keys are rejected:
- Verify the key is loaded:
ssh-add -l - Check that
USER_SSH_KEY/UNLOCK_SSH_KEYmatch your loaded keys - Verify the right SSH agent/keys are in use
| File | Purpose |
|---|---|
hz |
SSH/SCP wrapper with auto LUKS unlock |
provision.sh |
Main provisioning orchestrator |
monitor-boot.sh |
Handles LUKS unlock during boot |
finalize.sh |
Post-boot configuration and hardening |
post-install.sh |
Runs inside rescue to configure the installed system |
wait_for_ssh.sh |
SSH connectivity helper (sourced by other scripts) |
update-boot-ipv6.sh |
Updates initramfs IPv6 config (installed on target) |
For development guidance, code conventions, and common pitfalls, see AGENTS.md.
Run shellcheck *.sh before submitting changes.