Dev.to Security 🔐 Cybersecurity 👁 0 📖 5 min read

Lint your Dockerfile and K8s manifests against Pod Security Standards in one call

Disclosure: we're Hybrid Forge Lab and we built Shipshape, the API in this post. There's a free plan and a no-signup demo, so you can check everything below yourself. Take the nginx Deployment from the Kubernetes docs (

Disclosure: we're Hybrid Forge Lab and we built Shipshape, the API in this post. There's a free plan and a no-signup demo, so you can check everything below yourself.

Take the nginx Deployment from the Kubernetes docs (deployment.yaml), the one most of us copied at some point. Run it through Shipshape and it gets a D, 42/100:

  • 13 findings in total: 5 medium, 6 low and 2 info.
  • 4 of the mediums are Pod Security Standards "restricted" checks: capabilities not dropped, privilege escalation allowed, runAsNonRoot not set, no seccomp profile.
  • The fifth medium is the missing memory limit.

That's not a criticism of the docs: the example is there to show a Deployment, not a hardened one. But it's what a lot of real manifests look like.

What Shipshape checks

You POST a Dockerfile or one or more Kubernetes manifests (YAML or JSON, multi-document is fine). You get back findings, a score and a pass/fail flag.

  • 59 rules: 26 for Dockerfiles and 33 for Kubernetes.
  • Kubernetes:
    • Pod Security Standards, both baseline and restricted.
    • Resources and probes.
    • :latest images.
    • Service-account token automount.
    • Secrets in env vars.
  • Dockerfiles:
    • Root in the final stage, following FROM <stage> inheritance.
    • Unpinned base images.
    • curl | sh.
    • apt/apk/pip cache hygiene.
    • Secrets in ENV/ARG.
    • ADD vs COPY.
    • Shell-form CMD.
  • Every finding has a line number, the object and container it applies to, a fix, and the standard behind it: PSS, the NSA/CISA Kubernetes Hardening Guide, the CIS Docker Benchmark, or the hadolint id.

One finding from the nginx example, as the production API returns it:

{
  "id": "K8S-009",
  "severity": "medium",
  "title": "Capabilities do not drop ALL",
  "line": 16,
  "object": "Deployment/nginx-deployment",
  "container": "nginx",
  "fix": "Add `securityContext.capabilities.drop: [\"ALL\"]` and add back only NET_BIND_SERVICE if needed.",
  "reference": "PSS Restricted: Capabilities; NSA/CISA hardening",
  "pss": "restricted"
}

From D to A

Here's the same Deployment after working through the findings. The image is the unprivileged nginx build, so it listens on 8080 and runs as non-root.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  namespace: web
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      containers:
      - name: nginx
        image: nginxinc/nginx-unprivileged:1.27.2
        ports:
        - containerPort: 8080
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop: ["ALL"]
        resources:
          requests:
            cpu: 100m
            memory: 64Mi
          limits:
            memory: 128Mi
        livenessProbe:
          httpGet: {path: /, port: 8080}
        readinessProbe:
          httpGet: {path: /, port: 8080}
        volumeMounts:
        - {name: tmp, mountPath: /tmp}
      volumes:
      - name: tmp
        emptyDir: {}

Result: A, 100/100. One info finding is left, "No CPU limit", and it's info on purpose: CPU limits cause throttling, and plenty of teams choose not to set them.

Grade and passed are different things

The score is overall hygiene: 100, minus 20 per high, 8 per medium and 3 per low (info is free). passed is your CI gate: it's false when any finding is at or above fail_on, which is high by default.

So they can disagree, and that's deliberate:

  • The nginx Deployment: D, but passed: true. Nothing in it is high, so it doesn't break the build by default.
  • Docker's own welcome-to-docker Dockerfile: B (80/100), but passed: false. It has one high finding: the final stage never sets USER, so the container runs as root.

Adding USER node before CMD takes it to A, 100/100 and passed: true.

On every pull request: the GitHub Action

We published an open-source GitHub Action for this. With your RapidAPI key stored as the secret RAPIDAPI_KEY, it's one step:

name: shipshape
on: pull_request
permissions:
  contents: read
  pull-requests: read
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: hybridforgelab/shipshape-action@v1
        with:
          rapidapi-key: ${{ secrets.RAPIDAPI_KEY }}

What it does on each pull request:

  • It lints only the Dockerfiles and manifests the pull request changed. Helm templates and YAML that isn't Kubernetes are skipped locally, so a pull request usually costs 1 to 3 requests.
  • Findings show up as annotations on the diff: high as errors, medium as warnings, each with the fix.
  • The job summary has a table per file with the grade, the score and every finding.
  • The step fails on high by default (fail-on), or when a file grades worse than min-grade.
  • If the API is down or your quota runs out, you get a warning. Your build is never failed because of us.

The source is on GitHub (MIT).

The Action takes a RapidAPI key for now. Support for api.market keys is planned, but it isn't there yet.

Other CI systems

Prefer no third-party action, or using GitLab CI, Jenkins or a pre-commit hook? You only need curl and jq, one request per file. Here it is as a GitHub Actions step; the same commands work anywhere:

- name: Shipshape
  env:
    RAPIDAPI_KEY: ${{ secrets.RAPIDAPI_KEY }}
  run: |
    jq -Rs '{content: ., fail_on: "high"}' Dockerfile |
      curl -sS -X POST https://shipshape-dockerfile-and-kubernetes-lint-api.p.rapidapi.com/v1/dockerfile \
        -H "X-RapidAPI-Key: $RAPIDAPI_KEY" \
        -H "X-RapidAPI-Host: shipshape-dockerfile-and-kubernetes-lint-api.p.rapidapi.com" \
        -H "Content-Type: application/json" --data-binary @- > shipshape.json
    jq -r '.findings[] | "::\(if .severity == "high" then "error" elif .severity == "medium" then "warning" else "notice" end) file=Dockerfile,line=\(.line // 1)::\(.id) \(.title)"' shipshape.json
    jq -e '.passed' shipshape.json > /dev/null

The last line makes the step fail when passed is false. For manifests, use /v1/kubernetes, or use /v1/lint, which detects the type. To skip a rule you've decided to accept, add "ignore": ["DF-017"] to the body.

In your coding agent: the MCP server

If an AI coding agent writes your Dockerfiles and manifests, it can run the same check before handing them over.

The open-source Shipshape MCP server runs on your machine and uses your own key, from RapidAPI or api.market:

{
  "mcpServers": {
    "shipshape": {
      "command": "uvx",
      "args": ["--from", "hybridforgelab-shipshape-mcp", "shipshape-mcp"],
      "env": { "RAPIDAPI_KEY": "your-rapidapi-key" }
    }
  }
}
  • Its lint tool takes the file's content or a local path, and returns the grade and every finding with its fix.
  • With an api.market key, set API_MARKET_KEY instead of RAPIDAPI_KEY.
  • It's listed in the official MCP Registry as io.github.hybridforgelab/shipshape-mcp.
  • Each tool call is one request on your plan.

Limits

  • Helm templates aren't parsed. Render them first (helm template … | …).
  • Cross-object checks are out of scope. NetworkPolicies, PodDisruptionBudgets and RBAC aren't checked.
  • Base images aren't inspected. If a third-party base image sets USER itself, we can't see it, and the finding says so.
  • Line numbers are best effort: the instruction's first line in Dockerfiles, the key's line in YAML.

Privacy

The file is parsed in memory and nothing is stored or fetched. Manifests and Dockerfiles are never logged.

Try it

  • No signup: the demo on shipshape.hybridforgelab.com runs the real API in your browser. Paste a Dockerfile or a manifest.
  • Free plan: on RapidAPI, Basic is $0 for 15 requests a month, with a hard limit, so it never runs up a bill. Paid plans start at $9 for 10,000 requests.
  • Docs and plans: RapidAPI listing.
  • Also on api.market: a 7-day free trial, once per customer. Shipshape on api.market.

If a rule fires where it shouldn't, tell us. False positives are the bugs we fix first.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.