Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Harden Docker/container images and runtime deployments with secure base images, non-root users, CVE scanning, SBOM/signing, seccomp/AppArmor, and Kubernetes pod security controls. Use for Dockerfile security reviews, container CVEs, image scanning, distroless images, or production hardening.
.claude/skills/lingxling-container-security-hardening/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 253% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 288% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 371% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 533% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 388% | 0% |
A production-focused guide for building, scanning, and running containers securely — from Dockerfile authoring through runtime enforcement and supply chain integrity.
github-actions-advanceddocker-expertkubernetes-architectapi-security-best-practicesWhen invoked, first detect the current state:
bash# Find Dockerfiles in the project find . -name "Dockerfile*" -not -path "*/node_modules/*" | head -10 # Check for existing security tooling ls .trivyignore .hadolint.yaml .snyk docker-compose*.yml 2>/dev/null # Inspect base images currently in use grep -r "^FROM" $(find . -name "Dockerfile*") 2>/dev/null # Check if Kubernetes manifests exist find . -name "*.yaml" -path "*/k8s/*" -o -name "*.yaml" -path "*/manifests/*" | head -10
Then adapt recommendations to:
1. Image Build → Minimal base, no secrets, non-root, read-only FS
2. Image Scanning → CVE scanning, SBOM, secret detection, Dockerfile lint
3. Runtime Security → Capabilities, seccomp, AppArmor, resource limits
4. Supply Chain → Signed images, pinned digests, trusted registries
5. Kubernetes Layer → Pod Security Admission, NetworkPolicy, RBAC, Kyverno> Work through layers in order — hardening the image first gives the most leverage. > See references/base-image-comparison.md for a full size/CVE trade-off table.
dockerfile# ❌ AVOID — massive attack surface (~100–200 CVEs typical) FROM ubuntu:latest FROM node:20 # ✅ BETTER — slim variants (glibc, smaller apt footprint) FROM node:20-slim FROM python:3.12-slim # ✅ BEST — distroless (no shell, no package manager, built-in nonroot user) FROM gcr.io/distroless/nodejs20-debian12 FROM gcr.io/distroless/python3-debian12 FROM gcr.io/distroless/static-debian12 # Go/Rust fully-static binaries # ✅ ALSO GREAT — Alpine (musl libc; verify app compatibility first) FROM alpine:3.20 # ✅ ZERO ATTACK SURFACE — for fully static binaries only FROM scratch
See references/base-image-comparison.md for the full trade-off matrix.
Never ship build tools, compilers, or dev dependencies in a production image.
dockerfile# syntax=docker/dockerfile:1 # ── Stage 1: Install & Build ────────────────────────────── FROM node:20-slim AS builder WORKDIR /build COPY package*.json ./ RUN npm ci # Install all deps (including devDeps) COPY . . RUN npm run build && npm prune --production # ── Stage 2: Runtime — minimal, no build tools ──────────── FROM gcr.io/distroless/nodejs20-debian12@sha256:<digest> LABEL org.opencontainers.image.source="https://github.com/org/repo" LABEL org.opencontainers.image.revision="${BUILD_SHA}" LABEL org.opencontainers.image.licenses="MIT" WORKDIR /app COPY --from=builder --chown=nonroot:nonroot /build/dist ./dist COPY --from=builder --chown=nonroot:nonroot /build/node_modules ./node_modules USER nonroot:nonroot # UID 65532 — built into distroless EXPOSE 3000 CMD ["dist/server.js"]
Go / Rust static binary pattern:
dockerfileFROM golang:1.22-alpine AS builder WORKDIR /build COPY go.* ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o app . FROM scratch # Zero attack surface COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /build/app /app USER 65532:65532 ENTRYPOINT ["/app"]
dockerfile# For debian/ubuntu-based images — create dedicated user RUN groupadd -r appgroup --gid 10001 && \ useradd -r -g appgroup --uid 10001 --no-log-init appuser COPY --chown=appuser:appgroup . /app USER appuser # Switch before CMD/ENTRYPOINT — never run as root # ───────────────────────────────────────────────────────── # For Alpine-based images RUN addgroup -g 10001 -S appgroup && \ adduser -u 10001 -S appuser -G appgroup # For distroless — nonroot (UID 65532) is already built in USER nonroot:nonroot
dockerfile# ❌ UNSAFE — tags are mutable; image can be silently overwritten (supply chain attack) FROM node:20-slim # ✅ SAFE — SHA256 digest is cryptographically immutable FROM node:20-slim@sha256:a1b2c3d4e5f6789abcdef0123456789abcdef0123456789abcdef0123456789ab
Get the current digest:
bashdocker pull node:20-slim docker inspect node:20-slim --format='{{index .RepoDigests 0}}'
Automate digest pinning with Renovate or Dependabot:
json// .renovaterc.json { "extends": ["config:base"], "dockerfile": { "enabled": true }, "pinDigests": true }
dockerfile# ❌ NEVER — secret in ENV or RUN; visible in `docker history` and layer cache ENV AWS_SECRET_ACCESS_KEY=supersecret RUN curl -H "Authorization: Bearer $TOKEN" https://api.example.com > config.json ARG API_KEY # Also unsafe — visible in build args history # ✅ CORRECT — BuildKit secret mount (never persisted in any layer) # syntax=docker/dockerfile:1 RUN --mount=type=secret,id=api_token \ curl -H "Authorization: Bearer $(cat /run/secrets/api_token)" \ https://api.example.com/config > config.json
Build with: docker build --secret id=api_token,src=./token.txt .
Check your image for leaked secrets:
bashdocker history --no-trunc myapp:latest | grep -iE "secret|key|password|token" trivy image --scanners secret myapp:latest
dockerfile# In the Dockerfile — use exec form (no shell interpretation) ENTRYPOINT ["node", "server.js"] # ✅ exec form # ENTRYPOINT /bin/sh -c "node..." # ❌ shell form — spawns extra process # Define a HEALTHCHECK HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD ["node", "-e", "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"]
Enforce read-only at runtime (see Layer 3).
dockerignore# Always exclude these from build context .git .github .env .env.* *.pem *.key node_modules __pycache__ .pytest_cache coverage/ dist/ *.log .DS_Store Dockerfile* docker-compose* README.md docs/ tests/
dockerfile# syntax=docker/dockerfile:1 # ── Build stage ─────────────────────────────────────────── FROM node:20-slim AS builder WORKDIR /build COPY package*.json ./ RUN --mount=type=cache,target=/root/.npm \ npm ci COPY . . RUN npm run build && npm prune --production # ── Runtime stage ───────────────────────────────────────── FROM gcr.io/distroless/nodejs20-debian12@sha256:<pin-digest-here> LABEL org.opencontainers.image.source="https://github.com/org/repo" LABEL org.opencontainers.image.revision="${BUILD_SHA}" LABEL org.opencontainers.image.licenses="MIT" WORKDIR /app COPY --from=builder --chown=nonroot:nonroot /build/dist ./dist COPY --from=builder --chown=nonroot:nonroot /build/node_modules ./node_modules USER nonroot:nonroot EXPOSE 3000 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD ["node", "-e", "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode===200?0:1))"] CMD ["dist/server.js"]
bash# Install brew install trivy # macOS apt install trivy # Debian/Ubuntu tmpdir="$(mktemp -d)" trap 'rm -rf "$tmpdir"' EXIT curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \ -o "$tmpdir/trivy-install.sh" sed -n '1,160p' "$tmpdir/trivy-install.sh" sh "$tmpdir/trivy-install.sh" # Scan an image for CVEs trivy image myapp:latest # Fail CI on HIGH/CRITICAL severity trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:latest # Scan Dockerfile for misconfigurations trivy config ./Dockerfile # Scan entire repo (vulnerabilities + secrets + misconfigs) trivy fs --scanners vuln,secret,misconfig . # Generate SBOM (CycloneDX or SPDX) trivy image --format cyclonedx --output sbom.json myapp:latest trivy image --format spdx-json --output sbom.spdx.json myapp:latest # Ignore specific CVEs (add justification comments) trivy image --ignorefile .trivyignore myapp:latest
.trivyignore example:
# CVE-2023-1234 — only exploitable via X feature, not used in this app
CVE-2023-1234
# CVE-2023-5678 — fix not yet available; tracked in issue #42
CVE-2023-5678bash# Install tmpdir="$(mktemp -d)" trap 'rm -rf "$tmpdir"' EXIT curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh \ -o "$tmpdir/grype-install.sh" sed -n '1,160p' "$tmpdir/grype-install.sh" sh "$tmpdir/grype-install.sh" # Scan image grype myapp:latest # Fail on critical grype myapp:latest --fail-on critical # Output SARIF for GitHub Security tab grype myapp:latest -o sarif > results.sarif # Pair with Syft for SBOM generation syft myapp:latest -o cyclonedx-json > sbom.json grype sbom:sbom.json # Scan the SBOM directly
bash# Run directly docker run --rm -i hadolint/hadolint < Dockerfile # With config file hadolint --config .hadolint.yaml --failure-threshold warning Dockerfile
.hadolint.yaml:
yamlfailure-threshold: warning ignore: - DL3008 # Pin versions in apt-get (allow floating for base layer) trustedRegistries: - gcr.io - ghcr.io - public.ecr.aws
bash# Trivy covers secrets too trivy image --scanners secret myapp:latest # Dedicated: TruffleHog trufflehog docker --image myapp:latest # git-secrets to prevent committing secrets git secrets --scan
yamlpermissions: contents: read security-events: write # Required for uploading SARIF jobs: security-scan: runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Build image run: docker build -t myapp:${{ github.sha }} . - name: Lint Dockerfile uses: hadolint/hadolint-action@54c9adbab1582c2ef04b2016b760714a4bfde3cf # v3.1.0 with: dockerfile: Dockerfile failure-threshold: warning - name: Scan with Trivy uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # v0.28.0 with: image-ref: myapp:${{ github.sha }} format: sarif output: trivy-results.sarif severity: HIGH,CRITICAL exit-code: '1' - name: Upload results to GitHub Security tab uses: github/codeql-action/upload-sarif@4f3212b61783c3c68e8309a0f18a699764811cda # v3.27.1 if: always() # Upload even if scan found issues with: sarif_file: trivy-results.sarif
bashdocker run \ --read-only \ # Read-only root filesystem --tmpfs /tmp:noexec,nosuid,size=100m \ # Writable tmpfs for /tmp only --tmpfs /var/run \ # For PID files if needed --user 10001:10001 \ # Non-root UID:GID --cap-drop ALL \ # Drop ALL Linux capabilities --cap-add NET_BIND_SERVICE \ # Re-add only what's truly needed --security-opt no-new-privileges:true \ # Prevent privilege escalation via setuid --security-opt seccomp=seccomp.json \ # Custom seccomp profile --security-opt apparmor=docker-default \ # AppArmor profile --pids-limit 100 \ # Prevent fork bombs --memory 512m \ # OOM protection --memory-swap 512m \ # Disable swap --cpus 1.0 \ # CPU limit --network none \ # No network (if not needed) --health-cmd "curl -f http://localhost:3000/health || exit 1" \ --health-interval 30s \ myapp:latest
Drop ALL, then explicitly add only what your app requires:
| Capability | Purpose | Keep? | |---|---|---| | NET_BIND_SERVICE | Bind ports < 1024 | Only if binding a privileged port | | CHOWN | Change file ownership | No — set ownership at build time | | SETUID / SETGID | Switch user identity | No — drop always | | SYS_ADMIN | Broad privileged operations | No — most dangerous capability | | NET_ADMIN | Configure network interfaces | No (only network tools) | | SYS_PTRACE | Debug/trace processes | No (only debugger containers) | | DAC_OVERRIDE | Override file permissions | No — runs as correct user | | NET_RAW | Raw sockets (ping) | No (blocked by default seccomp anyway) |
> Most web apps need zero capabilities. --cap-drop ALL alone is often sufficient.
yamlservices: app: image: myapp:latest read_only: true user: "10001:10001" tmpfs: - /tmp:noexec,nosuid,size=100m - /var/run:noexec,nosuid,size=10m cap_drop: - ALL cap_add: - NET_BIND_SERVICE # Only if binding port < 1024 security_opt: - no-new-privileges:true - seccomp:./references/seccomp-profile-template.json pids_limit: 100 mem_limit: 512m memswap_limit: 512m cpus: 1.0 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 30s timeout: 5s retries: 3 start_period: 10s networks: - backend # Only expose externally if truly required # ports: ["8080:8080"] restart: unless-stopped logging: driver: json-file options: max-size: "10m" max-file: "3" networks: backend: driver: bridge internal: true # No external connectivity unless needed
The Docker default seccomp profile blocks ~44 dangerous syscalls. For stricter control:
bash# Step 1: Audit syscalls your app actually makes docker run --security-opt seccomp=unconfined \ --name audit-run myapp:latest & # Capture with strace strace -c -p $(docker inspect --format '{{.State.Pid}}' audit-run) # Or with sysdig (more container-friendly) sysdig -p "%syscall.type" container.name=audit-run | sort -u # Step 2: Build a custom profile from references/seccomp-profile-template.json # Step 3: Apply it docker run --security-opt seccomp=references/seccomp-profile-template.json myapp:latest
See references/seccomp-profile-template.json for a minimal starting allowlist for typical web servers.
bash# Load Docker's default AppArmor profile sudo apparmor_parser -r /etc/apparmor.d/docker-default # Apply at runtime docker run --security-opt apparmor=docker-default myapp:latest # Generate a custom profile aa-genprof myapp # Interactive — run app under aa-complain mode first
bash# Install cosign brew install cosign # macOS # or: https://github.com/sigstore/cosign/releases # Sign after push — keyless via OIDC (no long-lived keys) cosign sign ghcr.io/org/myapp:latest # Verify before deploy cosign verify ghcr.io/org/myapp:latest \ --certificate-identity-regexp="https://github.com/org/repo" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com"
GitHub Actions — Sign & Verify Pipeline:
yamlpermissions: id-token: write # Required for OIDC keyless signing packages: write steps: - uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 - name: Sign image (keyless via OIDC) run: | cosign sign --yes \ ghcr.io/${{ github.repository }}:${{ github.sha }} env: COSIGN_EXPERIMENTAL: "true" - name: Attach SBOM attestation run: | cosign attest --yes \ --predicate sbom.json \ --type cyclonedx \ ghcr.io/${{ github.repository }}:${{ github.sha }}
bash# Generate SBOM with Syft syft myapp:latest -o cyclonedx-json > sbom.json syft myapp:latest -o spdx-json > sbom.spdx.json # Attach to image as attestation cosign attest --predicate sbom.json --type cyclonedx ghcr.io/org/myapp:latest # Verify SBOM attestation before deployment cosign verify-attestation \ --type cyclonedx \ --certificate-identity-regexp="https://github.com/org/repo" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ ghcr.io/org/myapp:latest
| Registry | Built-in Scanning | Notes | |---|---|---| | GHCR (GitHub Container Registry) | No (use Trivy in CI) | Best for OSS, OIDC auth | | AWS ECR | Yes (enhanced scanning via Inspector) | Enable per-repo | | GCP Artifact Registry | Yes (Container Analysis) | Enabled by default | | Azure ACR | Yes (Defender for Containers) | Premium tier | | Docker Hub | Yes (limited on free tier) | Avoid for private images |
bash# Enable ECR enhanced scanning aws ecr put-registry-scanning-configuration \ --scan-type ENHANCED \ --rules '[{"repositoryFilters":[{"filter":"*","filterType":"WILDCARD"}],"scanFrequency":"CONTINUOUS_SCAN"}]'
yaml# Kyverno policy — require signed images before admission apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-signed-images spec: validationFailureAction: Enforce rules: - name: verify-image-signature match: resources: kinds: [Pod] verifyImages: - imageReferences: - "ghcr.io/org/*" attestors: - entries: - keyless: subject: "https://github.com/org/repo/.github/workflows/*" issuer: "https://token.actions.githubusercontent.com"
> Full reference: references/kubernetes-pod-security.md
yamlapiVersion: apps/v1 kind: Deployment metadata: name: myapp namespace: production spec: replicas: 3 template: spec: # ── Pod-level security context ───────────────────── securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 fsGroup: 10001 fsGroupChangePolicy: OnRootMismatch seccompProfile: type: RuntimeDefault # Use containerd/runc default seccomp supplementalGroups: [] automountServiceAccountToken: false # Disable unless needed # ── Container-level security context ────────────── containers: - name: app image: ghcr.io/org/myapp@sha256:<digest> # Always use digest securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] add: [] # Add nothing unless absolutely required runAsNonRoot: true runAsUser: 10001 seccompProfile: type: RuntimeDefault # ── Resource limits (required for restricted PSA) ── resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" # ── Writable tmpfs mounts ────────────────────── volumeMounts: - name: tmp mountPath: /tmp - name: varrun mountPath: /var/run volumes: - name: tmp emptyDir: medium: Memory sizeLimit: 100Mi - name: varrun emptyDir: medium: Memory sizeLimit: 10Mi
bash# Audit existing workloads before enforcing kubectl label namespace production \ pod-security.kubernetes.io/audit=restricted \ pod-security.kubernetes.io/audit-version=latest # Warn in staging, enforce in production kubectl label namespace staging \ pod-security.kubernetes.io/warn=restricted kubectl label namespace production \ pod-security.kubernetes.io/enforce=restricted \ pod-security.kubernetes.io/enforce-version=latest
| PSA Level | What It Blocks | |---|---| | privileged | No restrictions | | baseline | Blocks hostNetwork, hostPID, privileged containers, hostPath | | restricted | Also requires non-root, read-only FS, drops capabilities, seccomp |
yaml# Step 1: Deny all ingress and egress by default in the namespace apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-all namespace: production spec: podSelector: {} policyTypes: [Ingress, Egress] --- # Step 2: Selectively allow only required traffic apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-app namespace: production spec: podSelector: matchLabels: app: myapp policyTypes: [Ingress, Egress] ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: ingress-nginx podSelector: matchLabels: app.kubernetes.io/name: ingress-nginx ports: - port: 3000 egress: - to: - podSelector: matchLabels: app: postgres ports: - port: 5432 - to: # Allow only cluster DNS - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system podSelector: matchLabels: k8s-app: kube-dns ports: - port: 53 protocol: UDP - port: 53 protocol: TCP
yaml# Create minimal role — never use wildcards apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: app-reader namespace: production rules: - apiGroups: [""] resources: ["configmaps", "secrets"] resourceNames: ["myapp-config"] # Lock to specific resource names verbs: ["get"] # Never ["*"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: app-reader-binding namespace: production subjects: - kind: ServiceAccount name: myapp-sa namespace: production roleRef: kind: Role name: app-reader apiGroup: rbac.authorization.k8s.io
bash# Audit what permissions a service account has kubectl auth can-i --list --as=system:serviceaccount:production:myapp-sa # Find overly-permissive cluster roles kubectl get clusterrolebindings -o json | \ jq '.items[] | select(.roleRef.name == "cluster-admin") | .subjects'
yaml# Require non-root containers apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-non-root spec: validationFailureAction: Enforce rules: - name: check-run-as-non-root match: resources: kinds: [Pod] validate: message: "Containers must not run as root (runAsNonRoot: true required)" pattern: spec: containers: - securityContext: runAsNonRoot: true --- # Require image digest pinning apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: require-image-digest spec: validationFailureAction: Enforce rules: - name: check-digest match: resources: kinds: [Pod] validate: message: "Images must be pinned to a SHA256 digest, not just a tag" pattern: spec: containers: - image: "*@sha256:*" --- # Block privileged containers apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: disallow-privileged spec: validationFailureAction: Enforce rules: - name: check-privileged match: resources: kinds: [Pod] validate: message: "Privileged containers are not allowed" pattern: spec: containers: - =(securityContext): =(privileged): "false"
| Problem | Root Cause | Fix | |---|---|---| | Image runs as root | No USER directive | Add RUN useradd ... and USER appuser | | Secret in docker history | ENV or RUN curl -H "Bearer $TOKEN" | Use RUN --mount=type=secret | | Large image with many CVEs | Full base image (node:20, ubuntu) | Switch to node:20-slim or distroless | | App crashes with --read-only | Writes to /tmp or app directory | Add --tmpfs /tmp for writable temp space | | Trivy scan blocks CI on unfixable CVEs | No ignore file | Add .trivyignore with justified entries | | Container needs SYS_ADMIN | Missing --cap-drop context | Investigate why — almost always avoidable | | Tag-based images drift over time | Mutable tags | Pin to @sha256: digest; use Renovate to update | | K8s pod rejected by PSA | Missing security context fields | Add runAsNonRoot, readOnlyRootFilesystem, allowPrivilegeEscalation: false | | App can't write to filesystem | readOnlyRootFilesystem: true | Mount emptyDir volumes for writable paths |
USER declared before CMD/ENTRYPOINT@sha256:... digest (not just tag)ENV, ARG, or RUN commandsHEALTHCHECK definedorg.opencontainers.image.*).dockerignore excludes .git, .env, secrets, testsENTRYPOINT uses exec form, not shell formtrivy --scanners secret).trivyignore has justified entries for accepted CVEs--read-only filesystem--cap-drop ALL (add back only what's documented as required)--security-opt no-new-privileges:true--security-opt seccomp=<profile> applied--memory, --cpus, --pids-limit)readOnlyRootFilesystem: trueallowPrivilegeEscalation: falserunAsNonRoot: true with explicit UIDcapabilities.drop: ["ALL"]requests and limits definedautomountServiceAccountToken: falserestricted levelNetworkPolicy default-deny appliedreferences/base-image-comparison.md — Size, CVE count, shell/pkg-manager trade-offs: distroless vs alpine vs slim vs scratchreferences/seccomp-profile-template.json — Minimal syscall allowlist for typical web servers; start here and extendreferences/kubernetes-pod-security.md — NetworkPolicy, RBAC, OPA/Kyverno policies, service account hardening, PSAdocker-expert — General Docker usage, Compose orchestration, image optimizationgha-security-review — Security audit of GitHub Actions workflowsgithub-actions-advanced — CI pipeline patterns including scanner integrationkubernetes-architect — Full Kubernetes architecture, not just securityapi-security-best-practices — Application-level security (injection, auth, OWASP)k8s-security-policies — Extended Kubernetes security policies| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 23,291 | 24,903 | +7% | 1 | 1 | 0% | 3,477 | 12,285 | +253% | 0 | 0 | — |
case-02 | fail→pass | 20,530 | 24,283 | +18% | 1 | 1 | 0% | 3,245 | 12,601 | +288% | 0 | 0 | — |
case-03 | fail→pass | 13,675 | 19,563 | +43% | 1 | 1 | 0% | 2,543 | 11,966 | +371% | 0 | 0 | — |
case-04 | pass→pass | 16,939 | 17,295 | +2% | 1 | 1 | 0% | 2,345 | 11,445 | +388% | 0 | 0 | — |
case-05 | pass→pass | 11,504 | 16,148 | +40% | 1 | 1 | 0% | 2,104 | 11,376 | +441% | 0 | 0 | — |
case-06 | pass→pass | 20,788 | 14,412 | -31% | 1 | 1 | 0% | 2,022 | 10,961 | +442% | 0 | 0 | — |
case-07 | pass→pass | 10,577 | 7,070 | -33% | 1 | 1 | 0% | 1,184 | 9,774 | +726% | 0 | 0 | — |
case-08 | pass→pass | 36,721 | 13,639 | -63% | 1 | 1 | 0% | 2,333 | 10,842 | +365% | 0 | 0 | — |
case-09 | fail→pass | 10,715 | 10,344 | -3% | 1 | 1 | 0% | 1,649 | 10,445 | +533% | 0 | 0 | — |
case-10 | pass→pass | 17,796 | 19,288 | +8% | 1 | 1 | 0% | 2,809 | 12,232 | +335% | 0 | 0 | — |
case-11 | pass→pass | 38,950 | 14,450 | -63% | 1 | 1 | 0% | 1,803 | 11,404 | +533% | 0 | 0 | — |
case-12 | pass→pass | 9,380 | 28,083 | +199% | 1 | 1 | 0% | 1,706 | 10,784 | +532% | 0 | 0 | — |
case-13 | pass→pass | 12,866 | 10,919 | -15% | 1 | 1 | 0% | 2,011 | 10,759 | +435% | 0 | 0 | — |
case-14 | pass→pass | 11,679 | 8,749 | -25% | 1 | 1 | 0% | 1,771 | 10,441 | +490% | 0 | 0 | — |
case-15 | pass→pass | 7,139 | 7,633 | +7% | 1 | 1 | 0% | 1,350 | 10,235 | +658% | 0 | 0 | — |
case-16 | pass→pass | 20,040 | 18,445 | -8% | 1 | 1 | 0% | 3,814 | 12,241 | +221% | 0 | 0 | — |
case-17 | pass→pass | 11,494 | 9,865 | -14% | 1 | 1 | 0% | 2,191 | 10,404 | +375% | 0 | 0 | — |
case-18 | pass→pass | 11,914 | 8,992 | -25% | 1 | 1 | 0% | 2,141 | 10,272 | +380% | 0 | 0 | — |
case-19 | pass→pass | 18,090 | 20,166 | +11% | 1 | 1 | 0% | 2,926 | 12,106 | +314% | 0 | 0 | — |
case-20 | pass→pass | 7,923 | 8,613 | +9% | 1 | 1 | 0% | 1,369 | 10,386 | +659% | 0 | 0 | — |
case-21 | pass→pass | 15,627 | 5,190 | -67% | 1 | 1 | 0% | 2,178 | 9,643 | +343% | 0 | 0 | — |
case-22 | pass→pass | 16,619 | 19,183 | +15% | 1 | 1 | 0% | 3,051 | 12,230 | +301% | 0 | 0 | — |
case-23 | pass→pass | 12,318 | 11,203 | -9% | 1 | 1 | 0% | 2,375 | 10,748 | +353% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +17 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.