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.
- package/bundled-skills/.antigravity-install-manifest.json +5 -1
- package/bundled-skills/container-security-hardening/SKILL.md +988 -0
- package/bundled-skills/container-security-hardening/references/base-image-comparison.md +245 -0
- package/bundled-skills/container-security-hardening/references/kubernetes-pod-security.md +561 -0
- package/bundled-skills/container-security-hardening/references/seccomp-profile-template.json +337 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/github-actions-advanced/SKILL.md +1075 -0
- package/bundled-skills/longbridge/SKILL.md +91 -0
- package/bundled-skills/runaway-guard/SKILL.md +331 -0
- package/package.json +1 -1
- package/skills_index.json +131 -27
|
@@ -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.
|