Home | Send Feedback

TLS with Go in production

Published: 1. August 2023  •  go

In a previous blog post I showed you how to install TLS with a Go server in your local development. In this blog post, I will show you how to enable TLS connection in a productive environment.

Reverse Proxy

The most convenient method is when your Go server application sits behind an HTTP reverse proxy. This could be a load balancer or an HTTP server that terminates the TLS connection and sends requests via HTTP to the Go application.

In this scenario, the Go application does not have to care about TLS. All it sees is the HTTP traffic from the proxy.

This section will show you how to set up this scenario with Caddy.

Caddy is a web server written in Go that is designed to be easy to use and configure. Caddy contains a very convenient feature when it comes to TLS. It automatically creates Let's Encrypt certificates the first time it starts up, and it also automatically renews the certificates after 90 days.

For the following example, I use this trivial HTTP server with just one endpoint that returns "Hello World".

mkdir helloworld
cd helloworld
go mod init helloworld
touch main.go

Paste this program into main.go

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "Hello, World!")
    })
    
    err := http.ListenAndServe("127.0.0.1:8080", mux)
    if err != nil {
        log.Fatalf("Error starting the HTTP server : %v", err)
    }
}

Compile the application.

GOOS=linux GOARCH=amd64 go build -o helloworld -ldflags="-s" .

Change the architecture if you run the binary on a different target architecture.

Copy the binary onto a server. The following steps expect the binary in the /usr/local/bin/ folder.

Caddy installation

I run the following installation on a Debian 11. Run all the commands as the root user. If you are not logged in as root, prepend all commands with sudo or switch to the root environment with sudo -i

First, make sure that all installed packages are up-to-date.

apt update
apt full-upgrade

To install Caddy, follow the instructions on this website.


Systemd configuration

Next, we install the Go program as a systemd service. First, create a new service file in /etc/systemd/system

nano /etc/systemd/system/helloworld.service

Add the following content to the service file:

[Unit]
Description=HelloWorldService

[Service]
Type=simple
ExecStart=/usr/local/bin/helloworld
Restart=always
User=helloworld
Group=helloworld

[Install]
WantedBy=multi-user.target

It is recommended to run the services as a non-root user. Create a new user

adduser --system --no-create-home --group helloworld

Change the ownership of the program files to the new user account

chown -R helloworld:helloworld /usr/local/bin/helloworld

Now the program should be running as the new helloworld user account. Start the service to ensure that everything is working as expected:

systemctl start helloworld

And finally, enable the service so that during server startup, our tiny service get's automatically started.

systemctl enable helloworld

To check if the systemctl service is running or not, you can use the following command:

systemctl status helloworld

You should see an active (running) message in the output if the service is running. Otherwise, you will see an inactive (dead) or failed message in the output.

Caddy configuration

Before continuing with the Caddy configuration, make sure that you inserted an A or AAAA record in your DNS that points to the IP address of this server. When working with TLS, inserting a CAA record is also recommended. For this demo, I use the domain go.rasc.ch.

Open the file /etc/caddy/Caddyfile and replace the example configuration with the following configuration.

go.rasc.ch {
    reverse_proxy 127.0.0.1:8080
}

With this configuration, Caddy forwards all incoming traffic to the application listen on port 8080, our helloworld server.

For more information about the reverse proxy configuration in Caddy, check out the official documentation.

Finally, restart Caddy.

systemctl restart caddy

Sending a test request with curl should return the Hello, World! response:

curl https://go.rasc.ch

Direct

In this section, we will look at how to terminate the TLS connection in the Go application. For example, if you want to run only one HTTP service on one server and adding a proxy just for this one service might be too much overhead.

In the previous section, we have seen that Caddy has an automatic TLS certificate management built-in. The good news is that Caddy is written in Go and the developers of Caddy released the TLS certificate management part as a separate library: certmagic

Install the library with go get

cd helloworld
go get github.com/caddyserver/certmagic

In main.go remove the following line

err := http.ListenAndServe("127.0.0.1:8080", mux)

and instead, add the following code

// read and agree to your CA's legal documents
certmagic.DefaultACME.Agreed = true

// provide an email address
certmagic.DefaultACME.Email = "you@yours.com"

err := certmagic.HTTPS([]string{"go.rasc.ch"}, mux)
if err != nil {
    log.Fatal("certmagic.HTTPS failed", err)
}

The ACME email address is important since it's used to notify the domain owner in case there are any issues with the issued certificates, such as expiration or revocation.

certmagic.HTTPS starts HTTP and HTTPS listeners and redirects HTTP to HTTPS. Whenever a request comes in for the configured domain, certmagic will automatically obtain a TLS certificate from Let's Encrypt or from its cache, and start serving traffic securely.

If you run a program with certmagic, the user must have a home directory because certmagic stores certificates and keys there.

adduser --system --group helloworlddirect

Compile the changed application and copy it onto the server. Don't forget to change the user and group of the binary with chown and in the systemd service file.


Because this Go program listens on the privileged ports 80 and 443 We either have to run the service with the root user or add the CAP_NET_BIND_SERVICE capability.

There is a third option: systemd socket activation This mode requires program changes. To learn more about socket activation, check out this article.

CAP_NET_BIND_SERVICE is a capability in Linux that allows an application to bind to privileged ports (below 1024) without requiring root privileges. It is a security feature that enables a more fine-grained access control. An application with CAP_NET_BIND_SERVICE can bind to any port or IP address it is authorized for, but has no other special capabilities.

Either add the CAP_NET_BIND_SERVICE capability directly to the binary with setcap.

setcap cap_net_bind_service=+ep /usr/local/bin/helloworld

Or enable the feature in the systemd service file.

[Unit]
Description=HelloWorldService

[Service]
Type=simple
ExecStart=/usr/local/bin/helloworld
Restart=always
User=helloworlddirect
Group=helloworlddirect
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

Make sure that no other service listens on ports 80 and 443. If Caddy is still running from the previous example, stop it

systemctl stop caddy

Restart the Go helloworld service

systemctl restart helloworld

Like in the previous example with Caddy, the following command should return Hello, World!

curl https://go.rasc.ch

This concludes this tutorial about setting up TLS in a Go HTTP server in a productive environment. The most convenient way is running a proxy in front of the service. In that case, the Go application does not have to care about TLS.
But when you plan to put the Go application directly on the Internet, certmagic makes it easy to set up TLS.