Log adalah ingatan aplikasi: jejak setiap request, error, dan login yang kita andalkan saat menyelidiki insiden. Namun ingatan yang tidak pernah lupa juga menjadi masalah — baris log yang berisi password, token, atau data pribadi tersalin ke agregator log, dashboard, dan backup yang dibaca jauh lebih banyak orang daripada database produksi itu sendiri. Chapter terakhir section ini membahas praktik secure logging: menyamarkan field sensitif, mengubah identifier menjadi hash untuk korelasi, meminimalkan data pribadi (PII), lalu mempraktikkan semuanya dengan log/slog dan ReplaceAttr.

Log adalah Kebocoran yang Paling Sering Dilupakan #

Kesalahan klasiknya satu baris saja: slog.Info("login", "password", pwd) — dan password tersebut kini hidup selama umur retensi log, sering kali bertahun-tahun. Tiga sifat log membuatnya berbahaya: audiensnya luas (tim DevOps, vendor monitoring), volumenya disalin berkali-kali (agregator, backup), dan isinya jarang diaudit ulang. Perlakukan log sebagai output publik: data yang tidak boleh ditampilkan ke pengguna juga tidak boleh ditulis ke log.

Redaction: Menyamarkan Field Sensitif #

Redaction dilakukan sebelum data diserahkan ke logger: fungsi pembersih memeriksa setiap field dan mengganti nilai yang sensitif dengan penanda ***. Pada handler HTTP, pembersihan ini ditempatkan di middleware — satu titik yang dilewati seluruh request — sehingga tidak ada handler yang tidak sengaja mencatat payload mentah. Daftar field yang disamarkan minimal berisi password, token, dan authorization, lalu diperluas sesuai domain aplikasi (nomor kartu, secret jawaban keamanan, dan sejenisnya).

Hash Identifier untuk Korelasi #

Kadang kita justru butuh menandai siapa yang beraksi — misalnya menghubungkan serangkaian request gagal milik satu pengguna. Solusinya: catat hash dari identifier, bukan identifier aslinya. Email [email protected] menjadi 0b56f1259b38 — nilai yang sama setiap kali dihitung, sehingga log tetap bisa dikelompokkan per pengguna, tetapi orang yang membaca log tidak melihat alamat email aslinya. Panjang 12 karakter dari sha256 cukup untuk korelasi; hash penuh tidak diperlukan karena tujuannya bukan melawan penyerang yang brute-force, melainkan menghindari penyimpanan PII.

Minimalkan PII dalam Log #

Data pribadi — nama lengkap, nomor telepon, alamat — sebaiknya tidak masuk log sama sekali kecuali hukum atau proses audit menuntutnya. Dua pertanyaan penentu: apakah investigasi insiden benar-benar membutuhkan field ini, dan berapa lama log disimpan? Bila cukup dengan sidik jari hash, pakai hash; bila cukup dengan jumlah event, jangan sertakan identitas sama sekali.

Tabel Do dan Don’t #

LakukanHindari
Catat user sebagai hash identifierCatat email atau nomor telepon mentah
Samarkan password, token menjadi ***Catat secret apa adanya “untuk debugging”
Catat event + status (login, status=200)Catat seluruh body request mentah
Pakai level sesuai makna (Info/Warn/Error)Tumpuk semuanya di satu level
Tetapkan retensi log dan kurangi PIISimpan log selamanya tanpa ditinjau

Level Log untuk Audit Akses #

Audit akses memakai level secara disiplin: Info untuk event akses yang berhasil (login sukses, request penting), Warn untuk kegagalan autentikasi berulang yang patut dicurigai, dan Error untuk kesalahan sistem yang menuntut perhatian tim. Disiplin level membuat alert berbasis log bisa membedakan percobaan pembobolan dari gangguan jaringan biasa — topik lanjutan pembacaannya ada di section Logging dan Observability .

Demo: slog dengan ReplaceAttr #

slog menyediakan satu titik pembersihan yang lebih rapi daripada mengingat menyamarkan di setiap pemanggilan: opsi ReplaceAttr pada handler memodifikasi setiap atribut tepat sebelum ditulis. Program berikut mendemonstrasikan hash identifier, redaction map, dan logger yang otomatis menyamarkan field sensitif:

go
package main

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"log/slog"
	"os"
)

// hashIdentifier mengubah identifier (mis. email) menjadi sidik jari
// sha256 12 karakter hex untuk korelasi antar-log tanpa menyimpan PII.
func hashIdentifier(id string) string {
	sum := sha256.Sum256([]byte(id))
	return hex.EncodeToString(sum[:])[:12]
}

// redactMap menyamarkan field sensitif pada map sebelum dicatat ke log.
func redactMap(data map[string]string) map[string]string {
	bersih := make(map[string]string, len(data))
	for k, v := range data {
		if k == "password" || k == "token" || k == "authorization" {
			bersih[k] = "***"
		} else {
			bersih[k] = v
		}
	}
	return bersih
}

