Layanan cloud memang praktis, tetapi tagihannya datang setiap bulan, foto keluarga tersimpan di server milik orang lain, dan menyalin ratusan gigabyte lewat internet rumahan memakan waktu berhari-hari. Sementara itu, jaringan lokal di rumah dan kantor kecil sudah berjalan gigabit — menyalin berkas yang sama lewat LAN hanya soal menit. Proyek ini membangun alternatifnya: file server lokal di atas satu mesin Linux, dengan struktur penyimpanan yang direncanakan, share Samba untuk klien Windows dan ponsel, NFS untuk klien Linux, serta permission per grup sehingga anggota keluarga bisa menulis, tamu hanya membaca, dan backup tidak tersentuh siapa pun kecuali admin.

Server yang kita bangun menyediakan tiga hal sekaligus:

  • struktur penyimpanan terencana — tiga share terpisah per fungsi (dokumen, media, backup) di atas disk data sendiri yang dipasang lewat entri fstab dengan opsi nofail,
  • berbagi file dua protokol — Samba untuk klien Windows dan ponsel, NFS untuk klien Linux, masing-masing dengan batasan aksesnya,
  • permission per grup — grup keluarga membaca-menulis, grup tamu hanya membaca share media, dan share backup hanya untuk admin — semuanya ditegakkan bit setgid pada folder share sehingga berkas baru selalu jatuh ke grup yang benar.

Tampilan Akhir Server #

Sesi berikut menunjukkan keadaan akhir yang bisa dibuktikan di lingkungan latihan. Skrip buat-struktur.sh membangun ketiga share beserta permission-nya, lalu test suite memeriksa struktur, konfigurasi, pewarisan setgid, dan penolakan tulis pada berkas read-only:

