Skip to content
Client Panel

Proxmox VE Management Access Hardening

Cakupan: Proxmox VE standalone node dengan management interface langsung menghadap jaringan publik. Tujuan: membatasi akses management Proxmox tanpa mengubah atau mengganggu firewall milik VM/tenant.


Hardening ini ditujukan untuk kondisi ketika interface management Proxmox memiliki public IP dan tidak memungkinkan dipindahkan ke management LAN/private network.

Target akhirnya:

  • SSH TCP/22 hanya dapat diakses dari source administrator yang diizinkan.
  • Proxmox Web UI/API TCP/8006 hanya dapat diakses dari source administrator yang diizinkan.
  • Port management yang tidak digunakan tidak dibuka ke Internet.
  • Firewall VM/tenant tetap bekerja seperti sebelumnya.
  • Perubahan dapat divalidasi sebelum diterapkan.
  • Tersedia automatic rollback apabila akses management terputus.

Gunakan variabel agar dokumentasi tidak berisi IP produksi.

<PVE_HOSTNAME> = hostname node Proxmox
<PVE_PUBLIC_IP> = public IP management Proxmox
<ADMIN_NET_1> = subnet admin utama
<ADMIN_NET_2> = subnet admin backup
<ADMIN_NET_3> = subnet admin backup lainnya
<ADMIN_SINGLE_IP> = single trusted admin IP /32
<NON_ADMIN_TEST_IP> = IP yang dipakai untuk simulasi source non-admin

Contoh menggunakan documentation-only IP:

<PVE_HOSTNAME> = pve01
<PVE_PUBLIC_IP> = 198.51.100.10
<ADMIN_NET_1> = 203.0.113.16/28
<ADMIN_NET_2> = 198.51.100.64/29
<ADMIN_NET_3> = 192.0.2.128/29
<ADMIN_SINGLE_IP> = 203.0.113.250/32
<NON_ADMIN_TEST_IP> = 192.0.2.10

Untuk subnet, sebaiknya gunakan network address, bukan salah satu host address.

Contoh:

203.0.113.17/28

dinormalisasi menjadi:

203.0.113.16/28

3. Prinsip Penting: Host Firewall dan VM Firewall Terpisah

Section titled “3. Prinsip Penting: Host Firewall dan VM Firewall Terpisah”

Traffic yang menuju host Proxmox melewati jalur:

Internet
|
v
INPUT
|
v
PVEFW-INPUT
|
v
PVEFW-HOST-IN

Sedangkan traffic VM melewati:

Internet / Network
|
v
FORWARD
|
v
PVEFW-FWBR-IN / OUT
|
v
tap<VMID>i0-IN / OUT
|
v
VM

Karena itu hardening management host harus dibatasi pada host-side firewall.

Jangan melakukan tindakan berikut apabila firewall VM dikelola tenant/user:

iptables -F
iptables-legacy -F
pve-firewall stop
Datacenter Firewall enable: 0
mengubah firewall=1 pada NIC VM
mengubah /etc/pve/firewall/<VMID>.fw

Periksa status firewall:

Terminal window
pve-firewall status

Contoh output:

Status: enabled/running

Jika output seperti:

Status: disabled/stopped

jangan lanjut menggunakan prosedur yang mengasumsikan firewall guest sedang aktif.


5. Periksa Konfigurasi Datacenter Firewall

Section titled “5. Periksa Konfigurasi Datacenter Firewall”
Terminal window
cat /etc/pve/firewall/cluster.fw

Contoh kondisi yang terlalu terbuka:

[OPTIONS]
enable: 1
[RULES]
IN Web(ACCEPT) -log nolog
IN SSH(ACCEPT) -log nolog
IN ACCEPT -p udp -dport 8006 -log nolog
IN ACCEPT -p tcp -dport 8006 -log nolog

Konfigurasi seperti di atas berarti SSH dan 8006/tcp dapat diakses tanpa pembatasan source.


Terminal window
cat /etc/pve/nodes/$(hostname)/host.fw

Contoh:

[RULES]
IN ACCEPT -p icmp -log nolog

Rule node seperti ICMP dapat tetap dipertahankan apabila memang dibutuhkan.


Terminal window
pvecm status

Contoh standalone node:

Error: Corosync config '/etc/pve/corosync.conf' does not exist - is this node part of a cluster?

Ini menunjukkan node tidak tergabung dalam Corosync cluster.

