Muzak logomuzak
v0.1.10

Safe Defaults

Muzak starts from settings that are safe rather than permissive. Each of them can be relaxed; none of them is relaxed by omission.

The list

AreaDefaultRelax it with
Listener timeoutsAll four are non-zero. An http.Server left at its zero values holds a connection open forever, which is all a slow-loris client needsServerOptions, a negative value to disable one
Request bodiesCapped at 1 MiBAppOptions.MaxBodySize, MaxBodySize(n)
Form bodiesCapped at 32 MiB, refused with 413 while being readAppOptions.MaxUploadSize, MaxUploadSize(n)
Request headersThe header block is capped at 1 MiBServerOptions.MaxHeaderBytes
Unknown JSON membersRejected. A client's typo becomes an immediate 422 instead of a silently dropped valueAllowUnknownFields()
Duplicate members, invalid UTF-8Rejected by encoding/json/v2not relaxable
Cross-origin requestsDenied. No CORS header is emitted until a policy is configured, and a wildcard origin with credentials is refused outrightAppOptions.CORS
Cross-origin WebSocket handshakesRefused. A handshake is not subject to the same-origin policy and is never preflighted, so CORS cannot cover itWSOptions.AllowedOrigins
WebSocket messagesCapped at 1 MiB, refused before any of the payload is buffered, read a chunk at a timeWSOptions.ReadLimit
WebSocket message timeOne message has 30 seconds to arrive once it has begun, and a bounded number of frames to arrive in. Idle connections are left aloneWSOptions.ReadTimeout
WebSocket connections1024 per application and 64 per address, then 503 with a Retry-AfterWSOptions.MaxConnections, MaxConnectionsPerIP
WebSocket extensionsNone negotiated, so no peer can ask the server to hold compression state on its behalfnot relaxable
Event stream writesGiven up on after 10 seconds, so a client that opens a stream and never reads it cannot pin a goroutine and a growing socket bufferSSEOptions.WriteTimeout
Event streams1024 per application and 64 per address, with a keepalive comment every 15 secondsSSEOptions.MaxStreams, MaxStreamsPerIP, KeepAlive
Event fieldsA name or identifier carrying a line break is refused, because it would end its own field and let what follows be read as events of its ownnot relaxable
PanicsLogged with a full stack trace, answered with a generic 500. Nothing derived from the panic reaches the clientnot relaxable
ErrorsAn error that does not describe itself becomes an opaque 500 with the real cause logged and never transmittedAppOptions.ErrorRenderer
Request identifiersNot trusted from the client, because an attacker-controlled identifier is an attacker-controlled log fieldAppOptions.TrustRequestIDHeader
Forwarding headersNot believed. X-Forwarded-For is read only for a request that arrived from a proxy named in TrustedProxiesAppOptions.ClientIP
Rate limitingOff until a policy names a quota. Once one exists, counters are bounded in number and in key length, and a storage that stops answering refuses trafficAppOptions.RateLimit, FailOpen
Secrets in logsAttribute keys such as authorization, token and api_key are redactedLoggerOptions.RedactKeys
Token comparisonConstant-time, over hashed inputs, so neither the contents nor the length of a secret leaks through timingmuzak.SecureCompare
Security headersX-Content-Type-Options, X-Frame-Options and a referrer policy on every responseAppOptions.DisableSecurityHeaders
Documentation UISelf-contained. It fetches nothing from a third party, and is served under a content security policy that names its own script and stylesheet by hash and permits no network access beyond this originAppOptions.DisableDocs
ValidationAutomatic for any model that declares rules. There is no option to remember, so a model cannot be left unvalidated by forgetting oneSkipValidation()
VersioningOff until a type is named. Once on, a route that declares no version answers nothing rather than being served unversioned, so a resource is opted into being version-independent deliberatelyAppOptions.Versioning, WithVersion, VersionNeutral

The one deliberate exception

Rate limiting is off until a quota is declared. There is no limit that is right for every application, and a default one would be a number nobody chose refusing traffic nobody expected.

What is safe by default is what happens once a policy exists: the counters are bounded, the address is not taken from a header anyone can write, and a storage that stops answering stops traffic rather than stopping the limit. See Rate Limiting.

The defaults as constants

Every one of them is exported, so a deployment can read them rather than guess.

muzak.DefaultMaxBodySize          // 1 << 20
muzak.DefaultMaxUploadSize        // 32 << 20
muzak.DefaultMaxHeaderBytes       // 1 << 20

muzak.DefaultReadHeaderTimeout    // 5s
muzak.DefaultReadTimeout          // 30s
muzak.DefaultWriteTimeout         // 30s
muzak.DefaultIdleTimeout          // 120s
muzak.DefaultShutdownTimeout      // 15s

muzak.DefaultWSReadLimit          // 1 << 20
muzak.DefaultWSReadTimeout        // 30s
muzak.DefaultWSWriteTimeout       // 10s
muzak.DefaultWSMaxConnections     // 1024
muzak.DefaultWSMaxConnectionsPerIP // 64
muzak.DefaultWSCloseGracePeriod   // 250ms
muzak.DefaultWSPongTimeout        // 10s

muzak.DefaultSSEKeepAlive         // 15s
muzak.DefaultSSEWriteTimeout      // 10s
muzak.DefaultSSEMaxStreams        // 1024
muzak.DefaultSSEMaxStreamsPerIP   // 64

muzak.DefaultRateLimitMaxEntries  // 100000
muzak.DefaultRateLimitSweepInterval // 1m

muzak.DefaultCompressionMinSize   // 1400
muzak.DefaultForwardedHeader      // "X-Forwarded-For"

Things the type system does rather than a default

Some of what would be a runtime setting elsewhere is not a setting here at all.

A response cannot leak a field it does not declare. The handler's return type is the response model. There is no filtering pass to configure and no response_model to forget, because a field that is not on Out has no way to be written.

// Adding a column to this changes nothing a client can see.
type Item struct {
    ID           string
    Name         string
    InternalNote string
}

// Only these three members exist on the wire.
type ItemOut struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

A crafted body cannot reach a located field. When an input mixes a path parameter with body members, the body is decoded into a scratch value and only the body-bound fields are copied out.

Two concurrent requests cannot see each other's dependencies. Resolved values live on the request's Context and are cleared when it returns to the pool. There is a test for exactly that, run under the race detector.

A misconfiguration is a build error, not a surprise. Duplicate routes, unbindable input types, path parameters no field binds, duplicate operation identifiers, malformed prefixes, a wildcard CORS origin combined with credentials, a MaxConnections set on a route, a missing static directory: all reported together when the application is built, before a socket is opened.

A deployment checklist

Things worth deciding explicitly, because the safe default may not be the correct one for your deployment:

  • ClientIP.TrustedProxies if anything sits in front of the service, or every request is attributed to the proxy. See Behind a Proxy.
  • CORS if a browser on another origin calls the API. See CORS.
  • WSOptions.AllowedOrigins if a browser opens WebSocket connections.
  • A rate limit policy, and a shared storage if the service runs more than once.
  • Secure: true on every cookie once the service is served over TLS. See Cookies.
  • DisableDocs if the deployment must not describe itself.
  • A Servers list in the OpenAPI options, so a generated client points at the right host.

Where to go next

Authentication covers proving who a caller is, and Behind a Proxy covers the one default that is wrong in the other direction behind a load balancer.

Open source under MIT / Apache-2.0 · sustained by the people who ship on it.Built on net/http, and nothing else.