Ship, Securely, Ch. 2: DevSecOps in Action
Part of Ship, Securely, a hands-on DevSecOps book.
Getting tools to run is the easy part. This chapter is about making sense of their output, and turning it into a decision instead of a backlog.
Getting Started
Now, let’s clone the repo and make sure that everything works.
Clone the repository
Why this book is local-first
- Zero cloud accounts, zero ongoing cost, predictable setup.
- Everyone’s laptop is the same “platform”: Docker + Minikube + Make.
- Artifacts saved locally → easy “audit packet” and reproducible labs.
What you’ll install (once)
- Git, Docker Desktop (or Docker Engine), Make, Go, Minikube.
- Optional: VS Code (recommended).
Repo layout (quick tour)
ship-securely-starter/
├─ app/
│ ├─ main.go
│ ├─ go.mod
│ └─ Dockerfile
├─ infra/
│ ├─ docker/
│ │ └─ compose.yaml
│ └─ k8s/
│ ├─ deployment.yaml
│ ├─ service.yaml
│ └─ kustomization.yaml
├─ tools/
│ └─ zap/
│ └─ README.md # place ZAP context/auth if needed
├─ artifacts/ # CI reports written here (generated)
├─ .pre-commit-config.yaml
├─ .gitignore
├─ Makefile # `make ci`, `make cd`, mk-* targets
└─ README.md
Chapter 2: Make CI
First run (CI on your laptop)
# macOS/Linux
make ci
Windows (PowerShell)
make ci
what you should see
docker build -t ship-securely/app:dev app
[+] Building 1.2s (14/14) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 645B 0.0s
=> [internal] load metadata for docker.io/library/golang:1.22-alpine 0.4s
=> [internal] load metadata for docker.io/library/alpine:3.20 0.4s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [build 1/4] FROM docker.io/library/golang:1.22-alpine@sha256:1699c10032ca2582ec89a24a1312d986a3f094aed3d5c1147b19880afe40e052 0.0s
=> [stage-1 1/4] FROM docker.io/library/alpine:3.20@sha256:765942a4039992336de8dd5db680586e1a206607dd06170ff0a37267a9e01958 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 2.02kB 0.0s
=> CACHED [build 2/4] WORKDIR /src 0.0s
=> [build 3/4] COPY . . 0.0s
=> [build 4/4] 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 - 0.7s
=> CACHED [stage-1 2/4] RUN adduser -D -u 10001 appuser 0.0s
=> CACHED [stage-1 3/4] WORKDIR /home/appuser 0.0s
=> [stage-1 4/4] COPY --from=build /out/server /usr/local/bin/server 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:f72451a65103493a2c99c0b63a3768c21af2fdd1c5679e69b96d8878fc18d3fb 0.0s
=> => naming to docker.io/ship-securely/app:dev 0.0s
View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/t419rs4ety3zd51i77w098cc0
What's next:
View a summary of image vulnerabilities and recommendations → docker scout quickview
== Hadolint (Dockerfile) ==
docker run --rm -i hadolint/hadolint < app/Dockerfile > artifacts/hadolint.txt || true
== Semgrep (SAST) ==
docker run --rm -v /Users/t/go/src/ship-securely-starter:/src returntocorp/semgrep semgrep --config p/owasp-top-ten --sarif --output /src/artifacts/semgrep.sarif /src/app || true
┌─────────────┐
│ Scan Status │
└─────────────┘
Scanning 3 files tracked by git with 542 Code rules:
Language Rules Files Origin Rules
───────────────────────────── ───────────────────
<multilang> 5 3 Community 542
go 38 1
dockerfile 4 1
┌──────────────┐
│ Scan Summary │
└──────────────┘
✅ Scan completed successfully.
• Findings: 1 (1 blocking)
• Rules run: 47
• Targets scanned: 3
• Parsed lines: ~100.0%
• No ignore information available
Ran 47 rules on 3 files: 1 finding.
== Gitleaks (secrets) ==
docker run --rm -v /Users/t/go/src/ship-securely-starter:/work zricethezav/gitleaks:latest detect -s /work -r artifacts/gitleaks.json --no-banner || true
4:09PM FTL Report path is not writable: artifacts/gitleaks.json error="open artifacts/gitleaks.json: no such file or directory"
== Checkov (IaC) ==
docker run --rm -v /Users/t/go/src/ship-securely-starter:/work bridgecrew/checkov -d /work/infra --output-file-path artifacts/checkov.json || true
_ _
___| |__ ___ ___| | _______ __
/ __| '_ \ / _ \/ __| |/ / _ \ \ / /
| (__| | | | __/ (__| < (_) \ V /
\___|_| |_|\___|\___|_|\_\___/ \_/
By Prisma Cloud | version: 3.2.476
Update available 3.2.476 -> 3.2.477
Run pip3 install -U checkov to update
kubernetes scan results:
Passed checks: 83, Failed checks: 7, Skipped checks: 0
Check: CKV_K8S_75: "Ensure that the --authorization-mode argument includes Node"
PASSED for resource: Deployment.default.app
File: /k8s/deployment.yaml:1-48
Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/ensure-that-the-authorization-mode-argument-includes-node
Check: CKV_K8S_70: "Ensure that the --token-auth-file argument is not set"
PASSED for resource: Deployment.default.app
File: /k8s/deployment.yaml:1-48
Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/kubernetes-policies/kubernetes-policy-index/ensure-that-the-token-auth-file-parameter-is-not-set
Chapter 2: Make CD
Deploy Locally (CD to Minikube)
make cd
What you should see
t@Mac ship-securely-devsecops % make cd
⛵ Minikube (ship) is running
docker build -t ship-securely/app:dev app
[+] Building 0.3s (15/15) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 876B 0.0s
=> [internal] load metadata for docker.io/library/alpine:3.20 0.3s
=> [internal] load metadata for docker.io/library/golang:1.22-alpine 0.3s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [build 1/4] FROM docker.io/library/golang:1.22-alpine@sha256:1699c10032c 0.0s
=> [stage-1 1/5] FROM docker.io/library/alpine:3.20@sha256:765942a403999233 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 447B 0.0s
=> CACHED [stage-1 2/5] RUN addgroup -g 10001 app && adduser -D -H -u 10001 0.0s
=> CACHED [stage-1 3/5] WORKDIR /home/appuser 0.0s
=> CACHED [build 2/4] WORKDIR /src 0.0s
=> CACHED [build 3/4] COPY . . 0.0s
=> CACHED [build 4/4] RUN --mount=type=cache,target=/go/pkg/mod --mount 0.0s
=> CACHED [stage-1 4/5] COPY --from=build /out/server /usr/local/bin/server 0.0s
=> CACHED [stage-1 5/5] RUN chown -R 10001:10001 /home/appuser 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:7e699d375101f7b1246378e22a53a70e9d89f63138a7f666 0.0s
=> => naming to docker.io/ship-securely/app:dev 0.0s
View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/ipq16egtws0nkd6twx07u5xs7
What's next:
View a summary of image vulnerabilities and recommendations → docker scout quickview
minikube -p ship image load ship-securely/app:dev
kubectl get ns ship >/dev/null 2>&1 || kubectl create ns ship
service/app unchanged
deployment.apps/app unchanged
kubectl -n ship set image deploy/app app=ship-securely/app:dev
kubectl -n ship rollout status deploy/app
deployment "app" successfully rolled out
== OWASP ZAP (baseline via port-forward) ==
( kubectl -n ship port-forward deploy/app 8080:3000 >/dev/null 2>&1 & echo $! > .pf.pid )
Port-forward is live
ZAP target: http://host.docker.internal:8080
Using the Automation Framework
WARN-NEW: Missing Anti-clickjacking Header [10020] x 6
WARN-NEW: X-Content-Type-Options Header Missing [10021] x 7
WARN-NEW: Content Security Policy (CSP) Header Not Set [10038] x 7
WARN-NEW: Storable and Cacheable Content [10049] x 8
WARN-NEW: Permissions Policy Header Not Set [10063] x 7
WARN-NEW: Modern Web Application [10109] x 1
WARN-NEW: Insufficient Site Isolation Against Spectre Vulnerability [90004] x 19
FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 7 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 60
✅ Deployed app to Minikube (ship) in namespace ship
Evidence Packet
Open ./artifacts/ and skim:
- sbom.json (Syft) — supply-chain inventory
- trivy-image.json or grype.json — vuln scan
- checkov.json — IaC misconfigs
- hadolint.txt — Dockerfile issues
- trufflehog.json — secrets scan
- tools/zap/report.html — DAST baseline
What’s next
- If any prereqs are red ❌, follow the “Installing …” pages or Troubleshooting.
Troubleshooting Tips
Common issues & fixes
1) Docker Desktop won’t start (Mac/Windows)
- Ensure virtualization is enabled (Windows: BIOS/UEFI + WSL2).
- Windows:
wsl --installthen reboot. In Docker Desktop → Use WSL 2 based engine. - Try a factory reset (Docker Desktop Settings → Troubleshoot → Reset to factory defaults).
2) “permission denied” with Docker on Linux
sudo usermod -aG docker $USER
newgrp docker # or log out/in
docker run --rm hello-world
3) make: command not found
- macOS:
xcode-select --installorbrew install make - Windows:
winget install -e --id GnuWin32.Make(orchoco install make) - Linux:
sudo apt install -y build-essential(Debian/Ubuntu) /dnf install make(Fedora)
4) Minikube won’t start or is slow
minikube delete -p ship
minikube start -p ship --driver=docker --cpus=2 --memory=4096
minikube status -p ship
5) minikube service ... --url prints nothing
- Ensure Service/Deployment exist:
kubectl -n ship get deploy,svc
- Reapply:
make k8s-apply
6) Port already in use (app locally)
Stop conflicting processes or change port:
- Docker compose: edit
infra/docker/compose.yaml - K8s Service: edit
infra/k8s/service.yaml(port section)
- Docker compose: edit
7) “x509: certificate signed by unknown authority” (corporate proxies)
- Configure Docker to trust your corp CA.
- For CLI pulls:
docker loginto any internal registry if required. - Consider setting proxy env vars for the Docker daemon (see Docker docs).
8) Apple Silicon image mismatch
- Most images are multi-arch. If one isn’t, try:
docker run --platform linux/arm64 ...
9) ZAP cannot reach the app
- Confirm the URL from:
make k8s-port
- If still failing, verify the Pod is ready:
kubectl -n ship get pods -o wide
kubectl -n ship describe pod/<name>
10) “permission denied” reading repo (Windows)
- If using WSL: keep the repo inside your Linux home (
\\wsl$\<distro>\home\<you>\...), not on the Windows filesystem, to avoid permission quirks.
Quick resets (safe to run)
# Reset Minikube profile
minikube delete -p ship && minikube start -p ship --driver=docker --cpus=2 --memory=4096
# Nuke dangling tool images and space (be cautious)
docker system df
docker image prune -f
Checklist
- You can start Docker Desktop (or Docker Engine)
-
minikube start -p shipsucceeds;minikube status -p shipis green -
make cifinishes without errors; artifacts exist -
make cddeploys;make k8s-portprints a working URL
minikube config view
# (note defaults you might tweak: memory/cpus/driver)
Chapter 2. Checklist
Decide how you’ll triage, verify, fix, and prove risk reduction.
Policy & Thresholds
- Blocking policy: What fails builds now? (Secrets: verified only; SAST: High/Critical curated; SCA: Critical/High w/ fixes; IaC: privileged/root)
- Unfixed CVEs: Track but don’t block, or block for specific components (base image/runtime path)?
Ownership & SLAs
- Finding owner: Who owns each class of finding (Dev vs Sec vs Platform)?
- SLAs: Critical ≤7d, High ≤30d, Medium ≤90d — do these match your risk tolerance?
- Exception process: How do you grant time-boxed risk acceptances (with expiry & rationale)?
Tuning & Evidence
- False positives: Do you maintain allowlists/ignores (
.trivyignore, Semgrep suppressions with comments)? - Artifacts: Where do
./artifacts/*live long-term (bucket/share/repo)? Who reviews ARTIFACTS.md? - Defect tracking: Do you want a local tracker (e.g., DefectDojo) or your issue system (Jira/GitHub) as the SoT?
Secrets & Rotation
- Secrets policy: What constitutes a “verified secret” vs noise? Who can revoke/rotate quickly?
- Pre-commit hooks: Will you add local hooks to catch secrets before push?
SBOM & Licensing
- SBOM consumers: Who reads
sbom.jsontoday (security, procurement, customers)? - License enforcement: Do you start in WARN then ENFORCE once tuned?