Mengamankan server yang isinya belum kita kenal itu seperti menutup pintu rumah gelap sambil menebak-nebak letaknya: satu pintu terlewat, dan seluruh pekerjaan jadi sia-sia. Bagian ini menyalakan lampunya dulu. Kita audit server publik fiktif server-kantor dengan lima pertanyaan yang sama setiap kalinya — apa yang jalan, port apa yang terbuka, siapa yang bisa masuk, lewat apa mereka masuk, dan apa yang tertinggal pembaruan — lalu menjawabnya dengan alat yang sudah kita kenal: systemctl, ss, grep, dan apt. Hasil akhirnya bukan sekadar bacaan sekali lewat, melainkan laporan baseline yang dihasilkan ulang oleh skrip setiap kali kita butuh perbandingan.

Lima Pertanyaan Pembuka #

Audit yang baik punya daftar pertanyaan tetap, bukan eksplorasi sesuai suasana hati. Lima baris berikut adalah seluruh kerangka bagian ini:

PertanyaanSumber data di serverAlat pengumpul
Service apa yang sedang jalan?systemdsystemctl list-units
Port apa yang terbuka, di alamat mana?kernelss -tln
Akun mana yang bisa login?/etc/passwdgrep
Dengan kunci apa mereka masuk?~/.ssh/authorized_keys per usergrep -c
Paket apa yang tertinggal pembaruan?index paketapt list --upgradable

Urutannya sengaja dari lapisan terluar ke dalam: service yang jalan menjelaskan port, port menjelaskan permukaan serangan, akun dan kunci menjelaskan siapa yang berdiri di baliknya. Skrip di akhir bagian ini akan mengumpulkan kelimanya sekaligus.

Service yang Sedang Berjalan #

Setiap service berjalan adalah calon pintu — sebagian memang kita nyalakan dengan sengaja, sebagian menyala bawaan instalasi dan lupa kita matikan. Di server, daftarnya dibaca langsung dari systemd:

bash
systemctl list-units --type=service --state=running

Perintah ini butuh systemd yang benar-benar berjalan sebagai init, sehingga tidak dieksekusi di lingkungan latihan — tetapi bentuk outputnya stabil dan layak dikenali. Berikut contoh tampilannya di server-kantor:

text
UNIT                    LOAD   ACTIVE SUB     DESCRIPTION
nginx.service           loaded active running A high performance web server and reverse proxy
postgresql.service      loaded active running PostgreSQL RDBMS
redis-server.service    loaded active running Advanced key-value store
ssh.service             loaded active running OpenBSD Secure Shell server

4 loaded units listed. Pass --all to see loaded but inactive units, too.

Kolom ACTIVE yang kita cari selalu active, dan kolom SUB harus running — service bisa berstatus active tetapi exited (berjalan sekali lalu selesai), yang berarti tidak sedang mendengarkan apa pun. Untuk snapshot yang nanti diurai skrip, dua opsi tambahan merapikan outputnya: --no-pager mematikan tampilan interaktif, dan --no-legend membuang baris kepala serta baris penutup, menyisakan baris data saja. Opsi lengkap systemctl terdokumentasi di systemctl(1) .

Port yang Terbuka #

Service mengaku jalan, tetapi kebenaran di lapangan dibaca dari kernel: port mana yang benar-benar mendengarkan. Alatnya ss dengan kombinasi flag yang sudah kita bedah di Chapter ss dan netstat . Versi paling lengkap untuk audit:

bash
ss -tulpn

Lima flag itu berbunyi: -t untuk socket TCP dan -u untuk UDP, -l membatasi ke yang sedang listening, -n menampilkan alamat dan port sebagai angka tanpa resolusi nama, dan -p menambahkan proses pemilik socket — yang terakhir ini membutuhkan root untuk melihat proses milik user lain, itulah sebabnya audit port di server dijalankan dengan sudo. Referensi lengkapnya ada di ss(8) .

Kita buktikan cara bacanya dengan dua server latihan sungguhan di mesin ini — keduanya python3 -m http.server, satu diikat ke loopback, satu lagi ke semua interface:

bash
printf 'halo dari server latihan\n' > index.html
python3 -m http.server 8765 --bind 127.0.0.1 >/dev/null 2>&1 &
lokal=$!
python3 -m http.server 8766 --bind 0.0.0.0 >/dev/null 2>&1 &
publik=$!
for i in 1 2 3 4 5; do
    curl -s -o /dev/null 127.0.0.1:8765/ && curl -s -o /dev/null 127.0.0.1:8766/ && break
    sleep 0.2