func main() {
	// Logger dengan ReplaceAttr: field sensitif otomatis disamarkan,
	// dan waktu dibekukan agar output contoh deterministik.
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			switch a.Key {
			case slog.TimeKey:
				a.Value = slog.StringValue("2026-01-15T08:00:00Z")
			case "password", "token", "authorization":
				a.Value = slog.StringValue("***")
			}
			return a
		},
	}))

	fmt.Println("== Sidik jari identifier ==")
	fmt.Println("[email protected] →", hashIdentifier("[email protected]"))
	fmt.Println("[email protected] →", hashIdentifier("[email protected]"))

	fmt.Println()
	fmt.Println("== Redaction map ==")
	payload := map[string]string{
		"email":    "[email protected]",
		"password": "kunci-rahasia-budi",
		"token":    "YOUR_API_TOKEN",
	}
	fmt.Println("sebelum:", payload)
	fmt.Println("sesudah:", redactMap(payload))

	fmt.Println()
	fmt.Println("== slog dengan ReplaceAttr ==")
	logger.Info("login berhasil",
		slog.String("user", hashIdentifier("[email protected]")),
		slog.String("password", "kunci-rahasia-budi"),
	)
	logger.Info("permintaan API",
		slog.String("token", "YOUR_API_TOKEN"),
		slog.Int("status", 200),
	)
}

Output yang Dihasilkan:

text
== Sidik jari identifier ==
[email protected] → 0b56f1259b38
[email protected] → 3684a581ce82

== Redaction map ==
sebelum: map[email:[email protected] password:kunci-rahasia-budi token:YOUR_API_TOKEN]
sesudah: map[email:[email protected] password:*** token:***]

== slog dengan ReplaceAttr ==
time=2026-01-15T08:00:00Z level=INFO msg="login berhasil" user=0b56f1259b38 password=***
time=2026-01-15T08:00:00Z level=INFO msg="permintaan API" token=*** status=200

Mari kita telaah baris demi baris program di atas:

  1. Baris 11-16 (hashIdentifier): menghitung sha256 dari identifier lalu memotong representasi hex menjadi 12 karakter — hash [email protected] selalu 0b56f1259b38, kunci korelasi yang stabil.
  2. Baris 18-29 (redactMap): menyalin map ke map baru; tiga nama field sensitif diganti "***", field lain lewat apa adanya — input tidak pernah dimutasi.
  3. Baris 34-45 (slog.New dengan HandlerOptions): membangun logger TextHandler yang setiap atributnya melewati ReplaceAttr sebelum ditulis.
  4. Baris 38-39 (slog.TimeKey): nilai waktu dibekukan menjadi string tetap agar output contoh ini deterministik — di produksi, baris ini tidak ada.
  5. Baris 40-41: atribut bernama password, token, atau authorization otomatis diganti "***" — pemanggil logger.Info tidak perlu ingat menyamarkan apa pun.
  6. Baris 63-70: pemanggilan pertama tetap mengirim atribut password (untuk keperluan demo), dan output menunjukkan nilainya tersamarkan; pemanggilan kedua membuktikan token mendapat perlakuan yang sama.

Referensi #

Dengan redaction otomatis, identifier ter-hash, dan PII yang minim, jejak audit kita tetap tajam untuk investigasi tanpa menjadi gudang bocoran. Seluruh rantai keamanan — dari hashing password hingga log yang bersih — kini lengkap, dan saatnya melangkah ke wilayah berikutnya: Arsitektur Aplikasi , tempat semua praktik ini dirangkai menjadi aplikasi yang utuh.

Berkas Sumber

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

seclog-demo.go

Unduh berkas
go
package main

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"log/slog"
	"os"
)

// hashIdentifier mengubah identifier (mis. email) menjadi sidik jari
// sha256 12 karakter hex untuk korelasi antar-log tanpa menyimpan PII.
func hashIdentifier(id string) string {
	sum := sha256.Sum256([]byte(id))
	return hex.EncodeToString(sum[:])[:12]
}

// redactMap menyamarkan field sensitif pada map sebelum dicatat ke log.
func redactMap(data map[string]string) map[string]string {
	bersih := make(map[string]string, len(data))
	for k, v := range data {
		if k == "password" || k == "token" || k == "authorization" {
			bersih[k] = "***"
		} else {
			bersih[k] = v
		}
	}
	return bersih
}

func main() {
	// Logger dengan ReplaceAttr: field sensitif otomatis disamarkan,
	// dan waktu dibekukan agar output contoh deterministik.
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			switch a.Key {
			case slog.TimeKey:
				a.Value = slog.StringValue("2026-01-15T08:00:00Z")
			case "password", "token", "authorization":
				a.Value = slog.StringValue("***")
			}
			return a
		},
	}))

	fmt.Println("== Sidik jari identifier ==")
	fmt.Println("[email protected] →", hashIdentifier("[email protected]"))
	fmt.Println("[email protected] →", hashIdentifier("[email protected]"))

	fmt.Println()
	fmt.Println("== Redaction map ==")
	payload := map[string]string{
		"email":    "[email protected]",
		"password": "kunci-rahasia-budi",
		"token":    "YOUR_API_TOKEN",
	}
	fmt.Println("sebelum:", payload)
	fmt.Println("sesudah:", redactMap(payload))

	fmt.Println()
	fmt.Println("== slog dengan ReplaceAttr ==")
	logger.Info("login berhasil",
		slog.String("user", hashIdentifier("[email protected]")),
		slog.String("password", "kunci-rahasia-budi"),
	)
	logger.Info("permintaan API",
		slog.String("token", "YOUR_API_TOKEN"),
		slog.Int("status", 200),
	)
}