Pada environment cluster, desain firewall harus mempertimbangkan kebutuhan antar-node seperti migration, corosync, storage, dan sebagainya.


Terminal window
ip -4 -br addr

Contoh:

lo UNKNOWN 127.0.0.1/8
vmbr0 UP 198.51.100.10/30

Dalam contoh tersebut:

vmbr0 = management/public bridge
198.51.100.10 = public management IP

Periksa juga konfigurasi jaringan:

Terminal window
cat /etc/network/interfaces

Contoh:

auto vmbr0
iface vmbr0 inet static
address 198.51.100.10/30
gateway 198.51.100.9
bridge-ports eno1
bridge-stp off
bridge-fd 0

Terminal window
ip -6 addr show scope global

Jika kosong:

root@pve01:~#

berarti host saat ini tidak memiliki global IPv6 address.

Jika ada global IPv6, hardening IPv6 harus dilakukan juga karena sshd atau pveproxy mungkin listen di IPv6.


Terminal window
ss -lntup

Contoh:

tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",...))
tcp LISTEN 0 4096 *:8006 *:* users:(("pveproxy",...))
tcp LISTEN 0 4096 *:3128 *:* users:(("spiceproxy",...))

Service penting Proxmox biasanya:

TCP/22 SSH
TCP/8006 Web UI / API
TCP/3128 SPICE proxy

Jika tidak ada service yang listen pada 80/443, rule Web(ACCEPT) biasanya tidak diperlukan.

8006 menggunakan TCP, sehingga UDP/8006 tidak perlu dibuka.


Terminal window
iptables-legacy -S PVEFW-HOST-IN

Contoh rule terlalu terbuka:

-A PVEFW-HOST-IN -p tcp --dport 80 -j RETURN
-A PVEFW-HOST-IN -p tcp --dport 443 -j RETURN
-A PVEFW-HOST-IN -p tcp --dport 22 -j RETURN
-A PVEFW-HOST-IN -p udp --dport 8006 -j RETURN
-A PVEFW-HOST-IN -p tcp --dport 8006 -j RETURN

Pada Proxmox firewall legacy backend, RETURN di chain tersebut berfungsi sebagai jalur allow kembali menuju parent chain.


Periksa:

Terminal window
iptables-legacy -S PVEFW-FORWARD

Contoh:

-N PVEFW-FORWARD
-A PVEFW-FORWARD -m conntrack --ctstate INVALID -j DROP
-A PVEFW-FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A PVEFW-FORWARD -m physdev --physdev-in fwln+ --physdev-is-bridged -j PVEFW-FWBR-IN
-A PVEFW-FORWARD -m physdev --physdev-out fwln+ --physdev-is-bridged -j PVEFW-FWBR-OUT

Sedangkan host:

Terminal window
iptables-legacy -S PVEFW-INPUT

Contoh:

-N PVEFW-INPUT
-A PVEFW-INPUT -j PVEFW-HOST-IN

Ini menjadi salah satu validasi bahwa host management dan VM forwarding berada di chain berbeda.


Terminal window
pve-firewall localnet

Contoh sebelum hardening:

local hostname: pve01
local IP address: 198.51.100.10
network auto detect: 198.51.100.8/30
using detected local_network: 198.51.100.8/30

Masalahnya, seluruh /30 tersebut dapat masuk ke built-in management network Proxmox.

Cek:

Terminal window
ipset list PVEFW-0-management-v4

Contoh:

Name: PVEFW-0-management-v4
Type: hash:net
Number of entries: 1
Members:
198.51.100.8/30

Untuk standalone public host, lebih aman override local_network menjadi IP host sendiri.


Gunakan custom IPSet:

admin_sources

Contoh:

[IPSET admin_sources]
<ADMIN_NET_1>
<ADMIN_NET_2>
<ADMIN_NET_3>
<ADMIN_SINGLE_IP>

Contoh terisi:

[IPSET admin_sources]
203.0.113.16/28
198.51.100.64/29
192.0.2.128/29
203.0.113.250/32

Hindari memakai special IPSet bernama management untuk general allowlist apabila kebutuhan hanya SSH dan GUI, karena Proxmox menggunakan management network untuk beberapa service internal lain juga.


Target /etc/pve/firewall/cluster.fw:

[OPTIONS]
enable: 1
[ALIASES]
local_network <PVE_PUBLIC_IP>
[IPSET admin_sources]
<ADMIN_NET_1>
<ADMIN_NET_2>
<ADMIN_NET_3>
<ADMIN_SINGLE_IP>
[RULES]
IN SSH(ACCEPT) -source +admin_sources -log nolog
IN ACCEPT -source +admin_sources -p tcp -dport 8006 -log nolog

