Skip to documentation
Documentation navigation

Documentation navigation

Documentation / guides

Public bind protection

Prevent unauthenticated public network exposure.

developer

serve.Server refuses a public address when it cannot prove that authentication is installed. The check happens during server construction, before the caller starts listening.

Loopback classification

Server parses addr with net.SplitHostPort and classifies the host as loopback only in these cases:

HostClassification
localhostLoopback.
127.0.0.0/8Loopback.
::1Loopback.
Empty host, such as :8080Public wildcard.
Any other IP or hostnamePublic for this guard.

An address without a parseable host and port returns serve.InvalidAddrError{Addr, Cause}. The returned error wraps the net.SplitHostPort cause for trusted errors.Is and errors.As inspection.

srv, err := serve.Server("127.0.0.1:8080", handler)
if err != nil {
	var invalid serve.InvalidAddrError
	if errors.As(err, &invalid) {
		log.Printf("bad listen address %q: %v", invalid.Addr, invalid)
	}
	return err
}

Server only constructs and returns *http.Server; it does not call Listen, ListenAndServe, or Serve.

Public refusal

For a non-loopback host, the handler must carry the auth proof installed by serve.Handler(..., serve.WithAuth(authn)). Without that proof and without an explicit opt-in, construction returns serve.PublicBindWithoutAuthError and the server value is nil.

%%{init: {"theme":"dark"}}%%
flowchart TD
    A[Server addr, handler, options] --> B{Split host and port}
    B -- invalid --> C[InvalidAddrError]
    B -- valid --> D{Loopback host?}
    D -- yes --> E[Construct hardened http.Server]
    D -- no --> F{Handler proves auth?}
    F -- yes --> E
    F -- no --> G{WithInsecurePublicBind?}
    G -- no --> H[PublicBindWithoutAuthError]
    G -- yes --> E

The proof is structural. A plain http.Handler, or a wrapper that does not also implement the internal auth-aware marker, is treated as unauthenticated. This can produce a safe false-negative refusal; it cannot produce a false positive public bind.

Explicit proxy opt-in

serve.WithInsecurePublicBind() is the only option that relaxes the public bind guard. Use it only when an upstream authenticating proxy, service mesh, or equivalent network boundary is responsible for authentication. The option does not install authentication and does not alter request middleware.

handler, err := serve.Handler(rig, serve.WithAuth(authenticate))
if err != nil {
	return err
}

// A proxy terminates authentication before forwarding to this process.
srv, err := serve.Server("0.0.0.0:8080", handler,
	serve.WithInsecurePublicBind())
if err != nil {
	return err
}

The name is intentionally explicit because the server itself will not verify the proxy assertion. If the proxy is removed, the option leaves this process public without an in-process auth check.

Hardened server defaults

Every successfully constructed server receives these values:

FieldValue
ReadTimeout5s
ReadHeaderTimeout5s
IdleTimeout60s
MaxHeaderBytes1 << 20
WriteTimeout0, so a long-lived SSE stream is not truncated by a global write deadline
TLSConfig.MinVersiontls.VersionTLS12

Request bodies have a separate 1 MiB default cap in the Handler middleware.

Source and runnable proof

The bind policy and server defaults are implemented in server.go, and the typed failures are defined in errors.go. Loopback, malformed-address, auth-aware, opt-in, and default assertions are covered by server_test.go. Run:

go test ./pkg/serve

← back to documentation