done
echo "== socket yang mendengarkan (difilter port latihan) =="
ss -tln | grep -E ':(8765|8766)'
kill "$lokal" "$publik"

Output yang Dihasilkan:

text
== socket yang mendengarkan (difilter port latihan) ==
LISTEN 0      5          127.0.0.1:8765       0.0.0.0:*
LISTEN 0      5            0.0.0.0:8766       0.0.0.0:*

Bedahnya singkat tetapi menjadi fondasi audit:

  1. --bind 127.0.0.1 vs --bind 0.0.0.0: server pertama hanya menerima koneksi dari dalam mesin itu sendiri; server kedua mendengarkan di semua interface, termasuk alamat publik. Perbedaan satu argumen inilah yang memisahkan “layanan internal” dari “layanan yang terbuka ke dunia” — konsep yang sudah kita saksikan di Chapter Firewall .
  2. ss -tln: tiga flag tanpa -p — versi yang bisa dijalankan tanpa root dan tetap memuat informasi audit inti: state LISTEN, alamat lokal, dan port. Kolom keempat (Local Address:Port) adalah kolom yang nanti diurai skrip audit.
  3. grep -E ':(8765|8766)': di server sesungguhnya output-nya panjang; grep menyaring baris yang memuat port yang sedang kita selidiki. Alternatifnya, ss juga punya sintaks filter bawaan seperti ss -tln '( sport = :8765 )' yang memfilter di dalam kernel — pilihan selera, hasilnya sama.
  4. Loop for berisi curl: server butuh sepersekian detik untuk siap; loop mencoba lima kali dengan jeda 0,2 detik sebelum kita membaca socketnya, sehingga hasil ss tidak pernah mendahului servernya.

Dua baris hasil itu tadi adalah jawaban pertanyaan kedua: di mesin ini ada dua port terbuka, dan keduanya menjelaskan dirinya sendiri. Di server sungguhan, baris yang tidak bisa menjelaskan dirinya adalah yang paling menarik.

User yang Bisa Masuk #

Port adalah pintu bangunan; akun adalah pemegang kuncinya. Kita cari akun yang punya shell login — akun yang bisa benar-benar berdiri di dalam sistem:

bash
echo "== akun dengan shell login di mesin ini =="
grep -v nologin /etc/passwd | grep -v false | cut -d: -f1,7
echo "== jumlah akun dengan shell =="
grep -v nologin /etc/passwd | grep -v false | wc -l

Output yang Dihasilkan:

text
== akun dengan shell login di mesin ini ==
root:/bin/bash
sync:/bin/sync
bun:/bin/sh
z:/bin/bash
== jumlah akun dengan shell ==
4

Tiga langkah pipeline itu bekerja seperti saringan bertingkat. grep -v nologin membuang akun sistem yang shell-nya /usr/sbin/nologin — daemon tanpa kemampuan login — lalu grep -v false membuang varian /usr/sbin/false, dan cut -d: -f1,7 menyisakan kolom nama dan shell. Pada mesin latihan ini hasilnya empat akun: root, sync, dan dua akun kerja — di server Anda daftarnya tentu berbeda. Yang penting bukan jumlahnya, melainkan bahwa setiap baris punya penjelasan: root adalah akun administratif yang sebaiknya tidak dipakai harian, sync adalah akun sistem dengan shell /bin/sync (menjalankan satu perintah sinkronisasi disk lalu keluar, bukan shell interaktif), dan sisanya akun manusia yang memang kita kenal.

Kunci SSH per User #

Setelah tahu siapa yang bisa masuk, pertanyaan berikutnya dengan apa. Bila PasswordAuthentication masih aktif di sshd, akun shell tanpa kunci SSH adalah jalur login password — dan jalur password adalah jalur brute force. Di server, pemeriksaannya menyisir home directory tiap akun:

bash
for user in budi siti deploy; do
    sudo ls -la /home/"$user"/.ssh/ 2>/dev/null || echo "$user: tanpa folder .ssh"
done

Yang dicatat dari pemeriksaan ini bukan isi kuncinya, melainkan jumlah baris authorized_keys per user: kunci yang bertambah tanpa pemberitahuan adalah temuan tersendiri — bekas kontraktor, laptop hilang, atau rekan yang sudah pindah tim. Akun dengan shell tetapi tanpa kunci sama sekali lebih mencurigakan lagi: bersama PasswordAuthentication yes, itu berarti pintu yang dijaga tebakan. Konfigurasi sshd yang menutup jalur ini dibahas tuntas di Chapter SSH Hardening .