Contoh:

[OPTIONS]
enable: 1
[ALIASES]
local_network 198.51.100.10
[IPSET admin_sources]
203.0.113.16/28
198.51.100.64/29
192.0.2.128/29
203.0.113.250/32
[RULES]
IN SSH(ACCEPT) -source +admin_sources -log nolog
IN ACCEPT -source +admin_sources -p tcp -dport 8006 -log nolog

Jika firewall VM sudah aktif, jangan melakukan:

enable: 0

sebagai metode staging.

Mematikan Datacenter Firewall dapat berdampak pada enforcement firewall VM.

Sebagai gantinya buat candidate di /root:

Terminal window
nano /root/cluster.fw.candidate

atau:

Terminal window
cat > /root/cluster.fw.candidate <<'EOF'
...
EOF

File ini belum dibaca oleh Proxmox firewall.


Gunakan parser Proxmox langsung:

Terminal window
perl -MPVE::Firewall -e '
my $f = "/root/cluster.fw.candidate";
my $conf = PVE::Firewall::load_clusterfw_conf($f);
die "PARSE FAILED\n" if !$conf;
print "PARSE OK\n";
my ($ruleset, $ipset_rules, $ipset_changes, $ebtables) =
PVE::Firewall::compile($conf, undef, undef, undef);
print "COMPILE OK\n";
'

Hasil yang diharapkan:

PARSE OK
COMPILE OK

Jika muncul error, jangan apply candidate.


Fingerprint semua file firewall VM:

Terminal window
find /etc/pve/firewall -maxdepth 1 -type f -regextype posix-extended \
-regex '.*/[0-9]+\.fw' -print0 \
| sort -z \
| xargs -0 sha256sum \
| sha256sum

Contoh:

=== GUEST FW CONFIG FINGERPRINT ===
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -

Fingerprint NIC VM:

Terminal window
for id in $(qm list | awk 'NR>1 {print $1}' | sort -n); do
echo "VM $id"
qm config "$id" | grep -E '^net[0-9]+:'
done | sha256sum

Contoh:

bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb -

Simpan kedua hash tersebut.

Sesudah hardening, hash harus tetap sama.


Terminal window
TS="$(date +%Y%m%d-%H%M%S)"
BACKUP_DIR="/root/pve-fw-backup-$TS"
mkdir -p "$BACKUP_DIR"
cp /etc/pve/firewall/cluster.fw \
"$BACKUP_DIR/cluster.fw"
if [ -f /etc/pve/nodes/<PVE_HOSTNAME>/host.fw ]; then
cp /etc/pve/nodes/<PVE_HOSTNAME>/host.fw \
"$BACKUP_DIR/host.fw"
fi

Contoh hasil:

/root/pve-fw-backup-YYYYMMDD-HHMMSS/
├── cluster.fw
└── host.fw

Verifikasi:

Terminal window
sha256sum \
/etc/pve/firewall/cluster.fw \
"$BACKUP_DIR/cluster.fw"

Hasil yang diharapkan, kedua hash sama:

111111... /etc/pve/firewall/cluster.fw
111111... /root/pve-fw-backup-YYYYMMDD-HHMMSS/cluster.fw

cat > /root/pve-fw-rollback.sh <<'EOF'
#!/bin/bash
set -e
cp /root/pve-fw-backup-<TIMESTAMP>/cluster.fw \
/etc/pve/firewall/cluster.fw
echo "Rollback config restored at $(date)"
sleep 15
echo "=== FIREWALL STATUS ==="
pve-firewall status
echo
echo "=== HOST RULES ==="
iptables-legacy -S PVEFW-HOST-IN
EOF
chmod 700 /root/pve-fw-rollback.sh

Sebelum apply:

Terminal window
systemd-run \
--unit=pve-fw-rollback \
--on-active=10m \
--timer-property=AccuracySec=1s \
/root/pve-fw-rollback.sh

Contoh:

Running timer as unit: pve-fw-rollback.timer
Will run service as unit: pve-fw-rollback.service

Verifikasi:

Terminal window
systemctl list-timers pve-fw-rollback.timer --no-pager

Contoh:

NEXT LEFT UNIT
Mon YYYY-MM-DD HH:MM:SS 9min pve-fw-rollback.timer

Jangan apply sebelum rollback timer terlihat aktif.


