Serve trusted HTTPS from a Go program¶
By the end of this you'll have a Go server answering on https://localhost:8443 with a
certificate your browser accepts — no warning page, no clicking through, no mkcert
install. Allow about ten minutes.
Before you start¶
You'll need:
- Go 1.26.5 or later, and a module to work in.
- A machine you can install a trust anchor on. This tutorial installs a certificate
authority into your system trust store, which needs an administrator password once. On
Linux and macOS that's a
sudoprompt in the terminal you run from. - A terminal. The install can't prompt for a password without one. If you're on Windows, the certificate generation works but the trust install does not — read platform support before going further.
Everything here is reversible. The last step undoes it.
What you're about to install¶
localca mints a certificate authority that exists only on this machine, and installs it
into your trust stores. From then on, anything it signs is trusted here — and nowhere
else. The key never leaves the machine, and the whole thing can be removed again.
That's a real change to your machine's trust configuration, so it's worth knowing before the password prompt appears rather than after. The trust model explains what you're trading and why it's safe done this way.
Add the module¶
Write the server¶
Create main.go. Pick a DataDir that's stable — a hidden directory under your home is
the normal choice. Don't use a temporary directory: a new one means a new authority and a
new password prompt every run.
package main
import (
"context"
"log"
"log/slog"
"net/http"
"os"
"path/filepath"
"gitlab.com/phpboyscout/go/localca"
)
func main() {
home, err := os.UserHomeDir()
if err != nil {
log.Fatal(err)
}
cfg := localca.Config{
DataDir: filepath.Join(home, ".myapp"), // root + leaf live here; key files are 0600
AppName: "myapp", // names the root: "myapp local CA (you@host)"
}
// First run: mint the authority, install it (one password prompt), and return a
// certificate ready to serve. Later runs reuse both and prompt for nothing.
pair, err := localca.EnsureServed(context.Background(), cfg,
[]string{"localhost", "127.0.0.1"},
localca.WithLogger(slog.Default()), // without this, localca says nothing at all
)
if err != nil {
log.Fatal(err)
}
tlsCfg, err := pair.ServerConfig()
if err != nil {
log.Fatal(err)
}
http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("trusted HTTPS, no warning\n"))
})
srv := &http.Server{Addr: ":8443", TLSConfig: tlsCfg}
log.Fatal(srv.ListenAndServeTLS("", "")) // certificates are already in TLSConfig
}
Passing the logger is optional but worth doing here: it's what prints the line explaining why you're about to be asked for a password.
Run it and answer the prompt¶
You'll see the explanation, then the password prompt:
level=INFO msg="installing a local development CA into the trust store so this tool can
serve HTTPS locally; reverse with Uninstall" common_name="myapp local CA (you@host)"
[sudo] password for you:
Type your password. That prompt is the consent gate — nothing is installed into a system trust store without it. Afterwards the server starts and stays quiet.
If you get ErrElevationUnavailable instead, localca decided it couldn't obtain the
privileges — usually no sudo, or standard input isn't a terminal.
Troubleshooting
covers the ways round it.
Check it worked¶
From another terminal:
That's the meaningful result: plain curl, no -k, no --cacert. It's verifying against
your system trust store and the certificate passes.
Open https://localhost:8443/ in Chrome, Safari or Edge and you'll get the same — a padlock
and no interstitial. Firefox is the exception. It keeps its own certificate database and
only trusts the root if certutil was available when you installed. If Firefox warns and
other browsers don't, that's what happened, and
the fix is two commands.
Look at what was created¶
-rw-r--r-- leaf.pem
-rw------- leaf-key.pem
-rw-r--r-- rootCA.pem
-rw------- rootCA-key.pem
-rw-r--r-- trust-install.json
Five files, and their permissions matter. rootCA-key.pem is the signing key for an
authority your machine trusts, so it's 0600 and it must not leave this machine — copying
it somewhere else hands whoever has it the ability to mint a certificate for any site your
machine will accept. rootCA.pem is the public half and is safe to share; that's the one
you'd import onto a phone to make it trust your development server too.
Data directory layout covers what each file is for.
Stop the server and run it again. No prompt, no new files — the root is already trusted and
the cached leaf is still valid, so EnsureServed hands back the same pair and does nothing
else.
Take it back off¶
When you're done, remove the authority from your trust stores and delete its key:
a, err := localca.New(cfg)
if err != nil {
log.Fatal(err)
}
if err := a.Uninstall(context.Background(), localca.Purge()); err != nil {
log.Fatal(err)
}
This prompts for a password again, because removing from the system store needs the same privileges as adding to it.
One thing Purge() doesn't do is delete the cached leaf, which is still signed by the root
you just removed. Leave it behind and the next EnsureServed in this directory reuses it
under a fresh root, and nothing trusts the result. Tidy it up yourself:
Where to go next¶
- Serve your tool over HTTPS locally — LAN addresses so a phone can reach it, scoping which trust stores you touch, custom validity.
- Configuration reference — every field, its default, and what happens when it's wrong.
- Trust and security model — what installing a root really means, and why this one is per-machine.
- Limitations — what
localcadeliberately won't do, before you build on an assumption it doesn't hold.