You are reading the 0.3.0 documentation. The current release is 0.3.1. 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.

Set both or neither. A certificate without a key, or a key without a certificate, is a build error. With only one of CertFile and KeyFile set, the server used to come up in plaintext on the port its operator believed was HTTPS, with nothing said:

muzak: ServerOptions.CertFile is set without ServerOptions.KeyFile; set both to serve HTTPS, or neither

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:     "theme",
    Value:    theme,
    Path:     "/",
    HttpOnly: true,
    SameSite: http.SameSiteLaxMode,
    Secure:   true,
    MaxAge:   3600,
})

A session needs no cookie of your own: AppOptions.Sessions writes one that is Secure, HttpOnly and SameSite=Lax by default, under a __Host- name a browser accepts only from a secure origin. See Sessions.

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 three 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.
  • Redirect plain HTTP with AppOptions.RedirectHTTPS, if the proxy forwards it rather than redirecting itself. It believes the scheme a named proxy reports in X-Forwarded-Proto, answers 301 for GET and HEAD and 308 otherwise, and only ever names a host that AppOptions.AllowedHosts admitted or RedirectHTTPSOptions.Host gives. See Allowed Hosts and HTTPS.

HTTP/2 without TLS

Over TLS, HTTP/2 is negotiated as usual. A platform that speaks HTTP/2 to the container in the clear, such as Cloud Run with an h2c container port, needs ServerOptions.UnencryptedHTTP2, which accepts HTTP/2 without TLS, sent with prior knowledge, on the same port as HTTP/1:

app := muzak.New(muzak.AppOptions{
    Title:         "Awesome API",
    ServerOptions: muzak.ServerOptions{UnencryptedHTTP2: true},
})

Leave it off anywhere the proxy speaks HTTP/1, and never for a server reachable directly from an untrusted network. A WebSocket handshake is not carried over HTTP/2 and is answered 426. What was measured on Cloud Run, and why a request is cancelled the moment its client leaves over h2c but not over HTTP/1, is on Running on Cloud Run.

Strict transport security

Muzak's SecurityHeaders middleware sets X-Content-Type-Options, X-Frame-Options and a referrer policy on every response. A response to a request that arrived over TLS also gets Strict-Transport-Security: max-age=31536000, which tells the browser to use HTTPS for this host for a year, so that a visitor's next request cannot be downgraded to plain HTTP on the way.

It commits only the host that has just answered over TLS. It leaves out includeSubDomains and preload, which commit hosts this application may not serve and are hard to take back, and it is not sent for localhost, a .localhost name or an IP address, so a development server with a local certificate does not pin every other server on the developer's machine to HTTPS. A browser ignores it over plain HTTP, so it is not sent there, and behind a proxy that terminates TLS the request Muzak sees is plain, so the proxy is the place to set it. A value the handler sets replaces it.

To add includeSubDomains once every subdomain is on HTTPS, set the header yourself. Set replaces the default:

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, which AppOptions.RedirectHTTPS sends, is the more useful answer. Behind a terminating proxy the check has to read whatever header the proxy sets instead.

A host that also serves plain HTTP on another port, or may stop serving HTTPS, takes the header off instead, because a browser pins the whole host along with it. Remove it in middleware installed with App.Use, which runs after SecurityHeaders has set it, or send max-age=0:

app.Use(func(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // This host also serves plain HTTP on another port, which a pinned
        // browser could no longer reach.
        w.Header().Del("Strict-Transport-Security")
        next.ServeHTTP(w, r)
    })
})

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 on a loopback port in plain HTTP, whatever TLS is configured, so there is 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.Built on net/http, and nothing else.