Day 160 — AuthN/Z hardening & rate limiting¶
Month 6 · Week 3 · ⬅ Day 159 · Day 161 ➡ · Journal index
🎯 Learning Objective¶
Harden a Go HTTP service: separate authentication from authorization, carry identity through context, compare tokens safely, and shed abusive traffic with a token-bucket rate limiter.
📚 Topics¶
- AuthN (who are you) vs AuthZ (what may you do); middleware ordering
- Constant-time secret comparison; identity in
context;401vs403 - Per-client rate limiting;
429+Retry-After; defense in depth
📖 Reading / Sources¶
-
crypto/subtle.ConstantTimeCompare -
net/httpmiddleware &context·contextdocs -
golang.org/x/time/rate(Limiter) - OWASP — Authentication & Authorization Cheat Sheets
📝 Notes¶
- AuthN ≠ AuthZ. Authentication establishes identity (verify a token/credential); authorization decides permission (may this identity do this action?). Keep them in separate middleware: authenticate once, authorize per-resource → [[authn-authz]].
- Status codes:
401 Unauthorized= not (validly) authenticated;403 Forbidden= authenticated but not permitted. Don't leak which one reveals account existence on login (401generically). - Compare secrets in constant time. A normal
==/bytes.Equalreturns early on the first differing byte, leaking length/prefix via timing. Usesubtle.ConstantTimeCompare(a, b) == 1for API keys/tokens/HMACs → [[constant-time-compare]]. - Carry identity via
context, the right way. Use an unexported key type so other packages can't collide or read it:type ctxKey struct{}. Store viacontext.WithValuein the auth middleware; read it downstream. Context values are for request-scoped data like identity/trace-id — never for optional config → [[context-value-key]]. - Middleware order matters: recover → request-id/trace → log → rate-limit (cheap reject first) → authenticate → authorize → handler. Reject the unauthenticated/over-limit before doing expensive work.
- Rate limiting = availability defense. A token bucket allows bursts up to
burst, refills atrate/sec; over-limit requests get429 Too Many Requestswith aRetry-After. Key per client (API key/user, falling back to IP) and evict idle limiters with a janitor. The production tool isgolang.org/x/time/rate; the algorithm is short enough to build on the stdlib ([[ratelimit-example]]). - Hash, don't store, secrets at rest. Passwords use
bcrypt/argon2; API keys store a hash and compare the hash. Tokens (JWT/PASETO/opaque) should be validated (signature + expiry + audience) — prefer opaque tokens checked server-side when you can revoke. - Defense in depth: rate limit + auth + input validation + TLS + least-privilege ([[day-150]]) + dependency scanning ([[day-159]]). No single layer is the wall.
- Set the read header timeout and body size limits (
http.MaxBytesReader) so auth endpoints can't be tied up by slowloris or huge payloads.
💻 Code Examples¶
// API-key auth middleware: constant-time compare, identity into context.
type ctxKey struct{} // unexported → collision-proof context key
func Authenticate(valid map[string]string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("Authorization")
for user, secret := range valid {
if subtle.ConstantTimeCompare([]byte(key), []byte("Bearer "+secret)) == 1 {
ctx := context.WithValue(r.Context(), ctxKey{}, user)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
}
http.Error(w, "unauthorized", http.StatusUnauthorized) // 401
})
}
// Authorization reads the identity the auth layer stored.
func userFrom(ctx context.Context) (string, bool) {
u, ok := ctx.Value(ctxKey{}).(string)
return u, ok
}
Rate-limiter algorithm (runnable, stdlib):
examples/month-06/ratelimit/main.go· Run:go run ./examples/month-06/ratelimit
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| Token-bucket limiter (injected clock, deterministic) | ✅ | exercises/month-06/week-3/tokenbucket |
| Rate-limit demo + middleware shape | ✅ | examples/month-06/ratelimit |
🐛 Mistakes Made¶
- Compared API keys with
==, a timing side channel. Switched tosubtle.ConstantTimeCompare. - Used a bare
stringas a context key, risking collisions across packages. Changed to an unexportedstruct{}key type.
❓ Open Questions¶
- Distributed rate limiting across many instances (shared Redis token bucket) vs per-instance — when is per-instance "good enough"?
🧠 Active Recall (answer without looking)¶
- Q: Why use
subtle.ConstantTimeCompareinstead of==for tokens, and why an unexported context-key type?A
==/bytes.Equal short-circuit at the first differing byte, so response time leaks how much of the secret matched — a timing attack. ConstantTimeCompare always examines every byte. An unexported key type (type ctxKey struct{}) makes the context key unforgeable and collision-proof: no other package can read or overwrite the value, since they can't name the type.
2. Q: What's the difference between 401 and 403, and what does a rate limiter return? A
401 Unauthorized means you are not (validly) authenticated — no/invalid credentials. 403 Forbidden means you're authenticated but lack permission for this resource. A rate limiter returns 429 Too Many Requests, ideally with a Retry-After header telling the client when to try again.
🪶 Feynman Reflection¶
Authentication is the bouncer checking your ID at the door; authorization is the host deciding which rooms your wristband opens. Rate limiting is the doorman counting heads so the venue doesn't get crushed. Each is a separate job, and you want the cheapest gate (the head-count) to turn people away before the expensive ID check. And when checking the ID, you read every digit at the same pace so a forger can't learn the number by watching how fast you say "no."
🕳️ Knowledge Gaps¶
- JWT/PASETO validation pitfalls (alg confusion, clock skew, revocation).
- RBAC vs ABAC modeling for non-trivial authorization rules.
✅ Summary¶
I can layer middleware to authenticate (constant-time, identity-into-context with a private key type) then authorize, return the correct 401/403/429, and shed load with a per-client token-bucket limiter — as one layer of defense in depth.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 161: consolidate Week 3 (profiling → allocations → pooling → load → scanning → hardening) and re-quiz.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦🟦⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: docs(journal): authn/z hardening & rate limiting (day 160)