You are reading the 0.3.0 documentation. The current release is 0.3.1. Read this page for it →

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 needs. Over HTTP/2, WriteTimeout also closes a connection whose socket accepts no bytes for that longServerOptions, a negative value to disable one
Request bodiesCapped at 1 MiB. A body that declares a Content-Length over the limit is refused before any of it is readAppOptions.MaxBodySize, MaxBodySize(n)
Form bodiesA route that declares a file field is capped at 32 MiB. A route that binds only form values is capped at 1 MiB, urlencoded or multipartAppOptions.MaxUploadSize, MaxUploadSize(n), MaxBodySize(n)
Request headersThe header block is capped at 64 KiBServerOptions.MaxHeaderBytes
JSON bodiesA body with no Content-Type is refused with 415, so a page cannot drive a JSON route cross-site without a CORS preflightnot relaxable
Unknown JSON membersRejected. A client's typo becomes an immediate 422 instead of a silently dropped value. A member naming a path, query, header or cookie field is unknown tooAllowUnknownFields()
Duplicate members, invalid UTF-8Rejected by encoding/json/v2not relaxable
Located fieldsA path, query, header, cookie, form or file tag the binder cannot reach is a build error, so a request cannot set it from the JSON bodynot relaxable
NumbersNaN and infinity are refused in a parameter and by validate.Numbernot relaxable
Number parametersJSON's decimal grammar only: hexadecimal, 1_000, .5 and a leading + are refused, so a proxy reading the number by JSON's rules sees the value the handler doesnot relaxable
URL rulesURL(), HTTPS() and URLWithSchemes() require a hostname, so http://:8080/, which an HTTP client dials as the local machine, is refusednot relaxable
Scalar parameters sent twiceA query, header or form parameter that holds one value is refused with 422 when sent more than once, rather than one of its values being chosendeclare a slice to accept a list
Cross-origin requestsDenied. No CORS header is emitted until a policy is configured, and a wildcard origin with credentials is refused outright. Every response under a named-origin policy carries Vary: OriginAppOptions.CORS
Cross-site request forgeryRefused once AppOptions.CrossOriginProtection is set: a request a browser sends from another origin with any method but GET, HEAD and OPTIONS is answered 403 before any middleware, guard or handler runs, with no token to manage. See Cross-Origin RequestsTrustedOrigins, InsecureBypassPatterns
SessionsOff until configured. Once on, the cookie is HttpOnly, Secure, SameSite=Lax and named __Host-session, its contents are encrypted and authenticated, and CrossOriginProtection is required beside it, so a cookie a browser attaches on its own cannot be ridden from another site. See SessionsSessionOptions.SameSite, AllowInsecure, Name
Cross-origin WebSocket handshakesRefused. A handshake is not subject to the same-origin policy and is never preflighted, so CORS cannot cover it. The origin is read strictly, and a handshake with more than one Origin is refusedWSOptions.AllowedOrigins
WebSocket and DNS rebindingThe server's own origin is any whose host is the request's Host, until a route lists the names it answers to in WSOptions.AllowedHosts. AppOptions.AllowedHosts goes further: a request for any other host, a handshake included, is refused with 421 before it reaches a routeWSOptions.AllowedHosts, AppOptions.AllowedHosts
WebSocket messagesCapped at 1 MiB, refused with 1009 before any of the payload is buffered, read a chunk at a time. The length check cannot overflowWSOptions.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 keepaliveOn: a ping every 30 seconds, closed when nothing at all arrived for 10 seconds after a ping, whenever the handler was reading during itWSOptions.PingInterval, PongTimeout, a negative PingInterval to disable
WebSocket connections1024 per application and 64 per address, then 503 with a Retry-After. A slot is held until the socket is actually closed. 16 addresses (one IPv6 /52) whose peers answer every ping and send nothing hold every slot unless MaxLifetime is setWSOptions.MaxConnections, MaxConnectionsPerIP
WebSocket and event stream ageUnbounded, until a route sets oneWSOptions.MaxLifetime, SSEOptions.MaxLifetime
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 stops reading a stream that keeps writing cannot pin a goroutine and a full socket buffer once the buffers between them fill. A stream that sends only keepalives never fills them, and is never ended by thisSSEOptions.WriteTimeout
Event streams1024 per application and 64 per address, with a keepalive comment every 15 seconds. 16 addresses (one IPv6 /52) that read nothing hold every slot unless MaxLifetime is setSSEOptions.MaxStreams, MaxStreamsPerIP, KeepAlive
Per-address countsAn IPv4 client is counted exactly and an IPv6 client by its /56, for the connection caps and the rate limit alikeClientIPOptions.ConnectionIPv6Prefix, ConnectionIPv4Prefix
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, its value cut to 4 KiB, and answered with a generic 500. Nothing derived from the panic reaches the clientnot relaxable
A failure after the response startedLogged once and the connection aborted, so a truncated export cannot look completenot relaxable
ErrorsAn error that does not describe itself becomes an opaque 500 with the real cause logged and never transmitted. A StatusCoder renders only its own message, below 500AppOptions.ErrorRenderer
Error responsesDrop the headers set for the response they replace, and are sent Cache-Control: no-store, so an error is never served as HTML or cached publiclyAppOptions.ErrorRenderer
Guarded responsesA route or file mount behind a guard, a request-scoped provider or a verifying security scheme, and any response whose request read its session, is sent Cache-Control: private, no-cache, so a CDN cannot hand one user's response to another (an event stream and a WebSocket keep their own). A verifying scheme adds Vary on the header its credential arrives in, and a session Vary: Cookie. A response carrying the session cookie stays private whatever sets Cache-Control after itset Cache-Control in the handler, except on a response that carries the session cookie
Request identifiersNot trusted from the client, because an attacker-controlled identifier is an attacker-controlled log fieldAppOptions.TrustRequestIDHeader
Trace contextOff until a Tracer is named. A span is named after the route template, never the path, which carries identifiers, and traceparent and tracestate are read strictly. A valid traceparent is continued by default, so a service the internet reaches should believe only its proxy's. See ObservabilityTracingOptions.Parent
Forwarding headersNot believed. X-Forwarded-For is read only for a request that arrived from a proxy named in TrustedProxies, and from the rightAppOptions.ClientIP
Host headerNot checked until the application lists the hosts it serves; then a request for any other is answered 421 before anything else runs, and an HTTPS redirect only ever names a listed host. See Allowed Hosts and HTTPSAppOptions.AllowedHosts, AppOptions.RedirectHTTPS
Rate limitingOff until a policy names a quota. Once one exists, counters are bounded in number and in key length, a full table makes room within the quota being counted (only a quota holding no counter takes one from the largest), a storage that stops answering is cut off after 2 seconds, and a storage that fails refuses trafficAppOptions.RateLimit, FailOpen, StorageTimeout
Secrets in logsAny attribute key that contains a term such as authorization, token, api-key, password, session, jwt or dsn is redacted, and so is every value inside a matching group and every matching entry of a logged http.Header, url.Values or string mapLoggerOptions.RedactKeys
Log recordsOne line each. Control characters, C1 controls, bidirectional controls and invalid UTF-8 are escaped, so a request path cannot forge a line or drive a terminalnot relaxable
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 response, and Strict-Transport-Security on a response over TLS (not for localhost or an address). No Content-Security-Policy: a page sets its ownAppOptions.DisableSecurityHeaders
Documentation UINot served at all until AppOptions.DocsUI names one. A page that is configured is embedded in the binary, fetches nothing from a third party, is served under a content security policy that hashes its own inline script and permits no network access beyond this origin, runs the application's guards and providers, and counts against its rate limitAppOptions.DocsUI, AppOptions.DisableDocs
Static filesA dotfile answers 404, a file's type comes from its extension and never its content, HTML and SVG are sent under Content-Security-Policy: sandbox, a symbolic link cannot leave the directory, and a directory with a mount of its own is reachable only through itAllowDotfiles, AllowActiveContent
File mountsRun the rate limit, the guards and every provider of the router they were registered onSkipRateLimit(), a router without them
Files and byte bodiesBytes, Stream and FileResponse are sent with X-Content-Type-Options: nosniff. A FileResponse takes its type from the file's extension, never its content, refuses a dotfile, and sends HTML, SVG and XML sandboxed. See Files, Streams and RedirectsFileResponse.AllowActiveContent
Redirectsmuzak.Redirect accepts only a path on this origin. //, /\, control characters and a path that decodes or resolves to another host are refused, and an absolute URL is sent only to a host the route listsRedirectHosts(...), Redirect.External
Outbound requestsmuzak.NewClient refuses loopback, private, link-local and cloud metadata addresses at the moment it connects, so a name that resolves to one, DNS rebinding and a redirect all meet the same check. Every call is bounded (30 seconds, a 10 MiB body, five redirects, never from https to http), and only a request that is safe to send twice is retried. See HTTP ClientClientOptions.AllowPrivateNetworks, AllowedNetworks, DeniedNetworks
ConfigurationA required setting refuses an empty value, and a value read by a secret field is hidden from every other field that reads the same variabledrop required, or give it a default
ValidationAutomatic for any model that declares rules. There is no option to remember, so a model cannot be left unvalidated by forgetting one. A report holds at most 100 failures and follows nested models 32 levels deep, and Unique() is linear in the size of the collectionSkipValidation()
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 deliberately. A header sent on several lines names no versionAppOptions.Versioning, WithVersion, VersionNeutral
Late configurationUse, Include, route and mount registration after the application was built panic, so a guard added late cannot be dropped silentlynot relaxable

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, an IPv6 client cannot multiply its budget by rotating addresses inside its own range, and a storage that stops answering stops traffic rather than stopping the limit. See Rate Limiting.

