Mengamankan server yang isinya belum kita kenal itu seperti menutup pintu rumah gelap sambil menebak-nebak letaknya: satu pintu terlewat, dan seluruh pekerjaan jadi sia-sia. Bagian ini menyalakan lampunya dulu. Kita audit server publik fiktif server-kantor dengan lima pertanyaan yang sama setiap kalinya — apa yang jalan, port apa yang terbuka, siapa yang bisa masuk, lewat apa mereka masuk, dan apa yang tertinggal pembaruan — lalu menjawabnya dengan alat yang sudah kita kenal: systemctl, ss, grep, dan apt. Hasil akhirnya bukan sekadar bacaan sekali lewat, melainkan laporan baseline yang dihasilkan ulang oleh skrip setiap kali kita butuh perbandingan.
Lima Pertanyaan Pembuka #
Audit yang baik punya daftar pertanyaan tetap, bukan eksplorasi sesuai suasana hati. Lima baris berikut adalah seluruh kerangka bagian ini:
| Pertanyaan | Sumber data di server | Alat pengumpul |
|---|---|---|
| Service apa yang sedang jalan? | systemd | systemctl list-units |
| Port apa yang terbuka, di alamat mana? | kernel | ss -tln |
| Akun mana yang bisa login? | /etc/passwd | grep |
| Dengan kunci apa mereka masuk? | ~/.ssh/authorized_keys per user | grep -c |
| Paket apa yang tertinggal pembaruan? | index paket | apt list --upgradable |
Urutannya sengaja dari lapisan terluar ke dalam: service yang jalan menjelaskan port, port menjelaskan permukaan serangan, akun dan kunci menjelaskan siapa yang berdiri di baliknya. Skrip di akhir bagian ini akan mengumpulkan kelimanya sekaligus.
Service yang Sedang Berjalan #
Setiap service berjalan adalah calon pintu — sebagian memang kita nyalakan dengan sengaja, sebagian menyala bawaan instalasi dan lupa kita matikan. Di server, daftarnya dibaca langsung dari systemd:
systemctl list-units --type=service --state=runningPerintah ini butuh systemd yang benar-benar berjalan sebagai init, sehingga tidak dieksekusi di lingkungan latihan — tetapi bentuk outputnya stabil dan layak dikenali. Berikut contoh tampilannya di server-kantor:
UNIT LOAD ACTIVE SUB DESCRIPTION
nginx.service loaded active running A high performance web server and reverse proxy
postgresql.service loaded active running PostgreSQL RDBMS
redis-server.service loaded active running Advanced key-value store
ssh.service loaded active running OpenBSD Secure Shell server
4 loaded units listed. Pass --all to see loaded but inactive units, too.Kolom ACTIVE yang kita cari selalu active, dan kolom SUB harus running — service bisa berstatus active tetapi exited (berjalan sekali lalu selesai), yang berarti tidak sedang mendengarkan apa pun. Untuk snapshot yang nanti diurai skrip, dua opsi tambahan merapikan outputnya: --no-pager mematikan tampilan interaktif, dan --no-legend membuang baris kepala serta baris penutup, menyisakan baris data saja. Opsi lengkap systemctl terdokumentasi di systemctl(1)
.
Port yang Terbuka #
Service mengaku jalan, tetapi kebenaran di lapangan dibaca dari kernel: port mana yang benar-benar mendengarkan. Alatnya ss dengan kombinasi flag yang sudah kita bedah di Chapter ss dan netstat
. Versi paling lengkap untuk audit:
ss -tulpnLima flag itu berbunyi: -t untuk socket TCP dan -u untuk UDP, -l membatasi ke yang sedang listening, -n menampilkan alamat dan port sebagai angka tanpa resolusi nama, dan -p menambahkan proses pemilik socket — yang terakhir ini membutuhkan root untuk melihat proses milik user lain, itulah sebabnya audit port di server dijalankan dengan sudo. Referensi lengkapnya ada di ss(8)
.
Kita buktikan cara bacanya dengan dua server latihan sungguhan di mesin ini — keduanya python3 -m http.server, satu diikat ke loopback, satu lagi ke semua interface:
printf 'halo dari server latihan\n' > index.html
python3 -m http.server 8765 --bind 127.0.0.1 >/dev/null 2>&1 &
lokal=$!
python3 -m http.server 8766 --bind 0.0.0.0 >/dev/null 2>&1 &
publik=$!
for i in 1 2 3 4 5; do
curl -s -o /dev/null 127.0.0.1:8765/ && curl -s -o /dev/null 127.0.0.1:8766/ && break
sleep 0.2
done
echo "== socket yang mendengarkan (difilter port latihan) =="
ss -tln | grep -E ':(8765|8766)'
kill "$lokal" "$publik"Output yang Dihasilkan:
== socket yang mendengarkan (difilter port latihan) ==
LISTEN 0 5 127.0.0.1:8765 0.0.0.0:*
LISTEN 0 5 0.0.0.0:8766 0.0.0.0:*Bedahnya singkat tetapi menjadi fondasi audit:
--bind 127.0.0.1vs--bind 0.0.0.0: server pertama hanya menerima koneksi dari dalam mesin itu sendiri; server kedua mendengarkan di semua interface, termasuk alamat publik. Perbedaan satu argumen inilah yang memisahkan “layanan internal” dari “layanan yang terbuka ke dunia” — konsep yang sudah kita saksikan di Chapter Firewall .ss -tln: tiga flag tanpa-p— versi yang bisa dijalankan tanpa root dan tetap memuat informasi audit inti: stateLISTEN, alamat lokal, dan port. Kolom keempat (Local Address:Port) adalah kolom yang nanti diurai skrip audit.grep -E ':(8765|8766)': di server sesungguhnya output-nya panjang;grepmenyaring baris yang memuat port yang sedang kita selidiki. Alternatifnya,ssjuga punya sintaks filter bawaan sepertiss -tln '( sport = :8765 )'yang memfilter di dalam kernel — pilihan selera, hasilnya sama.- Loop
forberisicurl: server butuh sepersekian detik untuk siap; loop mencoba lima kali dengan jeda 0,2 detik sebelum kita membaca socketnya, sehingga hasilsstidak pernah mendahului servernya.
Dua baris hasil itu tadi adalah jawaban pertanyaan kedua: di mesin ini ada dua port terbuka, dan keduanya menjelaskan dirinya sendiri. Di server sungguhan, baris yang tidak bisa menjelaskan dirinya adalah yang paling menarik.
User yang Bisa Masuk #
Port adalah pintu bangunan; akun adalah pemegang kuncinya. Kita cari akun yang punya shell login — akun yang bisa benar-benar berdiri di dalam sistem:
echo "== akun dengan shell login di mesin ini =="
grep -v nologin /etc/passwd | grep -v false | cut -d: -f1,7
echo "== jumlah akun dengan shell =="
grep -v nologin /etc/passwd | grep -v false | wc -lOutput yang Dihasilkan:
== akun dengan shell login di mesin ini ==
root:/bin/bash
sync:/bin/sync
bun:/bin/sh
z:/bin/bash
== jumlah akun dengan shell ==
4Tiga langkah pipeline itu bekerja seperti saringan bertingkat. grep -v nologin membuang akun sistem yang shell-nya /usr/sbin/nologin — daemon tanpa kemampuan login — lalu grep -v false membuang varian /usr/sbin/false, dan cut -d: -f1,7 menyisakan kolom nama dan shell. Pada mesin latihan ini hasilnya empat akun: root, sync, dan dua akun kerja — di server Anda daftarnya tentu berbeda. Yang penting bukan jumlahnya, melainkan bahwa setiap baris punya penjelasan: root adalah akun administratif yang sebaiknya tidak dipakai harian, sync adalah akun sistem dengan shell /bin/sync (menjalankan satu perintah sinkronisasi disk lalu keluar, bukan shell interaktif), dan sisanya akun manusia yang memang kita kenal.
Kunci SSH per User #
Setelah tahu siapa yang bisa masuk, pertanyaan berikutnya dengan apa. Bila PasswordAuthentication masih aktif di sshd, akun shell tanpa kunci SSH adalah jalur login password — dan jalur password adalah jalur brute force. Di server, pemeriksaannya menyisir home directory tiap akun:
for user in budi siti deploy; do
sudo ls -la /home/"$user"/.ssh/ 2>/dev/null || echo "$user: tanpa folder .ssh"
doneYang dicatat dari pemeriksaan ini bukan isi kuncinya, melainkan jumlah baris authorized_keys per user: kunci yang bertambah tanpa pemberitahuan adalah temuan tersendiri — bekas kontraktor, laptop hilang, atau rekan yang sudah pindah tim. Akun dengan shell tetapi tanpa kunci sama sekali lebih mencurigakan lagi: bersama PasswordAuthentication yes, itu berarti pintu yang dijaga tebakan. Konfigurasi sshd yang menutup jalur ini dibahas tuntas di Chapter SSH Hardening
.
Paket yang Tertinggal Pembaruan #
Pertanyaan penutup: perangkat lunak versi berapa yang menjaga pintu-pintu tadi. Index paket menjawabnya:
apt list --upgradableOutput-nya daftar paket dengan versi terpasang dan versi kandidat — dan yang paling penting adalah paket yang menemani service mendengarkan: pustaka TLS, server web, sshd. Tiga paket usang di server-kantor akan terlihat di laporan sebentar lagi; kita tangani pembaruan otomatisnya di Bagian 3.
Skrip Audit: Satu Laporan, Lima Sumber #
Lima pemeriksaan manual di atas hanya sepadan kalau dilakukan sekali. Untuk perbandingan antar-waktu — sebelum dan sesudah mengubah sesuatu — kita butuh laporan yang cara pembuatannya selalu sama, dan itulah pekerjaan audit-baseline.sh. Skrip ini membaca satu folder data berisi snapshot: services.txt, port-terbuka.txt, passwd, upgradable.txt, dan home/ per user — di server sungguhan, kelima snapshot itu dibekukan dengan perintah yang sudah kita jalankan di atas; di latihan ini kita menulisnya sebagai fixture. Blok berikut menyiapkan fixture tersebut, menulis skrip lengkapnya (identik dengan config/audit-baseline.sh pada paket proyek), lalu menjalankannya:
mkdir -p audit-data/home/budi/.ssh audit-data/home/siti/.ssh \
audit-data/home/deploy audit-data/root
cat > audit-data/services.txt <<'EOF'
nginx.service loaded active running A high performance web server and reverse proxy
postgresql.service loaded active running PostgreSQL RDBMS
redis-server.service loaded active running Advanced key-value store
ssh.service loaded active running OpenBSD Secure Shell server
EOF
cat > audit-data/port-terbuka.txt <<'EOF'
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 511 0.0.0.0:443 0.0.0.0:*
LISTEN 0 511 0.0.0.0:8080 0.0.0.0:*
LISTEN 0 24 127.0.0.1:5432 0.0.0.0:*
LISTEN 0 511 127.0.0.1:6379 0.0.0.0:*
EOF
cat > audit-data/passwd <<'EOF'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
budi:x:1000:1000:Budi Santoso:/home/budi:/bin/bash
siti:x:1001:1001:Siti Rahma:/home/siti:/bin/bash
deploy:x:1002:1002:Deploy Bot:/home/deploy:/bin/bash
EOF
printf 'ssh-ed25519 AAAAC3NzaC1lZDI1...kunci-budi budi@laptop-budi\n' \
> audit-data/home/budi/.ssh/authorized_keys
printf 'ssh-ed25519 AAAAC3NzaC1lZDI1...kunci-siti-1 siti@laptop-siti\nssh-ed25519 AAAAC3NzaC1lZDI1...kunci-siti-2 siti@telepon-siti\n' \
> audit-data/home/siti/.ssh/authorized_keys
cat > audit-data/upgradable.txt <<'EOF'
Listing... Done
libssl3/stable 3.0.15-1~deb12u1 [upgradable from: 3.0.16-1~deb12u1]
openssl/stable 3.0.15-1~deb12u1 [upgradable from: 3.0.16-1~deb12u1]
redis-server/stable 5:7.0.15-1~deb12u1 [upgradable from: 5:7.0.16-1~deb12u1]
EOF
cat > audit-baseline.sh <<'BASH'
#!/usr/bin/env bash
# audit-baseline.sh — kumpulkan baseline keamanan server ke satu laporan.
#
# Pemakaian:
# audit-baseline.sh <dir-data> > laporan-audit.txt
#
# <dir-data> adalah miniatur filesystem server yang berisi snapshot:
# services.txt systemctl list-units --type=service
# --state=running --no-pager --no-legend
# port-terbuka.txt ss -tlnH
# passwd cuplikan /etc/passwd
# upgradable.txt apt list --upgradable
# home/<user>/... home directory tiap user (untuk authorized_keys)
#
# Kode keluar:
# 0 laporan berhasil dihasilkan (temuan dibaca isinya, bukan kodenya)
# 1 pemakaian salah: argumen hilang atau snapshot tidak lengkap
set -u
if [ "$#" -ne 1 ]; then
printf 'pemakaian: audit-baseline.sh <dir-data>\n' >&2
exit 1
fi
DATA=$1
if [ ! -d "$DATA" ]; then
printf 'audit-baseline.sh: folder data tidak ditemukan: %s\n' "$DATA" >&2
exit 1
fi
for berkas in services.txt port-terbuka.txt passwd upgradable.txt; do
if [ ! -f "$DATA/$berkas" ]; then
printf 'audit-baseline.sh: berkas snapshot hilang: %s/%s\n' \
"$DATA" "$berkas" >&2
exit 1
fi
done
printf '===== AUDIT BASELINE KEAMANAN =====\n'
printf 'sumber data: %s\n\n' "$DATA"
# --- [1] service yang sedang berjalan ---------------------------------
jumlah_service=$(awk '$4 == "running" { c++ } END { print c + 0 }' \
"$DATA/services.txt")
printf '[1] SERVICE BERJALAN: %d\n' "$jumlah_service"
awk '$4 == "running" { print $1 }' "$DATA/services.txt" | sort |
sed 's/^/ /'
printf '\n'
# --- [2] port TCP yang terbuka ----------------------------------------
jumlah_port=$(grep -c . "$DATA/port-terbuka.txt")
printf '[2] PORT TCP TERBUKA: %d\n' "$jumlah_port"
awk '{
ket = "semua interface"
if (substr($4, 1, 4) == "127.")
ket = "loopback saja"
printf " %-15s %s\n", $4, ket
}' "$DATA/port-terbuka.txt"
printf '\n'
# --- [3] user yang punya shell login ----------------------------------
jumlah_user=$(awk -F: '$7 !~ /(nologin|false)$/ { c++ } END { print c + 0 }' \
"$DATA/passwd")
printf '[3] USER DENGAN SHELL LOGIN: %d\n' "$jumlah_user"
awk -F: '$7 !~ /(nologin|false)$/ { printf " %-8s %s\n", $1, $7 }' \
"$DATA/passwd"
printf '\n'
# --- [4] kunci SSH per user --------------------------------------------
printf '[4] KUNCI SSH PER USER:\n'
awk -F: '$7 !~ /(nologin|false)$/ { print $1, $6 }' "$DATA/passwd" |
while read -r user home; do
kunci="$DATA$home/.ssh/authorized_keys"
if [ -f "$kunci" ]; then
printf ' %-8s %d kunci\n' "$user" "$(grep -c . "$kunci")"
else
printf ' %-8s tanpa authorized_keys\n' "$user"
fi
done
printf '\n'
# --- [5] paket yang tertinggal pembaruan --------------------------------
jumlah_paket=$(grep -c 'upgradable from' "$DATA/upgradable.txt")
printf '[5] PAKET SIAP DIPERBARUI: %d\n' "$jumlah_paket"
grep 'upgradable from' "$DATA/upgradable.txt" | awk '{ print " " $1 }'
printf '\n'
# --- ringkasan temuan ----------------------------------------------------
jumlah_luar=$(awk '{ if (substr($4, 1, 4) != "127.") c++ }
END { print c + 0 }' "$DATA/port-terbuka.txt")
tanpa_kunci=$(awk -F: '$7 !~ /(nologin|false)$/ { print $1, $6 }' \
"$DATA/passwd" |
while read -r user home; do
[ -f "$DATA$home/.ssh/authorized_keys" ] || printf '%s\n' "$user"
done | paste -sd' ' -)
jumlah_tanpa=$(printf '%s' "$tanpa_kunci" | wc -w)
printf 'RINGKASAN TEMUAN:\n'
printf ' - %d port mendengarkan di semua interface (terjangkau dari luar)\n' \
"$jumlah_luar"
printf ' - %d akun shell tanpa authorized_keys: %s\n' \
"$jumlah_tanpa" "$tanpa_kunci"
printf ' - %d paket tertinggal pembaruan\n' "$jumlah_paket"
exit 0
BASH
bash audit-baseline.sh audit-data
echo "== pemakaian yang salah ditolak =="
bash audit-baseline.sh 2>&1; echo "exit=$?"Output yang Dihasilkan:
===== AUDIT BASELINE KEAMANAN =====
sumber data: audit-data
[1] SERVICE BERJALAN: 4
nginx.service
postgresql.service
redis-server.service
ssh.service
[2] PORT TCP TERBUKA: 6
0.0.0.0:22 semua interface
0.0.0.0:80 semua interface
0.0.0.0:443 semua interface
0.0.0.0:8080 semua interface
127.0.0.1:5432 loopback saja
127.0.0.1:6379 loopback saja
[3] USER DENGAN SHELL LOGIN: 4
root /bin/bash
budi /bin/bash
siti /bin/bash
deploy /bin/bash
[4] KUNCI SSH PER USER:
root tanpa authorized_keys
budi 1 kunci
siti 2 kunci
deploy tanpa authorized_keys
[5] PAKET SIAP DIPERBARUI: 3
libssl3/stable
openssl/stable
redis-server/stable
RINGKASAN TEMUAN:
- 4 port mendengarkan di semua interface (terjangkau dari luar)
- 2 akun shell tanpa authorized_keys: root deploy
- 3 paket tertinggal pembaruan
== pemakaian yang salah ditolak ==
pemakaian: audit-baseline.sh <dir-data>
exit=1Bagian-bagian penting skrip ini:
- Folder data sebagai miniatur filesystem: seluruh path yang diperiksa skrip digabung dari
$DATA+ path absolut di server ($DATA+/home/budi+/.ssh/authorized_keys). Ganti satu variabel, seluruh audit pindah ke snapshot lain — pola yang sama dengan fixture pada proyek-proyek sebelumnya. awk '$4 == "running" { c++ } END { print c + 0 }': menghitung service dari kolomSUBsnapshot systemctl;c + 0menjaga hasil tetap angka meski tidak ada yang cocok. Baris kepalaUNIT LOAD ACTIVE SUBotomatis tersaring karena kolom keempatnya bukanrunning.substr($4, 1, 4) == "127.": empat karakter pertama kolom alamat membedakan loopback dari alamat lain;0.0.0.0,*, dan alamat publik sama-sama dihitung “semua interface”. Pemisahan sederhana ini yang menghasilkan temuan “4 port terjangkau dari luar”.$7 !~ /(nologin|false)$/: saringan akun shell yang sama dengan pipelinegrep -vdi atas, kini dalam satu awk, dan hasilnya dipakai ulang dua kali — untuk daftar[3]dan untuk pemeriksaan kunci di[4].[ -f "$kunci" ]di dalamwhile read: keberadaanauthorized_keysdiperiksa per user; jumlah barisnya dihitunggrep -c .sehingga berkas kosong pun terlapor sebagai nol kunci, bukan dianggap tidak ada.exit 0di akhir: audit tidak pernah “gagal” — temuan justru isinya. Kode keluar 1 hanya untuk pemakaian yang salah, seperti dicontohkan dua baris terakhir blok.
Membaca Laporan: Temuan yang Menuntun #
Laporan di atas layak dibaca pelan-pelan, karena dua temuan di dalamnya yang menentukan arah proyek. Pertama, bagian [2]: ada enam port terbuka, tetapi bagian [1] hanya menjelaskan lima — ssh.service di 22, nginx.service di 80 dan 443, postgresql.service di 5432, redis-server.service di 6379. Port 8080 tidak dijelaskan service mana pun, dan ia mendengarkan di 0.0.0.0 — terjangkau dari seluruh dunia. Setelah ditelusuri di server-kantor, port itu ternyata milik server dev yang lupa dimatikan saat migrasi aplikasi dua bulan lalu. Kedua, bagian [4]: akun deploy punya shell tetapi tanpa authorized_keys — akun otomatisasi seharusnya masuk dengan kunci, bukan password.
Baseline sudah di tangan: empat service, enam port (satu tidak dijelaskan), empat akun shell (dua tanpa kunci), tiga paket tertinggal. Pekerjaan Bagian 2 adalah mengubah daftar itu menjadi kebijakan: tulis dulu pintu mana yang boleh terbuka, tolak sisanya — lalu buktikan bahwa ruleset yang menuliskannya memang layak dipasang.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
config/audit-baseline.sh
Unduh berkas#!/usr/bin/env bash
# audit-baseline.sh — kumpulkan baseline keamanan server ke satu laporan.
#
# Pemakaian:
# audit-baseline.sh <dir-data> > laporan-audit.txt
#
# <dir-data> adalah miniatur filesystem server yang berisi snapshot:
# services.txt systemctl list-units --type=service
# --state=running --no-pager --no-legend
# port-terbuka.txt ss -tlnH
# passwd cuplikan /etc/passwd
# upgradable.txt apt list --upgradable
# home/<user>/... home directory tiap user (untuk authorized_keys)
#
# Kode keluar:
# 0 laporan berhasil dihasilkan (temuan dibaca isinya, bukan kodenya)
# 1 pemakaian salah: argumen hilang atau snapshot tidak lengkap
set -u
if [ "$#" -ne 1 ]; then
printf 'pemakaian: audit-baseline.sh <dir-data>\n' >&2
exit 1
fi
DATA=$1
if [ ! -d "$DATA" ]; then
printf 'audit-baseline.sh: folder data tidak ditemukan: %s\n' "$DATA" >&2
exit 1
fi
for berkas in services.txt port-terbuka.txt passwd upgradable.txt; do
if [ ! -f "$DATA/$berkas" ]; then
printf 'audit-baseline.sh: berkas snapshot hilang: %s/%s\n' \
"$DATA" "$berkas" >&2
exit 1
fi
done
printf '===== AUDIT BASELINE KEAMANAN =====\n'
printf 'sumber data: %s\n\n' "$DATA"
# --- [1] service yang sedang berjalan ---------------------------------
jumlah_service=$(awk '$4 == "running" { c++ } END { print c + 0 }' \
"$DATA/services.txt")
printf '[1] SERVICE BERJALAN: %d\n' "$jumlah_service"
awk '$4 == "running" { print $1 }' "$DATA/services.txt" | sort |
sed 's/^/ /'
printf '\n'
# --- [2] port TCP yang terbuka ----------------------------------------
jumlah_port=$(grep -c . "$DATA/port-terbuka.txt")
printf '[2] PORT TCP TERBUKA: %d\n' "$jumlah_port"
awk '{
ket = "semua interface"
if (substr($4, 1, 4) == "127.")
ket = "loopback saja"
printf " %-15s %s\n", $4, ket
}' "$DATA/port-terbuka.txt"
printf '\n'
# --- [3] user yang punya shell login ----------------------------------
jumlah_user=$(awk -F: '$7 !~ /(nologin|false)$/ { c++ } END { print c + 0 }' \
"$DATA/passwd")
printf '[3] USER DENGAN SHELL LOGIN: %d\n' "$jumlah_user"
awk -F: '$7 !~ /(nologin|false)$/ { printf " %-8s %s\n", $1, $7 }' \
"$DATA/passwd"
printf '\n'
# --- [4] kunci SSH per user --------------------------------------------
printf '[4] KUNCI SSH PER USER:\n'
awk -F: '$7 !~ /(nologin|false)$/ { print $1, $6 }' "$DATA/passwd" |
while read -r user home; do
kunci="$DATA$home/.ssh/authorized_keys"
if [ -f "$kunci" ]; then
printf ' %-8s %d kunci\n' "$user" "$(grep -c . "$kunci")"
else
printf ' %-8s tanpa authorized_keys\n' "$user"
fi
done
printf '\n'
# --- [5] paket yang tertinggal pembaruan --------------------------------
jumlah_paket=$(grep -c 'upgradable from' "$DATA/upgradable.txt")
printf '[5] PAKET SIAP DIPERBARUI: %d\n' "$jumlah_paket"
grep 'upgradable from' "$DATA/upgradable.txt" | awk '{ print " " $1 }'
printf '\n'
# --- ringkasan temuan ----------------------------------------------------
jumlah_luar=$(awk '{ if (substr($4, 1, 4) != "127.") c++ }
END { print c + 0 }' "$DATA/port-terbuka.txt")
tanpa_kunci=$(awk -F: '$7 !~ /(nologin|false)$/ { print $1, $6 }' \
"$DATA/passwd" |
while read -r user home; do
[ -f "$DATA$home/.ssh/authorized_keys" ] || printf '%s\n' "$user"
done | paste -sd' ' -)
jumlah_tanpa=$(printf '%s' "$tanpa_kunci" | wc -w)
printf 'RINGKASAN TEMUAN:\n'
printf ' - %d port mendengarkan di semua interface (terjangkau dari luar)\n' \
"$jumlah_luar"
printf ' - %d akun shell tanpa authorized_keys: %s\n' \
"$jumlah_tanpa" "$tanpa_kunci"
printf ' - %d paket tertinggal pembaruan\n' "$jumlah_paket"
exit 0
config/validasi-ruleset.sh
Unduh berkas#!/usr/bin/env bash
# validasi-ruleset.sh — periksa struktur ruleset nftables sebelum diterapkan.
#
# Pemakaian:
# validasi-ruleset.sh <ruleset.nft>
#
# Pemeriksaan memakai grep pada bentuk baku yang kita sepakati di
# proyek ini; ubah daftar port = ubah ruleset, kebijakan-port.txt,
# dan pola dport di bawah ini bersama-sama.
#
# Kode keluar:
# 0 seluruh pemeriksaan lolos
# N jumlah pemeriksaan yang gagal
# 1 berkas ruleset tidak ditemukan
set -u
berkas=${1:-}
if [ -z "$berkas" ] || [ ! -f "$berkas" ]; then
printf 'validasi-ruleset.sh: berkas ruleset tidak ditemukan: %s\n' \
"$berkas" >&2
exit 1
fi
gagal=0
# periksa <pola> <pesan>: pola harus ditemukan.
periksa() {
if grep -qE -- "$1" "$berkas"; then
printf 'OK %s\n' "$2"
else
printf 'GAGAL %s\n' "$2"
gagal=$((gagal + 1))
fi
}
# tolak <pola> <pesan>: pola justru tidak boleh ada.
tolak() {
if grep -qE -- "$1" "$berkas"; then
printf 'GAGAL %s\n' "$2"
gagal=$((gagal + 1))
else
printf 'OK %s\n' "$2"
fi
}
periksa '^#!/usr/sbin/nft -f' 'shebang nft -f (bisa dieksekusi langsung)'
periksa '^flush ruleset' 'ruleset lama dibuang dulu (flush ruleset)'
periksa '^table inet filter' 'table inet filter dideklarasikan'
periksa 'policy drop;' 'kebijakan chain input: drop'
periksa 'iifname "lo" accept' 'loopback diizinkan'
periksa 'ct state established,related accept' 'balasan koneksi sah diizinkan'
periksa 'tcp dport \{ 22, 80, 443 \} accept' 'hanya SSH, HTTP, HTTPS yang dibuka'
periksa 'limit rate .* log prefix' 'paket yang jatuh ke policy dicatat dengan rem'
tolak 'policy accept;' 'tidak ada kebijakan accept yang menyelip'
tolak '^[[:space:]]*accept[[:space:]]*$' 'tidak ada aturan accept tanpa syarat'
exit "$gagal"
config/verifikasi-keamanan.sh
Unduh berkas#!/usr/bin/env bash
# verifikasi-keamanan.sh — buktikan ruleset, kebijakan port, dan service
# hidup dalam satu laporan PASS/FAIL.
#
# Pemakaian:
# verifikasi-keamanan.sh <ruleset.nft> <kebijakan-port.txt> <url-uji>
#
# Pemeriksaan:
# [1] struktur ruleset — memanggil validasi-ruleset.sh dari folder sama
# [2] port ruleset == daftar port di kebijakan-port.txt
# [3] service merespons — curl ke <url-uji>; exit 0 = ada yang menjawab
#
# Kode keluar:
# 0 semua pemeriksaan lulus (PASS)
# N jumlah pemeriksaan yang gagal (1-3)
# 4 pemakaian salah: argumen kurang atau berkas tidak ada
set -u
DIR=$(cd "$(dirname "$0")" && pwd)
if [ "$#" -ne 3 ]; then
printf 'pemakaian: verifikasi-keamanan.sh <ruleset> <kebijakan-port> <url-uji>\n' >&2
exit 4
fi
RULESET=$1
KEBIJAKAN=$2
URL=$3
for berkas in "$RULESET" "$KEBIJAKAN"; do
if [ ! -f "$berkas" ]; then
printf 'verifikasi-keamanan.sh: berkas tidak ditemukan: %s\n' \
"$berkas" >&2
exit 4
fi
done
if [ ! -f "$DIR/validasi-ruleset.sh" ]; then
printf 'verifikasi-keamanan.sh: validasi-ruleset.sh tidak ada di %s\n' \
"$DIR" >&2
exit 4
fi
gagal=0
# Daftar port di kebijakan dihitung lebih dulu untuk kepala laporan.
port_kebijakan=$(awk '$1 ~ /^[0-9]+$/ { print $1 }' "$KEBIJAKAN" |
sort -n | paste -sd' ' -)
printf '== VERIFIKASI KEAMANAN SERVER ==\n'
printf 'ruleset : %s\n' "$RULESET"
printf 'kebijakan : %s (port: %s)\n\n' "$KEBIJAKAN" \
"${port_kebijakan:-(kosong)}"
# --- [1] struktur ruleset -----------------------------------------------
out=$(bash "$DIR/validasi-ruleset.sh" "$RULESET")
status_val=$?
jumlah_ok=$(printf '%s\n' "$out" | grep -c '^OK')
printf '[1] struktur ruleset\n'
if [ "$status_val" -eq 0 ]; then
printf ' OK lolos %d pemeriksaan validasi-ruleset.sh\n' "$jumlah_ok"
else
printf ' GAGAL ditolak validasi-ruleset.sh (%d pemeriksaan gagal)\n' \
"$status_val"
gagal=$((gagal + 1))
fi
# --- [2] port yang dibuka ruleset == kebijakan ---------------------------
port_ruleset=$(grep -oE 'dport[[:space:]]+\{[^}]*\}' "$RULESET" |
grep -oE '[0-9]+' | sort -n | paste -sd' ' -)
printf '[2] port yang dibuka ruleset\n'
printf ' ruleset : %s\n' "${port_ruleset:-(kosong)}"
printf ' kebijakan: %s\n' "${port_kebijakan:-(kosong)}"
if [ -n "$port_kebijakan" ] && [ "$port_ruleset" = "$port_kebijakan" ]; then
printf ' OK port ruleset sama dengan kebijakan\n'
else
printf ' GAGAL port ruleset berbeda dengan kebijakan\n'
gagal=$((gagal + 1))
fi
# --- [3] service merespons ------------------------------------------------
printf '[3] service merespons\n'
kode=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$URL")
curl_exit=$?
if [ "$curl_exit" -eq 0 ]; then
printf ' OK %s menjawab (kode %s)\n' "$URL" "$kode"
else
printf ' GAGAL %s tidak menjawab (curl exit %d)\n' "$URL" "$curl_exit"
gagal=$((gagal + 1))
fi
# --- ringkasan ------------------------------------------------------------
lulus=$((3 - gagal))
printf '\nHASIL: '
if [ "$gagal" -eq 0 ]; then
printf 'PASS — 3/3 pemeriksaan lulus\n'
else
printf 'FAIL — %d/3 pemeriksaan lulus\n' "$lulus"
fi
exit "$gagal"
tests/run-tests.sh
Unduh berkas#!/usr/bin/env bash
# run-tests.sh — test suite untuk proyek firewall dan hardening server.
# Suite menyiapkan folder kerja terisolasi (mktemp), menyalin berkas
# config/, membangun fixture audit, menyalakan server latihan python3
# sebagai pengganti service sungguhan, lalu menguji: laporan audit dari
# fixture, validasi ruleset (valid dan rusak), verifikasi keamanan
# (PASS dan FAIL), beserta exit code tiap cabangnya.
# Jalankan dari mana saja: bash tests/run-tests.sh
set -u
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJ_DIR=$(dirname "$SCRIPT_DIR")
CONFIG="$PROJ_DIR/config"
PASS=0
FAIL=0
WORK=""
SERVER=""
# assert_eq <harapan> <nyata> <pesan>: bandingkan dua nilai.
assert_eq() {
if [ "$1" = "$2" ]; then
PASS=$((PASS + 1))
printf 'OK %s\n' "$3"
else
FAIL=$((FAIL + 1))
printf 'GAGAL %s: harapan %s, nyata %s\n' "$3" "$1" "$2"
fi
}
# assert_ada <pola> <berkas> <pesan>: pola harus ditemukan di berkas.
assert_ada() {
if grep -qF -- "$1" "$2"; then
PASS=$((PASS + 1))
printf 'OK %s\n' "$3"
else
FAIL=$((FAIL + 1))
printf 'GAGAL %s: pola tidak ditemukan di %s\n' "$3" "$2"
fi
}
bersihkan() {
if [ -n "$SERVER" ]; then
kill "$SERVER" 2> /dev/null
fi
if [ -n "$WORK" ]; then
rm -rf "$WORK"
fi
}
trap bersihkan EXIT
# --- siapkan folder kerja terisolasi ------------------------------------
WORK=$(mktemp -d)
cp "$CONFIG/latihan.nft" "$CONFIG/kebijakan-port.txt" \
"$CONFIG/audit-baseline.sh" "$CONFIG/validasi-ruleset.sh" \
"$CONFIG/verifikasi-keamanan.sh" "$WORK/"
cd "$WORK" || exit 1
# --- uji 1: audit menghasilkan laporan deterministik dari fixture --------
mkdir -p audit-data/home/budi/.ssh audit-data/home/siti/.ssh \
audit-data/home/deploy audit-data/root
cat > audit-data/services.txt <<'EOF'
nginx.service loaded active running A high performance web server and reverse proxy
postgresql.service loaded active running PostgreSQL RDBMS
redis-server.service loaded active running Advanced key-value store
ssh.service loaded active running OpenBSD Secure Shell server
EOF
cat > audit-data/port-terbuka.txt <<'EOF'
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 511 0.0.0.0:443 0.0.0.0:*
LISTEN 0 511 0.0.0.0:8080 0.0.0.0:*
LISTEN 0 24 127.0.0.1:5432 0.0.0.0:*
LISTEN 0 511 127.0.0.1:6379 0.0.0.0:*
EOF
cat > audit-data/passwd <<'EOF'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
budi:x:1000:1000:Budi Santoso:/home/budi:/bin/bash
siti:x:1001:1001:Siti Rahma:/home/siti:/bin/bash
deploy:x:1002:1002:Deploy Bot:/home/deploy:/bin/bash
EOF
printf 'ssh-ed25519 AAAAC3NzaC1lZDI1...kunci-budi budi@laptop-budi\n' \
> audit-data/home/budi/.ssh/authorized_keys
printf 'ssh-ed25519 AAAAC3NzaC1lZDI1...kunci-siti-1 siti@laptop-siti\nssh-ed25519 AAAAC3NzaC1lZDI1...kunci-siti-2 siti@telepon-siti\n' \
> audit-data/home/siti/.ssh/authorized_keys
cat > audit-data/upgradable.txt <<'EOF'
Listing... Done
libssl3/stable 3.0.15-1~deb12u1 [upgradable from: 3.0.16-1~deb12u1]
openssl/stable 3.0.15-1~deb12u1 [upgradable from: 3.0.16-1~deb12u1]
redis-server/stable 5:7.0.15-1~deb12u1 [upgradable from: 5:7.0.16-1~deb12u1]
EOF
bash audit-baseline.sh audit-data > laporan-audit.txt
assert_eq 0 "$?" "audit menghasilkan laporan (exit 0)"
assert_ada '[1] SERVICE BERJALAN: 4' laporan-audit.txt \
"laporan memuat 4 service berjalan"
assert_ada '[2] PORT TCP TERBUKA: 6' laporan-audit.txt \
"laporan memuat 6 port terbuka"
assert_ada '0.0.0.0:8080 semua interface' laporan-audit.txt \
"port 8080 tercatat terbuka ke luar"
assert_ada '2 akun shell tanpa authorized_keys' laporan-audit.txt \
"temuan akun shell tanpa kunci"
assert_ada '3 paket tertinggal pembaruan' laporan-audit.txt \
"temuan paket tertinggal"
bash audit-baseline.sh > /dev/null 2>&1
assert_eq 1 "$?" "audit tanpa argumen keluar 1"
# --- uji 2: ruleset valid lolos validator --------------------------------
bash validasi-ruleset.sh latihan.nft > hasil-validasi.txt
assert_eq 0 "$?" "ruleset latihan.nft lolos validasi"
jumlah=$(grep -c '^OK' hasil-validasi.txt)
assert_eq 10 "$jumlah" "sepuluh pemeriksaan validasi semuanya OK"
# --- uji 3: ruleset rusak ditolak validator -------------------------------
cat > rusak.nft <<'EOF'
# rusak.nft — ruleset longgar: semua koneksi diterima.
table inet filter {
chain input {
type filter hook input priority filter; policy accept;
accept
}
}
EOF
bash validasi-ruleset.sh rusak.nft > hasil-rusak.txt
assert_eq 9 "$?" "ruleset rusak ditolak (9 pemeriksaan gagal)"
assert_ada 'GAGAL kebijakan chain input: drop' hasil-rusak.txt \
"policy yang salah dilaporkan"
assert_ada 'GAGAL tidak ada aturan accept tanpa syarat' hasil-rusak.txt \
"accept tanpa syarat dilaporkan"
bash validasi-ruleset.sh tidak-ada.nft > /dev/null 2>&1
assert_eq 1 "$?" "berkas ruleset hilang keluar 1"
# --- uji 4: verifikasi PASS pada port sesuai kebijakan --------------------
printf 'server latihan firewall\n' > index.html
python3 -m http.server 8765 --bind 127.0.0.1 > /dev/null 2>&1 &
SERVER=$!
siap=0
for i in 1 2 3 4 5 6 7 8 9 10; do
if curl -fsS -o /dev/null http://127.0.0.1:8765/ 2> /dev/null; then
siap=1
break
fi
sleep 0.2
done
assert_eq 1 "$siap" "server latihan siap di port 8765"
bash verifikasi-keamanan.sh latihan.nft kebijakan-port.txt \
http://127.0.0.1:8765/ > hasil-verifikasi.txt
assert_eq 0 "$?" "verifikasi PASS keluar 0"
assert_ada 'HASIL: PASS' hasil-verifikasi.txt "laporan berakhir PASS"
assert_ada 'port ruleset sama dengan kebijakan' hasil-verifikasi.txt \
"port ditandai sesuai kebijakan"
assert_ada 'menjawab (kode 200)' hasil-verifikasi.txt \
"service ditandai merespons"
# --- uji 5: kebijakan berbeda -> port melenceng ----------------------------
grep -v '^443' kebijakan-port.txt > kebijakan-tanpa-443.txt
bash verifikasi-keamanan.sh latihan.nft kebijakan-tanpa-443.txt \
http://127.0.0.1:8765/ > hasil-beda.txt
assert_eq 1 "$?" "kebijakan beda keluar 1"
assert_ada 'port ruleset berbeda dengan kebijakan' hasil-beda.txt \
"perbedaan port dilaporkan"
# --- uji 6: ruleset rusak di tangan verifikasi -----------------------------
bash verifikasi-keamanan.sh rusak.nft kebijakan-port.txt \
http://127.0.0.1:8765/ > hasil-verif-rusak.txt
assert_eq 2 "$?" "ruleset rusak + port melenceng keluar 2"
assert_ada 'ditolak validasi-ruleset.sh' hasil-verif-rusak.txt \
"kegagalan validator dilaporkan"
# --- uji 7: service mati -> FAIL, port kembali bebas -------------------------
kill "$SERVER" 2> /dev/null
wait "$SERVER" 2> /dev/null
SERVER=""
bash verifikasi-keamanan.sh latihan.nft kebijakan-port.txt \
http://127.0.0.1:8765/ > hasil-mati.txt
assert_eq 1 "$?" "service mati keluar 1"
assert_ada 'tidak menjawab' hasil-mati.txt "service mati dilaporkan"
jumlah=$(ss -tlnH '( sport = :8765 )' | grep -c .)
assert_eq 0 "$jumlah" "port 8765 kembali bebas setelah server mati"
# --- uji 8: pemakaian salah --------------------------------------------------
bash verifikasi-keamanan.sh latihan.nft > /dev/null 2>&1
assert_eq 4 "$?" "argumen kurang keluar 4"
bash verifikasi-keamanan.sh tidak-ada.nft kebijakan-port.txt \
http://127.0.0.1:8765/ > /dev/null 2>&1
assert_eq 4 "$?" "berkas hilang keluar 4"
# --- ringkasan ----------------------------------------------------------------
if [ "$FAIL" -eq 0 ]; then
printf 'PASSED: %d/%d test lulus\n' "$PASS" "$PASS"
else
printf 'FAILED: %d/%d test lulus\n' "$PASS" "$((PASS + FAIL))"
fi
[ "$FAIL" -eq 0 ]