Skip to content
HOMELAB

Proxmox + Bastion VM

Hypervisor install on the M70q + a bastion VM as the SSH entry point + network notes.

This doc turns the bare M70q from hardware.md into a usable virtualization host. Two artifacts come out of it: a Proxmox install on the metal, and a single Ubuntu bastion VM that becomes the SSH entry point for everything else in the homelab.

It lands pre-Phase-1 as Stage 2 of the Start Here checklist, after the box is assembled and before Phase 1 (OS Foundations) opens. From Phase 7 (Kubernetes + GitOps) onward this same Proxmox host is what K3s nodes get cloned from, and in Year 2 the API token below is what terralabs uses to provision K3s VMs from Terraform.


Why Proxmox

  • Free, OSS, debian-based — no licensing surprise
  • Run multiple VMs on one host (bastion + K3s nodes + Postgres + MinIO + …)
  • Web UI for the parts that should be web; API for everything else
  • ZFS-native (used for snapshots + replication)
  • Mature; the homelab default for a reason

Alternatives considered: ESXi (free tier killed; vCenter expensive), Hyper-V (Windows host), bare metal (not enough hardware to justify), K3s on Talos directly (loses VM flexibility for non-K8s workloads).


Install (Month 1)

Terminal window
# 1. Boot M70q from Proxmox 8.2 USB
# 2. During install:
# - Partition: ZFS RAID-0 on the 256GB NVMe (single disk; no real RAID at homelab scale)
# - Hostname: pve.local
# - IP: 192.168.0.50/24 with gateway 192.168.0.1
# - Root password (store in 1Password, NOT in this doc)
# 3. Reboot; web UI at https://192.168.0.50:8006
# 4. Login as root + password
# 5. Disable enterprise repo:
# pveam update # confirm warnings
# nano /etc/apt/sources.list.d/pve-enterprise.list # comment out
# nano /etc/apt/sources.list.d/ceph.list # comment out
# 6. Add no-subscription repo:
# echo "deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription" \
# > /etc/apt/sources.list.d/pve-no-subscription.list
# 7. apt update && apt full-upgrade -y
# 8. Reboot

After install:

  • SSH from ThinkPad: ssh root@192.168.0.50 works
  • Web UI accessible at https://192.168.0.50:8006
  • ZFS pool rpool covers the whole NVMe

Bastion VM (Month 1)

Single Ubuntu VM that’s the SSH entry point. Hardened. The only thing reachable from outside the LAN (via Tailscale).

VM ID: 100
Hostname: bastion
OS: Ubuntu 24.04 LTS Server
CPU: 2 vCPU
RAM: 2 GB
Disk: 20 GB (on rpool)
Network: bridge to vmbr0 (LAN), 192.168.0.10/24
User: ubuntu (sudoer; SSH-key only, no password)

Bastion install (in Proxmox UI):

  1. Create VM 100, attach Ubuntu 24.04 server ISO

  2. Install with:

    • Hostname bastion
    • User ubuntu
    • Enable OpenSSH server
    • No additional snap packages
  3. Boot; login via console; configure:

    Terminal window
    # Disable password auth
    sudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
    sudo systemctl reload ssh
    # Add SSH key
    mkdir -p ~/.ssh && chmod 700 ~/.ssh
    echo 'ssh-ed25519 AAAA...' >> ~/.ssh/authorized_keys
    chmod 600 ~/.ssh/authorized_keys
    # Configure static IP via netplan
    sudo nano /etc/netplan/00-installer-config.yaml
    # set: 192.168.0.10/24, gateway 192.168.0.1, DNS 1.1.1.1
    sudo netplan apply
    # Enable unattended-upgrades
    sudo apt install -y unattended-upgrades
    sudo dpkg-reconfigure --priority=low unattended-upgrades
    # Set up nftables default-deny (Phase 2 deepens this)
    sudo apt install -y nftables
  4. Test from ThinkPad: ssh ubuntu@192.168.0.10 — should work, no password.

Add the SSH key to authorized_keys before disabling password auth, and keep a Proxmox console session open while you reload sshd. Locking yourself out of the bastion at Month 1 is the most common pre-Phase-1 self-inflicted incident.


nftables (Phase 2 hardening)

Default-deny + explicit allow. Phase 2 (Networking) is where this gets serious; Month 1 has a minimal version:

/etc/nftables.conf
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept
ct state established,related accept
tcp dport 22 accept # SSH
icmp type echo-request accept
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}

sudo nft -f /etc/nftables.conf && sudo systemctl enable nftables.

Verify with sudo nft list ruleset — if SSH is missing or policy drop is on output, the next reboot will lock you out. Phase 2 is where you’ll add explicit allow rules for the Tailscale interface, the K3s API server (once Phase 7 lands), and any other inbound traffic.


Proxmox API token (for terralabs Y2 P9)

When terralabs/terraform/modules/proxmox-k3s-cluster lands in Y2, you’ll provision K3s VMs from Terraform. Generate an API token:

  1. Proxmox UI → Datacenter → Permissions → API Tokens
  2. Create token: user=root@pam, token-id=terralabs-readwrite, privilege-separation=off
  3. Save token to bastion as a sealed secret (Y2 P12 onward)

The token is the seam where terralabs plugs into the homelab. Until Year 2 you can leave this step uncreated; bookmark this section for when Phase 9 starts.


Snapshots + backup

# Weekly snapshot of bastion + critical VMs to external SSD
# (Cron on Proxmox host; runs Sunday 3am)
#!/bin/bash
DATE=$(date +%Y-%m-%d)
for vmid in 100 ; do
vzdump $vmid --dumpdir /mnt/external-ssd/backups --mode snapshot --compress zstd
done
# Retention: keep last 4 snapshots
find /mnt/external-ssd/backups -name "vzdump-qemu-*.zst" -mtime +28 -delete

The external SSD is mounted at /mnt/external-ssd (Month 10 upgrade). Until then, dump to the internal NVMe — but watch disk usage; the VM disks plus weekly backups will fill 256GB faster than expected.

Restore is qmrestore /mnt/external-ssd/backups/vzdump-qemu-100-*.zst 100 --force. Practice it once before you need it; an untested backup is a hopeful guess.


Troubleshooting

SymptomLikely causeFix
Web UI 502pveproxy crashedsystemctl restart pveproxy
VM won’t start, “no IOMMU”UEFI passthrough disabledenable VT-d in BIOS
ZFS pool full warningsnapshots accumulatedreview with zfs list -t snapshot; prune old
Bastion unreachablenftables locked you outconsole login via Proxmox UI; flush + reload

The bastion-unreachable scenario is the one to internalise: the Proxmox web UI’s noVNC console is your get-out-of-jail card. Bookmark https://192.168.0.50:8006 on every device that might need it.


Cross-references