TLS
App.Run serves TLS whenever ServerOptions supplies a certificate pair or a
*tls.Config. ServerOptions is embedded in AppOptions, so both are set inline.
From a certificate on disk
app := muzak.New(muzak.AppOptions{
Title: "Awesome API",
Version: "1.0.0",
Addr: ":8443",
CertFile: settings.TLSCertFile,
KeyFile: settings.TLSKeyFile,
})
log.Fatal(app.RunSignals())
Leaving both empty serves plain HTTP, which is what a development run and a deployment behind a TLS-terminating proxy both want.
From a tls.Config
A *tls.Config covers everything a certificate pair cannot: several certificates, client
certificates, a pinned minimum version, or a certificate that comes from somewhere other
than a file.
app := muzak.New(muzak.AppOptions{
Title: "Awesome API",
Addr: ":8443",
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS13,
Certificates: []tls.Certificate{cert},
},
})
Mutual TLS
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(caPEM)
app := muzak.New(muzak.AppOptions{
Title: "Internal API",
Addr: ":8443",
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS13,
Certificates: []tls.Certificate{serverCert},
ClientCAs: pool,
ClientAuth: tls.RequireAndVerifyClientCert,
},
})
The verified client certificate is on the request, so a provider can turn it into an identity like any other credential:
func GetClientIdentity(ctx *muzak.Context) (ClientIdentity, error) {
state := ctx.Request().TLS
if state == nil || len(state.PeerCertificates) == 0 {
return ClientIdentity{}, muzak.Unauthorized("a client certificate is required")
}
return ClientIdentity{CommonName: state.PeerCertificates[0].Subject.CommonName}, nil
}
A certificate that renews itself
GetCertificate is consulted per handshake, which is what an ACME client or a certificate
that rotates on disk needs.
app := muzak.New(muzak.AppOptions{
Title: "Awesome API",
Addr: ":8443",
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS13,
GetCertificate: certificates.Current,
},
})
Whatever keeps certificates up to date is a resource with a start and a stop, so give it
a muzak.Lifecycle and publish it. See
Lifecycle.
What changes once you serve HTTPS
Cookies. Set Secure: true on every cookie, so a browser refuses to send it over plain
HTTP.
ctx.SetCookie(&http.Cookie{
Name: "session_id",
Value: session,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: true,
MaxAge: 3600,
})
Origins. The https and http spellings of a host are different origins. Update
CORSOptions.AllowedOrigins and WSOptions.AllowedOrigins accordingly.
WebSocket URLs. A page served over https must dial wss, not ws.
The Servers list. Point the generated document at the address clients actually use:
Servers: []muzak.Server{
{URL: "https://api.example.com", Description: "production"},
},
Terminating TLS somewhere else
A load balancer, an ingress controller or a reverse proxy commonly holds the certificate and speaks plain HTTP to the service behind it. Muzak then serves HTTP, and two things need saying explicitly:
- Name the proxy in
ClientIPOptions.TrustedProxies, or every request is attributed to the proxy's address rather than the client's. See Behind a Proxy. - Keep cookies
Secureanyway. The browser leg of the connection is the one that matters, and it is HTTPS.
Strict transport security
Muzak's SecurityHeaders middleware sets X-Content-Type-Options, X-Frame-Options and a
referrer policy. It does not set Strict-Transport-Security, because that header commits
every future visitor's browser to HTTPS for the duration it names, and a service that is not
yet reachable over TLS on every hostname it answers to would lock itself out.
Add it deliberately once you are sure:
func HSTS(maxAge time.Duration) muzak.Middleware {
value := "max-age=" + strconv.Itoa(int(maxAge.Seconds())) + "; includeSubDomains"
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil {
w.Header().Set("Strict-Transport-Security", value)
}
next.ServeHTTP(w, r)
})
}
}
app.Use(HSTS(365 * 24 * time.Hour))
The r.TLS != nil check keeps the header off a plain-HTTP response, where it means nothing
and where a redirect is the more useful answer. Behind a terminating proxy the check has to
read whatever header the proxy sets instead.
Development certificates
go run filippo.io/mkcert@latest -install
go run filippo.io/mkcert@latest localhost 127.0.0.1
ADDR=:8443
TLS_CERT_FILE=./localhost+1.pem
TLS_KEY_FILE=./localhost+1-key.pem
Tests need none of this. The test client serves the application in-process over an in-memory network, so there is no socket and nothing to encrypt. See Testing.
Where to go next
Behind a Proxy covers running with TLS terminated in front of you, and Server Configuration covers the rest of the listener.