p2.5: redact reason/err for permit_restricted logs

Guard messages embed the hostname (resolve <host>, no addresses for
<host>). Restricted callers now log a coarse reason class and err=-.
Test dumps slog records and forbids the hostname anywhere in them.

docs: P3 review — fail-closed search, cache class, public dialer
This commit is contained in:
2026-09-23 11:47:37 -07:00
parent 2d376d3ad1
commit 7be4ed1d43
4 changed files with 148 additions and 41 deletions
+53 -3
View File
@@ -501,14 +501,14 @@ func (s *Server) writeFetchErr(w http.ResponseWriter, c *auth.Caller, rawURL str
fe.Msg = "internal error"
}
if fe.Status == 401 || fe.Status == 403 {
s.Log.Warn("refused", "caller", name, "host", host, "reason", fe.Msg)
s.Log.Warn("refused", "caller", name, "host", host, "reason", s.logReason(c, fe))
} else {
s.Log.Warn("fetch failed", "caller", name, "host", host, "reason", fe.Msg, "status", fe.Status)
s.Log.Warn("fetch failed", "caller", name, "host", host, "reason", s.logReason(c, fe), "status", fe.Status)
}
writeErr(w, fe.Status, fe.Msg, fe.Hint)
return
}
s.Log.Error("internal", "caller", name, "host", host, "err", err)
s.Log.Error("internal", "caller", name, "host", host, "err", s.logErr(c, err))
writeErr(w, 500, "internal error", "")
}
@@ -591,6 +591,56 @@ func (s *Server) logHost(c *auth.Caller, raw string) string {
return hostOf(raw)
}
func (s *Server) restrictedLog(c *auth.Caller) bool {
return c != nil && permittedCaller(c.Name, s.PermitRestricted)
}
// logReason is the reason field for fetch error logs. Restricted callers get a
// coarse class so the hostname in guard messages does not land in the journal.
func (s *Server) logReason(c *auth.Caller, fe *fetch.Error) string {
if fe == nil {
return "-"
}
if s.restrictedLog(c) {
return coarseReason(fe)
}
return fe.Msg
}
func (s *Server) logErr(c *auth.Caller, err error) any {
if s.restrictedLog(c) {
return "-"
}
return err
}
func coarseReason(fe *fetch.Error) string {
switch fe.Status {
case 401:
return "unauthorized"
case 403:
return "blocked"
case 400:
return "bad request"
case 413:
return "too large"
case 415:
return "unsupported"
case 429:
return "busy"
case 504:
return "timeout"
default:
if fe.Status >= 400 && fe.Status < 500 {
return "upstream 4xx"
}
if fe.Status >= 500 {
return "upstream 5xx"
}
return "-"
}
}
func writeErr(w http.ResponseWriter, status int, msg, hint string) {
if status == 502 {
status = 500
+31 -7
View File
@@ -284,7 +284,7 @@ func recHost(r slog.Record) string {
var host string
r.Attrs(func(a slog.Attr) bool {
if a.Key == "host" {
host = fmtString(a.Value)
host = a.Value.String()
}
return true
})
@@ -293,11 +293,17 @@ func recHost(r slog.Record) string {
func recMsg(r slog.Record) string { return r.Message }
func fmtString(v slog.Value) string {
if v.Kind() == slog.KindString {
return v.String()
}
return v.Any().(string)
func recDump(r slog.Record) string {
var b strings.Builder
b.WriteString(r.Message)
r.Attrs(func(a slog.Attr) bool {
b.WriteByte(' ')
b.WriteString(a.Key)
b.WriteByte('=')
b.WriteString(a.Value.String())
return true
})
return b.String()
}
func TestRestrictedCallerLogsBlankHost(t *testing.T) {
@@ -345,7 +351,7 @@ func TestRestrictedCallerLogsBlankHost(t *testing.T) {
src, _ := env["source"].(map[string]any)
final, _ := src["final_url"].(string)
if !strings.Contains(final, hostOf(ts.URL)) {
t.Fatalf("final_url still in body, got %q", final)
t.Fatalf("final_url missing from body, got %q", final)
}
rec = postJSON(t, s, "/v1/fetch", strings.TrimSpace(string(gsKey)), map[string]any{"url": "http://10.0.0.5/", "mode": "clean"})
@@ -353,6 +359,12 @@ func TestRestrictedCallerLogsBlankHost(t *testing.T) {
t.Fatalf("gs refused: %d %s", rec.Code, rec.Body.String())
}
const badHost = "this-host-does-not-exist-sift-p25.invalid"
rec = postJSON(t, s, "/v1/fetch", strings.TrimSpace(string(gsKey)), map[string]any{"url": "http://" + badHost + "/", "mode": "clean"})
if rec.Code != 403 {
t.Fatalf("gs nxdomain: %d %s", rec.Code, rec.Body.String())
}
rec = postJSON(t, s, "/v1/fetch", strings.TrimSpace(string(fleetKey)), map[string]any{"url": ts.URL, "mode": "clean"})
if rec.Code != 200 {
t.Fatalf("fleet fetch: %d %s", rec.Code, rec.Body.String())
@@ -386,6 +398,18 @@ func TestRestrictedCallerLogsBlankHost(t *testing.T) {
if fleetFetch != wantHost {
t.Fatalf("fleet fetch host=%q want %q", fleetFetch, wantHost)
}
h.mu.Lock()
for _, r := range h.recs {
if attrCaller(r) != "straylight-gs" {
continue
}
dump := recDump(r)
if strings.Contains(dump, badHost) || strings.Contains(dump, "10.0.0.5") {
t.Fatalf("restricted log leaked host: %s", dump)
}
}
h.mu.Unlock()
}
func attrCaller(r slog.Record) string {