Lewati ke konten

Cara Menyiapkan VPS untuk Pemula, dari Seorang Pemula (Bagian 2: Firewall, Ban, dan Update Otomatis)

Bagian 2 dari penyiapan VPS pertama saya di Debian 13. Firewall nftables, fail2ban dengan jail untuk pelanggar berulang, update keamanan otomatis, swap zram, tuning kernel, dan log yang dibatasi.

Seri 5 bagian.

Sampai Mana Kita

Bagian 1 mengunci login: satu pengguna, hanya kunci, tanpa root.

Sisanya masih terbuka lebar. Program apa pun yang mendengarkan di sebuah port bisa dijangkau dari seluruh internet, tidak ada yang memperhatikan login gagal yang menumpuk, dan patch keamanan cuma masuk kalau saya ingat menjalankan apt. Bagian ini membereskan itu. Tidak ada yang kelihatan hasilnya, tapi inilah yang bikin server aman ditinggal.

Firewall

Idenya sederhana: blokir semua yang masuk, lalu izinkan hanya yang memang perlu dijangkau. Itu SSH, ditambah 80 dan 443 untuk web server nanti. Kalau suatu saat saya tidak sengaja menjalankan sesuatu di port lain, tidak ada orang luar yang bisa menjangkaunya.

Saya pakai nftables langsung. Itu firewall yang dipakai Debian di baliknya, alat seperti ufw cuma lapisan di atasnya, dan dengan nftables seluruh aturannya ada di satu berkas yang bisa saya baca dari atas ke bawah dan saya cek sebelum dimuat.

sudo apt install -y nftables
sudo nano /etc/nftables.conf

Ganti seluruh isi berkasnya dengan ini. Pakai port SSH dari Bagian 1:

#!/usr/sbin/nft -f

table inet vps_filter
delete table inet vps_filter

table inet vps_filter {
  chain input {
    type filter hook input priority filter; policy drop;

    ct state established,related accept
    ct state invalid drop
    iif "lo" accept

    icmp type { destination-unreachable, time-exceeded, parameter-problem } accept
    icmp type echo-request limit rate 5/second burst 10 packets accept
    icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, mld-listener-query } accept
    icmpv6 type echo-request limit rate 5/second burst 10 packets accept
    ip6 saddr fe80::/10 udp dport 546 accept

    tcp dport 22022 accept comment "ssh"
    tcp dport { 80, 443 } accept comment "http, https"
    udp dport 443 accept comment "http/3"
  }

  chain forward {
    type filter hook forward priority filter; policy drop;
  }

  chain output {
    type filter hook output priority filter; policy accept;
  }
}

Dibaca dari atas:

  • policy drop di input itu bagian “blokir semua”. Aturan di bawahnya adalah pengecualiannya.
  • established,related membiarkan balasan masuk untuk koneksi yang dimulai server sendiri, misalnya apt yang mengunduh package. Tanpa ini, tidak ada yang jalan.
  • lo adalah interface loopback, supaya program di server bisa saling bicara.
  • Baris-baris ICMP menjaga bagian dari ping dan laporan error yang dibutuhkan jaringan supaya berjalan benar (path MTU discovery, neighbour discovery IPv6) dan membatasi ping biasa.
  • Baris fe80::/10 membiarkan balasan DHCPv6 masuk. Sebagian provider membagikan IPv6 dengan cara itu.
  • Lalu SSH, HTTP, dan HTTPS lewat TCP, plus 443 lewat UDP. Yang terakhir untuk HTTP/3, yang berjalan di atas UDP. Tanpanya, HTTPS tetap jalan, cuma HTTP/3 tidak pernah aktif.

Dua baris sebelum tabel itu kelihatan aneh. Yang pertama membuat tabelnya kalau belum ada, yang kedua menghapusnya, lalu blok di bawahnya membuatnya lagi. Dengan begitu berkas ini aman dimuat berulang kali, dan ia hanya menyentuh tabel milik saya. Berkas bawaan Debian diawali flush ruleset, yang menghapus semua tabel di mesin. Itu jadi penting begitu fail2ban menambahkan tabelnya sendiri di bawah nanti.

Cek berkasnya sebelum dimuat:

sudo nft -c -f /etc/nftables.conf

