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)
# 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. RebootAfter install:
- SSH from ThinkPad:
ssh root@192.168.0.50works - Web UI accessible at
https://192.168.0.50:8006 - ZFS pool
rpoolcovers 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: 100Hostname: bastionOS: Ubuntu 24.04 LTS ServerCPU: 2 vCPURAM: 2 GBDisk: 20 GB (on rpool)Network: bridge to vmbr0 (LAN), 192.168.0.10/24User: ubuntu (sudoer; SSH-key only, no password)Bastion install (in Proxmox UI):
-
Create VM 100, attach Ubuntu 24.04 server ISO
-
Install with:
- Hostname
bastion - User
ubuntu - Enable OpenSSH server
- No additional snap packages
- Hostname
-
Boot; login via console; configure:
Terminal window # Disable password authsudo sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_configsudo systemctl reload ssh# Add SSH keymkdir -p ~/.ssh && chmod 700 ~/.sshecho 'ssh-ed25519 AAAA...' >> ~/.ssh/authorized_keyschmod 600 ~/.ssh/authorized_keys# Configure static IP via netplansudo nano /etc/netplan/00-installer-config.yaml# set: 192.168.0.10/24, gateway 192.168.0.1, DNS 1.1.1.1sudo netplan apply# Enable unattended-upgradessudo apt install -y unattended-upgradessudo dpkg-reconfigure --priority=low unattended-upgrades# Set up nftables default-deny (Phase 2 deepens this)sudo apt install -y nftables -
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:
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:
- Proxmox UI → Datacenter → Permissions → API Tokens
- Create token: user=root@pam, token-id=
terralabs-readwrite, privilege-separation=off - 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/bashDATE=$(date +%Y-%m-%d)for vmid in 100 ; do vzdump $vmid --dumpdir /mnt/external-ssd/backups --mode snapshot --compress zstddone
# Retention: keep last 4 snapshotsfind /mnt/external-ssd/backups -name "vzdump-qemu-*.zst" -mtime +28 -deleteThe 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
| Symptom | Likely cause | Fix |
|---|---|---|
| Web UI 502 | pveproxy crashed | systemctl restart pveproxy |
| VM won’t start, “no IOMMU” | UEFI passthrough disabled | enable VT-d in BIOS |
| ZFS pool full warning | snapshots accumulated | review with zfs list -t snapshot; prune old |
| Bastion unreachable | nftables locked you out | console 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
- hardware.md — the M70q spec
- thinkpad-setup.md — SSH config from the daily driver
- Phase 1 prerequisites: Year 1 Phase 1
- Phase 7 (K8s on the homelab): Year 1 Phase 7