Security Hardening Overview
Security Hardening Overview
This part focuses on practical secure-by-default design in Go services: transport identity, access decisions, and secret lifecycle. The objective is to reduce both exploitability and operational misconfiguration risk.
Why / Overview
Security failures in Go services are often not exotic crypto bugs. They are:
- Expired certificates during a quiet holiday
- JWT validation that skips
aud/iss - Authorization checks only on the UI path
- Secrets in env dumps and CI logs
- mTLS configured on one side only
client
| TLS / mTLS
v
edge -- authn --> principal
|
+-- authz --> allow/deny on resource+action
|
+-- secrets --> short-lived credentials
Part 11 covered tooling and application hardening baselines. This part deepens transport, identity, and secrets.
Learning Path
| Chapter | Focus | Outcome |
|---|---|---|
| 171 TLS / mTLS | Trust stores, versions, rotation | Safe transport identity |
| 172 Authn / Authz | Principals vs policies | Correct access decisions |
| 173 Secrets | Injection, dual-read rotation | Credential lifecycle without outages |
Core Principles
- Authenticate then authorize — never authorize anonymous by accident.
- Default deny — missing policy is deny, not allow.
- Short-lived credentials — blast radius of leaks shrinks with TTL.
- Encrypt in transit everywhere that is not a deliberate exception.
- Separate data plane and control plane credentials.
- Fail closed on auth/crypto misconfiguration at startup when possible.
Go Building Blocks
| Area | Packages |
|---|---|
| TLS | crypto/tls, crypto/x509 |
| HTTP security | net/http + middleware |
| Tokens | golang-jwt or similar (review carefully), PASETO, session cookies |
| Secrets | env, files, Vault/cloud SM SDKs |
| Random | crypto/rand |
Relationship to Other Parts
- Hardening (81) — process, headers, input limits
- Network systems — where TLS terminates and proxies hop
- Observability — audit logs for authz denials; never log secrets
- Distributed infra — service identity for discovery peers
Literacy Checklist
- Configure TLS 1.2+ with sane cipher suites
- Explain mTLS client cert verification
- Rotate certs without downtime (overlap windows)
- Separate authn modules from authz policy engines
- Validate JWT/
iss/aud/exp correctly - Dual-read secrets during rotation
- Redact credentials in logs and errors
Part Warm-Up Exercises
- Find where TLS terminates in your architecture (app vs proxy vs mesh).
- List every secret your service reads at startup.
- Identify one endpoint that authenticates but might skip authorize.
- Check certificate expiry monitoring exists.
- Locate any hardcoded tokens in repo history (then rotate them).
Threat Model Snapshot (Service-Centric)
For a typical Go API, assume:
| Threat | Primary controls in this part |
|---|---|
| Network eavesdropping / tampering | TLS |
| Spoofed internal callers | mTLS or signed service identity |
| Stolen user session / token | short TTL, audience checks, revoke paths |
| Confused deputy / IDOR | resource-level authz |
| Leaked DB password | rotation, dual-read, least privilege |
| Misissued cert | trust pins, monitoring, automation |
Attackers prefer misconfiguration over breaking AES-GCM. Your design should make the secure path the default path.
Layering with Part 11
| Part 11 (earlier) | Part 17 (here) |
|---|---|
| govulncheck, gosec | TLS session identity |
| Process/container hardening | Authn/authz architecture |
| Input limits, headers | Secrets lifecycle |
| General checklist | Rotation choreography |
Use both: tools catch known issues; these chapters design the trust model.
Minimal Secure Service Skeleton
listen TLS (or receive from mesh)
-> request id middleware
-> authenticate (session/JWT/mTLS)
-> authorize (action + resource)
-> business handler (context with principal)
-> audit deny / sensitive allow
secrets: loaded at start, refreshed, never logged
If a route skips any of the middle boxes, that is a design smell requiring explicit justification (e.g. public /healthz).
Compliance-Adjacent Habits (Not Legal Advice)
Even without a formal compliance program, good habits transfer:
- Inventory secrets and certs
- Record who can read production secrets
- Prefer short-lived credentials
- Keep audit logs for admin and authz deny
- Separate prod and non-prod credentials completely
Secure Defaults Checklist (Part Preview)
Copy into PR templates for services that handle auth or secrets:
- TLS 1.2+ (or mesh-enforced equivalent)
- No
InsecureSkipVerifyoutside explicit test builds - Authn middleware before business handlers
- Authz checks resource ownership (anti-IDOR)
- Tokens validate
iss,aud,exp, algorithm - Cookies:
HttpOnly,Secure, deliberateSameSite - Secrets not in git; startup fails if missing
- Rotation dual-read tested in staging
- Authz denials audited with request id
- pprof/admin not on public interface
Testing Security Logic
| Kind | Example |
|---|---|
| Unit | Authorize table tests; JWT claim rejection |
| Integration | mTLS handshake success/fail matrices |
| Negative | expired cert, wrong audience, cross-tenant id |
| Chaos | rotate secret mid-traffic with dual-read |
Security tests that only cover the happy path give false confidence.
Where Identity Lives
Human user -> session cookie / OIDC access token -> API
Service A -> mTLS or signed JWT (client credentials) -> Service B
Job worker -> task-scoped token or instance identity -> APIs
Do not reuse human user tokens for service calls. Audience and lifetime should differ.
Mapping Incidents → Chapter
| Incident | Start here |
|---|---|
| TLS handshake errors / expired cert | 171 |
| Users see other tenants’ data | 172 (IDOR / authz) |
| 401 storms after deploy | 172 (token validation) or 173 (secret mismatch) |
| DB auth failures during rotation | 173 dual-read |
| “Works on internal network without TLS” | 171 threat model |
Non-Goals
This part does not replace:
- Full application penetration testing
- Formal cryptography design reviews
- Cloud IAM deep dives for every provider
It does give Go service authors a trustworthy baseline so specialists are not fixing the same footguns repeatedly.
Glossary
- Authn / Authz — identity vs permission
- mTLS — mutual TLS, both peers present certificates
- Trust bundle — set of CAs you accept
- Dual-read — accept old and new secret during rotation
- Principal — authenticated actor in your system
- Audience (
aud) — intended recipient of a token - IDOR — insecure direct object reference (authz bug)
- SPIFFE — workload identity URI SAN convention
More examples
Tour: bearer check + TLS client roots sketch
mkdir -p /tmp/go-sec-tour && cd /tmp/go-sec-tour
go mod init example.com/sec-tourSave as main.go:
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"net/http/httptest"
"strings"
)
func main() {
// Authn stub
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") || auth == "Bearer " {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
fmt.Fprintln(w, "ok")
})
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/", nil)
h.ServeHTTP(rr, req)
fmt.Println("no token:", rr.Code)
rr = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Bearer demo")
h.ServeHTTP(rr, req)
fmt.Println("with token:", rr.Code)
// TLS: trust httptest cert
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "https")
}))
defer ts.Close()
roots := x509.NewCertPool()
roots.AddCert(ts.Certificate())
client := &http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: roots, MinVersion: tls.VersionTLS12},
}}
resp, err := client.Get(ts.URL)
if err != nil {
panic(err)
}
resp.Body.Close()
fmt.Println("tls status:", resp.StatusCode)
}go run .Expected output:
no token: 401
with token: 200
tls status: 200
Runnable example
Tour of this part: HMAC token mint/verify (authn-ish), a tiny allow/deny policy (authz), and dual-secret verification (rotation)—stdlib crypto.
mkdir -p /tmp/go-sec-tour && cd /tmp/go-sec-tour
go mod init example.com/sec-tourSave as main.go:
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func sign(key, msg string) string {
m := hmac.New(sha256.New, []byte(key))
_, _ = m.Write([]byte(msg))
return hex.EncodeToString(m.Sum(nil))
}
func verifyDual(keys []string, msg, sig string) bool {
raw, err := hex.DecodeString(sig)
if err != nil {
return false
}
for _, k := range keys {
m := hmac.New(sha256.New, []byte(k))
_, _ = m.Write([]byte(msg))
if hmac.Equal(raw, m.Sum(nil)) {
return true
}
}
return false
}
func authorize(role, action string) bool {
policy := map[string][]string{
"reader": {"read"},
"admin": {"read", "write", "delete"},
}
for _, a := range policy[role] {
if a == action {
return true
}
}
return false
}
func main() {
msg := "user=42"
sig := sign("key-new", msg)
fmt.Println("verify new only:", verifyDual([]string{"key-new"}, msg, sig))
fmt.Println("dual-read old+new:", verifyDual([]string{"key-old", "key-new"}, msg, sig))
fmt.Println("authz reader write:", authorize("reader", "write"))
fmt.Println("authz admin write:", authorize("admin", "write"))
fmt.Println("tour: transport(identity) + authz + secret rotation")
}go run .Expected output:
verify new only: true
dual-read old+new: true
authz reader write: false
authz admin write: true
tour: transport(identity) + authz + secret rotation
What to notice
- Authn (who) and authz (what) are separate layers; TLS is transport identity (171).
- Dual-read prevents outages during secret rotation (173).
Try next
- Chapter 171 TLS client/server demo; 172 middleware; 173 fail-closed env secrets.
Next Chapter
TLS and mTLS Deep Dive — the foundation of service identity on the wire.