Skip to content

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; 401 vs 403
  • Per-client rate limiting; 429 + Retry-After; defense in depth

📖 Reading / Sources

📝 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 (401 generically).
  • Compare secrets in constant time. A normal ==/bytes.Equal returns early on the first differing byte, leaking length/prefix via timing. Use subtle.ConstantTimeCompare(a, b) == 1 for 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 via context.WithValue in 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 at rate/sec; over-limit requests get 429 Too Many Requests with a Retry-After. Key per client (API key/user, falling back to IP) and evict idle limiters with a janitor. The production tool is golang.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 to subtle.ConstantTimeCompare.
  • Used a bare string as a context key, risking collisions across packages. Changed to an unexported struct{} 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)

  1. Q: Why use subtle.ConstantTimeCompare instead 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)