Go Security Basics
9 examples to get you started with Security - 7 basic and 2 intermediate.
Prerequisites
- Install Go 1.26.x or later from go.dev/dl.
- Create a module:
mkdir seclab && cd seclab && go mod init example.com/seclab. - For TLS examples, generate a dev cert:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost". - Install govulncheck:
go install golang.org/x/vuln/cmd/govulncheck@latest.
Basic Examples
1. Minimum TLS Version on http.Server
Set MinVersion so clients cannot negotiate TLS 1.0 or 1.1.
package main
import (
"crypto/tls"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
srv := &http.Server{
Addr: ":8443",
Handler: mux,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
}
srv.ListenAndServeTLS("cert.pem", "key.pem")
}tls.Configon the server applies to all TLS handshakes on that listener.- TLS 1.2 is a common minimum; many teams require TLS 1.3 only for external endpoints.
- Certificate files are for local dev; production should use automated rotation (ACME, cert-manager).
Related: crypto/tls Configuration & Certificate Management - cipher suites and mTLS
2. Read Secrets from Environment
Never hardcode API keys; load at startup and fail if missing.
package main
import (
"fmt"
"os"
)
func mustEnv(key string) string {
v := os.Getenv(key)
if v == "" {
fmt.Fprintf(os.Stderr, "missing required env %s\n", key)
os.Exit(1)
}
return v
}
func main() {
dbURL := mustEnv("DATABASE_URL")
_ = dbURL
}- Missing secrets should stop the process at boot, not at first request.
- Inject env vars via Kubernetes secrets, Vault, or your platform - not committed
.envfiles. - Log secret names on failure, never secret values.
Related: Security for Go Services: Defaults and Gaps - what the stdlib does not provide
3. ReadHeaderTimeout Against Slowloris
Block clients that send headers too slowly.
package main
import (
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
srv.ListenAndServe()
}ReadHeaderTimeoutis the most important single server timeout for public listeners.- Zero value means no limit - the Go default when using
ListenAndServeconvenience helpers. - Pair with
ReadTimeout,WriteTimeout, andIdleTimeoutfor full lifecycle coverage.
Related: Rate Limiting, CORS & Security Headers - additional edge middleware
4. Limit Request Body Size
Reject oversized payloads before JSON decode work.
package main
import (
"io"
"net/http"
)
func createItem(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "payload too large", http.StatusRequestEntityTooLarge)
return
}
_ = body
w.WriteHeader(http.StatusCreated)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /items", createItem)
http.ListenAndServe(":8080", mux)
}MaxBytesReaderreturns 413 when the limit is exceeded.- Set limits per route based on expected upload size.
- Always
Closethe body so keep-alive connections can be reused.
Related: Input Validation & Safe HTML/JSON Handling - schema validation after size checks
5. Constant-Time Secret Comparison
Compare API keys or HMACs without early exit on first differing byte.
package main
import (
"crypto/subtle"
"fmt"
)
func main() {
provided := []byte("secret-token")
expected := []byte("secret-token")
if subtle.ConstantTimeCompare(provided, expected) == 1 {
fmt.Println("match")
}
}- Plain
==on strings can leak timing information through short-circuit comparison. crypto/subtle.ConstantTimeComparerequires equal-length slices; normalize length first if needed.- Use for webhooks, static API keys, and MAC verification - not for password hashes (use
bcryptorargon2).
6. Safe HTML Output with html/template
Auto-escape user content in HTML responses.
package main
import (
"html/template"
"net/http"
)
func main() {
tmpl := template.Must(template.New("p").Parse(`<p>{{.}}</p>`))
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
tmpl.Execute(w, name)
})
http.ListenAndServe(":8080", nil)
}html/templateescapes<,>,&, quotes, preventing reflected XSS in HTML context.text/templatedoes not escape - use only for non-HTML output.- For JSON APIs, set
Content-Type: application/jsonand encode withencoding/json.
Related: Input Validation & Safe HTML/JSON Handling - validation and encoding rules
7. Run govulncheck Locally
Scan your module graph for known vulnerabilities.
cd seclab
govulncheck ./...- Reports CVEs affecting packages you import, including transitive dependencies.
- Run in CI on every pull request; fail the build on actionable findings.
- A clean scan does not mean your own handler code is free of logic bugs.
Related: Dependency Scanning with govulncheck & SBOM - CI gates and SBOM export
Intermediate Examples
8. Outbound Client with Timeout
Never use http.DefaultClient from server handlers.
package main
import (
"context"
"io"
"net/http"
"time"
)
func fetch(ctx context.Context, url string) ([]byte, error) {
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
_, _ = fetch(ctx, "https://example.com")
}Client.Timeoutbounds the entire exchange including TLS and body read.NewRequestWithContexthonors cancellation when the caller or server shuts down.- Inject one configured client per upstream dependency in production services.
9. Auth Middleware Skeleton (Fail Closed)
Reject requests without a valid Bearer token before handlers run.
package main
import (
"context"
"net/http"
"strings"
)
type ctxKey string
const userKey ctxKey = "user"
func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := r.Header.Get("Authorization")
if !strings.HasPrefix(h, "Bearer ") {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
token := strings.TrimPrefix(h, "Bearer ")
if token == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userKey, token)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func main() {
mux := http.NewServeMux()
mux.Handle("/api/", auth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})))
http.ListenAndServe(":8080", mux)
}- Return 401 immediately when credentials are missing or malformed.
- Replace the placeholder with real JWT or session validation in production.
- Use unexported context key types to avoid collisions with other middleware.
Related: Authentication & Authorization Patterns - JWT, RBAC, and OAuth2
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify at build).