Captain Codeman Captain Codeman

Automatic HTTPS With Free SSL Certificates Using Go + Let's Encrypt

Adding SSL doesn't have to be complicated or expensive

Contents

Introduction

If you’re still paying $$$‘s for SSL certificates it may be time to look at Let’s Encrypt which describe themselves as “a free, automated and open certificate authority”.

SSL certificates are now effectively free.

Sounds too good to be true? Unless you need some fancy green-bar EV certificate there’s really no need to be paying for SSL certificates anymore. Especially now there is a Go package to support automatic certificate generation.

It turned out to be easier to setup the auto-certificate system than it was to renew a paid-for SSL certificate, here’s how …

One of the things I wanted to do with BlogServe, the cloud-based blog-engine that runs this blog and others, is to serve pages over HTTPS by default.

As well as the societal reasons for using encryption - to protect people’s privacy in a world where privacy controls are constantly being eroded, there’s also the performance benefits: more and more new browser features only work if they are being used over HTTPS whether that is client-caching with a service-worker, accessing the browser geolocation information or faster delivery through the use of http/2 server-push.

But I really didn’t want to do any manual work installing certificates. Even if you charge for support, it would be tedious and monotonous work guiding people though the issues of buying SSL certificates. Heck, I get nervous everytime I have to renew my own certs because it’s a year since I did it last! So when I saw that support for letsencrypt was being baked into Go I had to try it.

HTTPS Server

For the auto-certificate system to work it needs to hook into the server process so that when a request comes in it can check if the domain is allowed and whether a certificate already exists for it. For any valid domain that needs a new or renewed certificate the server will talk to letsencrypt to obtain one, storing it in a cache for future use.

package main

import (
	"context"
	"log"
	"time"

	"crypto/rand"
	"crypto/tls"
	"net/http"

	"cloud.google.com/go/datastore"
	"golang.org/x/crypto/acme/autocert"
	"golang.org/x/net/http2"
)

func main() {
	ctx := context.Background()

	datastoreClient, err := datastore.NewClient(ctx, cacheconfig.Match.Project)
	if err != nil {
		log.Fatalf("datastore client %v", err)
	}

	// this creates the server, returning a http.Handler impl.
	server := NewWebServer(datastoreClient)
	go http.ListenAndServe(":http", server)

	certcache := NewDatastoreCertCache(datastoreClient)
	hostPolicy := NewHostPolicy(datastoreClient)

	m := autocert.Manager{
		Cache:      certcache,
		Prompt:     autocert.AcceptTOS,
		HostPolicy: hostPolicy,
	}

	tlsConfig := &tls.Config{
		Rand:           rand.Reader,
		Time:           time.Now,
		NextProtos:     []string{http2.NextProtoTLS, "http/1.1"},
		MinVersion:     tls.VersionTLS12,
		GetCertificate: m.GetCertificate,
	}

	ln, err := tls.Listen("tcp", ":https", tlsConfig)
	if err != nil {
		log.Fatalf("ssl listener %v", err)
	}

	log.Fatal(http.Serve(ln, server))
}

Because I also want to limit cert generation to just the domains associated with active blogs, I have a HostPolicy() implementation to do this check (via the datastore) but that isn’t strictly necessary - you can hard-code the list of domains you want to limit things to using the provided HostWhitelist from the package.

Google Cloud DataStore Certificate Cache

Most of the examples (and the default implementation provided) store the certificates on the file-system which didn’t fit with my setup. I’m using Docker Optimized Google Compute Engine instances and each one is stateless (although they self-form into a cluster for caching purposes) so I really wanted to store the certs in some commonly accessible cloud-storage service - Google Cloud Datastore.

It’s was very easy to create a datastore-based implementation to do this and satisfy the simple interface that the Go ACME Autocert package expects:

package main

import (
	"context"

	"cloud.google.com/go/datastore"
	"golang.org/x/crypto/acme/autocert"
)

type (
	LetsencryptCert struct {
		Data []byte `datastore:"data,noindex"`
	}

	datastoreCertCache struct {
		client *datastore.Client
	}
)

func NewDatastoreCertCache(client *datastore.Client) *datastoreCertCache {
	return &datastoreCertCache{
		client: client,
	}
}

func (d *datastoreCertCache) Get(ctx context.Context, key string) ([]byte, error) {
	var cert LetsencryptCert
	k := datastore.NameKey("letsencrypt", key, nil)
	if err := d.client.Get(ctx, k, &cert); err != nil {
		if err == datastore.ErrNoSuchEntity {
			return nil, autocert.ErrCacheMiss
		}
		return nil, err
	}
	return cert.Data, nil
}

// Put stores the data in the cache under the specified key.
// Underlying implementations may use any data storage format,
// as long as the reverse operation, Get, results in the original data.
func (d *datastoreCertCache) Put(ctx context.Context, key string, data []byte) error {
	k := datastore.NameKey("letsencrypt", key, nil)
	cert := LetsencryptCert{
		Data: data,
	}
	if _, err := d.client.Put(ctx, k, &cert); err != nil {
		return err
	}
	return nil
}

// Delete removes a certificate data from the cache under the specified key.
// If there's no such key in the cache, Delete returns nil.
func (d *datastoreCertCache) Delete(ctx context.Context, key string) error {
	k := datastore.NameKey("letsencrypt", key, nil)
	if err := d.client.Delete(ctx, k); err != nil {
		return err
	}
	return nil
}

Final Steps

There are none, seriously, that’s it! Even this code is more than you really need to get running (but I needed to show something interesting, right?).

Well, you can look at adding the github.com/unrolled/secure package for an easy way to automatically redirect HTTP traffic to the secure version instead (that’s why the server also listened on http in the first code block).

But with or without that, just access the site over HTTPS and the system handles the certificate generation and renewal. If you’re reading this blog you should see a nice green secure HTTPS indicator in the address bar which was provided with this code. Here is what it should look like in Chrome:

secure https indicato chrome

All blogs running on the site automatically get HTTPS support, some other examples::

HTTPS is more important than ever and Google are going to start adding warnings when using non-HTTPS sites so it’s more important than ever to switch to using it.