console
$ bash deploy/buat-struktur.sh latihan
mode    : simulasi tanpa root
group   : keluarga, tamu, backup dicatat di latihan/etc/group
struktur: latihan/share/{dokumen,media,backup}
OK     2770 latihan/share/dokumen
OK     2775 latihan/share/media
OK     2770 latihan/share/backup
selesai tanpa error
$ stat -c '%a %n' latihan/share/*
2770 latihan/share/backup
2770 latihan/share/dokumen
2775 latihan/share/media
$ bash tests/run-tests.sh | tail -n 3
OK    isi file ro tidak berubah
OK    tulis diizinkan kembali setelah mode rw
PASSED: 20/20 test lulus

Mode 2770 pada dua folder pertama bukan typo — digit 2 di depan adalah bit setgid yang membuat semua berkas baru di dalamnya mewarisi grup folder, fondasi dari seluruh skema permission proyek ini.

Arsitektur Akhir #

Seluruh keputusan di atas bertemu dalam satu gambar berikut:

text
Klien di jaringan rumah — LAN 192.0.2.0/24
   Windows laptop ─┐         ┌─ ponsel Android ─ Samba, TCP 445
                    │         │
                    ▼         ▼
              [ smbd ]  port 445  — share Samba untuk dokumen, media, backup
                    │     valid users = @keluarga @tamu (per share)
                    │     media: write list @keluarga, read list @tamu
                    │
laptop-budi ──► [ nfsd ]  port 2049 — NFSv4 untuk klien Linux
                    │     dokumen: rw   media: ro   backup: 192.0.2.10 saja
                    ▼
        /srv/share/               ← disk data terpisah, fstab: defaults,nofail
        ├── dokumen/   grup keluarga   mode 2770   rw anggota, tamu ditolak
        ├── media/     grup keluarga   mode 2775   rw anggota, tamu baca
        └── backup/    grup backup     mode 2770   hanya admin (budi)

Setiap baris di gambar itu dibangun dan diperiksa satu per satu: disk data dan struktur folder di Bagian 1, kedua protokol berbagi di Bagian 2, dan lapisan user-grup-permission beserta auditnya di Bagian 3.

Tujuan Pembelajaran #

Setelah menyelesaikan proyek ini, kita mampu:

  • merancang struktur share per fungsi beserta pemetaan grup dan mode permission-nya sebelum mengetik satu perintah pun,
  • menyiapkan disk data terpisah dengan entri fstab ber-nofail yang aman saat boot,
  • menulis smb.conf lengkap dengan valid users, read list/write list, dan mask yang konsisten dengan permission di disk,
  • menyusun /etc/exports dengan opsi keamanan NFS yang tepat: rw/ro per klien, sync, no_subtree_check, dan root_squash yang tetap aktif,
  • menerapkan pola grup bersama: groupadd, usermod -aG, bit setgid 2770, dan pengecualian per user lewat ACL,
  • memvalidasi konfigurasi dan permission lewat skrip pemeriksa serta test suite bash yang bisa dijalankan ulang.

Prasyarat #

Proyek ini merangkai materi dari beberapa section sekaligus. Chapter yang paling sering dirujuk:

  • Samba — anatomi smb.conf, user Samba dua lapis, dan pemasangan dari klien,
  • NFS — format /etc/exports, opsi per klien, dan jebakan root_squash,
  • File Permissions — kelas owner/group/other dan mode oktal,
  • fstab — field entri mount permanen, UUID, dan opsi nofail.

Tahapan Pengerjaan #

Proyek dikerjakan dalam tiga bagian berurutan, dan tiap bagian diakhiri dengan sesuatu yang bisa diperiksa:

  1. Bagian 1: Konfigurasi Penyimpanan — merancang tabel share, menyiapkan disk data dengan entri fstab, lalu membangun struktur /srv/share beserta grup dan mode-nya lewat skrip.
  2. Bagian 2: Berbagi File — menulis smb.conf untuk tiga share dan /etc/exports untuk klien Linux, membuka port yang diperlukan, lalu memvalidasi semuanya dengan verifikasi-share.sh.
  3. Bagian 3: Akses dan Permissions — mengelola grup dan user, membuktikan setgid mewariskan grup, menyusun pengecualian ACL, menguji akses dari klien, dan mengaudit siapa mengakses apa.

Mulailah dari Bagian 1 karena tabel share dan mode-nya menjadi acuan langsung bagi konfigurasi Samba, ekspor NFS, dan test suite pada dua bagian berikutnya. Berkas proyek (config/ berisi smb.conf, exports, dan fstab-share; deploy/ berisi kedua skrip; tests/run-tests.sh; serta docs/README.txt) tersedia pada bagian Berkas Sumber di bawah halaman setiap bagian, sehingga hasil pekerjaan bisa dibandingkan dengan versi referensi kapan saja.

Berkas Sumber

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

deploy/buat-struktur.sh

Unduh berkas
bash
#!/usr/bin/env bash
# buat-struktur.sh — bangun struktur penyimpanan file server.
#
# Membuat tiga share (dokumen, media, backup), mengatur group dan
# mode permission-nya, lalu memverifikasi hasil akhirnya dengan stat.
#
# Dua mode eksekusi:
#   - sebagai root di server  : membuat group sungguhan (groupadd bila
#     belum ada), folder di bawah TARGET, dan chgrp ke group aslinya.
#   - sebagai user biasa      : simulasi — folder tetap dibuat, chgrp
#     digantikan ke group utama kita, dan rencana group dicatat ke
#     TARGET/etc/group sebagai fixture yang bisa dibaca ulang.
#
# Pemakaian:
#   sudo bash buat-struktur.sh /srv      # di server sungguhan
#   bash buat-struktur.sh latihan        # latihan lokal tanpa root
#
# Kode keluar:
#   0 sukses                             1 argumen tidak valid
#   2 struktur gagal dibuat/tidak sesuai

set -u

TARGET="${1:-latihan}"

# Rencana share: nama:group:mode (mode 4 digit oktal, setgid aktif).
RENCANA="dokumen:keluarga:2770 media:keluarga:2775 backup:backup:2770"

if [ -z "$TARGET" ] || [ "$TARGET" = "/" ]; then
    printf 'buat-struktur.sh: target tidak valid: %s\n' "$TARGET" >&2
    exit 1
fi
if [ "$TARGET" = "/srv" ] && [ "$(id -u)" -ne 0 ]; then
    printf 'buat-struktur.sh: /srv milik root; jalankan dengan sudo\n' >&2
    exit 1
fi

mkdir -p "$TARGET" || exit 1
SHARE_DIR="$TARGET/share"

# --- 1. Siapkan group ----------------------------------------------------
if [ "$(id -u)" -eq 0 ]; then
    printf 'mode    : server (root)\n'
    for grup in keluarga tamu backup; do
        if ! getent group "$grup" > /dev/null; then
            groupadd "$grup" || exit 2
        fi
    done
else
    printf 'mode    : simulasi tanpa root\n'
    mkdir -p "$TARGET/etc" || exit 2
    cat > "$TARGET/etc/group" <<'GRUP'
keluarga:x:1100:budi,siti
tamu:x:1101:tamu1
backup:x:1102:budi
GRUP
    printf 'group   : keluarga, tamu, backup dicatat di %s/etc/group\n' "$TARGET"
fi

# --- 2. Buat folder share beserta permission-nya -------------------------
printf 'struktur: %s/{dokumen,media,backup}\n' "$SHARE_DIR"
gagal=0
for entri in $RENCANA; do
    nama=${entri%%:*}
    sisa=${entri#*:}
    grup=${sisa%%:*}
    mode=${sisa##*:}
    dir="$SHARE_DIR/$nama"

    mkdir -p "$dir" || { gagal=$((gagal + 1)); continue; }
    if [ "$(id -u)" -eq 0 ]; then
        chgrp "$grup" "$dir" || { gagal=$((gagal + 1)); continue; }
    else
        # Tanpa root kita hanya boleh memindahkan group ke group sendiri;
        # di server, baris ini menjadi: chgrp "$grup" "$dir"
        chgrp "$(id -gn)" "$dir" || { gagal=$((gagal + 1)); continue; }
    fi
    chmod "$mode" "$dir"

    # --- 3. Verifikasi hasil dengan stat -------------------------------
    nyata=$(stat -c '%a' "$dir")
    if [ "$nyata" != "$mode" ]; then
        printf 'GAGAL  %s mode %s, harapan %s\n' "$dir" "$nyata" "$mode"
        gagal=$((gagal + 1))
    else
        printf 'OK     %s %s\n' "$mode" "$dir"
    fi
done

if [ "$gagal" -gt 0 ]; then
    printf 'GAGAL: %d pemeriksaan tidak sesuai\n' "$gagal"
    exit 2
fi
printf 'selesai tanpa error\n'

deploy/verifikasi-share.sh

Unduh berkas
bash
#!/usr/bin/env bash
# verifikasi-share.sh — periksa directive wajib smb.conf dan format exports.
#
# Pengganti testparm/exportfs di lingkungan tanpa Samba dan NFS: menerima
# folder yang memuat smb.conf dan exports, lalu memeriksa directive
# keamanan yang kita rancang satu per satu dengan grep dan awk.
#
# Pemakaian:
#   bash verifikasi-share.sh <folder-config>
#
# Kode keluar = jumlah pemeriksaan yang gagal.

set -u

if [ $# -ne 1 ]; then
    printf 'pemakaian: verifikasi-share.sh <folder-config>\n' >&2
    exit 1
fi

SMB="$1/smb.conf"
EXP="$1/exports"
gagal=0

for f in "$SMB" "$EXP"; do
    if [ ! -f "$f" ]; then
        printf 'verifikasi-share.sh: berkas tidak ditemukan: %s\n' "$f" >&2
        exit 1
    fi
done

ok()     { printf 'OK     %s\n' "$1"; }
hilang() { printf 'HILANG %s\n' "$1"; gagal=$((gagal + 1)); }

# --- smb.conf: section dan directive wajib --------------------------------
for pola in \
    '[global]' \
    '[dokumen]' \
    '[media]' \
    '[backup]' \
    'path = /srv/share/dokumen' \
    'path = /srv/share/media' \
    'path = /srv/share/backup' \
    'valid users = @keluarga' \
    'read only = no' \
    'create mask = 0660' \
    'directory mask = 2770' \
    'browseable = no' \
    'bind interfaces only = yes'
do
    if grep -qF -- "$pola" "$SMB"; then
        ok "smb.conf: $pola"
    else
        hilang "smb.conf: $pola"
    fi
done

# --- exports: format dan opsi keamanan -----------------------------------

# 1. Setiap entri aktif memuat kurung opsi di kolom klien.
n=$(awk '!/^[[:space:]]*#/ && NF && $2 !~ /\(/ { c++ } END { print c + 0 }' "$EXP")
if [ "$n" -eq 0 ]; then
    ok 'exports: semua entri memuat opsi klien'
else
    hilang 'exports: ada entri tanpa opsi klien'
fi

# 2. Setiap entri memuat sync dan no_subtree_check.
n=$(awk '
    !/^[[:space:]]*#/ && NF && ($2 !~ /sync/ || $2 !~ /no_subtree_check/) { c++ }
    END { print c + 0 }' "$EXP")
if [ "$n" -eq 0 ]; then
    ok 'exports: semua entri memuat sync + no_subtree_check'
else
    hilang 'exports: ada entri tanpa sync / no_subtree_check'
fi

# 3. root_squash tidak boleh dimatikan.
if grep -q 'no_root_squash' "$EXP"; then
    hilang 'exports: no_root_squash terdeteksi'
else
    ok 'exports: root_squash tetap aktif'
fi

# 4. dokumen rw, media ro.
mode_dokumen=$(awk '$1 == "/srv/share/dokumen" {
    print ($2 ~ /\(rw/) ? "rw" : "ro" }' "$EXP")
if [ "$mode_dokumen" = "rw" ]; then
    ok 'exports: dokumen dibagikan rw'
else
    hilang 'exports: dokumen bukan rw'
fi
mode_media=$(awk '$1 == "/srv/share/media" {
    print ($2 ~ /\(ro/) ? "ro" : "rw" }' "$EXP")
if [ "$mode_media" = "ro" ]; then
    ok 'exports: media dibagikan ro'
else
    hilang 'exports: media bukan ro'
fi

# 5. backup hanya untuk satu klien tepercaya.
klien=$(awk '$1 == "/srv/share/backup" {
    k = $2; sub(/\(.*/, "", k); print k }' "$EXP")