Apply:

Terminal window
cp /root/cluster.fw.candidate \
/etc/pve/firewall/cluster.fw

Gunakan plain cp.

Menggunakan:

Terminal window
cp -a

ke /etc/pve dapat menghasilkan warning seperti:

cp: clearing permissions for '/etc/pve/firewall/cluster.fw': Operation not permitted

karena /etc/pve menggunakan Proxmox Cluster File System (pmxcfs).

Tunggu reload:

Terminal window
sleep 10

Kemudian:

Terminal window
pve-firewall status

Hasil yang diharapkan:

Status: enabled/running

Terminal window
iptables-legacy -S PVEFW-HOST-IN

Bagian penting expected:

-A PVEFW-HOST-IN -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A PVEFW-HOST-IN -m set --match-set PVEFW-0-admin_sources-v4 src -p tcp --dport 22 -j RETURN
-A PVEFW-HOST-IN -m set --match-set PVEFW-0-admin_sources-v4 src -p tcp --dport 8006 -j RETURN
...
-A PVEFW-HOST-IN -j PVEFW-Drop
-A PVEFW-HOST-IN -j DROP

Rule berikut sudah tidak boleh ada:

-p tcp --dport 22 -j RETURN

tanpa source restriction.

Begitu juga:

-p tcp --dport 8006 -j RETURN

tanpa source restriction.


Terminal window
ipset list PVEFW-0-admin_sources-v4

Contoh:

Name: PVEFW-0-admin_sources-v4
Type: hash:net
Number of entries: 4
Members:
203.0.113.16/28
198.51.100.64/29
192.0.2.128/29
203.0.113.250

Jumlah dan isi harus sesuai dengan daftar administrator.


Terminal window
pve-firewall localnet

Hasil yang diharapkan:

local hostname: pve01
local IP address: 198.51.100.10
network auto detect: 198.51.100.8/30
using user defined local_network: 198.51.100.10

Perhatikan:

using user defined local_network

bukan:

using detected local_network

Terminal window
pve-firewall simulate \
--from outside \
--to host \
--source <NON_ADMIN_TEST_IP> \
--dest <PVE_PUBLIC_IP> \
--protocol tcp \
--dport 22 \
--verbose 1 | tail -n 12

Contoh:

Test packet:
from : outside
to : host
proto : tcp
source : 192.0.2.10
dest : 198.51.100.10
dport : 22
ACTION: DROP

Hasil yang diharapkan:

ACTION: DROP

Terminal window
pve-firewall simulate \
--from outside \
--to host \
--source <ADMIN_SINGLE_IP> \
--dest <PVE_PUBLIC_IP> \
--protocol tcp \
--dport 22 \
--verbose 1 | tail -n 12

Contoh output:

Test packet:
from : outside
to : host
proto : tcp
source : 203.0.113.250
dest : 198.51.100.10
dport : 22
ACTION: ACCEPT

Hasil yang diharapkan:

ACTION: ACCEPT

Terminal window
pve-firewall simulate \
--from outside \
--to host \
--source <NON_ADMIN_TEST_IP> \
--dest <PVE_PUBLIC_IP> \
--protocol tcp \
--dport 8006 \
--verbose 1 | tail -n 12

Hasil yang diharapkan:

Test packet:
from : outside
to : host
proto : tcp
source : 192.0.2.10
dest : 198.51.100.10
dport : 8006
ACTION: DROP

Terminal window
pve-firewall simulate \
--from outside \
--to host \
--source <ADMIN_SINGLE_IP> \
--dest <PVE_PUBLIC_IP> \
--protocol tcp \
--dport 8006 \
--verbose 1 | tail -n 12

Hasil yang diharapkan:

Test packet:
from : outside
to : host
proto : tcp
source : 203.0.113.250
dest : 198.51.100.10
dport : 8006
ACTION: ACCEPT

Jangan hanya mengandalkan session browser yang sudah terbuka.

Koneksi lama dapat tetap hidup karena rule:

RELATED,ESTABLISHED

Buka browser incognito/private baru lalu akses:

https://<PVE_PUBLIC_IP>:8006

Setelah berhasil, cek:

Terminal window
ss -tnp state established '( sport = :8006 )'

Contoh:

Recv-Q Send-Q Local Address:Port Peer Address:Port
0 0 198.51.100.10:8006 203.0.113.250:45123
0 0 198.51.100.10:8006 203.0.113.25:51290

