Security Hardening Overview

Updated

July 30, 2026

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

  1. Authenticate then authorize — never authorize anonymous by accident.
  2. Default deny — missing policy is deny, not allow.
  3. Short-lived credentials — blast radius of leaks shrinks with TTL.
  4. Encrypt in transit everywhere that is not a deliberate exception.
  5. Separate data plane and control plane credentials.
  6. 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

  1. Find where TLS terminates in your architecture (app vs proxy vs mesh).
  2. List every secret your service reads at startup.
  3. Identify one endpoint that authenticates but might skip authorize.
  4. Check certificate expiry monitoring exists.
  5. 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).

Secure Defaults Checklist (Part Preview)

Copy into PR templates for services that handle auth or secrets:

  • TLS 1.2+ (or mesh-enforced equivalent)
  • No InsecureSkipVerify outside explicit test builds
  • Authn middleware before business handlers
  • Authz checks resource ownership (anti-IDOR)
  • Tokens validate iss, aud, exp, algorithm
  • Cookies: HttpOnly, Secure, deliberate SameSite
  • 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-tour

Save 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-tour

Save 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.