if [ "$klien" = "192.0.2.10" ]; then
    ok 'exports: backup terbatas ke 192.0.2.10'
else
    hilang 'exports: klien backup bukan 192.0.2.10'
fi

exit "$gagal"

tests/run-tests.sh

Unduh berkas
bash
#!/usr/bin/env bash
# run-tests.sh — test suite untuk proyek server file lokal.
# Suite menyiapkan folder kerja terisolasi (mktemp), lalu menguji:
# struktur share hasil deploy/buat-struktur.sh (folder, mode, fixture
# group), directive wajib pada config/smb.conf dan config/exports
# lewat deploy/verifikasi-share.sh, entri config/fstab-share, pewarisan
# setgid pada folder share, serta penolakan tulis pada file read-only.
# Jalankan dari mana saja: bash tests/run-tests.sh

set -u

SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJ_DIR=$(dirname "$SCRIPT_DIR")

PASS=0
FAIL=0
WORK=""

# 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
}

bersihkan() {
    if [ -n "$WORK" ]; then
        rm -rf "$WORK"
    fi
}
trap bersihkan EXIT

WORK=$(mktemp -d)
cd "$WORK" || exit 1

# --- uji 1: buat-struktur.sh membangun struktur yang sesuai ---------------
bash "$PROJ_DIR/deploy/buat-struktur.sh" latihan > /dev/null 2>&1
assert_eq 0 "$?" "buat-struktur.sh keluar 0"
for nama in dokumen media backup; do
    [ -d "latihan/share/$nama" ]
    assert_eq 0 "$?" "share $nama dibuat"