Tidak ada output berarti sintaksnya benar. Tapi itu tidak berarti aturannya benar, jadi baca sekali lagi port SSH-nya. Kalau salah, perintah berikut mengunci kita di luar. Biarkan sesi tetap terbuka, sama seperti di Bagian 1.

sudo systemctl enable --now nftables
sudo nft list table inet vps_filter
Output
table inet vps_filter {
	chain input {
		type filter hook input priority filter; policy drop;
		...
		tcp dport 22022 accept comment "ssh"
		tcp dport { 80, 443 } accept comment "http, https"
		udp dport 443 accept comment "http/3"
	}
	...
}

Mulai sekarang, setelah mengedit berkasnya, pakai reload, jangan pernah restart:

sudo systemctl reload nftables

Menghentikan nftables.service menjalankan flush ruleset. Restart akan ikut membuang ban dari fail2ban bersama yang lain.

Kontainer Tidak Bisa Menembusnya

Pengguna Docker sering kena masalah ini: Docker menulis aturan firewall-nya sendiri, dan port kontainer yang dipublikasikan bisa dijangkau dari luar walaupun firewall bilang diblokir.

Di sini itu tidak akan terjadi. Kontainer di Bagian 3 itu rootless, jadi berjalan sebagai pengguna biasa yang tidak boleh menyentuh firewall sama sekali. Aturan-aturan ini satu-satunya yang ada.

Fail2ban

Firewall menentukan port mana yang terbuka. Ia tidak peduli apa yang terjadi di port itu. Port 22022 harus menerima koneksi dari siapa saja, jadi siapa saja bisa terus mencoba.

Karena password sudah dimatikan, tidak ada yang bisa masuk dengan menebak. Yang saya dapat dari fail2ban adalah log yang lebih tenang. Ia memperhatikan kegagalan berulang dan mem-ban alamatnya untuk sementara.

sudo apt install -y fail2ban python3-systemd

python3-systemd membuat fail2ban bisa membaca journal systemd. Debian 13 tidak lagi menulis login SSH ke /var/log/auth.log, jadi tanpanya jail SSH tidak punya apa-apa untuk diawasi.

Pertama, pastikan fail2ban menulis log-nya sendiri ke berkas. Jail untuk pelanggar berulang di bawah membacanya:

sudo nano /etc/fail2ban/fail2ban.d/90-local.local
[Definition]
logtarget = /var/log/fail2ban.log

Lalu jail-nya. Saya tidak pernah mengedit jail.conf langsung, karena update package menimpanya:

sudo nano /etc/fail2ban/jail.d/90-local.local
[DEFAULT]
backend = systemd
banaction = nftables-multiport
banaction_allports = nftables-allports
bantime = 1h
bantime.increment = true
bantime.maxtime = 1w
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1

[sshd]
enabled = true
port = 22022
mode = aggressive

[recidive]
enabled = true
backend = auto
logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s
bantime = 1w
findtime = 1d
maxretry = 3

Yang dilakukannya:

  • Lima kegagalan dalam sepuluh menit bikin sebuah alamat di-ban selama satu jam.
  • bantime.increment bikin setiap ban berikutnya untuk alamat yang sama makin lama, sampai seminggu.
  • mode = aggressive juga menghitung kegagalan yang lebih berisik, misalnya klien yang memutus koneksi di tengah key exchange. Kebanyakan scanner melakukan persis itu.
  • recidive mengawasi log fail2ban sendiri. Alamat yang di-ban tiga kali dalam sehari diblokir di semua port selama seminggu.
  • Ban-nya ditulis sebagai aturan nftables, di tabelnya sendiri.
  • port harus sama dengan port SSH yang asli. Kalau salah, jail-nya kelihatan sehat padahal mengawasi port yang salah.

Tes config-nya, lalu jalankan:

