@@ -49,24 +49,31 @@ type distHTTPServer struct {
4949 serveErr atomic.Pointer [error ]
5050}
5151
52- // DistHTTPAuth configures bearer-token authentication for the dist HTTP
53- // server (inbound) and the auto-created HTTP client (outbound). Zero-value
54- // disables auth — current behavior. When configured, *all* dist endpoints
55- // (including /health) require a valid token; operators who want a public
56- // health endpoint can supply a custom ServerVerify that exempts that path.
52+ // DistHTTPAuth configures authentication for the dist HTTP server
53+ // (inbound) and the auto-created HTTP client (outbound). The two sides
54+ // are independent: ServerVerify+Token govern inbound validation, while
55+ // ClientSign+Token govern outbound signing. Zero-value disables both.
5756//
58- // Most clusters need only Token: every node sets the same string, the
59- // server validates incoming Authorization: Bearer <token> headers via
60- // constant-time compare, and the client sends the same header on every
61- // outgoing request.
57+ // Symmetric clusters use Token alone: every node sets the same string,
58+ // the server validates incoming `Authorization: Bearer <token>` via
59+ // constant-time compare, and the client sends the same header.
6260//
63- // ServerVerify and ClientSign are escape hatches for JWT, mTLS-derived
64- // identity, HMAC signing, etc. When set they fully replace the default
65- // token check / header injection.
61+ // ServerVerify (inbound) and ClientSign (outbound) are escape hatches
62+ // for JWT, mTLS-derived identity, HMAC signing, etc. When set, each
63+ // fully replaces the corresponding Token-based default on its side.
64+ //
65+ // Asymmetric configs are valid but require explicit intent. In
66+ // particular, setting ClientSign without any inbound verifier (Token
67+ // or ServerVerify) is dangerous — the node would sign outbound traffic
68+ // while accepting unauthenticated inbound. NewDistMemory rejects that
69+ // shape with sentinel.ErrInsecureAuthConfig. Operators who genuinely
70+ // want signed-out / open-in (e.g. inbound is gated by an L4 firewall
71+ // or service mesh) must opt in via AllowAnonymousInbound.
6672type DistHTTPAuth struct {
67- // Token is the shared bearer string. When set (and ServerVerify is
68- // nil), the server requires `Authorization: Bearer <token>` on every
69- // request. The auto-created client sends the same header.
73+ // Token is the shared bearer string. When set, the server requires
74+ // `Authorization: Bearer <token>` on every request (unless
75+ // ServerVerify overrides) and the auto-created client sends the
76+ // same header (unless ClientSign overrides).
7077 Token string
7178 // ServerVerify (optional) inspects each incoming request and returns
7279 // non-nil to reject with HTTP 401. Use for JWT, OAuth introspection,
@@ -76,17 +83,46 @@ type DistHTTPAuth struct {
7683 // Use for HMAC signing, mTLS-derived headers, etc. When set it
7784 // replaces the default `Authorization: Bearer <token>` header.
7885 ClientSign func (* http.Request ) error
86+ // AllowAnonymousInbound permits this node to accept inbound requests
87+ // without authentication when no inbound verifier is configured
88+ // (neither Token nor ServerVerify) but ClientSign is. Without this
89+ // flag, that combination is rejected at construction time to prevent
90+ // silent inbound bypass when an operator wires only one side of an
91+ // HMAC scheme. Setting this flag is an explicit acknowledgment that
92+ // inbound traffic is protected at a layer below this server (L4
93+ // firewall, service mesh mTLS, etc.).
94+ AllowAnonymousInbound bool
95+ }
96+
97+ // inboundConfigured reports whether server-side validation is active —
98+ // drives whether incoming requests are auth-checked. ClientSign alone
99+ // does NOT count: it is an outbound concern. Outbound signing has no
100+ // equivalent predicate because sign() is already path-specific (it
101+ // short-circuits when both Token and ClientSign are zero).
102+ func (a DistHTTPAuth ) inboundConfigured () bool {
103+ return a .Token != "" || a .ServerVerify != nil
79104}
80105
81- // configured reports whether the auth policy is active.
82- func (a DistHTTPAuth ) configured () bool {
83- return a .Token != "" || a .ServerVerify != nil || a .ClientSign != nil
106+ // validate enforces the inbound/outbound coherence rules at construction
107+ // time. Returns sentinel.ErrInsecureAuthConfig when ClientSign is set
108+ // without any inbound verifier and the operator has not explicitly
109+ // opted into anonymous inbound — the configuration shape that previously
110+ // caused a silent inbound auth bypass.
111+ func (a DistHTTPAuth ) validate () error {
112+ signOnly := a .ClientSign != nil && ! a .inboundConfigured ()
113+ if signOnly && ! a .AllowAnonymousInbound {
114+ return sentinel .ErrInsecureAuthConfig
115+ }
116+
117+ return nil
84118}
85119
86- // verify validates the incoming request against the configured policy.
87- // Returns nil when the request is authorized, non-nil otherwise. The
88- // default (Token-only) check uses constant-time compare to defeat timing
89- // side-channels.
120+ // verify validates the incoming request against the configured inbound
121+ // policy. Returns nil when the request is authorized, non-nil otherwise.
122+ // The default (Token-only) check uses constant-time compare to defeat
123+ // timing side-channels. Callers must gate this behind inboundConfigured()
124+ // — verify itself returns nil when no inbound check is configured, which
125+ // is the intended behavior only when inbound is deliberately open.
90126func (a DistHTTPAuth ) verify (fctx fiber.Ctx ) error {
91127 if a .ServerVerify != nil {
92128 return a .ServerVerify (fctx )
@@ -255,10 +291,15 @@ func (s *distHTTPServer) LastServeError() error {
255291}
256292
257293// wrapAuth returns an auth-checking wrapper around the supplied handler
258- // when the server's auth policy is configured; otherwise returns the
259- // handler untouched (zero overhead for unauthenticated deployments).
294+ // when the server's *inbound* auth policy is configured; otherwise
295+ // returns the handler untouched (zero overhead for unauthenticated
296+ // deployments). Outbound-only configs (ClientSign without Token or
297+ // ServerVerify) intentionally fall through to the bare handler — that
298+ // shape is rejected at NewDistMemory unless AllowAnonymousInbound is
299+ // set, which is the operator's explicit acknowledgment that inbound is
300+ // protected by a layer below this server.
260301func (s * distHTTPServer ) wrapAuth (handler fiber.Handler ) fiber.Handler {
261- if ! s .auth .configured () {
302+ if ! s .auth .inboundConfigured () {
262303 return handler
263304 }
264305
0 commit comments