done
assert_eq 2770 "$(stat -c '%a' latihan/share/dokumen)" \
    "mode dokumen 2770"
assert_eq 2775 "$(stat -c '%a' latihan/share/media)" \
    "mode media 2775"
assert_eq 2770 "$(stat -c '%a' latihan/share/backup)" \
    "mode backup 2770"

# --- uji 2: fixture group tercatat, skrip idempoten ------------------------
grep -q '^keluarga:x:1100:budi,siti$' latihan/etc/group
assert_eq 0 "$?" "fixture group memuat keluarga:budi,siti"
grep -q '^tamu:x:1101:tamu1$' latihan/etc/group
assert_eq 0 "$?" "fixture group memuat tamu"
bash "$PROJ_DIR/deploy/buat-struktur.sh" latihan > /dev/null 2>&1
assert_eq 0 "$?" "buat-struktur.sh idempoten (dijalankan dua kali)"

# --- uji 3: konfigurasi bawaan lolos verifikasi-share.sh -------------------
bash "$PROJ_DIR/deploy/verifikasi-share.sh" "$PROJ_DIR/config" \
    > /dev/null 2>&1
assert_eq 0 "$?" "config/ lolos verifikasi-share.sh"

# --- uji 4: konfigurasi rusak tertangkap -----------------------------------
mkdir -p rusak
printf '[global]\n[dokumen]\npath = /srv/share/dokumen\nread only = no\n' \
    > rusak/smb.conf
