Proxy Headers #
Ketika Nginx meneruskan request ke backend, ia tidak sekadar memforward semua header mentah-mentah. Ada header yang diubah, ada yang dihapus, dan ada yang perlu ditambahkan secara eksplisit. Memahami perilaku default ini — dan cara mengontrolnya dengan presisi — sangat penting untuk memastikan backend mendapat informasi yang ia butuhkan, dan klien tidak mendapat informasi yang tidak seharusnya mereka lihat.
Perilaku Default Nginx terhadap Header #
Saat Nginx meneruskan request ke upstream, ia melakukan beberapa modifikasi yang mungkin tidak kita sadari:
flowchart LR
subgraph CLIENT["Request dari Klien"]
H1["Host: example.com"]
H2["X-Real-IP: (tidak ada)"]
H3["Cookie: session=abc"]
H4["Authorization: Bearer xyz"]
H5["Connection: keep-alive"]
H6["X-Custom_Header: value"]
H7["User-Agent: Chrome"]
end
subgraph NGINX["Nginx Modifikasi"]
direction TB
M1["Host → diubah ke\nnama host backend"]
M2["Connection → DIHAPUS"]
M3["X-Custom_Header → DIHAPUS\n(mengandung underscore)"]
M4["Header lain → diteruskan"]
end
subgraph BACKEND["Diterima Backend"]
B1["Host: localhost (BERUBAH!)"]
B2["Cookie: session=abc (ok)"]
B3["Authorization: Bearer xyz (ok)"]
B4["User-Agent: Chrome (ok)"]
B5["X-Real-IP: (tidak ada - perlu ditambah)"]
end
CLIENT --> NGINX --> BACKEND
style CLIENT fill:none,stroke-width:2px
style NGINX fill:none,stroke-width:2px
style BACKEND fill:none,stroke-width:2pxYang Dihapus Secara Default #
| Header | Alasan Dihapus |
|---|---|
Connection | Hop-by-hop header — tidak relevan untuk koneksi berikutnya |
Keep-Alive | Hop-by-hop header |
Upgrade | Hop-by-hop header (perlu ditambahkan kembali untuk WebSocket) |
Header dengan underscore _ | Dianggap invalid secara default |
Yang Diubah Secara Default #
| Header | Nilai Asli | Nilai Setelah Nginx |
|---|---|---|
Host | example.com | localhost atau IP backend |
Perubahan Host ini adalah yang paling sering menyebabkan masalah — backend yang melakukan redirect atau menghasilkan URL absolute akan menggunakan nilai Host yang salah.
Implikasi #
Tanpa konfigurasi tambahan, backend tidak tahu:
- IP asli klien (hanya melihat IP Nginx)
- Domain yang diakses klien (
example.com) - Apakah koneksi asli HTTPS atau HTTP
- Port yang digunakan klien
proxy_set_header: Menambah atau Mengubah Header Request ke Backend #
proxy_set_header adalah directive paling penting untuk mengontrol header yang dikirim ke backend.
Set Header Informasi Klien (Wajib di Setiap Proxy) #
location / {
proxy_pass http://backend;
# Host asli yang diakses klien
# $host = domain dari Host header klien (tanpa port)
# $http_host = Host header asli termasuk port jika ada
proxy_set_header Host $host;
# IP asli klien
proxy_set_header X-Real-IP $remote_addr;
# Chain IP — menggabungkan IP klien dengan X-Forwarded-For
# yang mungkin sudah ada (dari proxy sebelumnya)
# Contoh: "203.0.113.1" atau "203.0.113.1, 10.0.0.1"
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Skema koneksi asli: "http" atau "https"
proxy_set_header X-Forwarded-Proto $scheme;
# Port server
proxy_set_header X-Forwarded-Port $server_port;
# Host termasuk port (berguna untuk redirect)
proxy_set_header X-Forwarded-Host $host;
}
Menghapus Header dari Request ke Backend #
Berikan nilai kosong untuk menghapus header:
location /api/ {
proxy_pass http://api_backend;
# Contoh: API backend terpisah yang tidak perlu menerima cookie klien
proxy_set_header Cookie "";
# Hapus Authorization sebelum diteruskan ke backend tertentu
# (berguna jika kita punya sistem auth sendiri di Nginx)
proxy_set_header Authorization "";
# Sembunyikan informasi browser dari backend
proxy_set_header User-Agent "NginxProxy/1.0";
}
Header untuk Keepalive dan HTTP/1.1 #
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
# Hapus header Connection dari klien
# agar tidak mengganggu keepalive ke backend
proxy_set_header Connection "";
}
Membuat Snippet Header yang Reusable #
Karena set header proxy yang sama sering dipakai di banyak location, simpan dalam file snippet untuk menghindari duplikasi:
# /etc/nginx/snippets/proxy-headers.conf
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
Gunakan di konfigurasi:
server {
location /api/ {
proxy_pass http://api_backend;
include snippets/proxy-headers.conf;
# Tambahkan header khusus API di sini jika perlu
}
location /admin/ {
proxy_pass http://admin_backend;
include snippets/proxy-headers.conf;
}
location /ws/ {
proxy_pass http://ws_backend;
include snippets/proxy-headers.conf;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
}
}
proxy_hide_header: Sembunyikan Header dari Response Backend #
Secara default Nginx meneruskan semua header dari response backend ke klien. Gunakan proxy_hide_header untuk mencegah header tertentu sampai ke klien:
location / {
proxy_pass http://backend;
# ─── Header yang mengekspos teknologi backend ─────────────────────────────
# Sembunyikan agar penyerang tidak tahu stack yang digunakan
proxy_hide_header X-Powered-By; # "PHP/8.2", "Express", "Django/4.2"
proxy_hide_header X-Runtime; # Waktu pemrosesan (Rails, dsb)
proxy_hide_header X-AspNet-Version; # Ekspos versi .NET
proxy_hide_header Server; # Header Server dari backend
# ─── Header internal yang tidak relevan untuk klien ─────────────────────
proxy_hide_header X-Request-Id; # ID request internal
proxy_hide_header X-Trace-Id;
proxy_hide_header X-Debug-Info;
# ─── Header yang sensitif ─────────────────────────────────────────────────
proxy_hide_header X-Internal-Token;
proxy_hide_header X-Service-Name;
}
X-Powered-By adalah salah satu header paling penting untuk disembunyikan. Header seperti “PHP/8.2.0” atau “Express 4.18.2” langsung memberi tahu penyerang teknologi apa yang digunakan, sehingga mereka bisa mencari CVE yang spesifik.
proxy_pass_header: Izinkan Header yang Diblokir Nginx #
Beberapa header diblokir Nginx secara default. Gunakan proxy_pass_header untuk mengizinkannya diteruskan ke klien:
location / {
proxy_pass http://backend;
# Header Date biasanya di-override oleh Nginx
# Izinkan nilai dari backend yang melalui
proxy_pass_header Date;
# Izinkan header Server dari backend (berguna untuk custom server header)
proxy_pass_header Server;
}
add_header: Menambah Header ke Response Klien #
add_header menambahkan header ke response yang dikirim ke klien — bukan ke backend. Directive ini sangat penting untuk security headers.
server {
location / {
proxy_pass http://backend;
# ─── Security Headers ─────────────────────────────────────────────────
# Cegah browser dari MIME-type sniffing
add_header X-Content-Type-Options "nosniff" always;
# Cegah halaman dimuat dalam iframe (clickjacking protection)
add_header X-Frame-Options "SAMEORIGIN" always;
# Enforce HTTPS untuk semua request berikutnya (1 tahun)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Kontrol informasi Referer yang dikirim saat navigasi
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Batasi fitur browser yang bisa diakses halaman
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# Content Security Policy (sesuaikan dengan kebutuhan aplikasi)
# add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'" always;
# ─── Cache headers untuk API response ────────────────────────────────
add_header Cache-Control "no-store, no-cache" always;
add_header Pragma "no-cache" always;
}
}
Parameter always — Wajib untuk Security Headers
#
Tanpa parameter always, add_header hanya ditambahkan untuk response sukses (2xx dan 3xx). Security headers harus ada di semua response termasuk error:
# ANTI-PATTERN: tanpa always
add_header X-Frame-Options "SAMEORIGIN";
# → Header tidak ada saat 403, 404, 500, dll.
# → Penyerang bisa membuat clickjacking di halaman error!
# BENAR: dengan always
add_header X-Frame-Options "SAMEORIGIN" always;
# → Header selalu ada, termasuk saat error
Jebakan Inheritance add_header #
Ini adalah salah satu perilaku paling tidak intuitif di Nginx yang sering menyebabkan security hole yang tidak disadari.
Aturan: jika sebuah context (location, server) mendefinisikan directive add_header, ia tidak mewarisi add_header dari parent context-nya.
server {
# Security headers di level server
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin" always;
location / {
# Tidak ada add_header di sini
# → Mewarisi ketiga header dari server block ✓
proxy_pass http://main_backend;
}
location /api/ {
# Ada add_header di sini!
# → Ketiga header dari server block TIDAK diwarisi ✗
# → Hanya Cache-Control yang ada di response /api/
add_header Cache-Control "no-store" always;
proxy_pass http://api_backend;
}
location /admin/ {
# Juga ada add_header → tidak mewarisi dari server block ✗
add_header X-Robots-Tag "noindex" always;
proxy_pass http://admin_backend;
}
}
Di atas, response dari /api/ dan /admin/ tidak memiliki X-Frame-Options, X-Content-Type-Options, dan Referrer-Policy. Halaman admin yang rentan clickjacking!
Solusi: Selalu Gunakan Snippet #
# /etc/nginx/snippets/security-headers.conf
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
server {
location / {
include snippets/security-headers.conf;
proxy_pass http://main_backend;
}
location /api/ {
include snippets/security-headers.conf; # ← Sertakan semua security headers
add_header Cache-Control "no-store" always; # Tambahkan yang khusus
proxy_pass http://api_backend;
}
location /admin/ {
include snippets/security-headers.conf;
add_header X-Robots-Tag "noindex" always;
proxy_pass http://admin_backend;
}
}
Header untuk CORS (Cross-Origin Resource Sharing) #
API yang diakses dari domain berbeda memerlukan CORS headers. Konfigurasi di Nginx memungkinkan kita mengelola CORS secara terpusat tanpa perlu implementasi di setiap backend:
# /etc/nginx/snippets/cors-headers.conf
# Sesuaikan origin yang diizinkan
set $cors_origin "";
if ($http_origin ~* ^https://(app\.example\.com|admin\.example\.com)$) {
set $cors_origin $http_origin;
}
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Max-Age "3600" always;
server {
location /api/ {
# Handle preflight request
if ($request_method = OPTIONS) {
include snippets/cors-headers.conf;
add_header Content-Length 0;
return 204;
}
include snippets/cors-headers.conf;
proxy_pass http://api_backend;
}
}
Verifikasi Header #
# Cek semua header yang diterima klien
curl -I https://example.com/
# Cek header dengan verbose untuk melihat request DAN response headers
curl -v https://example.com/api/test 2>&1
# Cek security headers spesifik
curl -I https://example.com/ | grep -i "x-frame\|x-content\|strict-transport"
# Cek CORS headers untuk request cross-origin
curl -H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: GET" \
-X OPTIONS \
https://example.com/api/ -I
# Tools online untuk audit security headers:
# https://securityheaders.com
# https://observatory.mozilla.org
Debugging dan Audit Header #
Melihat Header yang Dikirim ke Backend #
Saat debugging masalah autentikasi atau routing, kita perlu tahu persis header apa yang diterima backend dari Nginx:
# Sementara di development — JANGAN di production
location /api/ {
proxy_pass http://backend;
# Tambahkan header debug ke request ke backend
proxy_set_header X-Debug-Remote-Addr $remote_addr;
proxy_set_header X-Debug-Scheme $scheme;
proxy_set_header X-Debug-Host $host;
proxy_set_header X-Debug-Request-URI $request_uri;
proxy_set_header X-Debug-Server-Name $server_name;
}
Cara lain yang lebih efektif: gunakan echo module atau backend sederhana yang mencetak semua header yang diterima:
# Jalankan server minimal untuk melihat semua header yang diterima
# (menggunakan netcat)
nc -l 9999 &
# Sementara ganti proxy_pass ke netcat
# proxy_pass http://localhost:9999;
# Lalu buat request dan lihat output netcat:
curl http://localhost/api/test
# Output netcat akan menunjukkan semua header yang Nginx kirim ke backend
Verifikasi Header yang Diterima Klien #
# Lihat semua response headers dari server
curl -s -I https://example.com/ | sort
# Cek security headers secara spesifik
curl -s -I https://example.com/ | grep -iE \
'x-frame|x-content-type|strict-transport|referrer-policy|permissions-policy|content-security'
# Lihat headers untuk request yang melibatkan CORS
curl -s -I \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: GET' \
-X OPTIONS \
https://example.com/api/
# Download securityheaders.com report via CLI
curl -s 'https://securityheaders.com/?q=https://example.com&hide=on&followRedirects=on' \
| grep -oP '(?<=<div class="score">).*?(?=</div>)'
Checklist Header Security #
Sebelum deploy ke production, pastikan semua header ini ada dan nilainya benar:
# Script bash sederhana untuk cek header
URL="https://example.com"
HEADERS=$(curl -s -I $URL)
check_header() {
local header=$1
if echo "$HEADERS" | grep -qi "^$header:"; then
echo "✓ $header: $(echo "$HEADERS" | grep -i "^$header:" | head -1)"
else
echo "✗ $header: MISSING"
fi
}
check_header "Strict-Transport-Security"
check_header "X-Content-Type-Options"
check_header "X-Frame-Options"
check_header "Referrer-Policy"
check_header "Permissions-Policy"
check_header "Content-Security-Policy"
# Check yang tidak seharusnya ada
BAD_HEADERS="X-Powered-By X-Runtime X-AspNet-Version"
for h in $BAD_HEADERS; do
if echo "$HEADERS" | grep -qi "^$h:"; then
echo "⚠ $h: DITEMUKAN — perlu disembunyikan dengan proxy_hide_header"
fi
done
Ringkasan #
- Nginx secara default mengubah
Hostdan menghapus beberapa header (Connection, hop-by-hop) — backend tidak otomatis tahu IP klien asli.proxy_set_header Host $hostwajib agar backend mendapat domain asli, bukan nama host internal.proxy_hide_header X-Powered-By— sembunyikan header yang mengekspos stack teknologi backend.add_headermenambahkan header ke response klien; selalu gunakan parameteralwaysagar berlaku untuk semua kode status termasuk error.- Jebakan inheritance
add_header: jikalocationmendefinisikanadd_headersendiri, ia tidak mewarisi dari parent — gunakan snippet (include snippets/security-headers.conf) untuk konsistensi.- Pisahkan security headers (X-Frame-Options, HSTS, dsb), proxy headers (X-Real-IP, X-Forwarded-For), dan CORS headers ke snippet terpisah untuk kemudahan management.