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
| Area | Default | Relax it with |
|---|---|---|
| Listener timeouts | All 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 | ServerOptions, a negative value to disable one |
| Request bodies | Capped at 1 MiB | AppOptions.MaxBodySize, MaxBodySize(n) |
| Form bodies | Capped at 32 MiB, refused with 413 while being read | AppOptions.MaxUploadSize, MaxUploadSize(n) |
| Request headers | The header block is capped at 1 MiB | ServerOptions.MaxHeaderBytes |
| Unknown JSON members | Rejected. A client's typo becomes an immediate 422 instead of a silently dropped value | AllowUnknownFields() |
| Duplicate members, invalid UTF-8 | Rejected by encoding/json/v2 | not relaxable |
| Cross-origin requests | Denied. No CORS header is emitted until a policy is configured, and a wildcard origin with credentials is refused outright | AppOptions.CORS |
| Cross-origin WebSocket handshakes | Refused. A handshake is not subject to the same-origin policy and is never preflighted, so CORS cannot cover it | WSOptions.AllowedOrigins |
| WebSocket messages | Capped at 1 MiB, refused before any of the payload is buffered, read a chunk at a time | WSOptions.ReadLimit |
| WebSocket message time | One message has 30 seconds to arrive once it has begun, and a bounded number of frames to arrive in. Idle connections are left alone | WSOptions.ReadTimeout |
| WebSocket connections | 1024 per application and 64 per address, then 503 with a Retry-After | WSOptions.MaxConnections, MaxConnectionsPerIP |
| WebSocket extensions | None negotiated, so no peer can ask the server to hold compression state on its behalf | not relaxable |
| Event stream writes | Given up on after 10 seconds, so a client that opens a stream and never reads it cannot pin a goroutine and a growing socket buffer | SSEOptions.WriteTimeout |
| Event streams | 1024 per application and 64 per address, with a keepalive comment every 15 seconds | SSEOptions.MaxStreams, MaxStreamsPerIP, KeepAlive |
| Event fields | A 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 own | not relaxable |
| Panics | Logged with a full stack trace, answered with a generic 500. Nothing derived from the panic reaches the client | not relaxable |
| Errors | An error that does not describe itself becomes an opaque 500 with the real cause logged and never transmitted | AppOptions.ErrorRenderer |
| Request identifiers | Not trusted from the client, because an attacker-controlled identifier is an attacker-controlled log field | AppOptions.TrustRequestIDHeader |
| Forwarding headers | Not believed. X-Forwarded-For is read only for a request that arrived from a proxy named in TrustedProxies | AppOptions.ClientIP |
| Rate limiting | Off 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 traffic | AppOptions.RateLimit, FailOpen |
| Secrets in logs | Attribute keys such as authorization, token and api_key are redacted | LoggerOptions.RedactKeys |
| Token comparison | Constant-time, over hashed inputs, so neither the contents nor the length of a secret leaks through timing | muzak.SecureCompare |
| Security headers | X-Content-Type-Options, X-Frame-Options and a referrer policy on every response | AppOptions.DisableSecurityHeaders |
| Documentation UI | Self-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 origin | AppOptions.DisableDocs |
| Validation | Automatic for any model that declares rules. There is no option to remember, so a model cannot be left unvalidated by forgetting one | SkipValidation() |
| Versioning | Off 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 | AppOptions.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.TrustedProxiesif anything sits in front of the service, or every request is attributed to the proxy. See Behind a Proxy.CORSif a browser on another origin calls the API. See CORS.WSOptions.AllowedOriginsif a browser opens WebSocket connections.- A rate limit policy, and a shared storage if the service runs more than once.
Secure: trueon every cookie once the service is served over TLS. See Cookies.DisableDocsif the deployment must not describe itself.- A
Serverslist 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.