Audit Bagian 1 menutup dengan tiga temuan yang menuntut keputusan: port 8080 terbuka tanpa penjelasan, dua akun shell tanpa kunci, dan tiga paket tertinggal. Bagian ini menjawab temuan pertama dengan kebijakan, bukan tambal sulam: kita tidak menutup port 8080 satu per satu lalu berharap port 8081 yang muncul bulan depan ikut ketutup — melainkan membalik arah keputusannya. Filosofi default-deny bertanya satu hal saja: pintu mana yang boleh terbuka? — seluruh sisanya tertutup karena tidak pernah kita buka. Di atas filosofi itu kita menulis ruleset nftables lengkap, membedahnya baris per baris, lalu menulis validator yang menolak ruleset sendirinya bila strukturnya melenceng dari kebijakan.

Filosofi Default-Deny #

Dua cara memandang firewall sudah kita bandingkan di Chapter Firewall : kebijakan ACCEPT menerima segalanya lalu menutup yang diketahui berbahaya, sedangkan DROP menolak segalanya lalu membuka yang diketahui dibutuhkan. Server pubik memilih yang kedua karena daftar pintu yang dibutuhkan jauh lebih pendek daripada daftar pintu berbahaya. Untuk server-kantor, daftar itu cukup tiga baris:

PortLayananAlasanAlamat sumber
22/tcpSSHakses admin, login kunci sajasemua (dengan rem)
80/tcpHTTPsitus publik, mengalihkan ke HTTPSsemua
443/tcpHTTPSsitus publik dengan TLSsemua

Daftar ini kita tuangkan dulu sebagai berkas kebijakan kebijakan-port.txt — satu sumber kebenaran yang dibaca manusia saat meninjau dan dibaca skrip saat memverifikasi — lalu sebagai ruleset latihan.nft. Blok berikut menulis keduanya persis seperti versi config/ pada paket proyek, lalu menampilkannya:

bash
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
echo "== kebijakan port =="
cat kebijakan-port.txt
echo "== ruleset =="
cat latihan.nft

Output yang Dihasilkan:

text
== kebijakan port ==
# 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
== ruleset ==
#!/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: "
    }
}

Mari kita bedah ruleset itu baris per baris:

  1. #!/usr/sbin/nft -f: shebang yang membuat berkas ruleset bisa dieksekusi langsung; nft -f berarti “baca perintah dari berkas ini”.
  2. flush ruleset: membuang seluruh aturan lama sebelum memasang yang baru — penerapan menjadi atomik dan hasil akhirnya selalu sama, tak peduli apa yang terpasang sebelumnya. Konsekuensinya dibahas di kotak peringatan di bawah.
  3. table inet filter: satu table bernama filter pada family inet, sehingga aturan yang sama berlaku untuk IPv4 maupun IPv6 — dua dunia yang dulu harus ditulis terpisah di zaman iptables.
  4. type filter hook input priority filter; policy drop;: baris terpenting. Chain ini mendaftar ke hook input netfilter — titik tempat seluruh paket masuk diadili — dan policy drop adalah kebijakan cadangannya: paket yang tidak cocok satu pun aturan dibuang. Default-deny lengkap hanya butuh satu kata ini.
  5. iifname "lo" accept: trafik loopback diloloskan tanpa syarat. Banyak program lokal berkomunikasi lewat 127.0.0.1; tanpa baris ini mereka rusak sejak ruleset aktif.
  6. ct state established,related accept: connection tracking — balasan dari koneksi yang kita mulai sendiri diterima. Inilah yang membuat server bebas menghubungi dunia (pembaruan paket, query DNS) sementara koneksi dari luar tetap diadili ketat.
  7. ip protocol icmp accept dan ip6 nexthdr ipv6-icmp accept: ping tetap bisa dijawab agar diagnosis jaringan (“server hidup atau mati?”) tetap mungkin. Dua baris karena ICMP dan ICMPv6 memang dua protokol berbeda.
  8. tcp dport { 22, 80, 443 } accept: tiga pintu layanan dalam satu aturan berkat set literal nftables. Daftar ini adalah satu-satunya tempat port dibuka — dan isinya wajib identik dengan kebijakan-port.txt. Port 8080 dari audit tidak ada di dalamnya, jadi ia tertutup bukan karena kita menutupnya, melainkan karena tidak pernah kita buka.
  9. limit rate 5/minute burst 10 packets log prefix "nft-drop: ": paket yang jatuh sampai baris terakhir ini dicatat ke log kernel — maksimal lima entri per menit dengan toleransi lonjakan sepuluh paket, supaya banjir pemindaian tidak memenuhi disk. Prefix "nft-drop: " membuat baris-baris itu mudah dicari dengan grep di jurnal.
  10. Tidak ada baris drop terakhir: penolakan justru dilakukan oleh policy drop — paket yang selesai dievaluasi chain tanpa verdict jatuh ke kebijakan cadangan. Aturan log di atasnya bersifat non-terminating: paket dicatat, lalu terus jatuh ke policy.

