Sebuah server blade hitam di dalam data center gelap, terbungkus perisai heksagonal berwarna zamrud yang bercahaya, dengan partikel-partikel cahaya hijau yang datang pecah menghantam penghalang tersebut
Panduan keamanan

Mengamankan VPS baru di jam pertama

Alamat IPv4 publik mulai diprobe hanya dalam hitungan menit setelah paket pertamanya, oleh mesin-mesin yang tidak pernah mendengar tentang Anda dan tidak akan pernah. Itu kabar baik: hampir semua serangan yang menyasar server baru bersifat generik, dan satu jam yang dikerjakan dengan sengaja bisa mengalahkan hampir semuanya. Jam yang sama, jika dikerjakan dengan urutan yang salah, justru mengunci Anda sendiri dari mesin yang tidak bisa dimasuki siapa pun atas nama Anda.

Kami menyerahkan shell root sekitar 47 detik setelah pembayaran Anda dikonfirmasi, lalu, dengan sengaja, berhenti di situ. Kami tidak memasang agent apa pun, tidak mengelola firewall Anda, dan tidak menyimpan salinan kredensial Anda — kunci yang kami pegang adalah kunci yang bisa dipaksa untuk kami serahkan, dan prinsip itu berlaku di seluruh platform. Konsekuensinya sederhana dan perlu dikatakan dengan jelas: keamanan server Anda adalah kondisi yang Anda tinggalkan selama jam pertama itu.

Berikut adalah jam itu sendiri, dalam urutan yang sama seperti yang kami jalankan sendiri, di Debian 13 dan Ubuntu 24.04. Dua pengaturan mengerjakan sebagian besar pekerjaan itu. Sisa halaman ini ada karena tiga hal yang tampak benar, lolos dari pemeriksaan akhir tutorial mana pun, namun diam-diam keliru: konfigurasi drop-in SSH yang tertimpa tanpa peringatan, pengaturan port yang diabaikan oleh sshd yang diaktifkan lewat socket, dan runtime container yang mempublikasikan port di bawah firewall Anda. Masing-masing pernah menjebak orang yang sudah melakukan semua hal lain dengan benar.

Siapa yang sebenarnya mengetuk pintu

Perhatikan journalctl -u ssh pada server yang sudah online selama satu jam, dan volumenya akan terasa mengkhawatirkan saat pertama kali Anda melihatnya. Padahal seharusnya tidak. Yang sedang Anda lihat adalah radiasi latar belakang internet: segelintir operasi scanning, sebagian akademis, sebagian komersial, sebagian kriminal, yang menyisir seluruh ruang IPv4 secara terus-menerus dan menyerahkan hasilnya ke bot penebak kredensial. Alamat Anda dijangkau karena memang ada, dalam urutan numerik, dan akan dijangkau lagi beberapa jam kemudian apa pun yang Anda lakukan.

Ini mengubah bentuk masalahnya dengan cara yang menguntungkan. Anda tidak sedang bertahan dari musuh yang secara khusus memilih Anda dan akan beradaptasi; Anda sedang bertahan dari skrip dengan daftar tetap — root, admin, ubuntu, test, git, oracle, postgres, dan dua puluh ribu password yang pernah muncul di breach dump. Skrip itu tidak punya kesabaran, tidak punya kreativitas, dan tidak tertarik pada mesin yang tidak merespons di percobaan pertama. Mematikan autentikasi password tidak memperlambat penyerang ini. Itu menyingkirkannya sepenuhnya dari papan permainan.

Ada dua detail yang layak diketahui. Pertama, IPv6 jauh lebih sepi, karena /64 tidak mungkin disisir habis — tapi begitu record AAAA Anda menjadi publik, atau alamat mesin Anda muncul di header email atau di log certificate transparency, kesunyian itu berakhir. Jangan pernah menganggap IPv6 sebagai tempat bersembunyi; anggap saja sebagai tumpukan jerami yang lebih kecil. Kedua, alamat IPv4 punya masa lalu. Ada penyewa sebelum Anda, dan jika penyewa itu mengelola mail server dengan buruk atau meng-host sesuatu yang akhirnya masuk daftar blokir, Anda mewarisi reputasi itu sampai akhirnya memudar. Jika email penting bagi Anda, cek alamat itu di blocklist yang umum dipakai sebelum membangun apa pun di atasnya — pengecekan lima menit ini menghemat dua minggu debugging masalah deliverability.

Dua pengaturan yang mengerjakan sembilan puluh persen pekerjaan

Hampir semua kompromi nyata terhadap server kecil bermula dari salah satu dari dua hal: password yang bisa ditebak, atau service yang mendengarkan (listening) padahal seharusnya tidak. Perbaikan yang sesuai adalah PasswordAuthentication no dan firewall dengan default policy drop. Keduanya tidak terlihat mengesankan, hanya butuh lima belas menit jika dikerjakan bersamaan, dan nilainya lebih besar daripada seluruh kontrol lain di halaman ini digabung jadi satu.

Alasannya bersifat struktural, bukan statistik. Keduanya tertutup secara default — keduanya gagal ke arah yang aman. sshd yang hanya menerima kunci tidak bisa di-brute-force betapa pun banyaknya percobaan yang datang, karena tidak ada satu pun jalur kode yang menerima password. Firewall default-deny melindungi service yang bahkan belum Anda pasang, termasuk database yang akan Anda tambahkan tiga bulan lagi dan lupa Anda ikat ke localhost. Semua hal lain dalam checklist hardening hanyalah pendataan hal-hal buruk: daftar hal spesifik yang harus dimatikan, yang kelengkapannya hanya sebatas daftar itu sendiri.

