t0ul

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 --install then 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 --install or brew install make
  • Windows: winget install -e --id GnuWin32.Make (or choco 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)

7) “x509: certificate signed by unknown authority” (corporate proxies)

  • Configure Docker to trust your corp CA.
  • For CLI pulls: docker login to 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 ship succeeds; minikube status -p ship is green
  • make ci finishes without errors; artifacts exist
  • make cd deploys; make k8s-port prints 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.json today (security, procurement, customers)?
  • License enforcement: Do you start in WARN then ENFORCE once tuned?

← Back to the blog