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 (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.
-
:latestimages. - 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.
-
ADDvsCOPY. - Shell-form
CMD.
- Root in the final stage, following
- 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 setsUSER, 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:
highas errors,mediumas 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
highby default (fail-on), or when a file grades worse thanmin-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
linttool 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_KEYinstead ofRAPIDAPI_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
USERitself, 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.