Urutan baris 5-6 sebelum baris 8 bukan kebetulan: trafik yang pasti sah (loopback, balasan koneksi) selesai diadili di baris awal, dan aturan pembuka pintu dievaluasi hanya untuk koneksi baru dari luar.

Padanan ufw #

Bila server Anda dikelola lewat frontend ufw — seperti pada Proyek Server Web Linux — filosofi yang sama ditulis dalam enam perintah:

text
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Padanannya satu-ke-satu: default deny incoming adalah policy drop, tiap allow adalah satu baris tcp dport ... accept, dan enable adalah nft -f. Perintah ufw sendiri di Debian modern menerjemahkan aturannya ke nftables di belakang layar — bedanya hanyalah siapa yang memegang pena. Pembacaan lengkap perintahnya dibahas di Chapter ufw dan firewalld .

Merem SSH #

SSH di port 22 adalah pintu paling sering diketuk bot, jadi layak diberi rem. nftables punya kata kunci limit yang dipasang pada aturan pembuka pintu:

text
ct state new tcp dport 22 limit rate 3/minute accept

Aturan itu hanya menerima maksimal tiga koneksi baru per menit. Satu kejujuran teknis penting: limit nftables menghitung laju secara keseluruhan per aturan, bukan per alamat sumber — sedangkan sudo ufw limit ssh menerapkan rem per alamat (lebih dari enam percobaan dalam 30 detik dari satu alamat ditolak). Untuk membatasi per penyerang, alat yang tepat adalah fail2ban yang membaca log — kita tempatkan di checklist Bagian 3.

Menerapkan ruleset di Server #

Di server sungguhan, ruleset yang sudah tertulis dipasang tiga langkah: salin ke lokasi konfigurasi, terapkan, lalu simpan hasilnya agar dibaca ulang setelah reboot:

bash
sudo cp latihan.nft /etc/nftables.conf
sudo nft -f /etc/nftables.conf
sudo nft list ruleset
sudo systemctl enable --now nftables.service

nft -f menerapkan seluruh berkas sekaligus, nft list ruleset mencetak ruleset aktif dalam bentuk kanonik untuk dibandingkan, dan systemctl enable memastikan ruleset dimuat ulang saat boot. Perintah-perintah ini membutuhkan root dan tidak dijalankan di lingkungan latihan — kita cukup percaya strukturnya, karena sintaks lengkapnya sudah dibedah di Chapter nftables dan referensinya ada di nft(8) . Sebelum menerapkan, sudo nft -c -f latihan.nft memeriksa sintaks tanpa menerapkan — tetapi sintaks yang benar belum tentu maksud yang benar, dan itulah celah yang ditutup bagian berikutnya.

Validator: Menolak Ruleset Sendirinya #

Ruleset ditulis manusia, dan manusia salah ketik: policy accept menggantikan policy drop karena satu kata, atau satu baris accept telanjang menyelip di dalam chain — keduanya lolos pemeriksaan sintaks tapi membatalkan seluruh kebijakan. Sebelum ruleset menyentuh server, kita saring maksudnya dengan validasi-ruleset.sh: sepuluh pemeriksaan grep yang menuntut bentuk baku proyek ini. Blok berikut menulis ruleset dan validatornya (identik dengan config/ pada paket proyek), lalu mengujinya pada ruleset yang benar dan yang sengaja dibuat longgar — setiap blok latihan mandiri, jadi berkas ditulis ulang di sini:

