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 connections in a production 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 manage 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 gets 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 listening 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, adding a proxy just for this one service might be too much overhead.
In the previous section, we have seen that Caddy has 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 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 production environment. The most convenient way is running a proxy in front of the service. In that case, the Go application does not have to manage TLS. But when you plan to put the Go application directly on the Internet, certmagic makes it easy to set up TLS.