sudo fail2ban-client -t
sudo systemctl enable fail2ban
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd
Output
Status for the jail: sshd
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     0
|  `- Journal matches:  _SYSTEMD_UNIT=ssh.service + _COMM=sshd
`- Actions
   |- Currently banned: 0
   |- Total banned:     0
   `- Banned IP list:

Semua nol di hari pertama itu normal. Baris yang perlu dicek adalah Journal matches. Kalau kosong, jail-nya jalan tapi tidak mengawasi apa-apa.

Sehari kemudian jadinya seperti ini:

Output
Status for the jail: sshd
|- Filter
|  |- Currently failed: 3
|  |- Total failed:     847
|  `- Journal matches:  _SYSTEMD_UNIT=ssh.service + _COMM=sshd
`- Actions
   |- Currently banned: 6
   |- Total banned:     41
   `- Banned IP list:   45.148.10.92 92.63.197.14 141.98.11.72 ...

847 percobaan gagal di server yang tidak dikenal siapa-siapa, di port yang tidak standar. Lalu lintas itu sudah ada sejak awal; sekarang saya bisa melihatnya.

Dan inilah kenapa berkas firewall tadi hanya mengurus tabelnya sendiri:

sudo nft list tables
Output
table inet vps_filter
table inet f2b-table

Aturan saya dan ban dari fail2ban hidup berdampingan, dan me-reload aturan saya tidak mengganggu milik fail2ban.

Update Keamanan Otomatis

Saya tidak akan ingat menambal server ini setiap minggu, jadi servernya yang mengerjakan sendiri:

sudo apt install -y unattended-upgrades apt-listchanges needrestart
sudo nano /etc/apt/apt.conf.d/20auto-upgrades
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";

Itu yang menyalakannya: perbarui daftar package setiap hari, pasang update setiap hari, bersihkan cache unduhan setiap minggu.

Pengaturannya saya taruh di berkas sendiri, supaya tidak ditimpa update package:

sudo nano /etc/apt/apt.conf.d/52unattended-upgrades-local
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-WithUsers "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
Unattended-Upgrade::SyslogEnable "true";

Secara default, Debian hanya memasang update dari repositori keamanan, jadi perubahan versi besar tidak akan masuk sendiri di malam hari.

Menghapus kernel lama menjaga /boot tidak penuh. Kalau sampai penuh, apt rusak, dan tidak kelihatan jelas kenapa.

Pengaturan reboot itu bagian yang paling lama saya pikirkan. Perbaikan keamanan kernel baru berlaku setelah reboot, dan kalau diserahkan ke saya, tidak akan tepat waktu. Jadi kalau ada update yang butuh reboot, server reboot jam 4 pagi. Itu aman karena semua yang saya jalankan kembali sendiri setelahnya, dan Bagian 3 yang memastikan itu.

needrestart mengurus separuh lainnya. Kalau library seperti OpenSSL ditambal, program yang sudah memuat versi lama tetap memakainya sampai di-restart. Ini menyuruh needrestart me-restart-nya otomatis tanpa bertanya:

sudo mkdir -p /etc/needrestart/conf.d
echo "\$nrconf{restart} = 'a';" | sudo tee /etc/needrestart/conf.d/90-local.conf

Lakukan dry run daripada menunggu sehari untuk tahu jalan atau tidak:

sudo unattended-upgrade --dry-run --debug
Output
Starting unattended upgrades script
Allowed origins are: origin=Debian,codename=trixie,label=Debian-Security, ...
Packages that will be upgraded: libssl3t64 openssl

Allowed origins harus menyebut repositori keamanan.

Swap di RAM dengan zram

Kebanyakan paket VPS murah punya RAM 1 sampai 2 GB dan tanpa swap. Kalau ada yang memakai memori terlalu banyak, kernel mematikannya, dan programnya biasanya tidak sempat bilang kenapa.

Saya pakai zram, bukan berkas swap. Itu swap yang tinggal di RAM, dalam bentuk terkompresi. Kedengarannya aneh, tapi memori yang dikompresi tetap muat jauh lebih banyak daripada tempat yang dipakainya, dan mengambil kembali halaman dari sana jauh lebih cepat daripada membacanya dari disk jaringan milik VPS. Disk-nya juga tidak cepat aus.

sudo apt install -y systemd-zram-generator
sudo nano /etc/systemd/zram-generator.conf
[zram0]
zram-size = min(ram * 50 / 100, 8192)
compression-algorithm = zstd
swap-priority = 100

Setengah RAM, maksimal 8 GB, dikompresi dengan zstd.