Paket yang Tertinggal Pembaruan #

Pertanyaan penutup: perangkat lunak versi berapa yang menjaga pintu-pintu tadi. Index paket menjawabnya:

bash
apt list --upgradable

Output-nya daftar paket dengan versi terpasang dan versi kandidat — dan yang paling penting adalah paket yang menemani service mendengarkan: pustaka TLS, server web, sshd. Tiga paket usang di server-kantor akan terlihat di laporan sebentar lagi; kita tangani pembaruan otomatisnya di Bagian 3.

Skrip Audit: Satu Laporan, Lima Sumber #

Lima pemeriksaan manual di atas hanya sepadan kalau dilakukan sekali. Untuk perbandingan antar-waktu — sebelum dan sesudah mengubah sesuatu — kita butuh laporan yang cara pembuatannya selalu sama, dan itulah pekerjaan audit-baseline.sh. Skrip ini membaca satu folder data berisi snapshot: services.txt, port-terbuka.txt, passwd, upgradable.txt, dan home/ per user — di server sungguhan, kelima snapshot itu dibekukan dengan perintah yang sudah kita jalankan di atas; di latihan ini kita menulisnya sebagai fixture. Blok berikut menyiapkan fixture tersebut, menulis skrip lengkapnya (identik dengan config/audit-baseline.sh pada paket proyek), lalu menjalankannya:

bash
mkdir -p audit-data/home/budi/.ssh audit-data/home/siti/.ssh \
    audit-data/home/deploy audit-data/root
cat > audit-data/services.txt <<'EOF'
nginx.service          loaded active running A high performance web server and reverse proxy
postgresql.service      loaded active running PostgreSQL RDBMS
redis-server.service    loaded active running Advanced key-value store
ssh.service             loaded active running OpenBSD Secure Shell server
EOF
cat > audit-data/port-terbuka.txt <<'EOF'
LISTEN 0      128        0.0.0.0:22           0.0.0.0:*
LISTEN 0      511        0.0.0.0:80           0.0.0.0:*
LISTEN 0      511        0.0.0.0:443          0.0.0.0:*
LISTEN 0      511        0.0.0.0:8080         0.0.0.0:*
LISTEN 0      24         127.0.0.1:5432       0.0.0.0:*
LISTEN 0      511        127.0.0.1:6379       0.0.0.0:*
EOF
cat > audit-data/passwd <<'EOF'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
budi:x:1000:1000:Budi Santoso:/home/budi:/bin/bash
siti:x:1001:1001:Siti Rahma:/home/siti:/bin/bash
deploy:x:1002:1002:Deploy Bot:/home/deploy:/bin/bash
EOF
printf 'ssh-ed25519 AAAAC3NzaC1lZDI1...kunci-budi budi@laptop-budi\n' \
    > audit-data/home/budi/.ssh/authorized_keys
printf 'ssh-ed25519 AAAAC3NzaC1lZDI1...kunci-siti-1 siti@laptop-siti\nssh-ed25519 AAAAC3NzaC1lZDI1...kunci-siti-2 siti@telepon-siti\n' \
    > audit-data/home/siti/.ssh/authorized_keys
cat > audit-data/upgradable.txt <<'EOF'
Listing... Done
libssl3/stable 3.0.15-1~deb12u1 [upgradable from: 3.0.16-1~deb12u1]
openssl/stable 3.0.15-1~deb12u1 [upgradable from: 3.0.16-1~deb12u1]
redis-server/stable 5:7.0.15-1~deb12u1 [upgradable from: 5:7.0.16-1~deb12u1]
EOF

cat > audit-baseline.sh <<'BASH'
#!/usr/bin/env bash
# audit-baseline.sh — kumpulkan baseline keamanan server ke satu laporan.
#
# Pemakaian:
#   audit-baseline.sh <dir-data> > laporan-audit.txt
#
# <dir-data> adalah miniatur filesystem server yang berisi snapshot:
#   services.txt      systemctl list-units --type=service
#                     --state=running --no-pager --no-legend
#   port-terbuka.txt  ss -tlnH
#   passwd            cuplikan /etc/passwd
#   upgradable.txt    apt list --upgradable
#   home/<user>/...   home directory tiap user (untuk authorized_keys)
#
# Kode keluar:
#   0  laporan berhasil dihasilkan (temuan dibaca isinya, bukan kodenya)
#   1  pemakaian salah: argumen hilang atau snapshot tidak lengkap

