Most teams compile their Go program and ship it straight to production. Run go build, toss the binary onto a server, done.

The problem is that the default build output hides quite a bit. Your source paths, build environment details, debug symbols — they’re all baked in. Harmless during development, but once that binary hits production, it becomes reconnaissance material for attackers.

I’ve put together a security baseline for releasing Go programs, covering build flags, dependency control, and distribution verification.

Paths and Memory Layout

Let’s start with a detail that’s easy to overlook. The Go compiler embeds the absolute path of your source code into the binary by default. Build from /home/zhangsan/project, and that path is permanently etched into the artifact. You can see it yourself with go version -m.

-trimpath wipes that clean. Not a sophisticated move, but a lot of projects skip it entirely.

Next step up: -buildmode=pie. PIE (Position Independent Executable) randomizes the memory addresses each time the program runs. Without PIE, the same binary loads at the same memory location every time, which lets attackers write exploits using hardcoded addresses. With PIE enabled, the addresses change on every run, raising the cost of an attack significantly. Most Linux distributions have made PIE the default in recent years — Go programs should follow suit.

Cutting Unnecessary Dependencies

One of Go’s selling points is compiling to a single binary with no external library dependencies. But once CGO enters the picture, that promise weakens.

CGO_ENABLED=0 disables CGO entirely, so the program no longer depends on the system’s libc. Two benefits: true static compilation, meaning the binary runs on any Linux machine; and you sidestep libc’s own security issues. glibc has had its share of vulnerabilities over the years — Heartbleed was OpenSSL’s problem, but similar underlying library security issues have never really stopped.

Pair this with scratch or distroless base images and it gets even better. These images don’t even have a shell. If an attacker gains container access, there are no tools to work with. Smaller attack surface, lower risk.

Making Artifacts Verifiable

Everyone uses checksums, but a checksum only proves the file wasn’t corrupted — it doesn’t prove who sent it. Supply chain attacks have been on the rise. How do you know the binary you downloaded hasn’t been tampered with?

Cosign can digitally sign container images or binaries. Verifiers use a public key to confirm the source. SBOM (Software Bill of Materials) adds another layer: it lists every dependency version used at compile time. When a vulnerability is disclosed, check the SBOM to see if you’re affected.

Provenance records “this binary was built in what environment, by whom, from which commit.” Put the three together: signatures confirm origin, SBOM lists contents, Provenance records the process.

The Secure Build Command

Combining the points above, here’s the build command:

1
2
3
4
5
6
7
CGO_ENABLED=0 \
go build \
    -trimpath \
    -buildvcs=false \
    -buildmode=pie \
    -ldflags="-s -w -extldflags=-static" \
    -o my-secure-app

Flag breakdown: -trimpath strips source paths, -buildmode=pie enables address randomization, -ldflags="-s -w" removes debug symbols and DWARF info, -extldflags=-static ensures pure static linking. -buildvcs=false prevents git information from being embedded in the binary.

These flags combined won’t slow down your build or affect runtime performance, but they close several common information leakage and attack vectors. There’s no reason not to use them.

References