Security Tools
Security Tools: Automating Defense
Overview
Security should not depend on heroic manual review. Go has a strong toolchain for static detection, reachable vulnerability analysis, capability inventory, and cryptographic testing. Wire these into CI so regressions fail the build before they reach production.
This chapter covers govulncheck, gosec, capslock, nilaway, modern crypto helpers (crypto/hpke, testing/cryptotest), experimental secret handling, and how to assemble them into a pipeline.
Suggested Security Pipeline
commit
--> go vet / golangci-lint
--> gosec (or security linters)
--> nilaway (optional, deeper nil analysis)
--> govulncheck (reachable vulns)
--> capslock (dependency capability review)
--> go test (incl. crypto vectors)
--> release / sign
trust boundary diagram
developer laptop --> CI runners --> artifact registry --> runtime
| | | |
secrets pinned actions cosign verify least privilege
1. govulncheck — Reachability-Based Vuln Scanning
govulncheck uses the Go vulnerability database and call graph reachability. It prefers reporting issues your code can actually hit, not every CVE in the module graph.
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
# JSON for CI parsing
govulncheck -json ./... > govulncheck.jsonCI snippet
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...Interpreting results
- Upgrade the module if a fixed version exists.
- If unused, confirm with
govulncheck(symbol level)—don’t ignore blindly. - Vendor or replace only with a documented exception and expiry.
go get example.com/module@v1.2.4
go mod tidy
govulncheck ./...2. gosec — AST Security Linter
gosec walks the AST looking for dangerous patterns.
go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec ./...
# Exclude generated code
gosec -exclude-generated ./...Common findings:
| Pattern | Risk |
|---|---|
| Hardcoded credentials | Secret leakage |
| SQL via string concat | Injection |
math/rand for security tokens |
Predictable values |
chmod 0777 / weak file perms |
Local privilege issues |
http without TLS in prod configs |
Cleartext |
unsafe / weak crypto APIs |
Memory / crypto breaks |
Safer random and SQL examples
// Bad: math/rand for tokens
// token := fmt.Sprintf("%d", rand.Int())
// Good: crypto/rand
func sessionToken() (string, error) {
var b [16]byte
if _, err := rand.Read(b[:]); err != nil {
return "", err
}
return hex.EncodeToString(b[:]), nil
}// Bad
// db.Query("SELECT * FROM users WHERE name = '" + name + "'")
// Good
rows, err := db.QueryContext(ctx, `SELECT id, name FROM users WHERE name = $1`, name)Tune severity in CI: fail on high/critical; track medium findings.
3. capslock — Capability Analysis
Google’s capslock reports what capabilities a package graph uses: network, filesystem, runtime unsafe, os exec, etc.
go install github.com/google/capslock/cmd/capslock@latest
capslock -packages ./...Use it when reviewing dependencies:
- A pure string helper that needs Network is a smell.
- A config parser that needs OsExec deserves scrutiny.
- Diff capslock output across PRs that bump dependencies.
expected for a CLI file tool: FileSystem, maybe Runtime
unexpected for left-pad clone: Network, CAP_SYS_ADMIN stories, etc.
4. nilaway — Static Nil Flows
Uber’s nilaway tracks potential nil dereferences across functions more deeply than many linters.
go install go.uber.org/nilaway/cmd/nilaway@latest
nilaway ./...It does not turn Go into Rust, but it catches real production panics:
func loadUser(id string) (*User, error) {
if id == "" {
return nil, fmt.Errorf("empty id")
}
// ...
return u, nil
}
func handler() {
u, err := loadUser(id)
if err != nil {
return
}
// nilaway helps when err is ignored or only partially checked
fmt.Println(u.Name)
}Run as a periodic or blocking CI job if the signal-to-noise ratio works for your codebase.
5. Modern Crypto Tooling
crypto/hpke (Hybrid Public Key Encryption)
HPKE (RFC 9180) combines KEM + KDF + AEAD for standardized envelope encryption—useful for config blobs, recipient-encrypted messages, and modern protocol designs.
import (
"crypto/hpke"
"crypto/rand"
)
func encryptHPKE(recipientPub hpke.PublicKey, msg []byte) (enc, ciphertext []byte, err error) {
suite := hpke.NewSuite(
hpke.KEM_X25519_HKDF_SHA256,
hpke.KDF_HKDF_SHA256,
hpke.AEAD_AES_128_GCM,
)
sender, err := suite.NewSender(recipientPub, nil)
if err != nil {
return nil, nil, err
}
enc, sealer, err := sender.Setup(rand.Reader)
if err != nil {
return nil, nil, err
}
ciphertext, err = sealer.Seal(msg, nil)
return enc, ciphertext, err
}flowchart LR
subgraph Sender
PubK["Recipient Public Key"] --> KEM["HPKE KEM"]
PlainText["Plaintext"] --> AEAD["AEAD Encrypt"]
KEM --> EncapsKey["enc"]
KEM --> SharedSecret["Shared Secret"] --> AEAD
end
EncapsKey --> Network
AEAD --> CipherText --> Network
subgraph Recipient
PrivK["Private Key"] --> Decaps["Decapsulate"]
Network --> Decaps
Decaps --> AEADDec["AEAD Decrypt"]
Network --> AEADDec
AEADDec --> Plain["Plaintext"]
end
Prefer stdlib crypto primitives over hand-rolled combinations of ECDH + HKDF + GCM.
testing/cryptotest
Use testing/cryptotest (where available in your Go version) to exercise cryptographic implementations against known vectors, basic correctness, and timing-related expectations for custom code that wraps stdlib primitives.
func TestHMACVectors(t *testing.T) {
// table-driven known-answer tests for your wrappers
mac := hmac.New(sha256.New, []byte("key"))
mac.Write([]byte("data"))
got := mac.Sum(nil)
want := mustHex("...") // known vector
if !hmac.Equal(got, want) {
t.Fatalf("mismatch")
}
}Always compare secrets with constant-time helpers (subtle.ConstantTimeCompare, hmac.Equal).
6. Experimental: runtime/secret
Experiments around tighter secret lifetimes appear behind build tags / GOEXPERIMENT. Treat them as opt-in and verify support for your Go version before relying on them in production.
//go:build goexperiment.runtimesecret
import "runtime/secret"
func useSecret(token []byte) {
secret.Do(func() {
// limited lifetime window for sensitive material
_ = token
})
}GOEXPERIMENT=runtimesecret go test ./...Still follow classic hygiene: minimize copies, zero buffers when practical, don’t log secrets, prefer OS keychains/HSMs for long-term keys.
7. Complementary Checks
| Tool | Role |
|---|---|
go vet |
Compiler-adjacent mistakes |
golangci-lint |
Aggregates many linters |
staticcheck |
High-signal correctness |
semgrep / CodeQL |
Org-wide custom rules |
govulncheck |
Module CVEs with reachability |
Image scanners (trivy) |
Container base CVEs |
Example golangci config fragment:
linters:
enable:
- gosec
- govet
- staticcheck
- errcheckWiring a Makefile / CI Target
.PHONY: sec
sec:
go vet ./...
gosec -quiet ./...
govulncheck ./...# GitHub Actions job fragment
- name: Security
run: make secProduction Checklist
govulncheck ./...on every PRgosecor equivalent security linters enabled- Dependency bumps reviewed with capslock when capabilities jump
- No
math/randfor security tokens - Parameterized SQL only
- Known-answer tests for crypto wrappers
- Secrets never in git; scanner in pre-commit optional
- Container images scanned on release
- Exceptions documented with owner + expiry
Common Pitfalls
- Ignoring govulncheck because “it’s transitive” — reachability exists for a reason; verify.
- Disabling gosec rules globally — narrow suppressions with comments and justification.
- Custom crypto — combining primitives incorrectly; prefer HPKE/TLS/libsodium-grade designs.
- Logging tokens — still the #1 operational leak.
- Security tools only on main — PRs need the same gates.
- False confidence — static tools don’t replace threat modeling or fuzzing of parsers.
Exercises
- Install and run — Run
govulncheckandgosecon this book’s sample modules or a toy service; fix or document each finding. - Token generator — Implement
sessionTokenwithcrypto/rand; add a test that 1000 tokens are unique and correct length. - SQL injection lab — Write a deliberate vulnerable query (in a
_testfile or clearly broken sample) and show gosec flags it; then fix with parameters. - Capslock review — Run capslock on a module that imports
net/httpvs a pure algorithm package; compare capability sets. - HMAC vectors — Table-test an HMAC helper against a published RFC vector; use
hmac.Equal. - CI gate — Add a
make sectarget and a workflow step that fails the build on findings.
More examples
Constant-time compare (HMAC / tokens)
mkdir -p /tmp/go-hmac-eq && cd /tmp/go-hmac-eq
go mod init example.com/hmac-eqSave as main.go:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func sign(secret, msg string) string {
m := hmac.New(sha256.New, []byte(secret))
_, _ = m.Write([]byte(msg))
return hex.EncodeToString(m.Sum(nil))
}
func valid(secret, msg, gotHex string) bool {
want := sign(secret, msg)
// Decode both; compare with hmac.Equal (constant-time on equal-length slices).
a, err1 := hex.DecodeString(want)
b, err2 := hex.DecodeString(gotHex)
if err1 != nil || err2 != nil || len(a) != len(b) {
return false
}
return hmac.Equal(a, b)
}
func main() {
secret, msg := "s3cr3t", "POST /pay|42"
sig := sign(secret, msg)
fmt.Println("sig ok:", valid(secret, msg, sig))
fmt.Println("tampered:", valid(secret, msg, sig[:len(sig)-1]+"0"))
fmt.Println("wrong secret:", valid("other", msg, sig))
}go run .Expected output:
sig ok: true
tampered: false
wrong secret: false
Path jail (block traversal)
mkdir -p /tmp/go-path-jail && cd /tmp/go-path-jail
go mod init example.com/path-jailSave as main.go:
package main
import (
"fmt"
"path/filepath"
"strings"
)
func underRoot(root, userPath string) (string, error) {
clean := filepath.Clean("/" + userPath) // force absolute-ish clean
clean = strings.TrimPrefix(clean, string(filepath.Separator))
full := filepath.Join(root, clean)
rel, err := filepath.Rel(root, full)
if err != nil || strings.HasPrefix(rel, "..") {
return "", fmt.Errorf("escape: %q", userPath)
}
return full, nil
}
func main() {
root := "/var/app/data"
for _, p := range []string{"docs/a.txt", "../etc/passwd", "/etc/passwd"} {
full, err := underRoot(root, p)
if err != nil {
fmt.Printf("%q -> ERR %v\n", p, err)
continue
}
fmt.Printf("%q -> %s\n", p, full)
}
}go run .Expected output:
"docs/a.txt" -> /var/app/data/docs/a.txt
"../etc/passwd" -> ERR escape: "../etc/passwd"
"/etc/passwd" -> ERR escape: "/etc/passwd"
Runnable example
Security tools are external; the patterns they enforce are stdlib. This program generates session tokens with crypto/rand, signs and verifies an HMAC, and compares digests in constant time—the kind of code gosec expects instead of math/rand.
mkdir -p /tmp/go-sec-tools && cd /tmp/go-sec-tools
go mod init example.com/sec-toolsSave as main.go:
package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
)
func sessionToken(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
func sign(key, msg []byte) []byte {
m := hmac.New(sha256.New, key)
_, _ = m.Write(msg)
return m.Sum(nil)
}
func main() {
tok, err := sessionToken(16)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println("token:", tok)
fmt.Println("token_len_hex:", len(tok))
key := make([]byte, 32)
if _, err := rand.Read(key); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
msg := []byte("order:42:ship")
sig := sign(key, msg)
fmt.Println("hmac:", hex.EncodeToString(sig))
// Constant-time compare (never == on secret slices).
again := sign(key, msg)
if !hmac.Equal(sig, again) {
fmt.Println("verify: FAIL")
os.Exit(1)
}
fmt.Println("verify: ok")
// Wrong key must fail.
badKey := make([]byte, 32)
_, _ = rand.Read(badKey)
if hmac.Equal(sig, sign(badKey, msg)) {
fmt.Println("bad key unexpectedly matched")
os.Exit(1)
}
fmt.Println("bad_key_rejected: ok")
// Uniqueness smoke test.
seen := map[string]struct{}{}
for i := 0; i < 1000; i++ {
t, err := sessionToken(16)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if _, ok := seen[t]; ok {
fmt.Println("duplicate token")
os.Exit(1)
}
seen[t] = struct{}{}
}
fmt.Println("unique_tokens: 1000")
}go run .Expected output (illustrative; tokens random):
token: a3f1c9...
token_len_hex: 32
hmac: 9b2e...
verify: ok
bad_key_rejected: ok
unique_tokens: 1000
What to notice
crypto/randis for security-sensitive values;math/randis a classicgosecfinding.hmac.Equal(orsubtle.ConstantTimeCompare) avoids timing leaks frombytes.Equalon secrets.- Known-answer tests and uniqueness checks belong in CI next to
govulncheck/gosec.
Try next
- Add a table-driven HMAC test with a fixed key/message and expected hex digest.
- Run
gosec ./...andgovulncheck ./...on this module once the tools are installed.