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:

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
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:

text
== 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: 200

Dua 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:

bash
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:

text
curl: (7) Failed to connect to 127.0.0.1 port 8123 after 0 ms: Could not connect to server

Tiga 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:

text
$ 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 seconds

Baris 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:

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
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:

text
== 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=1

Dua eksekusi dengan perintah yang sama persis menghasilkan vonis berbeda — dan itulah inti skripnya. Bagian-bagian pentingnya:

  1. 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.
  2. 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.
  3. grep -oE 'dport[[:space:]]+\{[^}]*\}': mengambil set dport dari ruleset, lalu tahap grep kedua menyaring angkanya; hasilnya dibandingkan sebagai string dengan daftar port kebijakan-port.txt yang melewati sort -n dan paste. Satu port berbeda — lebih atau kurang — membuat string tidak sama dan pemeriksaan gagal.
  4. 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.
  5. 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:

text
0 * * * * cd /home/budi/firewall && bash verifikasi-keamanan.sh \
    latihan.nft kebijakan-port.txt http://127.0.0.1:8765/ >> verifikasi.log 2>&1

Kontrak exit code ketiga skrip dirancang untuk dibaca scheduler ini:

Skripexit 0 artinyaexit selain 0
audit-baseline.shlaporan berhasil dibuat1: pemakaian salah
validasi-ruleset.shsepuluh pemeriksaan lolosN: jumlah pemeriksaan gagal
verifikasi-keamanan.shPASSN: 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:

PrioritasItemBentuknya
P1Firewall default-denyruleset proyek ini + validator + verifikasi berkala
P1SSH kunci sajaPasswordAuthentication no; akun deploy diberi kunci atau dicabut shellnya
P2Pembaruan otomatisunattended-upgrades untuk security update — panduan Debian
P2fail2banjail sshd — rem per alamat sumber yang tidak dimiliki limit nftables
P3Audit berkalaaudit-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
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 ]