Episode ini mengamankan lapisan transport dan input: konfigurasi trusted proxies agar ClientIP tidak bisa dipalsukan, TLS dengan ListenAndServeTLS, integrasi reverse proxy nginx dan caddy, pagination yang aman, serta pencegahan injection dan SSRF.

Di balik layar, aplikasi production jarang menerima request langsung dari klien. Ada proxy, load balancer, atau CDN di depan. Episode 15 ini membedah HTTPS, trusted proxies & input hardening: bagaimana mempercayai proxy yang tepat, menjalankan TLS, berintegrasi dengan reverse proxy nginx dan caddy, dan bagaimana mengeraskan input agar aplikasi kebal terhadap injection serta SSRF.
Dua masalah klasik muncul jika bagian ini diabaikan: IP klien yang bisa dipalsukan lewat header X-Forwarded-For, dan input pengguna yang dipakai langsung dalam query atau URL. Keduanya berujung pada serangan yang bisa dicegah dengan konfigurasi dan kebiasaan kecil.
Secara default, Gin mempercayai semua proxy. Akibatnya c.ClientIP() bisa dipalsukan siapa saja:
curl -H "X-Forwarded-For: 203.0.113.66" http://localhost:8080/apic.ClientIP() akan mengembalikan 203.0.113.66 karena header tersebut dipercaya. Rate limiter per IP dari episode 12-14 akan terkecoh, dan log audit mencatat IP yang salah.
Katakan dengan eksplisit proxy mana yang boleh mengisi header tersebut:
err := r.SetTrustedProxies([]string{
"127.0.0.1",
"10.0.0.0/8",
"172.16.0.0/12",
})
if err != nil {
log.Fatal(err)
}r.SetTrustedProxies([]string{"127.0.0.1", ...}) hanya mempercayai IP dan CIDR yang terdaftar. Request yang datang dari luar daftar tidak akan dipercaya header X-Forwarded-For-nya. Jika kalian tidak memakai proxy sama sekali, panggil r.SetTrustedProxies(nil) agar ClientIP selalu mengambil alamat socket langsung.
Jika Gin menangani TLS sendiri, gunakan RunTLS:
r.RunTLS(":8443", "./certs/server.crt", "./certs/server.key")r.RunTLS(":8443", "server.crt", "server.key") menjalankan server dengan sertifikat dan kunci privat. Sertifikat bisa diperoleh gratis dari Let's Encrypt atau dipasok dari internal CA. Di production, pastikan file sertifikat memiliki permission yang ketat dan tidak pernah masuk ke repository.
Pola umum di production: nginx menangani TLS dan HTTP/2, lalu meneruskan ke Gin di port internal. Konfigurasi nginx:
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
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_pass http://127.0.0.1:8080 meneruskan request ke Gin. nginx menambahkan X-Forwarded-For dan X-Forwarded-Proto; karena itu di sisi Gin, SetTrustedProxies harus mencantumkan 127.0.0.1 agar header tersebut dipercaya — dan hanya itu.
Caddy jauh lebih sederhana karena mengelola sertifikat otomatis:
api.example.com {
reverse_proxy localhost:8080
}Blok reverse_proxy localhost:8080 cukup untuk menyalakan HTTPS dengan sertifikat otomatis. Pilih nginx jika butuh kontrol granular dan ekosistem plugin luas; pilih caddy untuk kesederhanaan dan sertifikat yang terkelola.
Pagination yang aman membatasi jumlah data yang bisa diminta sekaligus. Jangan pernah memakai query langsung untuk LIMIT:
func paginate(c *gin.Context) (int, int) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
if page < 1 {
page = 1
}
if limit < 1 || limit > 100 {
limit = 20
}
offset := (page - 1) * limit
return limit, offset
}c.DefaultQuery("page", "1") membaca query dengan fallback. Batas atas limit > 100 mencegah klien meminta ?limit=999999 yang bisa menguras database. Nilai dipaksa masuk rentang aman sebelum dipakai di query.
users, err := repo.List(ctx, gorm.Clause{Limit: limit, Offset: offset})gorm.Clause{Limit: limit, Offset: offset} membatasi hasil di sisi database. Dengan input yang sudah divalidasi, klausa ini tidak pernah membawa nilai liar.
Injection SQL terjadi saat input digabung mentah ke string query. Selalu pakai placeholder:
rows, err := db.Query(ctx,
"SELECT id, name FROM users WHERE email = $1", email)db.Query(ctx, "... WHERE email = $1", email) mengirim nilai sebagai parameter terpisah, bukan bagian dari SQL. Dengan GORM, bentuk Where("email = ?", email) sama amannya. Aturan emas: jangan pernah menyambung input pengguna ke SQL.
SSRF terjadi ketika aplikasi memuat URL dari pengguna. Validasi protocol dan host sebelum memanggil:
func safeFetch(rawURL string) (*http.Response, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, fmt.Errorf("skema URL tidak diizinkan")
}
ip := net.ParseIP(u.Hostname())
if ip != nil && ip.IsPrivate() {
return nil, fmt.Errorf("host privat tidak diizinkan")
}
return http.Get(u.String())
}ip.IsPrivate() menolak target di jaringan internal seperti 127.0.0.1 atau 10.0.0.1. Kombinasikan dengan validasi skema, daftar host yang diizinkan (allowlist), dan timeout untuk menutup jalur SSRF sepenuhnya.
Inti yang harus dibawa pulang:
SetTrustedProxies dengan daftar eksplisit mencegah pemalsuan ClientIP.RunTLS untuk TLS langsung; nginx atau caddy untuk terminasi TLS di depan.X-Forwarded-Proto harus dipercaya hanya dari proxy yang sah.page dan limit masuk rentang aman.Di episode 16 selanjutnya kita akan membedah WebSocket, streaming & SSE — koneksi realtime dengan gorilla/websocket di handler Gin, Server-Sent Events dengan c.SSEvent, streaming upload dan download, serta use case notifikasi dan chat.