DEV Community

Cover image for 🎩💫 Magic HTTP certs in Go
Sam Thorogood
Sam Thorogood

Posted on • Edited on

🎩💫 Magic HTTP certs in Go

Update: I've shipped https-forward (you can install it on most Linuxes with snap) which transparently provides HTTPS certificates for internal 'dumb' services.

Like many folks, I'm incredibly pleased with the adoption of HTTPS/SSL everywhere on the web. But it's not an accident—free tools like Let's Encrypt have driven forward the adoption of certificates, and PAAS (Platform-As-A-Service) like App Engine now just give out certificates automagically.

Let's say you're writing your own server though, in Go. There's a package and idiom which will give you that same experience in your own code.

If you're running a webserver behind a frontend which handles HTTPS for you—like App Engine Flex does, as it just asks you to listen on :8080—this blog post isn't for you, your provider is handling your cert.
Stop reading now!


The package you need is golang.org/x/crypto/acme/autocert, and it's so amazingly simple to use. Let's see how:

    // add your listeners via http.Handle("/path", handlerObject)
    listener := autocert.NewListener("yourdomain.com")
    log.Fatal(http.Serve(listener, nil))
Enter fullscreen mode Exit fullscreen mode

The Longer Version

But there's a few reasons you might want to specify the configuration yourself. The slightly longer setup looks something like:

    certManager := autocert.Manager{
        Prompt:     autocert.AcceptTOS,
        HostPolicy: autocert.HostWhitelist("yourdomainname.com"),
        Cache:      autocert.DirCache("cache-path"),
    }
    server := &http.Server{
        Addr: ":https",
        TLSConfig: &tls.Config{
            GetCertificate: certManager.GetCertificate,
                        NextProtos:     []string{acme.ALPNProto},
        },
    }
    // add your listeners via http.Handle("/path", handlerObject)
    log.Fatal(server.ListenAndServeTLS("", ""))
Enter fullscreen mode Exit fullscreen mode

(For complete source you can download and run, see here ⤵️💻)

Regardless of the approach, your server will run on port 443 (this has to happen: the process calls you back on this port), and automagically talk to Let's Encrypt to provide certificates.

If you're having trouble:

  • make sure the domain is correctly configured to point to your server, and remember you can't just run wget localhost—you need to specify the full domain
  • for additional domains (e.g., a "www." prefix), just add them to NewListener or HostWhitelist

For bonus points, you should also listen on plain old HTTP. The autocert package provides a built-in helper which redirects users to HTTPS:

    go func() {
        h := certManager.HTTPHandler(nil)
        log.Fatal(http.ListenAndServe(":http", h))
    }()
Enter fullscreen mode Exit fullscreen mode

These two handlers are how I serve my test domain, affoga.to. ☕🍨


⚠️ Caveats

If you're directly hosting your own software on your own machines (virtualized or not), it's worth listing some caveats and thoughts about web servers generally.

Building with an old Go version

Ubuntu 16.04 ships with Go version 1.6. As of April 2018, autocert needs a later version (you'll get errors about missing context).

The instructions to install a later Go are here.

Listening on system ports

On *nix, if you want to listen on ports 80 and 443, your Go binary naïvely needs to run as a privileged user (e.g. root). This is typically a Bad Idea™.

You can use setcap to privilege your binary. Every time you build server, you'll need to grant the CAP_NET_BIND_SERVICE capability, which allows the binary to listen on system ports (0-1024):

sudo setcap CAP_NET_BIND_SERVICE+ep server
Enter fullscreen mode Exit fullscreen mode

Any user who runs this binary will now be permitted to listen on the correct ports, and e.g. you can run your binary as nobody.

Cache needs to be writable

The cache folder used by autocert.Manager can't be shared between users (which is a challenge for testing), and its internal error messages about this aren't great.

My preference is to just use a consistent cache path per-user. So generate a path based on the current username, rather than hard-coding it:

import (
    "path/filepath"
    "os"
    "os/user"
)

func cacheDir() (dir string) {
    if u, _ := user.Current(); u != nil {
        dir = filepath.Join(os.TempDir(), "cache-golang-autocert-" + u.Username)
        if err := os.MkdirAll(dir, 0700); err == nil {
            return dir
        }
    }
    return ""
}
Enter fullscreen mode Exit fullscreen mode

You don't need to provide a cache, but removing it will slow down startup and your server won't be resilient to Let's Encrypt being down.

Sending a HSTS header

While the example at the top of this post includes a pure HTTP handler to redirect users to your HTTPS listener, ideally, you'd like to instruct a user's browser to do this for you and avoid the delay (and/or security implications).

By returning a HSTS header on every request, you instruct the client's browser to only talk to you over HTTPS. To ensure this for the next six months, add:

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Strict-Transport-Security", "max-age=15768000 ; includeSubDomains")
        // ... rest of handler here
    })
Enter fullscreen mode Exit fullscreen mode

⚙️ Using SystemD to run at startup

If you're not using a helper service like Snap, then you'll need it to start up at boot. You can use SystemD for this.

Let's create a service file which you can add to /etc/systemd/system. Here's my httpd.service:

[Unit]
Description=Go webserver
After=network.target

[Service]
ExecStart=/home/sam/http/server  # path to binary
WorkingDirectory=/home/sam/http  # folder for binary
User=nobody
Group=nogroup
ProtectSystem=yes
AmbientCapabilities=CAP_NET_BIND_SERVICE  # lets `nobody` user bind ports 80, 443

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode

Once you've installed the service file, you can run:

sudo systemctl start httpd

# and see its output:
sudo journalctl -f -u httpd

# and enable on boot:
sudo systemctl enable httpd
Enter fullscreen mode Exit fullscreen mode

And that's it.


I hope this has been useful, at least as a reference guide for folks learning how to get started with certs! If this post has been useful, click one of those heart 👉❤️ buttons below, or let me know on Twitter.

Top comments (1)

Collapse
 
dabnisweb profile image
Jonathan Parker • Edited

Great Post, but I'm using Centos 7 & I'm unsure if such as setcap & AmbientCapabilities are available in Centos 7.

If anybody could show me a centos 7 alternative to the service file I'd very much appreciate it.

Thanks.
Jonathan