Chapter 3. Triage
Plan (short & actionable)
Scope parsed:
artifacts/semgrep.json,trufflehog.json,checkov.json,trivy-image.json,sbom.jsonCode & k8s:app/main.go,app/Dockerfile,infra/k8s/deployment.yaml,service.yaml,kustomization.yamlTop issues to fix now (P0/P1):
- SCA (Trivy): 1× HIGH CVE in Go stdlib (Go 1.22.12) with fixes available.
- IaC (Checkov): missing readiness/liveness probes, service account token hardening, seccomp, caps drop, default namespace, image digest, imagePullPolicy, network policy.
- Secrets (TruffleHog): AWS key pattern in
app/main.go, unverified canary (training string) — clean up or allowlist. - SAST (Semgrep): “HTTP server without TLS” warning (local dev) + we know a reflected XSS in
/echo(not flagged by the current ruleset) — we’ll remediate.
Retest targets:
make sca iac sast secrets cd zapthen capture deltas under./artifacts.
Triage (actual findings)
| Tool | Finding (short) | Evidence | Priority | Rationale |
|---|---|---|---|---|
| Trivy (SCA) | CVE-2025-47907 in Go stdlib v1.22.12; fixed in 1.23.12 / 1.24.6 | artifacts/trivy-image.json → HIGH (1) | P0 | Known high with fix; affects runtime (Go stdlib). |
| Checkov (IaC) | Default namespace used | CKV_K8S_21 in infra/k8s/deployment.yaml | P1 | We apply into ship at runtime, but static file lacks metadata.namespace → fail. |
| Checkov (IaC) | No readiness/liveness probes | CKV_K8S_8, CKV_K8S_9 | P1 | Basic resilience & security posture. |
| Checkov (IaC) | SA token hardening | CKV_K8S_38 | P1 | Add automountServiceAccountToken: false + SA binding as needed. |
| Checkov (IaC) | Seccomp not set | CKV_K8S_31 | P1 | Add seccompProfile: RuntimeDefault. |
| Checkov (IaC) | Capabilities not explicitly dropped / NET_RAW | CKV_K8S_37, CKV_K8S_28 | P1 | Explicitly drop ALL; add only what’s needed (likely none). |
| Checkov (IaC) | Image pull policy & digest | CKV_K8S_15, CKV_K8S_43 | P2 | In dev this is flexible; still recommend a digest pin pattern for prod chapter. |
| Checkov (IaC) | No NetworkPolicy | CKV2_K8S_6 | P1 | Add minimal default deny + allow app ingress/DNS egress. |
| TruffleHog (Secrets) | AWS canary token string in code, Verified=false | artifacts/trufflehog.json → file /repo/app/main.go:28 | P1 | Training string; remove or allowlist to prevent noise. |
| Semgrep (SAST) | HTTP server without TLS (WARNING) | artifacts/semgrep.json | P2 | Dev cluster uses Ingress/Tunnel; note TLS termination guidance. |
| Known (not flagged) | Reflected XSS at /echo | Code review app/main.go | P1 | Intentionally insecure; we’ll fix now and show DAST improvement. |
Validate (quick proofs)
- CVE-2025-47907 present: Trivy HIGH lists
stdlib v1.22.12with fixed versions1.23.12 / 1.24.6. Ourgo.modisgo 1.22; build base image isgolang:1.22-alpine. - K8s misconfigs: Checkov flags map to manifest conditions (namespace missing, no probes, no seccomp, etc.).
- Secrets:
Raw&RawV2include AWS-looking key;Verified=falseandis_canary=trueinExtraData→ safe training token, still noisy. - XSS:
/echo?input=<script>alert(1)</script>will reflect unescaped input (seefmt.Fprintf(w, "You said: %s", input)).
Remediation (concrete diffs/snippets)
Keep changes minimal, safe, and easy to teach. I’ve split by domain.
1) SCA: upgrade Go toolchain & runtime image
app/Dockerfile (build base)
Change base to a fixed Go stream (e.g., 1.24.6):
# build stage
-FROM golang:1.22-alpine AS build
+FROM golang:1.24.6-alpine AS build
WORKDIR /src
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/server ./main.go
(Optionally keep alpine runtime, or use gcr.io/distroless/static:nonroot in a later “golden images” chapter.)
Retest: make build sca → confirm HIGH count drops to 0.
2) App: fix reflected XSS & add basic headers
app/main.go — escape user input and set common headers:
import (
"fmt"
"html/template"
"html" // add
"log"
"net/http"
"os"
)
mux.HandleFunc("/echo", func(w http.ResponseWriter, r *http.Request) {
input := r.URL.Query().Get("input")
w.Header().Set("Content-Type", "text/html; charset=utf-8")
// Security headers (baseline)
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Referrer-Policy", "no-referrer")
// Optional minimal CSP for demo
w.Header().Set("Content-Security-Policy", "default-src 'self'")
// Escape untrusted input to prevent reflected XSS
fmt.Fprintf(w, "You said: %s", html.EscapeString(input))
})
Retest: make cd && make zap → XSS should no longer be reported (and manual <script> should render as text).
3) Secrets: remove canary key from code (or allowlist)
If it’s in the source (line ~28), delete it or replace with a clearly fake, non-pattern string. If you need it for teaching, add a comment and allowlist it in your TruffleHog config later so CI doesn’t nag students.
Retest: make secrets → verified_secrets: 0, no AWS key finding.
4) K8s Hardening (pass key Checkov rules)
infra/k8s/deployment.yaml
Add namespace, SA token control, seccomp, drop caps, probes, and (optionally) imagePullPolicy:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
namespace: ship # add to satisfy CKV_K8S_21
labels: { app: app }
spec:
replicas: 1
selector: { matchLabels: { app: app } }
template:
metadata: { labels: { app: app } }
spec:
automountServiceAccountToken: false # CKV_K8S_38
securityContext:
seccompProfile:
type: RuntimeDefault # CKV_K8S_31
containers:
- name: app
image: ship-securely/app:dev
+ imagePullPolicy: Always # CKV_K8S_15 (dev-friendly but satisfies rule)
ports: [{ containerPort: 3000 }]
securityContext:
runAsUser: 10001
runAsGroup: 10001
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
+ capabilities:
+ drop: ["ALL"] # CKV_K8S_37 / CKV_K8S_28
resources:
requests: { cpu: "50m", memory: "64Mi" }
limits: { cpu: "250m", memory: "256Mi" }
+ readinessProbe:
+ httpGet: { path: /healthz, port: 3000 }
+ initialDelaySeconds: 2
+ periodSeconds: 5
+ livenessProbe:
+ httpGet: { path: /healthz, port: 3000 }
+ initialDelaySeconds: 5
+ periodSeconds: 10
NetworkPolicy (satisfy CKV2_K8S_6): add default deny + allow app ingress & DNS egress:
# infra/k8s/netpol/00-default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: ship
spec:
podSelector: {}
policyTypes: ["Ingress", "Egress"]
# infra/k8s/netpol/20-allow-app-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-ingress
namespace: ship
spec:
podSelector:
matchLabels: { app: app }
ingress:
- from:
- podSelector: {} # relax as needed for demo
ports:
- protocol: TCP
port: 3000
# infra/k8s/netpol/10-allow-dns-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: ship
spec:
podSelector: {}
policyTypes: ["Egress"]
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
(You already have netpol-apply in your full Makefile; for the starter, include these three files and a simple make target later in the book.)
Retest: make iac → Checkov failed set should drop (probes, seccomp, SA token, caps, default ns, netpol).
Note on image digest (
CKV_K8S_43): For the local-only starter, we can document digest pinning and enforce it in the “golden images” chapter (students pin a digest when they have a registry). We’ll mark this P2 for now.
Retest (commands to run)
# Rebuild and rescan image after Go upgrade
make build sca
# Re-scan code and secrets after app/main.go changes
make sast secrets
# Re-apply K8s and re-scan IaC
make cd
make iac
# Optional: run ZAP baseline to confirm no reflected XSS
make zap
Capture summaries (before/after) into ARTIFACTS.md, and drop a dated bundle via your evidence target (if present).
Close & Document
Closed (fixed):
- CVE-2025-47907 (Go stdlib) — fixed by upgrading to
golang:1.24.6-alpine; Trivy HIGH=0. - Reflected XSS in
/echo— fixed byhtml.EscapeStringand security headers; ZAP/Manual confirm. - Checkov: namespace, probes, seccomp, caps drop, SA token, netpol — fixed with manifest edits;
make iacclean for these checks. - TruffleHog AWS token — removed or allowlisted with clear comment (no verified secrets).
- CVE-2025-47907 (Go stdlib) — fixed by upgrading to
Accepted/Deferred (documented with reason & revisit date):
- Image digest pinning (
CKV_K8S_43) in local starter — defer to “Golden Images” chapter; add note in text and set a reminder to enforce later.
- Image digest pinning (
Prevention:
- Keep Go base image current; consider pinning digests in golden-image workflow.
- Keep Semgrep rules curated; add
.semgrepignoreif needed for tests/gen code. - Maintain
.trivyignorefor vetted, low-risk CVEs if/when needed. - Preserve K8s hardening defaults in base manifests; teach overrides deliberately.
Quick “diff of impact” (what should change in artifacts)
artifacts/trivy-image.json:HIGHcount from 1 → 0.artifacts/semgrep.json: still a TLS warning (expected in local dev), but no XSS path (it wasn’t flagged before; now provably mitigated).artifacts/trufflehog.json: no AWS detector hit /verified_secrets=0.artifacts/checkov.json: failures forCKV_K8S_8/9/31/37/28/38/21/CKV2_K8S_6drop.artifacts/zap/: XSS alert (if any) gone; headers findings likely improved.