🎩💫 Magic HTTP certs in Go
Tags: letsencrypt, go, ubuntu
Published: 2018-04-11 05:42:03

---
title: 🎩💫 Magic HTTP certs in Go
published: true
description: Learn how to use the `autocert` package in a Go web server, with a bonus on running the code on Ubuntu & SystemD.
tags: letsencrypt, go, ubuntu
cover_image: https://thepracticaldev.s3.amazonaws.com/i/j8mnpscskwmubi2gp17k.jpg
---

*Update: I've shipped [https-forward](https://github.com/samthor/https-forward) (you can install it on most Linuxes [with snap](https://snapcraft.io/https-forward)) 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](https://cloudplatform.googleblog.com/2017/09/introducing-managed-SSL-for-Google-App-Engine.html).

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](https://cloud.google.com/appengine/docs/flexible/go/quickstart) 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!

<hr />

The package you need is [`golang.org/x/crypto/acme/autocert`](https://godoc.org/golang.org/x/crypto/acme/autocert), and it's so amazingly simple to use. Let's see how:

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

## The Longer Version

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

```go
	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("", ""))
```

<small>(For complete source you can download and run, [see here ⤵️💻](https://gist.github.com/samthor/5ff8cfac1f80b03dfe5a9be62b29d7f2))</small>

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
	go func() {
		h := certManager.HTTPHandler(nil)
		log.Fatal(http.ListenAndServe(":http", h))
	}()
```

These two handlers are how I serve my test domain, [affoga.to](https://affoga.to). ☕🍨

<hr />

# ⚠️ 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](https://github.com/golang/go/wiki/Ubuntu).

## 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):

```bash
sudo setcap CAP_NET_BIND_SERVICE+ep server
```

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:

```go
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 ""
}
```

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:

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

<hr />

# ⚙️ 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
```

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

```bash
sudo systemctl start httpd

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

# and enable on boot:
sudo systemctl enable httpd
```

And that's it.

<hr />

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](https://twitter.com/samthor).