R-753: the box tells visitors apart — cloudflared at a fixed address, traefik trusts only it, the controller reads the hop traefik saw
- felhom-tunnel network 172.16.253.0/29 (ip-range .4/30): cloudflared alone at .2, traefik at .3; traefik's websecure trusts forwarded headers from 172.16.253.2/32 only, and every request passes felhom-forwarded@file, which removes the client-writable host/path/address headers (X-Forwarded-Host/-Uri/-Method/-Prefix, Forwarded, True-Client-Ip, …) and fixes X-Forwarded-Port to 443 (measured: Cloudflare passes a client's X-Forwarded-Host/-Port). - EnsureBaseStack reconciles a RUNNING traefik/cloudflared whose rendered files changed (recreate), refuses a rewrite that would drop a certificate resolver, and moves cloudflared only once traefik is on the tunnel network. - clientIP: believed only when the TCP peer is traefik; the rightmost X-Forwarded-For entry (the hop traefik saw); the tunnel hop → CF-Connecting-IP (the edge refuses a client-sent one, measured 403). rateKey: IPv6 per /64. Dashboard login, claim, share and escrow counters key on it; the setup gate logs it. - Dashboard login messages: keys, informal voice, both languages. Red-proofs: RP-A1 (leftmost hop), RP-A2 (shared tunnel key), RP-A3 (no reconcile) — felhom.eu audits/visitors-2026-10-01/A. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -150,20 +150,20 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
password := r.FormValue("password")
|
||||
nextURL := r.FormValue("next")
|
||||
|
||||
// The counter's key is the VISITOR (clientaddr.go, R-753): through the tunnel each visitor has its own address, so a
|
||||
// stranger's wrong passwords lock only the stranger. Before v0.286.0 every tunnel visitor shared cloudflared's
|
||||
// address here, and five wrong tries locked the whole household out of its own dashboard for a minute.
|
||||
ip := rateKey(r)
|
||||
if s.isDebug() {
|
||||
s.logger.Printf("[DEBUG] [web] login attempt from %s (X-Forwarded-For: %s)", r.RemoteAddr, r.Header.Get("X-Forwarded-For"))
|
||||
s.logger.Printf("[DEBUG] [web] login attempt: visitor %s (peer %s, X-Forwarded-For %q, CF-Connecting-IP %q)",
|
||||
ip, r.RemoteAddr, r.Header.Get("X-Forwarded-For"), r.Header.Get("CF-Connecting-IP"))
|
||||
}
|
||||
|
||||
if password == "" {
|
||||
s.renderLogin(w, r, "Kérjük adja meg a jelszót", "")
|
||||
s.renderLogin(w, r, s.msg(r, "login.msg.empty_password"), "")
|
||||
return
|
||||
}
|
||||
|
||||
// Rate limit: check failed attempts from this host. clientIP strips the ephemeral port
|
||||
// (CAMPAIGN-4 F-B) so distinct direct connections from one host share a key and the counter
|
||||
// actually accrues; XFF first-hop still wins for proxied clients.
|
||||
ip := clientIP(r)
|
||||
|
||||
s.loginAttemptMu.Lock()
|
||||
attempt := s.loginAttempts[ip]
|
||||
if attempt != nil && time.Since(attempt.lastFail) > loginWindowDuration {
|
||||
@@ -174,14 +174,14 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if attempt != nil && attempt.count >= loginMaxAttempts {
|
||||
s.loginAttemptMu.Unlock()
|
||||
s.logger.Printf("[WARN] [web] Login rate limited for %s (%d attempts)", ip, attempt.count)
|
||||
s.renderLogin(w, r, "Túl sok sikertelen próbálkozás, próbálja újra 1 perc múlva", "")
|
||||
s.renderLogin(w, r, s.msg(r, "login.msg.rate_limited"), "")
|
||||
return
|
||||
}
|
||||
s.loginAttemptMu.Unlock()
|
||||
|
||||
effectiveHash := s.effectivePasswordHash()
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(effectiveHash), []byte(password)); err != nil {
|
||||
s.logger.Printf("[WARN] [web] Failed login from %s", r.RemoteAddr)
|
||||
s.logger.Printf("[WARN] [web] Failed login from %s", ip)
|
||||
s.loginAttemptMu.Lock()
|
||||
if s.loginAttempts[ip] == nil {
|
||||
s.loginAttempts[ip] = &loginAttempt{}
|
||||
@@ -189,7 +189,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
s.loginAttempts[ip].count++
|
||||
s.loginAttempts[ip].lastFail = time.Now()
|
||||
s.loginAttemptMu.Unlock()
|
||||
s.renderLogin(w, r, "Hibás jelszó", "")
|
||||
s.renderLogin(w, r, s.msg(r, "login.msg.wrong_password"), "")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
// browser is closed". The durable opt-out is a separate, explicit choice and is untouched here.
|
||||
http.SetCookie(w, &http.Cookie{Name: recoveryBannerCookie, Value: "", Path: "/", MaxAge: -1})
|
||||
|
||||
s.logger.Printf("[INFO] [web] Login from %s", r.RemoteAddr)
|
||||
s.logger.Printf("[INFO] [web] Login from %s", ip)
|
||||
|
||||
// Redirect to ?next= target if provided, otherwise to dashboard
|
||||
redirectTo := "/"
|
||||
|
||||
Reference in New Issue
Block a user