One of the things I like about golang (and Rust too, by the way) is that it’s quite simple to build really small container images by statically linking the executables, and using scratch as the base image. I’ve done this a few times in the past, and was doing it again just recently. Except that this time around, I ran into issues: the container would crash soon after it started.

Examining the logs, it was easy to see why. Here’s an excerpt:

[...] tls: failed to verify certificate: x509: certificate signed by unknown authority

I need to add: the typical golang tools I’ve built and run from scratch-based containers in the past would not make requests out to the internet, this one did.

So the error should have been expected: after all, how could the HTTPS client know to trust the remote HTTPS server when it was running from an image that had no hints as to which CAs to trust?

Trust some CAs

The solution is simple: do what other base images do. Let the HTTP client libraries know which CAs to trust. Since I’m already using a multi-stage build in my Dockerfile, I simply added one more stage to it. In the golang crypto library sources you can see where it looks for trusted CA certificates. So we just put some (from Alpine in this case) there. Depending on your needs, you may want to update the package that installs the ca-certificates first, too. But it’s even easier than that. The COPY command nowadays also supports fetching files directly from another image, as shown below. Here, the directory /etc/ssl/certs is copied directly from the alpine:latest image.

# [...] other stages

# Build the final image
FROM scratch

USER 1000:1000
ENTRYPOINT [ "/app/myexecutable" ]

# Copy set of CA certificates from latest alpine image.
COPY --from=alpine /etc/ssl/certs /etc/ssl/certs
# [...] COPY and setup stuff from other stages

That’s all.

Summary

Trust doesn’t come from nowhere. We need to tell the libraries which CAs we trust, so certificate chains can be verified properly.