II — Praktik
Artikel 6 dari 8

Praktik: Secrets, HTTPS & Security Headers

Menutup Security Misconfiguration — secrets management dengan prinsip fail secure, kenapa TLS hidup di reverse proxy, mengunci CORS, dan memasang HTTP security headers yang murah tapi efektif.

10 Juli 2026

Logic aplikasi kita sudah aman (Bagian 35). Sekarang kita tutup ancaman terakhir yang paling sering luput: Security Misconfiguration — kesalahan bukan di kode, tapi di cara aplikasinya dikonfigurasi dan di-deploy.

Secrets Management — Jangan Pernah Percaya Default

Lihat kembali config.py kita:

class Settings(BaseSettings):
    ...
    secret_key: str = "CHANGE-ME-INSECURE-DEFAULT-DO-NOT-USE-IN-PRODUCTION"

Default-nya SENGAJA dibuat jelek dan mencolok. Kalau seseorang lupa meng-set SECRET_KEY lewat environment variable, dan aplikasi tetap jalan pakai default ini, konsekuensinya fatal: SIAPA PUN yang tahu isi kode kita (yang open source, atau ter-leak) bisa membuat JWT token PALSU yang akan diterima server sebagai valid — karena signature-nya dihitung dari secret key yang sama-sama diketahui.

Kita naikkan levelnya satu tingkat lagi di docker-compose.yml, dengan sintaks yang MEMAKSA SECRET_KEY harus di-set:

api:
  environment:
    SECRET_KEY: ${SECRET_KEY:?SECRET_KEY wajib di-set, jangan pakai default}

Sintaks ${VAR:?pesan error} di Docker Compose berarti: kalau SECRET_KEY tidak ada di environment host, GAGALKAN startup sepenuhnya, jangan diam-diam lanjut pakai nilai kosong. Buktikan langsung:

docker compose up -d
error while interpolating services.api.environment.SECRET_KEY:
required variable SECRET_KEY is missing a value:
SECRET_KEY wajib di-set, jangan pakai default

Aplikasi menolak start sama sekali — bukan start dengan diam-diam tidak aman. Ini prinsip fail secure: kalau ada kesalahan konfigurasi, sistem harus gagal dengan cara yang AMAN (berhenti total, kelihatan jelas ada yang salah), bukan gagal dengan cara yang DIAM-DIAM BERBAHAYA (tetap jalan dengan konfigurasi lemah).

export SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
docker compose up -d
# sekarang jalan normal

Aturan lain yang sama pentingnya: secret_key, DATABASE_URL yang mengandung password, dan credential lain TIDAK PERNAH ditulis langsung di kode atau ter-commit ke git — bahkan di file .env sekalipun (.gitignore kita sudah mengecualikan .env). Kalau kamu sudah mengikuti seri CI/CD for Beginners, ini prinsip yang sama persis — secret hidup di GitHub Secrets atau vault khusus, bukan di repository.

HTTPS/TLS — Melindungi Data Selama Perjalanan

Sejauh ini kita melindungi data SAAT DISIMPAN (hashing) dan SAAT DIPROSES (validasi, otorisasi). Ada satu celah lagi: data SAAT DIKIRIM lewat jaringan. Tanpa HTTPS, request (termasuk password saat login, dan token JWT) dikirim dalam bentuk teks biasa yang bisa disadap siapa pun yang berada "di tengah" jalur jaringan (misalnya di WiFi publik yang sama).

Di aplikasi kita, TLS/HTTPS biasanya TIDAK diimplementasikan di kode Python itu sendiri — ini tanggung jawab lapisan di depannya: reverse proxy (Nginx, seperti yang kita pakai di seri Scalability), load balancer cloud, atau platform hosting (Vercel, seperti di seri CI/CD) yang menangani terminasi TLS, lalu meneruskan trafik ke aplikasi lewat jaringan internal yang terpercaya.

