Proxy Headers

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

Yang Dihapus Secara Default #

HeaderAlasan Dihapus
ConnectionHop-by-hop header — tidak relevan untuk koneksi berikutnya
Keep-AliveHop-by-hop header
UpgradeHop-by-hop header (perlu ditambahkan kembali untuk WebSocket)
Header dengan underscore _Dianggap invalid secara default

Yang Diubah Secara Default #

HeaderNilai AsliNilai Setelah Nginx
Hostexample.comlocalhost 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 Host dan menghapus beberapa header (Connection, hop-by-hop) — backend tidak otomatis tahu IP klien asli.
  • proxy_set_header Host $host wajib agar backend mendapat domain asli, bukan nama host internal.
  • proxy_hide_header X-Powered-By — sembunyikan header yang mengekspos stack teknologi backend.
  • add_header menambahkan header ke response klien; selalu gunakan parameter always agar berlaku untuk semua kode status termasuk error.
  • Jebakan inheritance add_header: jika location mendefinisikan add_header sendiri, 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.

← Sebelumnya: proxy_pass   Berikutnya: Buffer & Timeout →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact