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:
+53
-3
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user