Ruleset sudah tertulis dan lolos validator — dan justru di titik inilah proyek keamanan paling sering ditinggalkan setengah jalan, karena aturan yang terpasang terasa seperti aturan yang bekerja. Bagian ini menolak perasaan itu dan menggantinya dengan bukti: kita menguji dari sisi klien bahwa port yang dijanjikan terbuka benar-benar menjawab, port lain benar-benar ditolak, lalu merangkum keduanya — ditambah validasi ruleset dari Bagian 2 — menjadi satu laporan PASS/FAIL yang keluar dengan kode terbaca mesin. Terakhir, verifikasi itu kita jadwalkan agar tidak bergantung pada ingatan, dan seluruh proyek ditutup dengan checklist hardening berprioritas.
Port Terbuka Harus Merespons #
Uji pertama berdiri di sisi klien: bila kebijakan membuka port untuk sebuah service, maka ada service yang harus menjawab di baliknya. Server latihan python3 -m http.server di port 8765 berperan sebagai si service; kita melihat socketnya dari sisi kernel lewat ss, lalu menagih jawaban lewat curl:
printf 'server latihan firewall\n' > index.html
python3 -m http.server 8765 --bind 127.0.0.1 >/dev/null 2>&1 &
server=$!
for i in 1 2 3 4 5; do curl -s -o /dev/null 127.0.0.1:8765/ && break; sleep 0.2; done
echo "== socket yang mendengarkan =="
ss -tln '( sport = :8765 )'
echo "== permintaan dari klien =="
curl -s http://127.0.0.1:8765/
curl -s -o /dev/null -w 'kode HTTP: %{http_code}\n' http://127.0.0.1:8765/
kill "$server"Output yang Dihasilkan:
== socket yang mendengarkan ==
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 5 127.0.0.1:8765 0.0.0.0:*
== permintaan dari klien ==
server latihan firewall
kode HTTP: 200Dua perintah terakhir itu adalah dua bentuk jawaban yang berbeda: curl -s menampilkan isi balasan (baris isi dari index.html), sedangkan curl -w '%{http_code}' membaca kode statusnya — 200 berarti service bukan sekadar hidup, melainkan melayani permintaan dengan benar. Kombinasi inilah yang nanti dipakai skrip verifikasi: keberadaan socket dibuktikan ss, kesehatan service dibuktikan curl. Daftar lengkap kode status HTTP dan opsi curl lainnya ada di curl(1)
.
Port Lain Harus Ditolak #
Uji kedua adalah cerminannya: port yang tidak ada di kebijakan tidak boleh ada yang menjawab. Kita pilih port 8123 — tidak dipakai siapa pun — dan mengirim permintaan ke sana:
curl -sS http://127.0.0.1:8123/ 2>&1 | sed 's/after [0-9]* ms/after 0 ms/'
exit "${PIPESTATUS[0]}"Output yang Dihasilkan:
curl: (7) Failed to connect to 127.0.0.1 port 8123 after 0 ms: Could not connect to serverTiga hal terjadi di blok dua baris itu. Pertama, curl gagal dan mencetak alasannya: exit code 7 berarti connection refused — tidak ada proses yang mendengarkan di port tersebut, sehingga kernel menjawab dengan RST. Kedua, sed menormalkan angka milidetik pada pesan itu (after 0 ms) karena waktunya bervariasi antar eksekusi sedangkan sisanya identik. Ketiga, exit "${PIPESTATUS[0]}" mengambil exit code dari curl — bukan dari sed yang berada di ujung pipeline — sehingga blok tetap keluar dengan kode 7 dan kodenya ikut diverifikasi.
Ada nuansa yang layak dipahami dari sisi firewall. Connection refused (exit 7) adalah bentuk klasik “tidak ada yang mendengarkan” tanpa firewall. Pada server dengan policy drop, paket ke port tertutup justru dibuang tanpa jawaban, sehingga klien menunggu sampai kadaluarsa — curl melaporkannya dengan exit code 28 (--max-time tercapai). Bagi pemindai port, perbedaan ini terlihat sebagai closed versus filtered; bagi kita keduanya bermuara sama: port tidak melayani siapa pun.
Melihat dari Luar: Simulasi nmap #
Dua uji di atas dijalankan dari dalam mesin. Pemandangan dari luar berbeda, dan alat klasiknya adalah nmap — dipasang di laptop Anda (sudo apt install nmap), lalu dijalankan terhadap server dari luar server. Berikut contoh tampilannya terhadap server-kantor setelah ruleset Bagian 2 aktif:
$ nmap -sV 203.0.113.10
Starting Nmap 7.94 ( https://nmap.org )
Nmap scan report for 203.0.113.10
Host is up (0.00045s latency).
Not shown: 997 filtered tcp ports (no-response)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.2p1 Debian (protocol 2.0)
80/tcp open http nginx
443/tcp open ssl/http nginx
Nmap done: 1 IP address (1 host up) scanned in 8.42 secondsBaris yang paling penting justru yang paling pendek: Not shown: 997 filtered tcp ports (no-response). Filtered berarti paket pemindai dibuang tanpa jawaban — persis perilaku policy drop kita — dan 997 port lainnya tidak menampakkan diri sama sekali. Tiga baris open sisanya cocok satu-ke-satu dengan kebijakan-port.txt. Alat pemindai tidak terpasang di lingkungan latihan ini, jadi tampilan di atas adalah contoh dari server sungguhan, bukan klaim eksekusi lokal.
Verifikasi Otomatis: verifikasi-keamanan.sh #
Uji manual layak dilakukan sekali; yang menjaga server setiap hari adalah skrip. verifikasi-keamanan.sh merangkai tiga pemeriksaan menjadi satu vonis: struktur ruleset (memanggil validator Bagian 2), daftar port yang dibuka ruleset (dibandingkan dengan kebijakan-port.txt), dan service yang harus merespons (menagih URL dengan curl). Blok berikut menulis keempat berkas — identik dengan paket config/ — menyalakan server latihan, menjalankan verifikasi dalam keadaan sehat, mematikan servernya, lalu menjalankan verifikasi yang sama untuk kedua kalinya:
cat > kebijakan-port.txt <<'KB'
# kebijakan-port.txt — satu-satunya port yang boleh terbuka di server-kantor.
# Berkas ini adalah rujukan bersama: verifikasi-keamanan.sh membandingkan
# ruleset dengan daftar ini, dan manusia membacanya saat meninjau audit.
#
# Format: <port> <protokol> <layanan> <alasan>
22 tcp ssh akses admin; hanya kunci, tanpa password
80 tcp http situs publik; mengalihkan ke HTTPS
443 tcp https situs publik dengan TLS
KB
cat > latihan.nft <<'NFT'
#!/usr/sbin/nft -f
# latihan.nft — ruleset default-deny untuk server publik "server-kantor".
# Tiga pintu yang dibuka: SSH (22), HTTP (80), HTTPS (443).
# Seluruh sisanya dibuang oleh policy; sebagian dicatat untuk audit.
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
# komunikasi antar-proses lokal lewat loopback
iifname "lo" accept
# balasan koneksi yang sudah sah (connection tracking)
ct state established,related accept
# ping supaya diagnosis jaringan tetap mungkin
ip protocol icmp accept
ip6 nexthdr ipv6-icmp accept
# tiga pintu layanan publik
tcp dport { 22, 80, 443 } accept
# sisanya dicatat secukupnya, lalu jatuh ke policy drop
limit rate 5/minute burst 10 packets log prefix "nft-drop: "
}
}
NFT
cat > validasi-ruleset.sh <<'BASH'
#!/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"
BASH
cat > verifikasi-keamanan.sh <<'BASH'
#!/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"
BASH
printf 'server latihan firewall\n' > index.html
python3 -m http.server 8765 --bind 127.0.0.1 >/dev/null 2>&1 &
server=$!
for i in 1 2 3 4 5; do curl -s -o /dev/null 127.0.0.1:8765/ && break; sleep 0.2; done
bash verifikasi-keamanan.sh latihan.nft kebijakan-port.txt http://127.0.0.1:8765/
echo "exit=$?"
kill "$server"
wait "$server" 2>/dev/null
echo "== service mati: verifikasi harus FAIL =="
bash verifikasi-keamanan.sh latihan.nft kebijakan-port.txt http://127.0.0.1:8765/
echo "exit=$?"Output yang Dihasilkan:
== VERIFIKASI KEAMANAN SERVER ==
ruleset : latihan.nft
kebijakan : kebijakan-port.txt (port: 22 80 443)
[1] struktur ruleset
OK lolos 10 pemeriksaan validasi-ruleset.sh
[2] port yang dibuka ruleset
ruleset : 22 80 443
kebijakan: 22 80 443
OK port ruleset sama dengan kebijakan
[3] service merespons
OK http://127.0.0.1:8765/ menjawab (kode 200)
HASIL: PASS — 3/3 pemeriksaan lulus
exit=0
== service mati: verifikasi harus FAIL ==
== VERIFIKASI KEAMANAN SERVER ==
ruleset : latihan.nft
kebijakan : kebijakan-port.txt (port: 22 80 443)
[1] struktur ruleset
OK lolos 10 pemeriksaan validasi-ruleset.sh
[2] port yang dibuka ruleset
ruleset : 22 80 443
kebijakan: 22 80 443
OK port ruleset sama dengan kebijakan
[3] service merespons
GAGAL http://127.0.0.1:8765/ tidak menjawab (curl exit 7)
HASIL: FAIL — 2/3 pemeriksaan lulus
exit=1Dua eksekusi dengan perintah yang sama persis menghasilkan vonis berbeda — dan itulah inti skripnya. Bagian-bagian pentingnya:
- Tiga argumen, tiga sumber kebenaran: ruleset (apa yang dikatakan aturan), kebijakan (apa yang kita maksudkan), dan URL uji (apa yang dirasakan klien). Verifikasi yang hanya membaca satu sumber akan dengan senang hati meloloskan ruleset yang rapi tapi melayani service yang mati.
bash "$DIR/validasi-ruleset.sh" "$RULESET": lapisan pertama memanggil validator Bagian 2 apa adanya — kode keluarnya yang berjumlah pemeriksaan gagal langsung dilaporkan, tidak ditulis ulang. Satu kebijakan, satu penegak.grep -oE 'dport[[:space:]]+\{[^}]*\}': mengambil setdportdari ruleset, lalu tahapgrepkedua menyaring angkanya; hasilnya dibandingkan sebagai string dengan daftar portkebijakan-port.txtyang melewatisort -ndanpaste. Satu port berbeda — lebih atau kurang — membuat string tidak sama dan pemeriksaan gagal.curl -s -o /dev/null -w '%{http_code}'plus--max-time 5: kode HTTP dan exit code diambil bersamaan. Koneksi yang dibuang firewall akan menunggu hingga lima detik lalu keluar 28 — kegagalan jaringan dan service mati sama-sama dilaporkan sebagai “tidak menjawab”, tanpa menggantung skrip selamanya.exit "$gagal": vonis PASS keluar 0, selain itu jumlah pemeriksaan yang gagal. Kontrak sederhana ini yang membuat cron bisa bersikap: exit bukan nol = kirim kabar.
Verifikasi Berkelanjutan #
Verifikasi sekali jalan membuktikan keadaan hari ini; server hidup lebih lama dari sesi terminal kita. Jadwalkan ketiga skrip lewat crontab — satu baris per jam sudah lebih dari cukup untuk memulai:
0 * * * * cd /home/budi/firewall && bash verifikasi-keamanan.sh \
latihan.nft kebijakan-port.txt http://127.0.0.1:8765/ >> verifikasi.log 2>&1Kontrak exit code ketiga skrip dirancang untuk dibaca scheduler ini:
| Skrip | exit 0 artinya | exit selain 0 |
|---|---|---|
audit-baseline.sh | laporan berhasil dibuat | 1: pemakaian salah |
validasi-ruleset.sh | sepuluh pemeriksaan lolos | N: jumlah pemeriksaan gagal |
verifikasi-keamanan.sh | PASS | N: pemeriksaan yang gagal; 4: pemakaian salah |
Cron secara bawaan mengirim email ke pemilik crontab setiap kali perintahnya keluar dengan kode bukan nol — peringatan gratis tanpa layanan tambahan. Pola penjadwalan lengkapnya, dari environment minimal hingga systemd timer, sudah kita bangun pada proyek Otomasi dengan cron dan systemd .
Checklist Hardening Akhir #
Firewall adalah lapisan pertama, bukan satu-satunya. Tiga temuan audit Bagian 1 menutup tuntas lewat tabel berprioritas ini — kerjakan dari atas, dan jadikan config/checklist-hardening.txt pada paket proyek sebagai berkas kerjanya:
| Prioritas | Item | Bentuknya |
|---|---|---|
| P1 | Firewall default-deny | ruleset proyek ini + validator + verifikasi berkala |
| P1 | SSH kunci saja | PasswordAuthentication no; akun deploy diberi kunci atau dicabut shellnya |
| P2 | Pembaruan otomatis | unattended-upgrades untuk security update — panduan Debian |
| P2 | fail2ban | jail sshd — rem per alamat sumber yang tidak dimiliki limit nftables |
| P3 | Audit berkala | audit-baseline.sh lewat cron; laporan dibandingkan dengan baseline |
Tiga paket usang dari laporan audit ikut tertutup oleh baris P2: unattended-upgrades memasang pembaruan keamanan secara otomatis, termasuk pustaka TLS yang menemani nginx. Pembahasan lengkap per lapisan — instalasi minimal, sysctl, service yang dihapus, umask — dirangkum di Chapter Security Hardening
.
Server server-kantor kini menutup ceritanya dengan rapi: audit menemukan enam port dan dua akun mencurigakan, kebijakan menuliskan tiga pintu yang dibenarkan, dan verifikasi membuktikan aturan itu bekerja setiap saat lewat jadwal. Disiplin yang sama — rancang, tulis, validasi, buktikan — adalah bekal langsung untuk proyek penutup section ini, Lingkungan Development Linux
, tempat satu berkas deklaratif membuat lingkungan kerja baru tumbuh utuh dalam satu perintah.
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 ]