20170708

Plugging TLS into your Go Backend - for free!

I don't think I need sell anyone on the concept that securing client-server traffic is a good idea. These days we can even do it for free through Let's Encrypt.

In my example of how to implement it, I'll assume running an Ubuntu server. First order of business is getting Let's Encrypts' Certificate Generator up and running on your system, cutely named Certbot. This will create your TLS certificate, and later update it.

The one downside of using Let's Encrypt is that the certificates only last 3 months before needing to be re-issued. If you're clever, you'll have your backend do it automatically by either generating new ones on each deploy (assuming reasonably high frequency of updates) and/or during daily Cron jobs, check the timestamp of the certificate files, and renew if getting close to the expiration date.

Another gotcha is that Let's Encrypt won't issue certificates to typical AWS domain names like <ec2-12-123-12-123.eu-central-1.compute.amazonaws.com> as these servers can easily be used for various nefarious purposes. So you're going to need a proper address to plug into your certificate. Any DNS service will let you point to your AWS EC2 instance, so it's no biggie. If you already got one, say for a website, just set up a subdomain there that points to your AWS IP.

I'll not go into those topics in any detail here though. So moving on, the very first thing on the agenda, is installing Certbot and generating the certificates.

Once you've SSH'ed into your server, this will get you the bot:

$ sudo apt-get update
$ sudo apt-get install software-properties-common
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install certbot


Next, since we're not running some external webserver or anything like that, but let the goodness of Go do it for us, we'll need a 'standalone webserver' certificate that we can use.

$ sudo certbot certonly --standalone -d katzrule.com -d www.katzrule.com

This will create the two files you need import into your Go app. You'll find them under '/etc/letsencrypt/live/katzrule.com/' and they should be named 'fullchain.pem' and 'privkey.pem'.

How you import them into Go is a matter of preference. I'm a bit lazy, so I didn't want to comment in and out the loading of the certs for when developing (just testing using localhost) and when deploying (running live on AWS). So I set up a little type to hold the location of the certs, made a function to test for if I am on live server or not, import the certificate location into the previous made type if on the server, and then finally in the API function, serve up over https or http depending on if certificates are found or not.

So the implementation itself.

In package structs:

type TLS_t struct {
    Fullchain string
    PrivKey   string
}


In package fileIO:

func GetTLSExists() (structs.TLS_t, bool) {

    var fullchain = "/etc/letsencrypt/live/katzrule.com/fullchain.pem"
    var privKey = "/etc/letsencrypt/live/katzrule.com/privkey.pem"

    var TLS structs.TLS_t
    var exists bool

    if _, err := os.Stat("./conf/app.ini"); err != nil {
        exists = false
    } else {
        exists = true
        TLS.Fullchain = fullchain
        TLS.PrivKey = privKey
    }
    return TLS, exists
}


The file I check for - './conf/app.ini' only exist on my server. You can check for any file you wish that you know only exist on the server - or have an initialization file you load on startup, with one field (assuming JSON or XML encoding) saying if on live or dev. Anyway, this little hack works for me.

Finally in my API package:

func Initialize() {

    router := mux.NewRouter()

    ip, err := fileIO.GetPrivateIP()
    if err != nil {
        fmt.Println("Error loading IP config - exiting")
        postgres.Close()
        os.Exit(0)
    }

    server := &http.Server{
        Handler:      router,
        Addr:         ip,
        WriteTimeout: 10 * time.Second,
        ReadTimeout:  10 * time.Second,
    }

    router.HandleFunc("/noDogParkPetition", noDogPark)

    TLS, live := fileIO.GetTSLExists()
    if live {
        fmt.Println("TLS Certs loaded - running over https")
        log.Fatal(server.ListenAndServeTLS(TLS.Fullchain, TLS.PrivKey))
    } else {
        fmt.Println("No TLS Certs - running over http")
        log.Fatal(server.ListenAndServe())
    }
}


And that is about it. Those with keen eyes may notice my call to fileIO.GetPrivateIP() - this is merely a call to load the IP address. For the same reason as mentioned earlier. By loading the IP from file rather than hardcode it, I can move the app freely between dev and live by just uploading the updated binary. On dev, the file just tells the app to serve on localhost, on live it loads up my AWS outward pointing IP and port.

Makes deployment super easy. SSH in, close down the app (and do the usual linux/Ubuntu update/upgrade since this is a very good time to do so), use Filezilla to FTP over SSH the new binary, then start it up all good to Go - if you pardon the pun.

Happy - and secure - coding! :)