Jadi kalau Anda hanya sempat membaca satu bagian lalu menutup tab ini, bacalah bagian ini, kerjakan langkah 2 sampai 5 di bawah, dan anggap jam itu sudah terpakai dengan baik. Sisanya benar-benar berguna, tapi benar-benar sekunder.

Di mana konfigurasi SSH sekarang berada, dan jebakan di dalamnya

Di Debian 13 dan Ubuntu 24.04, /etc/ssh/sshd_config dibuka dengan baris Include /etc/ssh/sshd_config.d/*.conf, dan cloud image menyertakan sebuah file di direktori itu — biasanya 50-cloud-init.conf — yang sudah mengatur PasswordAuthentication. Mengedit file utama dan menambahkan directive Anda sendiri di bagian bawah terasa wajar, tapi menghasilkan konfigurasi yang tidak melakukan apa yang tertulis di dalamnya.

Aturannya adalah satu hal tentang sshd yang mengejutkan hampir semua orang: untuk setiap keyword, nilai pertama yang diperoleh yang menang. Ini kebalikan dari hampir semua sistem penggabungan konfigurasi lain yang pernah Anda pakai. Include berada dekat bagian atas file utama, sehingga drop-in dibaca sebelum isi sshd_config — dan di antara sesama drop-in, urutan alfabet yang memutuskan. File bernama 10-hardening.conf mengalahkan 50-cloud-init.conf. File bernama 60-hardening.conf kalah darinya, secara diam-diam, dan Anda tidak akan mengetahui hal ini dari restart yang melaporkan sukses.

Itulah sebabnya Anda tidak boleh pernah percaya begitu saja pada file yang baru saja Anda tulis. Tanyakan langsung ke daemon-nya apa yang sebenarnya sudah ia resolve:

sshd -t                 # syntax check — silence means valid
sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|pubkeyauthentication|kbdinteractive|allowusers|port) '

sshd -T menampilkan konfigurasi efektif setelah semua include, override, dan default selesai di-resolve. Jika output itu menyatakan passwordauthentication yes, berarti autentikasi password sedang aktif, apa pun yang diklaim oleh file di disk. Tidak ada cara verifikasi lain yang berlaku.

Jebakan kedua masih satu keluarga dengan yang pertama. Ubuntu 24.04 menjalankan sshd lewat socket activation: ssh.socket yang memiliki port listening, dan Port di sshd_config diabaikan sepenuhnya. Periksa dengan systemctl is-enabled ssh.socket; jika itu enabled dan Anda ingin port yang berbeda, ubah dengan systemctl edit ssh.socket dan override ListenStream= — bukan di sshd_config, tempat pengaturan itu akan terlihat benar padahal tidak berpengaruh sama sekali.

Pindah dari port 22: teater, tapi teater yang murah

Mari jujur soal yang satu ini, karena internet sendiri tidak jujur. Memindahkan sshd ke port 2222 atau 47000 tidak menghentikan satu pun attacker yang kompeten. Full TCP scan terhadap satu host hanya butuh hitungan detik, service itu mengumumkan dirinya sendiri lewat banner-nya, dan siapa pun yang sudah memutuskan untuk menyerang Anda secara spesifik akan menemukannya sebelum kopinya habis.

Yang benar-benar dilakukannya adalah memangkas log autentikasi Anda sekitar sembilan puluh lima persen, dan itu punya nilai nyata: itulah bedanya antara log yang cuma Anda lirik sekilas dengan log yang benar-benar Anda baca. Sinyal yang bisa Anda lihat mengalahkan sinyal yang terkubur. Kalau Anda memonitor apa pun sama sekali, auth.log yang sepi adalah cara termurah untuk membuat anomali jadi terlihat.

Biayanya kecil tapi nyata, dan itulah alasan kami menyebutnya opsional, bukan direkomendasikan. Anda akan lupa nomor port itu saat kembali delapan bulan kemudian. Port non-standar di atas 1024 secara prinsip bisa saja diklaim oleh proses tanpa privilege kalau sshd sedang tidak listening. Firewall outbound yang ketat di jaringan klien memblokir port-port yang tidak umum, jadi sesekali Anda tidak akan bisa menjangkau server Anda sendiri dari kantor atau hotel. Dan pada sistem yang diaktifkan lewat socket, Anda harus mengubahnya di tempat yang benar, sesuai bagian sebelumnya. Lakukan ini kalau log yang sepi penting bagi Anda. Jangan lakukan ini lalu merasa aman, dan jangan pernah melakukannya sebagai pengganti dari keys-only.

Default-deny, dan ruleset yang benar-benar kami jalankan

Firewall yang mendaftar apa yang harus diblokir hanyalah sistem pengarsipan. Firewall yang mendaftar apa yang boleh diizinkan adalah kontrol keamanan sungguhan. Perbedaan ini adalah keseluruhan intinya, karena hanya yang kedua yang mencakup service yang Anda pasang bulan depan, port debug yang Anda buka di suatu hari Jumat, dan container yang tiba-tiba memutuskan untuk mengekspos dirinya sendiri.

Di platform kami, Anda punya dua lapisan, dan keduanya independen. Edge filter sifatnya opsional, dikonfigurasi per server lewat panel dan diberlakukan di level hypervisor, sehingga traffic yang diblokir tidak pernah sampai ke guest Anda sama sekali — berguna untuk rule layer-4 yang ingin Anda terapkan sebelum kernel Anda membuang satu siklus pun untuk memprosesnya, dan untuk mencegah guest yang sudah dikompromikan tetap bisa dijangkau selagi Anda menanganinya. Firewall guest sepenuhnya milik Anda: nftables, iptables, pf, apa pun yang disertakan oleh image Anda. Kami tidak pernah menyentuhnya. Gunakan keduanya kalau mesin itu penting; gunakan setidaknya yang kedua, selalu.

Ada dua hal dalam ruleset di langkah 5 yang layak dijelaskan, karena kebanyakan ruleset hasil copy-paste salah menanganinya. Jangan drop semua ICMP. Rasanya rapi, tapi ini merusak path-MTU discovery, yang menghasilkan kelas bug terburuk yang ada: request kecil berjalan lancar, response besar menggantung, dan tidak ada satu pun di log Anda yang menyebut soal firewall. Terima minimal destination-unreachable, time-exceeded, dan parameter-problem. Jangan filter ICMPv6 berdasarkan tipe kecuali Anda tahu persis daftarnya. IPv6 bergantung pada ICMPv6 untuk neighbour discovery dan router advertisement; blokir secara luas dan konektivitas IPv6 Anda akan mati dengan cara yang terlihat seperti masalah routing. Menerima semua ICMPv6 pada satu host adalah kompromi yang masuk akal, dan itulah persis yang dilakukan ruleset di bawah ini.

Satu peringatan sebelum Anda menjalankan flush ruleset: kalau Docker terpasang, baris itu akan menghapus rule NAT dan filter yang ditulis Docker, dan networking container akan berhenti sampai systemctl restart docker mengembalikannya. Muat ruleset Anda dulu, baru restart Docker, dan baca bagian berikutnya sebelum Anda mempublikasikan satu pun port container.

Port yang tidak Anda sadari sedang terbuka

Tanyakan langsung ke mesin itu, apa yang sedang ia dengarkan (listen). Bukan apa yang Anda kira sudah Anda pasang — tapi apa yang benar-benar sedang bound saat ini:

ss -tulpn | grep -vE '127\.0\.0\.1|\[::1\]'

Semua yang lolos dari filter itu berarti bisa dijangkau dari tempat lain, bukan cuma dari mesin itu sendiri. Pada image bawaan, temuan yang biasa muncul adalah rpcbind di port 111 (tidak ada satu pun yang Anda jalankan yang membutuhkannya), listener exim4 sisa dari instalasi dasar, dan stub systemd-resolved yang tidak berbahaya kalau di loopback tapi menjadi open resolver kalau tidak. Yang benar-benar berbahaya muncul belakangan, seiring software yang sengaja Anda pasang: PostgreSQL di 0.0.0.0:5432 karena sebuah tutorial menyuruh Anda mengedit listen_addresses, Redis tanpa password karena Redis memang tidak punya password default selama sebagian besar masa hidupnya, node Elasticsearch, exporter Prometheus, notebook Jupyter. Masing-masing pernah menjadi langkah pertama dari breach sungguhan, berkali-kali.

Sekarang jebakan yang bahkan menjerat orang yang berhati-hati. Docker tidak meminta izin ke firewall Anda. Saat Anda menulis -p 5432:5432, Docker menyisipkan rule DNAT di chain PREROUTING pada tabel nat, yang dievaluasi kernel sebelum chain input Anda sempat melihat paket itu sama sekali; traffic itu kemudian diteruskan (forward) ke container. Policy input default-deny Anda sama sekali tidak dilibatkan, ufw status menunjukkan port itu tertutup, padahal database Anda ada di internet publik. Ini adalah perilaku Docker yang memang terdokumentasi, sudah begini selama satu dekade, dan sudah mengekspos database dalam jumlah yang sangat banyak.

Perbaikannya cuma satu string, dan inilah kebiasaan yang layak dibangun:

# wrong — reachable from the entire internet, whatever your firewall says
docker run -p 5432:5432 postgres

# right — bound to loopback, reachable only through an SSH tunnel or a proxy
docker run -p 127.0.0.1:5432:5432 postgres

Terakhir, cari pendapat dari luar. Port scan dari server itu sendiri hanya memberi tahu Anda apa yang dipikirkan kernel; scan dari tempat lain memberi tahu Anda apa yang dilihat dunia luar, dan itulah satu-satunya angka yang benar-benar penting. Jalankan nmap -Pn -p- <your-ip> dari laptop Anda, bukan dari mesin itu sendiri, lalu bandingkan hasilnya dengan daftar yang Anda harapkan.

Pembaruan otomatis: aktifkan, dan sungguhan

Celah waktu yang membuat server kecil terkompromi bukanlah zero-day. Justru empat minggu di antara sebuah CVE dipublikasikan lengkap dengan proof of concept yang berfungsi, dan waktu berikutnya Anda kebetulan login lagi. Tooling penyerangan menyerap bug remote baru hanya dalam hitungan hari; mesin yang Anda patch “kalau sempat” akan terekspos selama seluruh interval itu, dan setiap operator yang jujur tahu betul seberapa panjang interval itu sebenarnya.

Keberatan terhadap pembaruan otomatis biasanya rasa takut sesuatu akan rusak, dan itu layak dipertimbangkan serius — lalu dipersempit. Batasi otomatisasi hanya pada pocket security dari distro Anda, tempat maintainer melakukan backport perbaikan ke versi yang sudah Anda pakai, bukan merilis versi upstream yang baru. Pembaruan security Debian untuk openssl adalah build yang sudah diperbaiki dari versi yang sama dengan yang sudah Anda jalankan; risiko perubahan perilakunya jauh lebih kecil dibanding risiko membiarkan lubang yang bisa diakses dari jarak jauh terbuka selama sebulan. Upgrade fitur tetap manual, sebagaimana seharusnya.

Bagian yang sering dilewatkan orang adalah reboot. libssl yang sudah diperbaiki di disk tidak berguna apa-apa bagi proses yang sudah memetakan versi lamanya saat start-up, dan update kernel sama sekali tidak berpengaruh sampai Anda benar-benar boot ke dalamnya. Anda bisa menerima reboot tanpa pengawasan pada jendela waktu yang Anda pilih sendiri, atau memasang needrestart dan membiarkannya memberi tahu Anda service mana saja yang masih berjalan dengan library yang sudah terhapus — tapi lakukan salah satunya. “Pembaruan otomatis sudah aktif” sementara uptime menunjukkan 340 hari adalah ilusi yang menenangkan, dan itulah ilusi paling umum yang kami temui.

fail2ban, CrowdSec, atau tidak sama sekali

fail2ban membaca log Anda, mendeteksi kegagalan berulang dari satu alamat, dan mem-banned-nya untuk sementara waktu. CrowdSec melakukan hal yang sama dan membagikan verdict-nya lewat jaringan sesama pengguna, sehingga Anda bisa memblokir alamat yang sudah berulah di tempat lain bahkan sebelum ia sampai ke server Anda. Keduanya adalah software yang bagus. Tapi tidak satu pun benar-benar melakukan apa yang dikira kebanyakan orang, pada server SSH yang sudah keys-only.

Begitu PasswordAuthentication bernilai no, brute-force SSH tidak mungkin berhasil. Bukan “kecil kemungkinan berhasil” — tapi memang tidak ada jalur kode yang memungkinkannya. Karena itu, mem-banned alamat setelah lima kegagalan sama sekali tidak mencegah apa pun; itu hanya mengurangi volume log dan sedikit pemakaian CPU. Itu manfaat yang nyata, hanya saja bukan manfaat keamanan, dan kompromi ini tidak gratis: jail yang ditulis dengan buruk dan mengamati log yang salah justru sudah lebih sering mengunci administrator keluar dari server mereka sendiri, dibanding mengunci attacker.

Tempat di mana tool-tool ini benar-benar pantas digunakan adalah satu lapis di atasnya, pada service yang benar-benar Anda ekspos: form login, admin WordPress, API dengan rate limit, mail server dengan SMTP AUTH. Semua itu memang menerima password, memang bisa di-brute-force, dan ban list adalah kontrol yang tepat untuk itu. Jadi kesimpulan kami sempit dan spesifik. Lewati jail untuk SSH. Pasang CrowdSec atau fail2ban di depan sesuatu yang punya kolom password. Dan apa pun yang Anda pilih, whitelist dulu alamat manajemen Anda sendiri — ignoreip ada justru untuk malam yang kalau tidak, akan Anda kenang karena alasan yang salah.

Membuat mesin memberi tahu Anda saat sesuatu berubah

Pencegahan adalah apa yang bisa Anda lakukan dalam satu jam. Deteksi adalah apa yang memberi tahu Anda bahwa satu jam itu ternyata tidak cukup. Tidak perlu rumit, dan pada satu server saja, tiga hal murah sudah mencakup sebagian besar kebutuhan.

Simpan log-nya. Pada banyak image, journal tersimpan di /run dan menguap saat reboot, artinya rekam jejak sebuah insiden justru hilang tepat saat reboot yang menyusul insiden itu. Membuat /var/log/journal adalah perbaikan satu baris dan hal paling bernilai di bagian ini.

Minta diberi tahu soal setiap login. Satu baris di /etc/ssh/sshrc yang memanggil logger di setiap awal sesi tidak memakan biaya apa pun dan memberi Anda rekam jejak yang bersih dan mudah di-grep, terpisah dari riuhnya log sshd sendiri. Ada satu catatan yang sering menjebak orang: sshd menjalankan ~/.ssh/rc alih-alih /etc/ssh/sshrc ketika user punya file itu, sehingga file system-wide justru dilewati tepat untuk akun yang paling mungkin memiliki dotfile custom. Kalau Anda butuh hook yang tidak bisa disembunyikan seperti ini, gunakan pam_exec sebagai gantinya.

Ketahui seperti apa filesystem Anda di hari pertama. AIDE mencatat hash dari binary, library, dan unit file Anda, lalu melaporkan apa saja yang berubah sejak itu. Masalah mendasarnya, yang biasanya diabaikan: database yang disimpan di mesin yang sedang ia awasi bisa saja dibuat ulang oleh siapa pun yang berhasil mengompromikannya, dan sejak saat itu pemeriksaan akan selamanya melaporkan “tidak ada perubahan”. Salin database itu keluar dari mesin, atau setidaknya catat hash-nya di tempat lain, dan tool ini baru sepadan dengan dua puluh menit yang Anda habiskan. Kalau dibiarkan begitu saja di tempatnya, itu cuma selimut penenang.

Prinsip umumnya layak dinyatakan tersendiri: log yang tidak bisa Anda percaya adalah log yang berada di mesin yang sudah dikompromikan. Apa pun yang benar-benar ingin Anda andalkan — journal, database integritas, backup — sebaiknya punya salinan di tempat yang tidak dikuasai oleh attacker yang sama. Server kecil kedua di yurisdiksi lain adalah jawaban yang sah untuk ini, dan yang seharga lima dolar pun sudah cukup.

Apa yang tidak dilakukan oleh semua ini

Semua yang di atas adalah pekerjaan level perimeter, dan penting untuk jelas soal batasannya, supaya Anda tidak salah mengira pintu depan yang terkunci sebagai brankas.

Ini tidak melindungi data pada mesin yang sedang berjalan. Kernel yang sudah di-harden dan firewall yang tertutup sama sekali tidak relevan bagi siapa pun yang punya akses hypervisor, maupun terhadap isi RAM selama mesin itu menyala. Kalau kekhawatiran Anda adalah disk saat server dimatikan, disita, atau dinonaktifkan, itu ranahnya enkripsi disk, dan itu prosedur yang berbeda dengan mode kegagalan yang berbeda pula — lihat mengenkripsi disk VPS dengan LUKS.

Ini tidak membuat Anda anonim. Server bisa saja di-harden dengan sempurna dan tetap membocorkan siapa pemiliknya, lewat record WHOIS, SSH key yang dipakai ulang, tag analytics, client SSH yang connect dari rumah tanpa hop tambahan, atau sertifikat yang menghubungkan dua identitas sekaligus. Membayar dengan Monero lalu login dari alamat pribadi Anda sendiri membatalkan seluruh manfaat pembayaran itu. Mode kegagalan itu punya halamannya sendiri: kesalahan-kesalahan yang men-deanonimisasi Anda.

Ini tidak memperbaiki aplikasi Anda. SQL injection, admin endpoint tanpa autentikasi, atau dependency dengan backdoor sama sekali tidak peduli dengan pengaturan sshd Anda. Sebagian besar kompromi pada server yang dikelola dengan baik justru datang lewat port yang sengaja, dengan penuh kesadaran, Anda buka untuk service yang Anda tulis atau pasang sendiri.

Ini bukan backup. Ransomware, rm -rf dengan variabel yang ternyata kosong, dan upgrade yang gagal, semuanya berakhir dengan cara yang sama. Buat salinan, taruh di tempat lain, dan lakukan restore darinya setidaknya sekali sebelum Anda benar-benar membutuhkannya — disiplin yang sama yang membuat pindah ke host lain bisa dilalui dengan selamat, juga membuat hari Selasa yang buruk bisa dilalui dengan selamat.

Tidak satu pun dari ini menjadi alasan untuk tidak melakukan jam itu. Ini justru alasan untuk tahu persis apa yang sudah dibeli oleh jam itu.

  1. Deploy dengan kunci, jangan pernah dengan password

    Buat keypair di mesin Anda sendiri dan tempelkan bagian publiknya ke form deploy; image akan memasangnya untuk Anda dan Anda tidak akan pernah punya password root yang bisa hilang. Kalau Anda deploy dari image yang ditandai cloud-init, Anda malah bisa mengirimkan seluruh konfigurasi jam pertama sebagai user-data — sampai 64 KiB — dan mesin akan langsung menyala dalam keadaan sudah di-harden.

    # on your laptop, not on the server
    ssh-keygen -t ed25519 -a 100 -C "$(whoami)@laptop-cvh"
    cat ~/.ssh/id_ed25519.pub        # paste this into the deploy form

    Gunakan ed25519 kecuali ada sesuatu di toolchain Anda yang menolaknya, dalam hal ini RSA 4096 bit juga tidak masalah. Lindungi private key dengan passphrase dan muat ke dalam agent; file kunci tanpa passphrase di laptop sama saja dengan password yang ditulis di layar monitor. Set kunci yang sama juga disuntikkan ke rescue mode, dan itulah yang membuat langkah 4 bisa dipulihkan.

  2. Buka sesi kedua sebelum menyentuh apa pun

    Ini bukan opsional dan bukan paranoia. Mulai dari sini Anda akan mengubah daemon yang sedang menghubungkan Anda, dan firewall yang memungkinkan Anda menjangkaunya. Biarkan satu sesi tetap terbuka dan idle sebagai tali penyelamat kembali ke mesin itu, dan lakukan setiap perubahan di sesi yang lain. Kalau ada perubahan yang salah, sesi yang terbuka itu masih terautentikasi dan bisa membatalkannya; koneksi baru justru akan ditolak.

    # terminal A — the rope. Log in and leave it alone.
    ssh -i ~/.ssh/id_ed25519 root@<server-ip>
    
    # terminal B — where every command below runs.
    ssh -i ~/.ssh/id_ed25519 root@<server-ip>

    Uji setiap perubahan dengan membuka koneksi ketiga, jangan pernah dengan menyambungkan ulang koneksi yang sedang Anda pakai untuk bekerja. Kalau koneksi ketiga itu gagal, Anda tetap punya dua shell yang masih berfungsi dan sebuah masalah, bukan krisis.

  3. Buat akun yang benar-benar akan Anda pakai

    Root lewat SSH itu praktis untuk tepat satu jam. Setelah itu, akun bernama dengan sudo memberi Anda audit trail, melindungi Anda dari salah ketik perintah yang berjalan dengan privilege penuh, dan memungkinkan Anda menonaktifkan akun yang terkompromi tanpa harus menonaktifkan mesinnya.

    adduser --disabled-password --gecos "" deploy
    install -d -m 0700 -o deploy -g deploy /home/deploy/.ssh
    cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
    chown deploy:deploy /home/deploy/.ssh/authorized_keys
    chmod 0600 /home/deploy/.ssh/authorized_keys
    usermod -aG sudo deploy
    
    # a long random password, stored in your manager, used only by sudo
    passwd deploy

    Set password itu. Akun sudo yang password-nya tidak pernah diset tidak bisa memakai sudo, dan menyadari hal ini setelah Anda menonaktifkan login root adalah cara klasik untuk mengunci diri Anda sendiri padahal semua file sudah benar. Verifikasi dulu sebelum lanjut: dari terminal baru, ssh deploy@<server-ip>, lalu sudo -v. Keduanya harus berhasil.

  4. Tulis drop-in SSH, lalu tanyakan ke daemon apa yang ia baca

    Drop-in bernomor yang urutannya lebih awal akan mengalahkan apa pun yang sudah disertakan oleh cloud image, sesuai aturan pengurutan di atas. Tulis file-nya, validasi sintaksnya, baca ulang konfigurasi efektifnya, dan baru setelah itu reload.

    cat > /etc/ssh/sshd_config.d/10-hardening.conf <<'EOF'
    PermitRootLogin no
    PasswordAuthentication no
    KbdInteractiveAuthentication no
    PubkeyAuthentication yes
    AuthenticationMethods publickey
    PermitEmptyPasswords no
    AllowUsers deploy
    MaxAuthTries 3
    LoginGraceTime 20
    X11Forwarding no
    AllowAgentForwarding no
    AllowTcpForwarding no      # remove this line if you use ssh -L / -D tunnels
    ClientAliveInterval 300
    ClientAliveCountMax 2
    EOF
    chmod 0644 /etc/ssh/sshd_config.d/10-hardening.conf
    
    sshd -t                                    # silence means the syntax is valid
    sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|allowusers) '
    systemctl reload ssh

    Baca output sshd -T itu sebelum reload, bukan sesudahnya. Output itu harus menyatakan permitrootlogin no dan passwordauthentication no. Kalau tidak, file Anda sedang ditimpa oleh file lain yang urutannya lebih awal — jalankan ls /etc/ssh/sshd_config.d/ dan ganti nama file Anda dengan angka yang lebih kecil. Sekarang buka terminal ketiga dan login sebagai deploy. Hanya setelah itu berhasil, Anda boleh menutup tali penyelamat tadi.

  5. Muat firewall default-deny

    nftables sudah tersedia bawaan di kedua distro dan menggantikan ruleset iptables dengan satu file yang mudah dibaca. Sesuaikan port yang diterima dengan service yang benar-benar Anda jalankan — daftar di bawah ini mengasumsikan SSH dan sebuah web server, dan tidak ada yang lain.

    apt install -y nftables
    cat > /etc/nftables.conf <<'EOF'
    #!/usr/sbin/nft -f
    flush ruleset
    
    table inet filter {
      chain input {
        type filter hook input priority filter; policy drop;
    
        ct state established,related accept
        ct state invalid drop
        iif lo accept
    
        # ICMP must live: dropping it blackholes path-MTU discovery.
        ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem } accept
        ip6 nexthdr icmpv6 accept
    
        tcp dport 22 ct state new accept
        tcp dport { 80, 443 } ct state new accept
    
        limit rate 5/minute burst 5 packets log prefix "nft-drop: " level info
      }
      chain forward { type filter hook forward priority filter; policy drop; }
      chain output  { type filter hook output  priority filter; policy accept; }
    }
    EOF
    
    nft -c -f /etc/nftables.conf              # check before you commit to it
    systemctl enable --now nftables
    nft list ruleset | head -40

    Kalau Docker terpasang, flush ruleset juga akan menghapus rule milik Docker: jalankan systemctl restart docker sesudahnya dan pastikan container Anda masih merespons. Lalu, dari laptop Anda, jalankan nmap -Pn -p- <server-ip> dan periksa apakah daftar port yang terbuka cocok dengan port yang baru saja Anda izinkan — tidak lebih, dan tidak kurang.

  6. Aktifkan unattended security upgrades

    Hanya pocket security, dengan jendela reboot yang Anda pilih sendiri, bukan yang terus-menerus Anda tunda. Pilih jam yang sepi bagi user Anda dan bukan tepat di awal jam, supaya tidak bertabrakan dengan cron milik orang lain.

    apt install -y unattended-upgrades needrestart
    cat > /etc/apt/apt.conf.d/51-cvh-auto <<'EOF'
    // Debian. On Ubuntu, replace the two Origins-Pattern lines with:
    //   "${distro_id}:${distro_codename}-security";
    Unattended-Upgrade::Origins-Pattern {
      "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
      "origin=Debian,codename=${distro_codename},label=Debian-Security";
    };
    Unattended-Upgrade::Automatic-Reboot "true";
    Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
    Unattended-Upgrade::Automatic-Reboot-Time "04:17";
    Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
    APT::Periodic::Update-Package-Lists "1";
    APT::Periodic::Unattended-Upgrade "1";
    EOF
    
    unattended-upgrade --dry-run --debug | tail -25
    systemctl status unattended-upgrades --no-pager

    Kalau reboot tanpa pengawasan benar-benar tidak bisa diterima, set ke "false" dan biarkan needrestart -b melaporkan service mana saja yang masih berjalan dengan library yang sudah terhapus dan apakah ada kernel yang lebih baru terpasang — lalu tindak lanjuti laporannya. Yang tidak bisa diterima adalah tidak melakukan keduanya sama sekali.

  7. Tutup apa yang sedang listening, dan periksa dari luar

    Daftar semua yang sedang bound ke sesuatu selain loopback, hapus apa yang tidak Anda pakai, dan konfirmasi hasilnya dari mesin lain, bukan dari mesin ini.

    ss -tulpn | grep -vE '127\.0\.0\.1|\[::1\]'
    
    # the usual leftovers on a base image
    systemctl disable --now rpcbind.socket rpcbind 2>/dev/null
    apt purge -y exim4-base exim4-config exim4-daemon-light 2>/dev/null
    apt autoremove --purge -y
    
    # what each running unit is allowed to reach
    systemd-analyze security --no-pager | head -20

    Untuk apa pun yang harus tetap berjalan tapi tidak butuh penonton — database, cache, admin panel, metrics exporter — ikat (bind) ke 127.0.0.1 di konfigurasinya sendiri dan jangkau lewat SSH tunnel: ssh -L 5432:127.0.0.1:5432 deploy@<server-ip>. Ini jauh lebih baik daripada membuka port dan berharap autentikasi milik service itu sendiri sudah cukup kuat, dan inilah pola yang sebaiknya jadi pilihan default Anda.

  8. Catat baseline dan aktifkan alarm

    Sepuluh menit yang baru terasa manfaatnya pada hari sesuatu benar-benar salah — dan itulah hari yang Anda tidak akan bisa merekonstruksi apa pun lagi hanya dari ingatan.

    # persistent journal, so a reboot stops erasing the evidence
    mkdir -p /var/log/journal
    systemd-tmpfiles --create --prefix /var/log/journal
    systemctl restart systemd-journald
    
    # a clean, greppable line per SSH session
    cat > /etc/ssh/sshrc <<'EOF'
    logger -t ssh-login "user=$USER from=${SSH_CONNECTION%% *} tty=${SSH_TTY:-none}"
    EOF
    
    # file-integrity baseline — then get the database off the machine
    apt install -y aide
    aideinit
    mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
    sha256sum /var/lib/aide/aide.db     # copy this hash somewhere else
    
    last -n 20 ; lastb -n 20            # who got in, and who tried

    Akhiri dengan mencatat, di luar server, empat hal: fingerprint SSH host key dari ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub, hash AIDE di atas, port yang sengaja Anda buka, dan di mana private key itu tersimpan. Catatan itulah yang mengubah pagi yang buruk menjadi sekadar checklist, bukan investigasi.

Perbandingan

Kontrol-kontrol ini, diurutkan berdasarkan apa yang benar-benar Anda dapatkan

Setiap kontrol jam pertama diuji dengan dua pertanyaan yang menentukan apakah ia layak dilakukan: apa yang benar-benar dihentikannya, dan apa yang tidak — apa pun yang tersirat dari tutorial-tutorial di luar sana.
KontrolApa yang dihentikanApa yang tidak dihentikanBiayaKesimpulan
SSH keys-only (PasswordAuthentication no)Semua bot penebak kredensial di internet, secara permanen dan by designSiapa pun yang memegang private key Anda, atau cacat pada sshd itu sendiri5 menit, sekali sajaWajib, tidak bisa ditawar
Firewall default-deny (nftables)Service yang Anda lupa sedang listening, dan setiap service yang tidak sengaja Anda pasang di kemudian hariSerangan terhadap port yang memang sengaja Anda buka10 menit, sekali sajaWajib, tidak bisa ditawar
Unattended security upgradesCelah n-day — rentang minggu antara exploit yang sudah publik dan login Anda berikutnyaZero-day, dan apa pun yang butuh reboot yang tidak pernah Anda jadwalkan5 menit ditambah jendela rebootWajib, tidak bisa ditawar
User bernama dengan sudo, bukan rootSalah ketik perintah dengan privilege penuh, dan proses yang berjalan sebagai root tanpa alasan jelasLocal privilege escalation di tangan seseorang yang sudah berhasil masuk3 menitLayak dilakukan
Baseline integritas file (AIDE)Perubahan diam-diam pada binary sistem, library, dan unit fileApa pun, kalau database-nya tetap disimpan di mesin yang sedang ia awasi20 menit ditambah storage di luar mesinLayak dilakukan kalau mesin ini penting
Memindahkan SSH dari port 22Sekitar 95% volume log autentikasi AndaSiapa pun yang menjalankan port scan, yaitu semua orang yang sebenarnya perlu diwaspadai2 menit, ditambah satu malam saat Anda lupa nomor port-nyaOpsional, demi log yang lebih sepi
fail2ban / CrowdSec pada sshdPelaku berulang, dan CPU yang mereka habiskan di server AndaTidak ada, begitu autentikasi password sudah dimatikan10 menit, ditambah risiko nyata terkunci sendiriLewati untuk SSH; pakai untuk aplikasi Anda
FAQ

Pertanyaan yang perlu dijawab

Berapa lama setelah deploy sebelum scanning mulai terjadi?

Biasanya hanya hitungan menit. Scanner yang mencakup seluruh internet menyisir seluruh ruang IPv4 secara terus-menerus, sehingga alamat baru akan terjangkau pada putaran berikutnya, apa pun yang berjalan di dalamnya. Anggap saja Anda sudah diprobe sejak momen mesin itu merespons paket pertamanya — itulah sebabnya hardening dilakukan di jam pertama, bukan di akhir pekan pertama.

Apakah saya masih perlu fail2ban kalau login password sudah dinonaktifkan?

Untuk SSH, tidak. Dengan PasswordAuthentication no tidak ada jalur kode yang bisa dimenangkan oleh brute-force, sehingga mem-banned setelah lima kegagalan tidak mencegah apa pun — ini hanya mengurangi noise di log, yang memang ada nilainya tapi bukan nilai keamanan. Tempat tool-tool ini sebenarnya pantas dipasang adalah di depan apa pun yang benar-benar menerima password: form login web, admin panel, SMTP AUTH. Kalau Anda memasang salah satunya, whitelist dulu alamat Anda sendiri.

ufw, firewalld, atau nftables?

Ketiganya boleh saja, asal default policy-nya drop dan Anda paham apa yang Anda deploy. ufw paling ramah dan di baliknya tetap menulis rule nftables pada Debian dan Ubuntu versi terkini. nftables murni hanyalah satu file yang mudah dibaca, bisa Anda diff, dan bisa masuk version control, itulah sebabnya kami memakainya. Pilihan ini jauh lebih tidak penting dibanding default policy-nya — dan tak satu pun dari ketiganya mengubah fakta bahwa Docker tetap mempublikasikan port di bawah level ketiganya.

Perlukah saya memindahkan SSH dari port 22?

Hanya demi log yang lebih sepi. Ini tidak menghentikan satu pun attacker yang kompeten — full scan hanya butuh hitungan detik dan banner-nya langsung mengidentifikasi service tersebut. Yang benar dilakukannya adalah menghilangkan sebagian besar noise dari log autentikasi Anda, sehingga anomali sungguhan jadi terlihat. Perlakukan ini sebagai kebersihan log, jangan pernah sebagai kontrol keamanan, dan periksa dulu apakah sshd Anda diaktifkan lewat socket sebelum mengubahnya: di Ubuntu 24.04, directive Port di sshd_config diabaikan dan port-nya justru dimiliki oleh ssh.socket.

Apakah pembaruan otomatis akan merusak situs saya jam empat pagi?

Kalau dibatasi hanya pada pocket security, sangat jarang. Paket-paket itu adalah perbaikan backport ke versi yang sudah Anda jalankan, bukan rilis upstream yang baru. Risiko yang realistis adalah reboot-nya, bukan patch-nya — jadi pilih sendiri jendela waktunya, set Automatic-Reboot-WithUsers ke false supaya ia menunggu selama masih ada yang login, dan kalau service itu benar-benar tidak bisa direstart tanpa pengawasan, jalankan needrestart -b secara terjadwal dan tindak lanjuti apa yang dilaporkannya. Satu-satunya posisi yang tidak bisa dibela adalah pembaruan otomatis dengan uptime yang diukur dalam hitungan tahun.

Saya terkunci sendiri dari server saya. Apa opsi yang tersedia?

Boot server Anda ke rescue mode lewat panel. Ini akan menjalankan environment Alpine di RAM dengan SSH key yang sama dengan akun Anda, dan disk Anda dalam keadaan unmounted di /dev/vda, sehingga Anda bisa mount, memperbaiki drop-in sshd atau file firewall, unmount, lalu reboot. Tidak ada apa pun di disk yang tersentuh oleh proses boot rescue itu sendiri. Fingerprint host key dari rescue memang berbeda dari yang biasa Anda pakai — itu wajar, bukan tanda penyadapan.

Apakah menjalankan Lynis atau skrip hardening CIS sebagai pengganti panduan ini ide yang bagus?

Sebagai audit, ya; sebagai pengganti, tidak. Lynis benar-benar berguna sebagai second opinion dan akan menemukan hal-hal yang tidak disebutkan di halaman ini. Skrip remediasi CIS otomatis adalah hal yang berbeda: skrip itu menerapkan ratusan perubahan yang dirancang untuk armada workstation perusahaan, beberapa di antaranya justru merusak server dengan cara yang sulit dilacak berminggu-minggu kemudian. Kerjakan dulu jam ini secara manual, supaya Anda paham apa yang sedang dilakukan mesin Anda, baru jalankan tool audit dan baca temuannya satu per satu.

Apakah hardening membuat server saya anonim?

Tidak, dan mencampuradukkan keduanya adalah kesalahan umum yang mahal harganya. Hardening mengontrol siapa yang bisa masuk; anonimitas mengontrol siapa yang bisa tahu itu milik Anda. Server yang dikunci dengan sempurna pun tetap bisa membocorkan kepemilikannya lewat record WHOIS, SSH key yang dipakai ulang, tag analytics, atau login admin dari alamat rumah Anda sendiri. Membayar dengan Monero lalu langsung connect dari koneksi pribadi Anda sendiri membatalkan seluruh manfaat pembayaran itu — ini dibahas di kesalahan-kesalahan yang men-deanonimisasi Anda.

Deploy your offshore server.

Pilih wilayah. Pilih paket. Tempel kunci. Bayar. 47 detik berikutnya tanggung jawab kami.