One more thing is unbounded until a route says otherwise, and it is worth saying: how long a WebSocket or an event stream may stay open. Keepalive closes a peer that stops answering, and the write timeout bounds one write, but a peer that answers every ping and says nothing, or a client that reads nothing while comments trickle in, costs a connection, a goroutine and a file descriptor until the server stops, and nothing but a ceiling on age can tell it from one that is listening. MaxLifetime puts that decision in your hands, and it is left unset because there is no age that is right for every stream.

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       // 64 << 10

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.DefaultWSPingInterval       // 30s
muzak.DefaultWSPongTimeout        // 10s

muzak.DefaultSSEKeepAlive         // 15s
muzak.DefaultSSEWriteTimeout      // 10s
muzak.DefaultSSEMaxStreams        // 1024
muzak.DefaultSSEMaxStreamsPerIP   // 64
muzak.DefaultSSEHandshakeTimeout  // 30s, for SSEDial

muzak.DefaultConnectionIPv6Prefix // 56
muzak.DefaultConnectionIPv4Prefix // 32

muzak.DefaultRateLimitMaxEntries  // 100000
muzak.DefaultRateLimitSweepInterval // 1m
muzak.DefaultRateLimitStorageTimeout // 2s

muzak.MaxValidationDetails        // 100
muzak.MaxNestedDepth              // 32

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 struct of the body members alone, so a member naming a located field is an unknown member, refused with 422, rather than a value that reaches it or is silently dropped.

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 or declared twice, a static route that could never match, duplicate operation identifiers, malformed prefixes, a wildcard CORS origin combined with credentials, a CORS or WebSocket origin that could never match or is "null", a WebSocket AllowedHosts entry no Host header can equal, a MaxConnections set on a route, a missing static directory, two mounts at one path, a location tag the binder cannot reach, a body type the JSON decoder refuses, a Nested that would validate nothing, a certificate without its key, a security scheme, category or title that cannot be described, alternatives that mix a verifying security scheme with a descriptive one, a Dep field no provider fills, sessions without cross-origin protection or with a secret under 32 bytes, an HTTPS redirect with no host it may name, an AllowedHosts entry that can never match, a Timeout on an event stream or WebSocket route, a health path a route already answers, and an OpenAPI document that cannot be encoded as JSON: 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.
  • CrossOriginProtection if a browser sends the service cookies, which sessions require anyway. See Cross-Origin Requests and Sessions.
  • AppOptions.AllowedHosts, so a request with a forged Host header, a WebSocket handshake included, is refused with 421, and RedirectHTTPS if plain HTTP reaches the service. See Allowed Hosts and HTTPS.
  • WSOptions.AllowedOrigins if a browser opens WebSocket connections, and a host list if the server is on a loopback or private address: AppOptions.AllowedHosts for every request, or WSOptions.AllowedHosts for one route's handshakes alone.
  • MaxLifetime, in minutes rather than hours, on every WebSocket and event stream route a client you do not control can open. Without it, 16 addresses can hold every slot of either.
  • SSEOptions.MaxStreams against MaxStreamsPerIP, since the one divided by the other is how many addresses fill every stream slot, and ClientIPOptions.ConnectionIPv6Prefix: 48 so that an IPv6 client counts by its site's allocation, for streams and WebSocket connections alike. See Server-Sent Events.
  • 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, or a guard declared where the routers are included, rather than on New, if only the API is private.
  • ServerOptions.UnencryptedHTTP2 on a platform that speaks h2c to the container, and a TrustedProxies range for it. See Running on Cloud Run.
  • Cache-Control in a handler that serves public data from behind a guard.
  • Required() on every JSON body member a client must send, since the decoder accepts a body that leaves any member out and the generated document follows the rules you declare.
  • 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.Built on net/http, and nothing else.