set -u

if [ "$#" -ne 1 ]; then
    printf 'pemakaian: audit-baseline.sh <dir-data>\n' >&2
    exit 1
fi
DATA=$1
if [ ! -d "$DATA" ]; then
    printf 'audit-baseline.sh: folder data tidak ditemukan: %s\n' "$DATA" >&2
    exit 1
fi
for berkas in services.txt port-terbuka.txt passwd upgradable.txt; do
    if [ ! -f "$DATA/$berkas" ]; then
        printf 'audit-baseline.sh: berkas snapshot hilang: %s/%s\n' \
            "$DATA" "$berkas" >&2
        exit 1
    fi
done

printf '===== AUDIT BASELINE KEAMANAN =====\n'
printf 'sumber data: %s\n\n' "$DATA"

# --- [1] service yang sedang berjalan ---------------------------------
jumlah_service=$(awk '$4 == "running" { c++ } END { print c + 0 }' \
    "$DATA/services.txt")
printf '[1] SERVICE BERJALAN: %d\n' "$jumlah_service"
awk '$4 == "running" { print $1 }' "$DATA/services.txt" | sort |
    sed 's/^/    /'
printf '\n'

# --- [2] port TCP yang terbuka ----------------------------------------
jumlah_port=$(grep -c . "$DATA/port-terbuka.txt")
printf '[2] PORT TCP TERBUKA: %d\n' "$jumlah_port"
awk '{
    ket = "semua interface"
    if (substr($4, 1, 4) == "127.")
        ket = "loopback saja"
    printf "    %-15s %s\n", $4, ket
}' "$DATA/port-terbuka.txt"
printf '\n'

# --- [3] user yang punya shell login ----------------------------------
jumlah_user=$(awk -F: '$7 !~ /(nologin|false)$/ { c++ } END { print c + 0 }' \
    "$DATA/passwd")
printf '[3] USER DENGAN SHELL LOGIN: %d\n' "$jumlah_user"
awk -F: '$7 !~ /(nologin|false)$/ { printf "    %-8s %s\n", $1, $7 }' \
    "$DATA/passwd"
printf '\n'

# --- [4] kunci SSH per user --------------------------------------------
printf '[4] KUNCI SSH PER USER:\n'
awk -F: '$7 !~ /(nologin|false)$/ { print $1, $6 }' "$DATA/passwd" |
while read -r user home; do
    kunci="$DATA$home/.ssh/authorized_keys"
    if [ -f "$kunci" ]; then
        printf '    %-8s %d kunci\n' "$user" "$(grep -c . "$kunci")"
    else
        printf '    %-8s tanpa authorized_keys\n' "$user"
    fi
done
printf '\n'

# --- [5] paket yang tertinggal pembaruan --------------------------------
jumlah_paket=$(grep -c 'upgradable from' "$DATA/upgradable.txt")
printf '[5] PAKET SIAP DIPERBARUI: %d\n' "$jumlah_paket"
grep 'upgradable from' "$DATA/upgradable.txt" | awk '{ print "    " $1 }'
printf '\n'

