Muzak logomuzak
v0.2.00
You are reading the 0.1.1 documentation. The current release is 0.2.0. Read this page for it →

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 Secure anyway. 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.

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