bash
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
echo "== ruleset hasil rancangan Bagian ini =="
bash validasi-ruleset.sh latihan.nft; echo "exit=$?"
echo
echo "== ruleset longgar: semua koneksi diterima =="
cat > rusak.nft <<'NFT'
# rusak.nft — ruleset longgar: semua koneksi diterima.
table inet filter {
    chain input {
        type filter hook input priority filter; policy accept;
        accept
    }
}
NFT
bash validasi-ruleset.sh rusak.nft; echo "exit=$?"

Output yang Dihasilkan:

text
== ruleset hasil rancangan Bagian ini ==
OK    shebang nft -f (bisa dieksekusi langsung)
OK    ruleset lama dibuang dulu (flush ruleset)
OK    table inet filter dideklarasikan
OK    kebijakan chain input: drop
OK    loopback diizinkan
OK    balasan koneksi sah diizinkan
OK    hanya SSH, HTTP, HTTPS yang dibuka
OK    paket yang jatuh ke policy dicatat dengan rem
OK    tidak ada kebijakan accept yang menyelip
OK    tidak ada aturan accept tanpa syarat
exit=0

== ruleset longgar: semua koneksi diterima ==
GAGAL shebang nft -f (bisa dieksekusi langsung)
GAGAL ruleset lama dibuang dulu (flush ruleset)
OK    table inet filter dideklarasikan
GAGAL kebijakan chain input: drop
GAGAL loopback diizinkan
GAGAL balasan koneksi sah diizinkan
GAGAL hanya SSH, HTTP, HTTPS yang dibuka
GAGAL paket yang jatuh ke policy dicatat dengan rem
GAGAL tidak ada kebijakan accept yang menyelip
GAGAL tidak ada aturan accept tanpa syarat
exit=9

Beda hasil dua eksekusi itu adalah inti validernya. Bagian pentingnya:

  1. Dua fungsi kecil periksa dan tolak: periksa menuntut sebuah pola ada, tolak menuntut pola itu tidak ada — keduanya memakai grep -qE yang diam, hanya exit code yang dipakai. Sepuluh panggilan di bawahnya adalah seluruh daftar kebijakan ruleset, dari shebang sampai larangan accept telanjang.
  2. Pola policy drop; dengan titik koma: kebijakan chain selalu ditulis policy drop; di baris yang sama dengan deklarasinya hook — menuntut titik koma mencegah kecocokan palsu pada komentar yang kebetulan memuat kata policy drop.
  3. tolak '^[[:space:]]*accept[[:space:]]*$': satu baris yang isinya hanya accept adalah aturan menerima segalanya tanpa syarat — kebalikan seluruh filosofi kita. Begitu baris ini ada di mana pun, validator menolak ruleset.
  4. Pola 'tcp dport \{ 22, 80, 443 \} accept': bentuk baku tiga pintu. Konsekuensi kesengajaan: mengubah daftar port berarti mengubah tiga tempat sekaligus — ruleset, kebijakan-port.txt, dan pola validator ini — dan itulah yang membuat perubahan kebijakan selalu disadari, tidak pernah diam-diam.
  5. exit "$gagal": jumlah pemeriksaan yang gagal menjadi exit code — 0 berarti sepuluh pemeriksaan lolos, 9 berarti ruleset longgar tadi melanggar sembilan aturan sekaligus. Angka ini nanti dibaca langsung oleh verifikasi-keamanan.sh di Bagian 3 dan oleh cron setelahnya.

Ruleset kini terlindung dua lapis: sintaksnya diperiksa nft -c di server, maksudnya diperiksa validator di sini. Tetapi ruleset yang tervalidasi tetap baru seujung kertas sampai dibuktikan menghadapi trafik sungguhan — Bagian 3 menguji pintu yang terbuka, pintu yang harusnya tertutup, dan merangkum semuanya menjadi laporan PASS/FAIL yang dijadwalkan.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

config/audit-baseline.sh

Unduh berkas
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

config/validasi-ruleset.sh

Unduh berkas
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"

config/verifikasi-keamanan.sh

Unduh berkas
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"

tests/run-tests.sh

Unduh berkas
bash
#!/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 ]