# --- ringkasan temuan ----------------------------------------------------
jumlah_luar=$(awk '{ if (substr($4, 1, 4) != "127.") c++ }
    END { print c + 0 }' "$DATA/port-terbuka.txt")
tanpa_kunci=$(awk -F: '$7 !~ /(nologin|false)$/ { print $1, $6 }' \
    "$DATA/passwd" |
while read -r user home; do
    [ -f "$DATA$home/.ssh/authorized_keys" ] || printf '%s\n' "$user"
done | paste -sd' ' -)
jumlah_tanpa=$(printf '%s' "$tanpa_kunci" | wc -w)
printf 'RINGKASAN TEMUAN:\n'
printf '    - %d port mendengarkan di semua interface (terjangkau dari luar)\n' \
    "$jumlah_luar"
printf '    - %d akun shell tanpa authorized_keys: %s\n' \
    "$jumlah_tanpa" "$tanpa_kunci"
printf '    - %d paket tertinggal pembaruan\n' "$jumlah_paket"
exit 0
BASH
bash audit-baseline.sh audit-data
echo "== pemakaian yang salah ditolak =="
bash audit-baseline.sh 2>&1; echo "exit=$?"

Output yang Dihasilkan:

text
===== AUDIT BASELINE KEAMANAN =====
sumber data: audit-data

[1] SERVICE BERJALAN: 4
    nginx.service
    postgresql.service
    redis-server.service
    ssh.service

[2] PORT TCP TERBUKA: 6
    0.0.0.0:22      semua interface
    0.0.0.0:80      semua interface
    0.0.0.0:443     semua interface
    0.0.0.0:8080    semua interface
    127.0.0.1:5432  loopback saja
    127.0.0.1:6379  loopback saja

[3] USER DENGAN SHELL LOGIN: 4
    root     /bin/bash
    budi     /bin/bash
    siti     /bin/bash
    deploy   /bin/bash

[4] KUNCI SSH PER USER:
    root     tanpa authorized_keys
    budi     1 kunci
    siti     2 kunci
    deploy   tanpa authorized_keys

[5] PAKET SIAP DIPERBARUI: 3
    libssl3/stable
    openssl/stable
    redis-server/stable

RINGKASAN TEMUAN:
    - 4 port mendengarkan di semua interface (terjangkau dari luar)
    - 2 akun shell tanpa authorized_keys: root deploy
    - 3 paket tertinggal pembaruan
== pemakaian yang salah ditolak ==
pemakaian: audit-baseline.sh <dir-data>
exit=1

Bagian-bagian penting skrip ini:

  1. Folder data sebagai miniatur filesystem: seluruh path yang diperiksa skrip digabung dari $DATA + path absolut di server ($DATA + /home/budi + /.ssh/authorized_keys). Ganti satu variabel, seluruh audit pindah ke snapshot lain — pola yang sama dengan fixture pada proyek-proyek sebelumnya.
  2. awk '$4 == "running" { c++ } END { print c + 0 }': menghitung service dari kolom SUB snapshot systemctl; c + 0 menjaga hasil tetap angka meski tidak ada yang cocok. Baris kepala UNIT LOAD ACTIVE SUB otomatis tersaring karena kolom keempatnya bukan running.
  3. substr($4, 1, 4) == "127.": empat karakter pertama kolom alamat membedakan loopback dari alamat lain; 0.0.0.0, *, dan alamat publik sama-sama dihitung “semua interface”. Pemisahan sederhana ini yang menghasilkan temuan “4 port terjangkau dari luar”.
  4. $7 !~ /(nologin|false)$/: saringan akun shell yang sama dengan pipeline grep -v di atas, kini dalam satu awk, dan hasilnya dipakai ulang dua kali — untuk daftar [3] dan untuk pemeriksaan kunci di [4].
  5. [ -f "$kunci" ] di dalam while read: keberadaan authorized_keys diperiksa per user; jumlah barisnya dihitung grep -c . sehingga berkas kosong pun terlapor sebagai nol kunci, bukan dianggap tidak ada.
  6. exit 0 di akhir: audit tidak pernah “gagal” — temuan justru isinya. Kode keluar 1 hanya untuk pemakaian yang salah, seperti dicontohkan dua baris terakhir blok.

Membaca Laporan: Temuan yang Menuntun #

Laporan di atas layak dibaca pelan-pelan, karena dua temuan di dalamnya yang menentukan arah proyek. Pertama, bagian [2]: ada enam port terbuka, tetapi bagian [1] hanya menjelaskan lima — ssh.service di 22, nginx.service di 80 dan 443, postgresql.service di 5432, redis-server.service di 6379. Port 8080 tidak dijelaskan service mana pun, dan ia mendengarkan di 0.0.0.0 — terjangkau dari seluruh dunia. Setelah ditelusuri di server-kantor, port itu ternyata milik server dev yang lupa dimatikan saat migrasi aplikasi dua bulan lalu. Kedua, bagian [4]: akun deploy punya shell tetapi tanpa authorized_keys — akun otomatisasi seharusnya masuk dengan kunci, bukan password.

Baseline sudah di tangan: empat service, enam port (satu tidak dijelaskan), empat akun shell (dua tanpa kunci), tiga paket tertinggal. Pekerjaan Bagian 2 adalah mengubah daftar itu menjadi kebijakan: tulis dulu pintu mana yang boleh terbuka, tolak sisanya — lalu buktikan bahwa ruleset yang menuliskannya memang layak dipasang.

Berkas Sumber

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

config/audit-baseline.sh

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