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.
- Bagian 1: Mengamankan Sistem Dasar
- Bagian 2: Firewall, Ban & Update
- Bagian 3: Podman Rootless
- Bagian 4: Traefik & HTTPS
- Bagian 5: Deploy, dan Satu Skrip untuk Semuanya
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.confGanti 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 dropdiinputitu bagian “blokir semua”. Aturan di bawahnya adalah pengecualiannya.established,relatedmembiarkan balasan masuk untuk koneksi yang dimulai server sendiri, misalnyaaptyang mengunduh package. Tanpa ini, tidak ada yang jalan.loadalah 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::/10membiarkan 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.confTidak 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_filtertable 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 nftablesMenghentikan 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-systemdpython3-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.logLalu 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 = 3Yang dilakukannya:
- Lima kegagalan dalam sepuluh menit bikin sebuah alamat di-ban selama satu jam.
bantime.incrementbikin setiap ban berikutnya untuk alamat yang sama makin lama, sampai seminggu.mode = aggressivejuga menghitung kegagalan yang lebih berisik, misalnya klien yang memutus koneksi di tengah key exchange. Kebanyakan scanner melakukan persis itu.recidivemengawasi 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.
portharus 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 sshdStatus 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:
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 tablestable inet vps_filter
table inet f2b-tableAturan 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-upgradesAPT::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-localUnattended-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.confLakukan dry run daripada menunggu sehari untuk tahu jalan atau tidak:
sudo unattended-upgrade --dry-run --debugStarting unattended upgrades script
Allowed origins are: origin=Debian,codename=trixie,label=Debian-Security, ...
Packages that will be upgraded: libssl3t64 opensslAllowed 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 = 100Setengah RAM, maksimal 8 GB, dikompresi dengan zstd.
sudo systemctl daemon-reload
sudo systemctl start systemd-zram-setup@zram0.service
swapon --showNAME TYPE SIZE USED PRIO
/dev/zram0 partition 979M 0B 100Swap 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.confpage-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 = 50Dua 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_controlnet.ipv4.tcp_congestion_control = bbrMembatasi 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=1monthStorage=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-usageArchived 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.confPosisi 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.