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))
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("", ""))
(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
orHostWhitelist
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))
}()
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
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 ""
}
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
})
⚙️ 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:
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.
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)
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