Yang BISA kita lakukan di level aplikasi: memberi tahu browser untuk SELALU memakai HTTPS lewat header Strict-Transport-Security (dibahas di bagian berikutnya) — begitu browser pernah menerima header ini sekali, dia otomatis menolak mengakses domain yang sama lewat HTTP biasa untuk periode waktu tertentu, bahkan kalau user mengetik http:// secara eksplisit.

CORS — Mengontrol Siapa yang Boleh Memanggil API dari Browser

CORS (Cross-Origin Resource Sharing) adalah mekanisme browser yang secara default MEMBLOKIR JavaScript di satu domain memanggil API di domain lain — kecuali API itu secara eksplisit mengizinkan. Tanpa CORS diatur, browser modern otomatis menolak permintaan API dari frontend yang beda origin.

app.add_middleware(
    CORSMiddleware,
    allow_origins=[origin.strip() for origin in settings.cors_allowed_origins.split(",")],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PATCH", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)

Kesalahan paling umum: mengatur allow_origins=["*"] (izinkan semua origin) supaya "gampang, tidak perlu mikir". Ini terutama berbahaya kalau dikombinasikan dengan allow_credentials=True — kombinasi itu berarti SITUS APA PUN di internet bisa membuat request ke API kita dengan credential user (cookie, header auth) ikut terbawa. Selalu daftarkan origin yang SPESIFIK dan dipercaya (cors_allowed_origins di .env), bukan wildcard.

Security Headers — Instruksi Tambahan untuk Browser

Terakhir, beberapa header HTTP yang memberi instruksi ekstra ke browser soal cara memperlakukan response kita:

@app.middleware("http")
async def security_headers_middleware(request: Request, call_next):
    response = await call_next(request)
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    response.headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains"
    response.headers["Referrer-Policy"] = "no-referrer"
    return response

Masing-masing:

  • X-Content-Type-Options: nosniff — mencegah browser "menebak-nebak" tipe konten response (MIME sniffing), yang kadang bisa dieksploitasi untuk menjalankan konten sebagai script padahal seharusnya cuma data.
  • X-Frame-Options: DENY — mencegah halaman/response kita ditampilkan di dalam <iframe> situs lain, proteksi dasar terhadap serangan clickjacking (menipu user mengklik sesuatu yang tersembunyi di iframe).
  • Strict-Transport-Security — instruksi "selalu pakai HTTPS untuk domain ini", seperti dibahas di bagian sebelumnya.
  • Referrer-Policy: no-referrer — mencegah URL lengkap (yang bisa mengandung token atau ID sensitif di query string) ikut terkirim ke situs lain lewat header Referer saat user mengklik link keluar.
  • Buktikan headernya benar-benar terpasang:

    curl -s -D - -o /dev/null localhost:8000/health | grep -iE "X-Content-Type-Options|X-Frame-Options|Strict-Transport-Security|Referrer-Policy"
    
    x-content-type-options: nosniff
    x-frame-options: DENY
    strict-transport-security: max-age=63072000; includeSubDomains
    referrer-policy: no-referrer
    

    Header-header ini gratis untuk diterapkan — tidak menambah kompleksitas logic, cuma beberapa baris middleware — tapi menutup beberapa kelas serangan sekaligus.

    Rangkuman

  • Secrets tidak boleh punya default yang "diam-diam masih jalan" — pakai nilai default yang jelas jelek, dan idealnya gagalkan startup sepenuhnya kalau tidak di-set (${VAR:?pesan} di Docker Compose).
  • Prinsip fail secure: kalau konfigurasi salah, sistem harus berhenti dengan jelas, bukan lanjut diam-diam dengan celah keamanan.
  • HTTPS/TLS biasanya diterapkan di reverse proxy/load balancer, bukan kode aplikasi — tapi aplikasi bisa memaksanya lewat header Strict-Transport-Security.
  • CORS harus mengizinkan origin spesifik yang dipercaya, jangan pernah wildcard * dikombinasikan dengan allow_credentials=True.
  • Security headers (X-Content-Type-Options, X-Frame-Options, dst.) murah diterapkan, menutup beberapa kelas serangan browser-side sekaligus.
  • Topik

    SecuritySecretsHTTPS