printf '/srv/share/dokumen  192.0.2.0/24(rw,async)\n' > rusak/exports
bash "$PROJ_DIR/deploy/verifikasi-share.sh" rusak > /dev/null 2>&1
if [ "$?" -gt 0 ]; then
    PASS=$((PASS + 1))
    printf 'OK    konfigurasi rusak tertangkap (exit bukan 0)\n'
else
    FAIL=$((FAIL + 1))
    printf 'GAGAL konfigurasi rusak tertangkap: verifikasi-share.sh keluar 0\n'
fi

# --- uji 5: entri fstab-share memuat nofail dan enam field -----------------
grep -q 'nofail' "$PROJ_DIR/config/fstab-share"
assert_eq 0 "$?" "fstab-share memuat nofail"
jumlah=$(awk '!/^[[:space:]]*#/ && NF { print NF }' \
    "$PROJ_DIR/config/fstab-share" | sort -u)
assert_eq 6 "$jumlah" "entri fstab-share berisi enam field"
mnt=$(awk '$2 == "/srv/share" { print $2 }' "$PROJ_DIR/config/fstab-share")
assert_eq "/srv/share" "$mnt" "mountpoint fstab-share /srv/share"

# --- uji 6: setgid diwarisi oleh isi baru folder share ---------------------
mkdir -p latihan/share/dokumen/laporan
touch latihan/share/dokumen/laporan/catatan.txt
assert_eq "$(stat -c '%G' latihan/share/dokumen)" \
    "$(stat -c '%G' latihan/share/dokumen/laporan/catatan.txt)" \
    "file baru mewarisi group folder share"
mode_sub=$(stat -c '%a' latihan/share/dokumen/laporan)
if [ "$(( 8#$mode_sub & 8#2000 ))" -ne 0 ]; then
    PASS=$((PASS + 1))
    printf 'OK    folder baru mewarisi bit setgid (%s)\n' "$mode_sub"
else
    FAIL=$((FAIL + 1))
    printf 'GAGAL folder baru mewarisi bit setgid: mode %s tanpa bit 2000\n' \
        "$mode_sub"
fi

# --- uji 7: tulis ditolak pada file read-only ------------------------------
printf 'isi awal\n' > latihan/kunci.txt
chmod 444 latihan/kunci.txt
if (echo 'tambahan' >> latihan/kunci.txt) 2> /dev/null; then
    FAIL=$((FAIL + 1))
    printf 'GAGAL tulis ke file ro ditolak: append justru berhasil\n'
else
    PASS=$((PASS + 1))
    printf 'OK    tulis ke file ro ditolak\n'
fi
assert_eq 'isi awal' "$(cat latihan/kunci.txt)" "isi file ro tidak berubah"
chmod 644 latihan/kunci.txt
echo 'tambahan' >> latihan/kunci.txt
assert_eq 0 "$?" "tulis diizinkan kembali setelah mode rw"

# --- 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 ]