opencode-skills-collection 3.0.31 → 3.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,988 @@
1
+ ---
2
+ name: container-security-hardening
3
+ description: >
4
+ Harden Docker/container images and runtime deployments with secure base images,
5
+ non-root users, CVE scanning, SBOM/signing, seccomp/AppArmor, and Kubernetes
6
+ pod security controls. Use for Dockerfile security reviews, container CVEs,
7
+ image scanning, distroless images, or production hardening.
8
+ category: security
9
+ risk: safe
10
+ source: community
11
+ date_added: "2026-05-30"
12
+ ---
13
+
14
+ # Container Security Hardening Skill
15
+
16
+ A production-focused guide for building, scanning, and running containers securely — from Dockerfile authoring through runtime enforcement and supply chain integrity.
17
+
18
+ ---
19
+
20
+ ## When to Use This Skill
21
+
22
+ - User mentions Docker security, container hardening, or Dockerfile security review
23
+ - User asks about distroless images, non-root containers, or read-only filesystems
24
+ - User wants to scan images for CVEs with Trivy, Grype, or Snyk
25
+ - User mentions seccomp, AppArmor, Linux capabilities, or runtime security
26
+ - User asks "is my Dockerfile secure?" or "how do I reduce my image attack surface?"
27
+ - User wants to sign/verify images with Cosign or generate SBOMs
28
+ - User asks about Kubernetes pod security, NetworkPolicy, or RBAC hardening
29
+ - User says "fix container CVEs" or "harden my container for production"
30
+
31
+ ## When NOT to Use This Skill
32
+
33
+ - The user is primarily asking about GitHub Actions CI/CD → recommend `github-actions-advanced`
34
+ - The user needs general Docker usage help (not security) → recommend `docker-expert`
35
+ - The user is working with Kubernetes orchestration beyond security → recommend `kubernetes-architect`
36
+ - The user needs application-level security (SQL injection, XSS) → recommend `api-security-best-practices`
37
+
38
+ ---
39
+
40
+ ## Step 1: Understand Context Before Responding
41
+
42
+ When invoked, first detect the current state:
43
+
44
+ ```bash
45
+ # Find Dockerfiles in the project
46
+ find . -name "Dockerfile*" -not -path "*/node_modules/*" | head -10
47
+
48
+ # Check for existing security tooling
49
+ ls .trivyignore .hadolint.yaml .snyk docker-compose*.yml 2>/dev/null
50
+
51
+ # Inspect base images currently in use
52
+ grep -r "^FROM" $(find . -name "Dockerfile*") 2>/dev/null
53
+
54
+ # Check if Kubernetes manifests exist
55
+ find . -name "*.yaml" -path "*/k8s/*" -o -name "*.yaml" -path "*/manifests/*" | head -10
56
+ ```
57
+
58
+ Then adapt recommendations to:
59
+ - The tech stack (Node, Python, Go, Java — affects base image choice)
60
+ - Whether this is Docker-only or Kubernetes-deployed
61
+ - The CI platform in use (for scanner integration)
62
+ - The existing base images and how far they are from best practice
63
+
64
+ ---
65
+
66
+ ## The Five Layers of Container Security
67
+
68
+ ```
69
+ 1. Image Build → Minimal base, no secrets, non-root, read-only FS
70
+ 2. Image Scanning → CVE scanning, SBOM, secret detection, Dockerfile lint
71
+ 3. Runtime Security → Capabilities, seccomp, AppArmor, resource limits
72
+ 4. Supply Chain → Signed images, pinned digests, trusted registries
73
+ 5. Kubernetes Layer → Pod Security Admission, NetworkPolicy, RBAC, Kyverno
74
+ ```
75
+
76
+ > Work through layers in order — hardening the image first gives the most leverage.
77
+ > See `references/base-image-comparison.md` for a full size/CVE trade-off table.
78
+
79
+ ---
80
+
81
+ ## Layer 1: Dockerfile Hardening
82
+
83
+ ### 1.1 Use a Minimal Base Image
84
+
85
+ ```dockerfile
86
+ # ❌ AVOID — massive attack surface (~100–200 CVEs typical)
87
+ FROM ubuntu:latest
88
+ FROM node:20
89
+
90
+ # ✅ BETTER — slim variants (glibc, smaller apt footprint)
91
+ FROM node:20-slim
92
+ FROM python:3.12-slim
93
+
94
+ # ✅ BEST — distroless (no shell, no package manager, built-in nonroot user)
95
+ FROM gcr.io/distroless/nodejs20-debian12
96
+ FROM gcr.io/distroless/python3-debian12
97
+ FROM gcr.io/distroless/static-debian12 # Go/Rust fully-static binaries
98
+
99
+ # ✅ ALSO GREAT — Alpine (musl libc; verify app compatibility first)
100
+ FROM alpine:3.20
101
+
102
+ # ✅ ZERO ATTACK SURFACE — for fully static binaries only
103
+ FROM scratch
104
+ ```
105
+
106
+ See `references/base-image-comparison.md` for the full trade-off matrix.
107
+
108
+ ### 1.2 Multi-Stage Build — Separate Build from Runtime
109
+
110
+ Never ship build tools, compilers, or dev dependencies in a production image.
111
+
112
+ ```dockerfile
113
+ # syntax=docker/dockerfile:1
114
+
115
+ # ── Stage 1: Install & Build ──────────────────────────────
116
+ FROM node:20-slim AS builder
117
+ WORKDIR /build
118
+ COPY package*.json ./
119
+ RUN npm ci # Install all deps (including devDeps)
120
+ COPY . .
121
+ RUN npm run build && npm prune --production
122
+
123
+ # ── Stage 2: Runtime — minimal, no build tools ────────────
124
+ FROM gcr.io/distroless/nodejs20-debian12@sha256:<digest>
125
+ LABEL org.opencontainers.image.source="https://github.com/org/repo"
126
+ LABEL org.opencontainers.image.revision="${BUILD_SHA}"
127
+ LABEL org.opencontainers.image.licenses="MIT"
128
+ WORKDIR /app
129
+ COPY --from=builder --chown=nonroot:nonroot /build/dist ./dist
130
+ COPY --from=builder --chown=nonroot:nonroot /build/node_modules ./node_modules
131
+ USER nonroot:nonroot # UID 65532 — built into distroless
132
+ EXPOSE 3000
133
+ CMD ["dist/server.js"]
134
+ ```
135
+
136
+ **Go / Rust static binary pattern:**
137
+ ```dockerfile
138
+ FROM golang:1.22-alpine AS builder
139
+ WORKDIR /build
140
+ COPY go.* ./
141
+ RUN go mod download
142
+ COPY . .
143
+ RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o app .
144
+
145
+ FROM scratch # Zero attack surface
146
+ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
147
+ COPY --from=builder /build/app /app
148
+ USER 65532:65532
149
+ ENTRYPOINT ["/app"]
150
+ ```
151
+
152
+ ### 1.3 Run as Non-Root User
153
+
154
+ ```dockerfile
155
+ # For debian/ubuntu-based images — create dedicated user
156
+ RUN groupadd -r appgroup --gid 10001 && \
157
+ useradd -r -g appgroup --uid 10001 --no-log-init appuser
158
+
159
+ COPY --chown=appuser:appgroup . /app
160
+
161
+ USER appuser # Switch before CMD/ENTRYPOINT — never run as root
162
+
163
+ # ─────────────────────────────────────────────────────────
164
+ # For Alpine-based images
165
+ RUN addgroup -g 10001 -S appgroup && \
166
+ adduser -u 10001 -S appuser -G appgroup
167
+
168
+ # For distroless — nonroot (UID 65532) is already built in
169
+ USER nonroot:nonroot
170
+ ```
171
+
172
+ ### 1.4 Pin Base Images to Digest
173
+
174
+ ```dockerfile
175
+ # ❌ UNSAFE — tags are mutable; image can be silently overwritten (supply chain attack)
176
+ FROM node:20-slim
177
+
178
+ # ✅ SAFE — SHA256 digest is cryptographically immutable
179
+ FROM node:20-slim@sha256:a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789ab
180
+ ```
181
+
182
+ **Get the current digest:**
183
+ ```bash
184
+ docker pull node:20-slim
185
+ docker inspect node:20-slim --format='{{index .RepoDigests 0}}'
186
+ ```
187
+
188
+ **Automate digest pinning** with Renovate or Dependabot:
189
+ ```json
190
+ // .renovaterc.json
191
+ {
192
+ "extends": ["config:base"],
193
+ "dockerfile": { "enabled": true },
194
+ "pinDigests": true
195
+ }
196
+ ```
197
+
198
+ ### 1.5 Never Bake Secrets into Images
199
+
200
+ ```dockerfile
201
+ # ❌ NEVER — secret in ENV or RUN; visible in `docker history` and layer cache
202
+ ENV AWS_SECRET_ACCESS_KEY=supersecret
203
+ RUN curl -H "Authorization: Bearer $TOKEN" https://api.example.com > config.json
204
+ ARG API_KEY # Also unsafe — visible in build args history
205
+
206
+ # ✅ CORRECT — BuildKit secret mount (never persisted in any layer)
207
+ # syntax=docker/dockerfile:1
208
+ RUN --mount=type=secret,id=api_token \
209
+ curl -H "Authorization: Bearer $(cat /run/secrets/api_token)" \
210
+ https://api.example.com/config > config.json
211
+ ```
212
+
213
+ Build with: `docker build --secret id=api_token,src=./token.txt .`
214
+
215
+ **Check your image for leaked secrets:**
216
+ ```bash
217
+ docker history --no-trunc myapp:latest | grep -iE "secret|key|password|token"
218
+ trivy image --scanners secret myapp:latest
219
+ ```
220
+
221
+ ### 1.6 Read-Only Filesystem & No New Privileges
222
+
223
+ ```dockerfile
224
+ # In the Dockerfile — use exec form (no shell interpretation)
225
+ ENTRYPOINT ["node", "server.js"] # ✅ exec form
226
+ # ENTRYPOINT /bin/sh -c "node..." # ❌ shell form — spawns extra process
227
+
228
+ # Define a HEALTHCHECK
229
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
230
+ CMD ["node", "-e", "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"]
231
+ ```
232
+
233
+ Enforce read-only at runtime (see Layer 3).
234
+
235
+ ### 1.7 Minimal .dockerignore
236
+
237
+ ```dockerignore
238
+ # Always exclude these from build context
239
+ .git
240
+ .github
241
+ .env
242
+ .env.*
243
+ *.pem
244
+ *.key
245
+ node_modules
246
+ __pycache__
247
+ .pytest_cache
248
+ coverage/
249
+ dist/
250
+ *.log
251
+ .DS_Store
252
+ Dockerfile*
253
+ docker-compose*
254
+ README.md
255
+ docs/
256
+ tests/
257
+ ```
258
+
259
+ ### 1.8 Full Hardened Dockerfile Example
260
+
261
+ ```dockerfile
262
+ # syntax=docker/dockerfile:1
263
+
264
+ # ── Build stage ───────────────────────────────────────────
265
+ FROM node:20-slim AS builder
266
+ WORKDIR /build
267
+ COPY package*.json ./
268
+ RUN --mount=type=cache,target=/root/.npm \
269
+ npm ci
270
+ COPY . .
271
+ RUN npm run build && npm prune --production
272
+
273
+ # ── Runtime stage ─────────────────────────────────────────
274
+ FROM gcr.io/distroless/nodejs20-debian12@sha256:<pin-digest-here>
275
+
276
+ LABEL org.opencontainers.image.source="https://github.com/org/repo"
277
+ LABEL org.opencontainers.image.revision="${BUILD_SHA}"
278
+ LABEL org.opencontainers.image.licenses="MIT"
279
+
280
+ WORKDIR /app
281
+ COPY --from=builder --chown=nonroot:nonroot /build/dist ./dist
282
+ COPY --from=builder --chown=nonroot:nonroot /build/node_modules ./node_modules
283
+
284
+ USER nonroot:nonroot
285
+ EXPOSE 3000
286
+
287
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
288
+ CMD ["node", "-e", "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode===200?0:1))"]
289
+
290
+ CMD ["dist/server.js"]
291
+ ```
292
+
293
+ ---
294
+
295
+ ## Layer 2: Image Scanning
296
+
297
+ ### 2.1 Trivy (Recommended — Fast, Comprehensive)
298
+
299
+ ```bash
300
+ # Install
301
+ brew install trivy # macOS
302
+ apt install trivy # Debian/Ubuntu
303
+ curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
304
+ -o /tmp/trivy-install.sh
305
+ less /tmp/trivy-install.sh
306
+ sh /tmp/trivy-install.sh
307
+
308
+ # Scan an image for CVEs
309
+ trivy image myapp:latest
310
+
311
+ # Fail CI on HIGH/CRITICAL severity
312
+ trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:latest
313
+
314
+ # Scan Dockerfile for misconfigurations
315
+ trivy config ./Dockerfile
316
+
317
+ # Scan entire repo (vulnerabilities + secrets + misconfigs)
318
+ trivy fs --scanners vuln,secret,misconfig .
319
+
320
+ # Generate SBOM (CycloneDX or SPDX)
321
+ trivy image --format cyclonedx --output sbom.json myapp:latest
322
+ trivy image --format spdx-json --output sbom.spdx.json myapp:latest
323
+
324
+ # Ignore specific CVEs (add justification comments)
325
+ trivy image --ignorefile .trivyignore myapp:latest
326
+ ```
327
+
328
+ **.trivyignore example:**
329
+ ```
330
+ # CVE-2023-1234 — only exploitable via X feature, not used in this app
331
+ CVE-2023-1234
332
+
333
+ # CVE-2023-5678 — fix not yet available; tracked in issue #42
334
+ CVE-2023-5678
335
+ ```
336
+
337
+ ### 2.2 Grype (Anchore Alternative)
338
+
339
+ ```bash
340
+ # Install
341
+ curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh \
342
+ -o /tmp/grype-install.sh
343
+ less /tmp/grype-install.sh
344
+ sh /tmp/grype-install.sh
345
+
346
+ # Scan image
347
+ grype myapp:latest
348
+
349
+ # Fail on critical
350
+ grype myapp:latest --fail-on critical
351
+
352
+ # Output SARIF for GitHub Security tab
353
+ grype myapp:latest -o sarif > results.sarif
354
+
355
+ # Pair with Syft for SBOM generation
356
+ syft myapp:latest -o cyclonedx-json > sbom.json
357
+ grype sbom:sbom.json # Scan the SBOM directly
358
+ ```
359
+
360
+ ### 2.3 Hadolint — Dockerfile Linting
361
+
362
+ ```bash
363
+ # Run directly
364
+ docker run --rm -i hadolint/hadolint < Dockerfile
365
+
366
+ # With config file
367
+ hadolint --config .hadolint.yaml --failure-threshold warning Dockerfile
368
+ ```
369
+
370
+ **.hadolint.yaml:**
371
+ ```yaml
372
+ failure-threshold: warning
373
+ ignore:
374
+ - DL3008 # Pin versions in apt-get (allow floating for base layer)
375
+ trustedRegistries:
376
+ - gcr.io
377
+ - ghcr.io
378
+ - public.ecr.aws
379
+ ```
380
+
381
+ ### 2.4 Secret Scanning in Images
382
+
383
+ ```bash
384
+ # Trivy covers secrets too
385
+ trivy image --scanners secret myapp:latest
386
+
387
+ # Dedicated: TruffleHog
388
+ trufflehog docker --image myapp:latest
389
+
390
+ # git-secrets to prevent committing secrets
391
+ git secrets --scan
392
+ ```
393
+
394
+ ### 2.5 CI Integration (GitHub Actions — SHA-Pinned)
395
+
396
+ ```yaml
397
+ permissions:
398
+ contents: read
399
+ security-events: write # Required for uploading SARIF
400
+
401
+ jobs:
402
+ security-scan:
403
+ runs-on: ubuntu-24.04
404
+ timeout-minutes: 20
405
+ steps:
406
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
407
+
408
+ - name: Build image
409
+ run: docker build -t myapp:${{ github.sha }} .
410
+
411
+ - name: Lint Dockerfile
412
+ uses: hadolint/hadolint-action@54c9adbab1582c2ef04b2016b760714a4bfde3cf # v3.1.0
413
+ with:
414
+ dockerfile: Dockerfile
415
+ failure-threshold: warning
416
+
417
+ - name: Scan with Trivy
418
+ uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # v0.28.0
419
+ with:
420
+ image-ref: myapp:${{ github.sha }}
421
+ format: sarif
422
+ output: trivy-results.sarif
423
+ severity: HIGH,CRITICAL
424
+ exit-code: '1'
425
+
426
+ - name: Upload results to GitHub Security tab
427
+ uses: github/codeql-action/upload-sarif@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1
428
+ if: always() # Upload even if scan found issues
429
+ with:
430
+ sarif_file: trivy-results.sarif
431
+ ```
432
+
433
+ ---
434
+
435
+ ## Layer 3: Runtime Security
436
+
437
+ ### 3.1 docker run Hardening Flags
438
+
439
+ ```bash
440
+ docker run \
441
+ --read-only \ # Read-only root filesystem
442
+ --tmpfs /tmp:noexec,nosuid,size=100m \ # Writable tmpfs for /tmp only
443
+ --tmpfs /var/run \ # For PID files if needed
444
+ --user 10001:10001 \ # Non-root UID:GID
445
+ --cap-drop ALL \ # Drop ALL Linux capabilities
446
+ --cap-add NET_BIND_SERVICE \ # Re-add only what's truly needed
447
+ --security-opt no-new-privileges:true \ # Prevent privilege escalation via setuid
448
+ --security-opt seccomp=seccomp.json \ # Custom seccomp profile
449
+ --security-opt apparmor=docker-default \ # AppArmor profile
450
+ --pids-limit 100 \ # Prevent fork bombs
451
+ --memory 512m \ # OOM protection
452
+ --memory-swap 512m \ # Disable swap
453
+ --cpus 1.0 \ # CPU limit
454
+ --network none \ # No network (if not needed)
455
+ --health-cmd "curl -f http://localhost:3000/health || exit 1" \
456
+ --health-interval 30s \
457
+ myapp:latest
458
+ ```
459
+
460
+ ### 3.2 Linux Capabilities — What to Drop and Keep
461
+
462
+ Drop ALL, then explicitly add only what your app requires:
463
+
464
+ | Capability | Purpose | Keep? |
465
+ |---|---|---|
466
+ | `NET_BIND_SERVICE` | Bind ports < 1024 | Only if binding a privileged port |
467
+ | `CHOWN` | Change file ownership | No — set ownership at build time |
468
+ | `SETUID` / `SETGID` | Switch user identity | No — drop always |
469
+ | `SYS_ADMIN` | Broad privileged operations | No — most dangerous capability |
470
+ | `NET_ADMIN` | Configure network interfaces | No (only network tools) |
471
+ | `SYS_PTRACE` | Debug/trace processes | No (only debugger containers) |
472
+ | `DAC_OVERRIDE` | Override file permissions | No — runs as correct user |
473
+ | `NET_RAW` | Raw sockets (ping) | No (blocked by default seccomp anyway) |
474
+
475
+ > **Most web apps need zero capabilities.** `--cap-drop ALL` alone is often sufficient.
476
+
477
+ ### 3.3 Docker Compose Hardening
478
+
479
+ ```yaml
480
+ services:
481
+ app:
482
+ image: myapp:latest
483
+ read_only: true
484
+ user: "10001:10001"
485
+ tmpfs:
486
+ - /tmp:noexec,nosuid,size=100m
487
+ - /var/run:noexec,nosuid,size=10m
488
+ cap_drop:
489
+ - ALL
490
+ cap_add:
491
+ - NET_BIND_SERVICE # Only if binding port < 1024
492
+ security_opt:
493
+ - no-new-privileges:true
494
+ - seccomp:./references/seccomp-profile-template.json
495
+ pids_limit: 100
496
+ mem_limit: 512m
497
+ memswap_limit: 512m
498
+ cpus: 1.0
499
+ healthcheck:
500
+ test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
501
+ interval: 30s
502
+ timeout: 5s
503
+ retries: 3
504
+ start_period: 10s
505
+ networks:
506
+ - backend
507
+ # Only expose externally if truly required
508
+ # ports: ["8080:8080"]
509
+ restart: unless-stopped
510
+ logging:
511
+ driver: json-file
512
+ options:
513
+ max-size: "10m"
514
+ max-file: "3"
515
+
516
+ networks:
517
+ backend:
518
+ driver: bridge
519
+ internal: true # No external connectivity unless needed
520
+ ```
521
+
522
+ ### 3.4 Seccomp Profiles
523
+
524
+ The Docker default seccomp profile blocks ~44 dangerous syscalls. For stricter control:
525
+
526
+ ```bash
527
+ # Step 1: Audit syscalls your app actually makes
528
+ docker run --security-opt seccomp=unconfined \
529
+ --name audit-run myapp:latest &
530
+
531
+ # Capture with strace
532
+ strace -c -p $(docker inspect --format '{{.State.Pid}}' audit-run)
533
+
534
+ # Or with sysdig (more container-friendly)
535
+ sysdig -p "%syscall.type" container.name=audit-run | sort -u
536
+
537
+ # Step 2: Build a custom profile from references/seccomp-profile-template.json
538
+ # Step 3: Apply it
539
+ docker run --security-opt seccomp=references/seccomp-profile-template.json myapp:latest
540
+ ```
541
+
542
+ See `references/seccomp-profile-template.json` for a minimal starting allowlist for typical web servers.
543
+
544
+ ### 3.5 AppArmor Profile (Linux hosts)
545
+
546
+ ```bash
547
+ # Load Docker's default AppArmor profile
548
+ sudo apparmor_parser -r /etc/apparmor.d/docker-default
549
+
550
+ # Apply at runtime
551
+ docker run --security-opt apparmor=docker-default myapp:latest
552
+
553
+ # Generate a custom profile
554
+ aa-genprof myapp # Interactive — run app under aa-complain mode first
555
+ ```
556
+
557
+ ---
558
+
559
+ ## Layer 4: Supply Chain Security
560
+
561
+ ### 4.1 Sign Images with Cosign (Sigstore — Keyless)
562
+
563
+ ```bash
564
+ # Install cosign
565
+ brew install cosign # macOS
566
+ # or: https://github.com/sigstore/cosign/releases
567
+
568
+ # Sign after push — keyless via OIDC (no long-lived keys)
569
+ cosign sign ghcr.io/org/myapp:latest
570
+
571
+ # Verify before deploy
572
+ cosign verify ghcr.io/org/myapp:latest \
573
+ --certificate-identity-regexp="https://github.com/org/repo" \
574
+ --certificate-oidc-issuer="https://token.actions.githubusercontent.com"
575
+ ```
576
+
577
+ **GitHub Actions — Sign & Verify Pipeline:**
578
+ ```yaml
579
+ permissions:
580
+ id-token: write # Required for OIDC keyless signing
581
+ packages: write
582
+
583
+ steps:
584
+ - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
585
+
586
+ - name: Sign image (keyless via OIDC)
587
+ run: |
588
+ cosign sign --yes \
589
+ ghcr.io/${{ github.repository }}:${{ github.sha }}
590
+ env:
591
+ COSIGN_EXPERIMENTAL: "true"
592
+
593
+ - name: Attach SBOM attestation
594
+ run: |
595
+ cosign attest --yes \
596
+ --predicate sbom.json \
597
+ --type cyclonedx \
598
+ ghcr.io/${{ github.repository }}:${{ github.sha }}
599
+ ```
600
+
601
+ ### 4.2 SBOM Generation & Attestation
602
+
603
+ ```bash
604
+ # Generate SBOM with Syft
605
+ syft myapp:latest -o cyclonedx-json > sbom.json
606
+ syft myapp:latest -o spdx-json > sbom.spdx.json
607
+
608
+ # Attach to image as attestation
609
+ cosign attest --predicate sbom.json --type cyclonedx ghcr.io/org/myapp:latest
610
+
611
+ # Verify SBOM attestation before deployment
612
+ cosign verify-attestation \
613
+ --type cyclonedx \
614
+ --certificate-identity-regexp="https://github.com/org/repo" \
615
+ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
616
+ ghcr.io/org/myapp:latest
617
+ ```
618
+
619
+ ### 4.3 Use Trusted Registries & Enable Registry Scanning
620
+
621
+ | Registry | Built-in Scanning | Notes |
622
+ |---|---|---|
623
+ | GHCR (GitHub Container Registry) | No (use Trivy in CI) | Best for OSS, OIDC auth |
624
+ | AWS ECR | Yes (enhanced scanning via Inspector) | Enable per-repo |
625
+ | GCP Artifact Registry | Yes (Container Analysis) | Enabled by default |
626
+ | Azure ACR | Yes (Defender for Containers) | Premium tier |
627
+ | Docker Hub | Yes (limited on free tier) | Avoid for private images |
628
+
629
+ ```bash
630
+ # Enable ECR enhanced scanning
631
+ aws ecr put-registry-scanning-configuration \
632
+ --scan-type ENHANCED \
633
+ --rules '[{"repositoryFilters":[{"filter":"*","filterType":"WILDCARD"}],"scanFrequency":"CONTINUOUS_SCAN"}]'
634
+ ```
635
+
636
+ ### 4.4 Admission Control — Block Unsigned/Unscanned Images
637
+
638
+ ```yaml
639
+ # Kyverno policy — require signed images before admission
640
+ apiVersion: kyverno.io/v1
641
+ kind: ClusterPolicy
642
+ metadata:
643
+ name: require-signed-images
644
+ spec:
645
+ validationFailureAction: Enforce
646
+ rules:
647
+ - name: verify-image-signature
648
+ match:
649
+ resources:
650
+ kinds: [Pod]
651
+ verifyImages:
652
+ - imageReferences:
653
+ - "ghcr.io/org/*"
654
+ attestors:
655
+ - entries:
656
+ - keyless:
657
+ subject: "https://github.com/org/repo/.github/workflows/*"
658
+ issuer: "https://token.actions.githubusercontent.com"
659
+ ```
660
+
661
+ ---
662
+
663
+ ## Layer 5: Kubernetes Pod Security
664
+
665
+ > Full reference: `references/kubernetes-pod-security.md`
666
+
667
+ ### 5.1 Pod Security Context
668
+
669
+ ```yaml
670
+ apiVersion: apps/v1
671
+ kind: Deployment
672
+ metadata:
673
+ name: myapp
674
+ namespace: production
675
+ spec:
676
+ replicas: 3
677
+ template:
678
+ spec:
679
+ # ── Pod-level security context ─────────────────────
680
+ securityContext:
681
+ runAsNonRoot: true
682
+ runAsUser: 10001
683
+ runAsGroup: 10001
684
+ fsGroup: 10001
685
+ fsGroupChangePolicy: OnRootMismatch
686
+ seccompProfile:
687
+ type: RuntimeDefault # Use containerd/runc default seccomp
688
+ supplementalGroups: []
689
+
690
+ automountServiceAccountToken: false # Disable unless needed
691
+
692
+ # ── Container-level security context ──────────────
693
+ containers:
694
+ - name: app
695
+ image: ghcr.io/org/myapp@sha256:<digest> # Always use digest
696
+ securityContext:
697
+ allowPrivilegeEscalation: false
698
+ readOnlyRootFilesystem: true
699
+ capabilities:
700
+ drop: ["ALL"]
701
+ add: [] # Add nothing unless absolutely required
702
+ runAsNonRoot: true
703
+ runAsUser: 10001
704
+ seccompProfile:
705
+ type: RuntimeDefault
706
+
707
+ # ── Resource limits (required for restricted PSA) ──
708
+ resources:
709
+ requests:
710
+ memory: "128Mi"
711
+ cpu: "100m"
712
+ limits:
713
+ memory: "512Mi"
714
+ cpu: "500m"
715
+
716
+ # ── Writable tmpfs mounts ──────────────────────
717
+ volumeMounts:
718
+ - name: tmp
719
+ mountPath: /tmp
720
+ - name: varrun
721
+ mountPath: /var/run
722
+
723
+ volumes:
724
+ - name: tmp
725
+ emptyDir:
726
+ medium: Memory
727
+ sizeLimit: 100Mi
728
+ - name: varrun
729
+ emptyDir:
730
+ medium: Memory
731
+ sizeLimit: 10Mi
732
+ ```
733
+
734
+ ### 5.2 Pod Security Admission (K8s 1.25+)
735
+
736
+ ```bash
737
+ # Audit existing workloads before enforcing
738
+ kubectl label namespace production \
739
+ pod-security.kubernetes.io/audit=restricted \
740
+ pod-security.kubernetes.io/audit-version=latest
741
+
742
+ # Warn in staging, enforce in production
743
+ kubectl label namespace staging \
744
+ pod-security.kubernetes.io/warn=restricted
745
+
746
+ kubectl label namespace production \
747
+ pod-security.kubernetes.io/enforce=restricted \
748
+ pod-security.kubernetes.io/enforce-version=latest
749
+ ```
750
+
751
+ | PSA Level | What It Blocks |
752
+ |---|---|
753
+ | `privileged` | No restrictions |
754
+ | `baseline` | Blocks hostNetwork, hostPID, privileged containers, hostPath |
755
+ | `restricted` | Also requires non-root, read-only FS, drops capabilities, seccomp |
756
+
757
+ ### 5.3 NetworkPolicy — Zero-Trust Networking
758
+
759
+ ```yaml
760
+ # Step 1: Deny all ingress and egress by default in the namespace
761
+ apiVersion: networking.k8s.io/v1
762
+ kind: NetworkPolicy
763
+ metadata:
764
+ name: default-deny-all
765
+ namespace: production
766
+ spec:
767
+ podSelector: {}
768
+ policyTypes: [Ingress, Egress]
769
+
770
+ ---
771
+ # Step 2: Selectively allow only required traffic
772
+ apiVersion: networking.k8s.io/v1
773
+ kind: NetworkPolicy
774
+ metadata:
775
+ name: allow-app
776
+ namespace: production
777
+ spec:
778
+ podSelector:
779
+ matchLabels:
780
+ app: myapp
781
+ policyTypes: [Ingress, Egress]
782
+ ingress:
783
+ - from:
784
+ - namespaceSelector:
785
+ matchLabels:
786
+ kubernetes.io/metadata.name: ingress-nginx
787
+ ports:
788
+ - port: 3000
789
+ egress:
790
+ - to:
791
+ - podSelector:
792
+ matchLabels:
793
+ app: postgres
794
+ ports:
795
+ - port: 5432
796
+ - to: {} # Allow DNS
797
+ ports:
798
+ - port: 53
799
+ protocol: UDP
800
+ - port: 53
801
+ protocol: TCP
802
+ ```
803
+
804
+ ### 5.4 RBAC — Least Privilege
805
+
806
+ ```yaml
807
+ # Create minimal role — never use wildcards
808
+ apiVersion: rbac.authorization.k8s.io/v1
809
+ kind: Role
810
+ metadata:
811
+ name: app-reader
812
+ namespace: production
813
+ rules:
814
+ - apiGroups: [""]
815
+ resources: ["configmaps", "secrets"]
816
+ resourceNames: ["myapp-config"] # Lock to specific resource names
817
+ verbs: ["get"] # Never ["*"]
818
+
819
+ ---
820
+ apiVersion: rbac.authorization.k8s.io/v1
821
+ kind: RoleBinding
822
+ metadata:
823
+ name: app-reader-binding
824
+ namespace: production
825
+ subjects:
826
+ - kind: ServiceAccount
827
+ name: myapp-sa
828
+ namespace: production
829
+ roleRef:
830
+ kind: Role
831
+ name: app-reader
832
+ apiGroup: rbac.authorization.k8s.io
833
+ ```
834
+
835
+ ```bash
836
+ # Audit what permissions a service account has
837
+ kubectl auth can-i --list --as=system:serviceaccount:production:myapp-sa
838
+
839
+ # Find overly-permissive cluster roles
840
+ kubectl get clusterrolebindings -o json | \
841
+ jq '.items[] | select(.roleRef.name == "cluster-admin") | .subjects'
842
+ ```
843
+
844
+ ### 5.5 Kyverno Policy Examples
845
+
846
+ ```yaml
847
+ # Require non-root containers
848
+ apiVersion: kyverno.io/v1
849
+ kind: ClusterPolicy
850
+ metadata:
851
+ name: require-non-root
852
+ spec:
853
+ validationFailureAction: Enforce
854
+ rules:
855
+ - name: check-run-as-non-root
856
+ match:
857
+ resources:
858
+ kinds: [Pod]
859
+ validate:
860
+ message: "Containers must not run as root (runAsNonRoot: true required)"
861
+ pattern:
862
+ spec:
863
+ containers:
864
+ - securityContext:
865
+ runAsNonRoot: true
866
+
867
+ ---
868
+ # Require image digest pinning
869
+ apiVersion: kyverno.io/v1
870
+ kind: ClusterPolicy
871
+ metadata:
872
+ name: require-image-digest
873
+ spec:
874
+ validationFailureAction: Enforce
875
+ rules:
876
+ - name: check-digest
877
+ match:
878
+ resources:
879
+ kinds: [Pod]
880
+ validate:
881
+ message: "Images must be pinned to a SHA256 digest, not just a tag"
882
+ pattern:
883
+ spec:
884
+ containers:
885
+ - image: "*@sha256:*"
886
+
887
+ ---
888
+ # Block privileged containers
889
+ apiVersion: kyverno.io/v1
890
+ kind: ClusterPolicy
891
+ metadata:
892
+ name: disallow-privileged
893
+ spec:
894
+ validationFailureAction: Enforce
895
+ rules:
896
+ - name: check-privileged
897
+ match:
898
+ resources:
899
+ kinds: [Pod]
900
+ validate:
901
+ message: "Privileged containers are not allowed"
902
+ pattern:
903
+ spec:
904
+ containers:
905
+ - =(securityContext):
906
+ =(privileged): "false"
907
+ ```
908
+
909
+ ---
910
+
911
+ ## Common Pitfalls & Fixes
912
+
913
+ | Problem | Root Cause | Fix |
914
+ |---|---|---|
915
+ | Image runs as root | No `USER` directive | Add `RUN useradd ...` and `USER appuser` |
916
+ | Secret in `docker history` | `ENV` or `RUN curl -H "Bearer $TOKEN"` | Use `RUN --mount=type=secret` |
917
+ | Large image with many CVEs | Full base image (`node:20`, `ubuntu`) | Switch to `node:20-slim` or `distroless` |
918
+ | App crashes with `--read-only` | Writes to `/tmp` or app directory | Add `--tmpfs /tmp` for writable temp space |
919
+ | Trivy scan blocks CI on unfixable CVEs | No ignore file | Add `.trivyignore` with justified entries |
920
+ | Container needs `SYS_ADMIN` | Missing `--cap-drop` context | Investigate why — almost always avoidable |
921
+ | Tag-based images drift over time | Mutable tags | Pin to `@sha256:` digest; use Renovate to update |
922
+ | K8s pod rejected by PSA | Missing security context fields | Add `runAsNonRoot`, `readOnlyRootFilesystem`, `allowPrivilegeEscalation: false` |
923
+ | App can't write to filesystem | `readOnlyRootFilesystem: true` | Mount `emptyDir` volumes for writable paths |
924
+
925
+ ---
926
+
927
+ ## Security Checklist
928
+
929
+ ### Dockerfile
930
+ - [ ] Minimal base image (distroless, slim, or alpine — not full debian/ubuntu)
931
+ - [ ] Multi-stage build — no build tools, devDependencies, or compilers in runtime image
932
+ - [ ] Non-root `USER` declared before `CMD`/`ENTRYPOINT`
933
+ - [ ] Base image pinned to `@sha256:...` digest (not just tag)
934
+ - [ ] No secrets in `ENV`, `ARG`, or `RUN` commands
935
+ - [ ] `HEALTHCHECK` defined
936
+ - [ ] OCI labels present (`org.opencontainers.image.*`)
937
+ - [ ] `.dockerignore` excludes `.git`, `.env`, secrets, tests
938
+ - [ ] `ENTRYPOINT` uses exec form, not shell form
939
+
940
+ ### Image Scanning
941
+ - [ ] Trivy or Grype scan in CI (fails on HIGH/CRITICAL)
942
+ - [ ] Hadolint passes with no warnings
943
+ - [ ] Secret scan run on image (`trivy --scanners secret`)
944
+ - [ ] SBOM generated and stored
945
+ - [ ] `.trivyignore` has justified entries for accepted CVEs
946
+
947
+ ### Runtime
948
+ - [ ] `--read-only` filesystem
949
+ - [ ] `--cap-drop ALL` (add back only what's documented as required)
950
+ - [ ] `--security-opt no-new-privileges:true`
951
+ - [ ] `--security-opt seccomp=<profile>` applied
952
+ - [ ] Resource limits set (`--memory`, `--cpus`, `--pids-limit`)
953
+ - [ ] Image signed with Cosign; verified before deploy
954
+
955
+ ### Kubernetes
956
+ - [ ] `readOnlyRootFilesystem: true`
957
+ - [ ] `allowPrivilegeEscalation: false`
958
+ - [ ] `runAsNonRoot: true` with explicit UID
959
+ - [ ] `capabilities.drop: ["ALL"]`
960
+ - [ ] Resource `requests` and `limits` defined
961
+ - [ ] `automountServiceAccountToken: false`
962
+ - [ ] Namespace PSA enforced at `restricted` level
963
+ - [ ] `NetworkPolicy` default-deny applied
964
+ - [ ] RBAC uses specific resource names and minimal verbs
965
+
966
+ ---
967
+
968
+ ## Reference Files
969
+
970
+ - `references/base-image-comparison.md` — Size, CVE count, shell/pkg-manager trade-offs: distroless vs alpine vs slim vs scratch
971
+ - `references/seccomp-profile-template.json` — Minimal syscall allowlist for typical web servers; start here and extend
972
+ - `references/kubernetes-pod-security.md` — NetworkPolicy, RBAC, OPA/Kyverno policies, service account hardening, PSA
973
+
974
+ ## Related Skills
975
+
976
+ - `docker-expert` — General Docker usage, Compose orchestration, image optimization
977
+ - `gha-security-review` — Security audit of GitHub Actions workflows
978
+ - `github-actions-advanced` — CI pipeline patterns including scanner integration
979
+ - `kubernetes-architect` — Full Kubernetes architecture, not just security
980
+ - `api-security-best-practices` — Application-level security (injection, auth, OWASP)
981
+ - `k8s-security-policies` — Extended Kubernetes security policies
982
+
983
+ ## Limitations
984
+
985
+ - Use this skill only when the task clearly matches the scope described above.
986
+ - Do not treat the output as a substitute for environment-specific penetration testing or a formal security audit.
987
+ - Seccomp profiles and AppArmor are Linux-only; macOS/Windows Docker Desktop uses different mechanisms.
988
+ - Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.