sudo systemctl daemon-reload
sudo systemctl start systemd-zram-setup@zram0.service
swapon --show
Output
NAME       TYPE      SIZE USED PRIO
/dev/zram0 partition 979M   0B  100

Swap ke zram itu murah, jadi kernel sebaiknya mau memakainya. Ini kebalikan dari saran biasa untuk swap di disk:

printf 'vm.swappiness = 100\nvm.page-cluster = 0\n' | sudo tee /etc/sysctl.d/91-swap.conf

page-cluster = 0 mencegah kernel membaca beberapa halaman sekaligus, yang cuma membantu di hard disk berputar.

Pengaturan Kernel dan Jaringan

Kernel punya banyak pengaturan di sysctl. Beberapa layak diubah di server yang menghadap internet:

sudo nano /etc/sysctl.d/90-hardening.conf
# jangan terima trik yang membelokkan lalu lintas
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_rfc1337 = 1

# sembunyikan detail kernel dari pengguna biasa
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
kernel.unprivileged_bpf_disabled = 1
net.core.bpf_jit_harden = 2
kernel.yama.ptrace_scope = 1
kernel.kexec_load_disabled = 1
fs.protected_fifos = 2
fs.protected_regular = 2
fs.suid_dumpable = 0

# TCP lebih cepat
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
net.core.somaxconn = 4096
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_slow_start_after_idle = 0
vm.vfs_cache_pressure = 50

Dua kelompok pertama itu hardening standar. Server ini bukan router, jadi tidak ada alasan menerima redirect atau paket source-routed, dan pengguna biasa tidak perlu alamat kernel, log kernel, atau proses milik pengguna lain.

Kelompok terakhir soal kecepatan. BBR adalah congestion control TCP yang lebih baru dari Google, dan jauh lebih baik menghadapi koneksi lambat atau yang sering kehilangan paket, seperti data seluler. Modulnya perlu dimuat saat boot:

echo tcp_bbr | sudo tee /etc/modules-load.d/bbr.conf
sudo modprobe tcp_bbr
sudo sysctl --system
sysctl net.ipv4.tcp_congestion_control
Output
net.ipv4.tcp_congestion_control = bbr

Membatasi Log

Semua yang ada di server ini menulis log ke journal systemd, termasuk setiap kontainer nanti. Secara default ukurannya boleh sampai 10% disk, dan tidak ada yang butuh sebanyak itu. Saya pakai berkas drop-in, bukan mengedit config utama:

sudo mkdir -p /etc/systemd/journald.conf.d
sudo nano /etc/systemd/journald.conf.d/90-local.conf
[Journal]
Storage=persistent
Compress=yes
SystemMaxUse=500M
SystemKeepFree=1G
MaxRetentionSec=1month

Storage=persistent menyimpan log melewati reboot, yang jadi penting begitu server me-reboot dirinya sendiri jam 4 pagi. Lalu batas 500 MB, minimal 1 GB selalu tersisa, dan tidak ada yang lebih tua dari sebulan.

sudo systemctl restart systemd-journald
journalctl --disk-usage
Output
Archived and active journals take up 46.8M in the file system.

Sekalian, saya matikan core dump. Kalau sebuah program crash, core dump menyimpan isi memorinya ke disk, dan memori itu bisa berisi password dan kunci. Saya juga tidak akan men-debug crash dari dump di server ini:

sudo mkdir -p /etc/systemd/coredump.conf.d
printf '[Coredump]\nStorage=none\nProcessSizeMax=0\n' | sudo tee /etc/systemd/coredump.conf.d/90-local.conf

Posisi Server Sekarang

Setelah Bagian 2:

  • Lalu lintas masuk diblokir kecuali SSH, 80, dan 443.
  • Kegagalan SSH berulang di-ban, dan pelanggar berulang di-ban di semua port selama seminggu.
  • Update keamanan terpasang sendiri, dan server reboot jam 4 pagi kalau perbaikan kernel membutuhkannya.
  • Swap zram menahan lonjakan memori, dan kernel sedikit lebih ketat dan sedikit lebih cepat.
  • Log tidak bisa memenuhi disk.

Tapi masih belum ada yang berjalan di atasnya. Bagian 3 menyiapkan Podman rootless supaya server ini benar-benar bisa menjalankan sesuatu.