Pastikan Peer Address berasal dari salah satu <ADMIN_NET_X>.


Jalankan ulang fingerprint firewall VM:

Terminal window
find /etc/pve/firewall -maxdepth 1 -type f -regextype posix-extended \
-regex '.*/[0-9]+\.fw' -print0 \
| sort -z \
| xargs -0 sha256sum \
| sha256sum

Contoh sebelum:

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -

Contoh sesudah:

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa -

Harus sama.

Kemudian NIC:

Terminal window
for id in $(qm list | awk 'NR>1 {print $1}' | sort -n); do
echo "VM $id"
qm config "$id" | grep -E '^net[0-9]+:'
done | sha256sum

Sebelum:

bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb -

Sesudah:

bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb -

Jika hash berubah, investigasi sebelum cancel rollback.


Rollback baru boleh dicancel apabila seluruh checklist ini lolos:

[OK] Firewall enabled/running
[OK] Non-admin -> TCP/22 = DROP
[OK] Admin -> TCP/22 = ACCEPT
[OK] Non-admin -> TCP/8006 = DROP
[OK] Admin -> TCP/8006 = ACCEPT
[OK] Fresh browser connection berhasil
[OK] Guest firewall fingerprint sama
[OK] Guest NIC fingerprint sama

Stop timer:

Terminal window
systemctl stop pve-fw-rollback.timer

Verifikasi:

Terminal window
systemctl list-timers --all --no-pager \
| grep 'pve-fw-rollback' \
|| echo 'No active rollback timers'

Hasil yang diharapkan:

No active rollback timers

Bandingkan candidate dengan live config:

Terminal window
sha256sum \
/etc/pve/firewall/cluster.fw \
/root/cluster.fw.candidate

Hasil yang diharapkan:

cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc /etc/pve/firewall/cluster.fw
cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc /root/cluster.fw.candidate

Hash harus identik.


Opsional:

Terminal window
systemctl reset-failed \
pve-fw-rollback.service \
pve-fw-rollback-apply.service \
pve-fw-rollback-verify.service \
pve-fw-rollback-finalcheck.service 2>/dev/null || true

Kemudian:

Terminal window
systemctl list-units --all 'pve-fw-rollback*' --no-pager

Contoh hasil bersih:

UNIT LOAD ACTIVE SUB DESCRIPTION
0 loaded units listed.

Backup sebaiknya jangan langsung dihapus.


Pada:

Datacenter
└── Firewall
└── Rules

rule yang terlihat cukup:

OnTypeActionMacroProtocolSourceD.Port
inACCEPTSSH+admin_sources
inACCEPTtcp+admin_sources8006

Sedangkan:

Datacenter
└── Firewall
└── IPSet

berisi:

admin_sources
├── <ADMIN_NET_1>
├── <ADMIN_NET_2>
├── <ADMIN_NET_3>
└── <ADMIN_SINGLE_IP>

Dan:

Datacenter
└── Firewall
└── Alias

memiliki:

local_network = <PVE_PUBLIC_IP>

Setelah firewall allowlist stabil, audit SSH:

Terminal window
sshd -T | grep -E \
'^(permitrootlogin|passwordauthentication|pubkeyauthentication|maxauthtries) '

Contoh kondisi awal:

maxauthtries 6
permitrootlogin yes
pubkeyauthentication yes
passwordauthentication yes

Target jangka panjang dapat berupa:

pubkeyauthentication yes
passwordauthentication no

Untuk PermitRootLogin, sesuaikan dengan operational model.

Jangan disable password authentication sebelum semua lokasi administrator telah diuji menggunakan public key.


INTERNET
|
+--------------+--------------+
| |
NON-ADMIN ADMIN
| admin_sources
| |
TCP/22 DROP TCP/22 ACCEPT
TCP/8006 DROP TCP/8006 ACCEPT
| |
+--------------+--------------+
|
v
+-------------+
| PVE HOST |
| public mgmt |
+-------------+
|
+--------------+--------------+
| |
v v
HOST INPUT VM FORWARD
PVEFW-HOST-IN PVEFW-FWBR-*
| |
| v
admin-only access tap<VMID>
|
v
VM Firewall
|
Tenant Managed
|
DO NOT MODIFY

Harden the Proxmox host without treating tenant VM firewall rules as part of the host hardening scope.

Management access harus bersifat explicit allowlist, sedangkan konfigurasi firewall VM/tenant harus dianggap sebagai domain terpisah yang tidak diubah selama proses hardening host.