Your chart works on k3s.
Here is why it dies on OpenShift.
kubeport is a portability linter and translator for Kubernetes workloads. Point it at your manifests or Helm chart, say where they run today and where they need to run next, and it tells you exactly what will break there. Then it rewrites what it safely can.
kubeport 0.1.0 · 6 objects · k3s:1.31 -> k3s:1.31 ERROR stor/rwx-unsupported StatefulSet/uploads spec.volumeClaimTemplates[0].spec.accessModes claim "shared" asks for ReadWriteMany on StorageClass "local-path", which only provides single-node volumes INFO img/latest-tag Deployment/api spec.template.spec.containers[0].image container "api" image "ghcr.io/acme/api:latest" has no pinned tag; different clusters will pull different images INFO sec/capabilities Deployment/api spec.template.spec.containers[0].securityContext.capabilities.drop container "api" does not drop ALL capabilities INFO sec/hostpath Deployment/api spec.template.spec.volumes[1].hostPath volume "docker" mounts host path /var/run/docker.sock; allowed by PSA privileged here, refused by OpenShift restricted-v2 and PSA baseline/restricted 4 finding(s) (1 error, 0 warn, 3 info) · 0 autofixable
kubeport 0.1.0 · 6 objects · k3s:1.31 -> openshift:4.19/vsphere ERROR k3s/traefik-crd Middleware/strip apiVersion Middleware is a Traefik custom resource; OpenShift 4.19 (vSphere, thin-csi) does not run Traefik. Use an Ingress (or Route on OpenShift) or install Traefik on the target ERROR sec/allow-privilege-escalation Deployment/api spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation container "api" must set allowPrivilegeEscalation: false fix available: kubeport translate --to openshift:4.19/vsphere --rule sec/allow-privilege-escalation ERROR sec/capabilities Deployment/api spec.template.spec.containers[0].securityContext.capabilities.drop container "api" does not drop ALL capabilities fix available: kubeport translate --to openshift:4.19/vsphere --rule sec/capabilities ERROR sec/fixed-uid Deployment/api spec.template.spec.securityContext.runAsUser pod pins runAsUser=1000; restricted-v2 assigns UIDs from the project range and rejects fixed UIDs outside it fix available: kubeport translate --to openshift:4.19/vsphere --rule sec/fixed-uid ERROR sec/hostpath Deployment/api spec.template.spec.volumes[1].hostPath volume "docker" mounts host path /var/run/docker.sock; refused by SCC restricted-v2 ERROR stor/rwx-unsupported StatefulSet/uploads spec.volumeClaimTemplates[0].spec.accessModes claim "shared" asks for ReadWriteMany on StorageClass "local-path", which only provides single-node volumes ERROR stor/storageclass-missing PersistentVolumeClaim/api-data spec.storageClassName claim "api-data" uses StorageClass "local-path" which OpenShift 4.19 (vSphere, thin-csi) does not have; suggested: thin-csi fix available: kubeport translate --to openshift:4.19/vsphere --rule stor/storageclass-missing ERROR stor/storageclass-missing StatefulSet/uploads spec.volumeClaimTemplates[0].spec.storageClassName claim "shared" uses StorageClass "local-path" which OpenShift 4.19 (vSphere, thin-csi) does not have; suggested: thin-csi fix available: kubeport translate --to openshift:4.19/vsphere --rule stor/storageclass-missing WARN net/ingress-on-openshift Ingress/api metadata.annotations 2 annotation(s) for another Ingress controller are ignored by OpenShift 4.19 (vSphere, thin-csi): traefik.ingress.kubernetes.io/router.middlewares, traefik.ingress.kubernetes.io/router.entrypoints. The OpenShift Router serves the Ingress but drops these; behaviour such as rewrites, middlewares or rate limits is lost fix available: kubeport translate --to openshift:4.19/vsphere --rule net/ingress-on-openshift WARN net/ingress-on-openshift Ingress/api spec.ingressClassName ingressClassName "traefik" targets a traefik controller; OpenShift 4.19 (vSphere, thin-csi) default class is "openshift-default" fix available: kubeport translate --to openshift:4.19/vsphere --rule net/ingress-on-openshift WARN sec/fixed-uid Deployment/api spec.template.spec.securityContext.fsGroup pod pins fsGroup=1000; restricted-v2 requires fsGroup in the project range fix available: kubeport translate --to openshift:4.19/vsphere --rule sec/fixed-uid INFO img/latest-tag Deployment/api spec.template.spec.containers[0].image container "api" image "ghcr.io/acme/api:latest" has no pinned tag; different clusters will pull different images 12 finding(s) (8 error, 3 warn, 1 info) · 8 autofixable
kubeport 0.1.0 · 6 objects · k3s:1.31 -> k8s:1.32/eks ERROR k3s/traefik-crd Middleware/strip apiVersion Middleware is a Traefik custom resource; Kubernetes 1.32 (Amazon EKS) does not run Traefik. Use an Ingress (or Route on OpenShift) or install Traefik on the target ERROR stor/rwx-unsupported StatefulSet/uploads spec.volumeClaimTemplates[0].spec.accessModes claim "shared" asks for ReadWriteMany on StorageClass "local-path", which only provides single-node volumes; RWX-capable on this target: efs-sc ERROR stor/storageclass-missing PersistentVolumeClaim/api-data spec.storageClassName claim "api-data" uses StorageClass "local-path" which Kubernetes 1.32 (Amazon EKS) does not have; suggested: gp3 fix available: kubeport translate --to k8s:1.32/eks --rule stor/storageclass-missing ERROR stor/storageclass-missing StatefulSet/uploads spec.volumeClaimTemplates[0].spec.storageClassName claim "shared" uses StorageClass "local-path" which Kubernetes 1.32 (Amazon EKS) does not have; suggested: gp3 fix available: kubeport translate --to k8s:1.32/eks --rule stor/storageclass-missing WARN net/foreign-ingress-annotations Ingress/api metadata.annotations 2 annotation(s) for another Ingress controller are ignored by Kubernetes 1.32 (Amazon EKS): traefik.ingress.kubernetes.io/router.middlewares, traefik.ingress.kubernetes.io/router.entrypoints fix available: kubeport translate --to k8s:1.32/eks --rule net/foreign-ingress-annotations WARN net/foreign-ingress-annotations Ingress/api spec.ingressClassName ingressClassName "traefik" targets a traefik controller; Kubernetes 1.32 (Amazon EKS) default class is "alb" fix available: kubeport translate --to k8s:1.32/eks --rule net/foreign-ingress-annotations INFO img/latest-tag Deployment/api spec.template.spec.containers[0].image container "api" image "ghcr.io/acme/api:latest" has no pinned tag; different clusters will pull different images INFO sec/capabilities Deployment/api spec.template.spec.containers[0].securityContext.capabilities.drop container "api" does not drop ALL capabilities INFO sec/hostpath Deployment/api spec.template.spec.volumes[1].hostPath volume "docker" mounts host path /var/run/docker.sock; allowed by PSA privileged here, refused by OpenShift restricted-v2 and PSA baseline/restricted 9 finding(s) (4 error, 2 warn, 3 info) · 4 autofixable
kubeport 0.1.0 · 6 objects · k3s:1.31 -> k8s:1.34/talos ERROR k3s/traefik-crd Middleware/strip apiVersion Middleware is a Traefik custom resource; Kubernetes 1.34 (Talos Linux, PSA restricted) does not run Traefik. Use an Ingress (or Route on OpenShift) or install Traefik on the target ERROR net/servicelb-assumption Service/api spec.type Service type LoadBalancer stays <pending> on Kubernetes 1.34 (Talos Linux, PSA restricted) (no LoadBalancer implementation); on k3s ServiceLB (Klipper) made this work by binding node IPs. Use NodePort, an Ingress, or install MetalLB ERROR sec/allow-privilege-escalation Deployment/api spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation container "api" must set allowPrivilegeEscalation: false fix available: kubeport translate --to k8s:1.34/talos --rule sec/allow-privilege-escalation ERROR sec/capabilities Deployment/api spec.template.spec.containers[0].securityContext.capabilities.drop container "api" does not drop ALL capabilities fix available: kubeport translate --to k8s:1.34/talos --rule sec/capabilities ERROR sec/hostpath Deployment/api spec.template.spec.volumes[1].hostPath volume "docker" mounts host path /var/run/docker.sock; refused by PSA restricted ERROR sec/seccomp-profile Deployment/api spec.template.spec.securityContext.seccompProfile container "api" has no seccompProfile; PSA restricted requires RuntimeDefault or Localhost fix available: kubeport translate --to k8s:1.34/talos --rule sec/seccomp-profile ERROR sec/seccomp-profile StatefulSet/uploads spec.template.spec.securityContext.seccompProfile container "uploads" has no seccompProfile; PSA restricted requires RuntimeDefault or Localhost fix available: kubeport translate --to k8s:1.34/talos --rule sec/seccomp-profile ERROR stor/rwx-unsupported StatefulSet/uploads spec.volumeClaimTemplates[0].spec.accessModes claim "shared" asks for ReadWriteMany on StorageClass "local-path", which only provides single-node volumes WARN net/foreign-ingress-annotations Ingress/api metadata.annotations 2 annotation(s) for another Ingress controller are ignored by Kubernetes 1.34 (Talos Linux, PSA restricted): traefik.ingress.kubernetes.io/router.middlewares, traefik.ingress.kubernetes.io/router.entrypoints WARN stor/storageclass-missing PersistentVolumeClaim/api-data spec.storageClassName claim "api-data" uses StorageClass "local-path", which is specific to k3s; Kubernetes 1.34 (Talos Linux, PSA restricted) ships something else (set storage.classes in a profile to get an exact answer) WARN stor/storageclass-missing StatefulSet/uploads spec.volumeClaimTemplates[0].spec.storageClassName claim "shared" uses StorageClass "local-path", which is specific to k3s; Kubernetes 1.34 (Talos Linux, PSA restricted) ships something else (set storage.classes in a profile to get an exact answer) INFO img/latest-tag Deployment/api spec.template.spec.containers[0].image container "api" image "ghcr.io/acme/api:latest" has no pinned tag; different clusters will pull different images 12 finding(s) (8 error, 3 warn, 1 info) · 4 autofixable
Severity is decided per target. A fixed runAsUser is fine on k3s, a warning nowhere, and fatal on OpenShift's restricted-v2. local-path exists only on k3s. A Traefik Middleware needs Traefik. kubeport knows which cluster has what.
The API is the same everywhere. The contract around it is not.
Existing tools check API deprecations (kubent, Pluto), schemas (kubeconform) or best practices (kube-score, Polaris). None of them know that the three big distributions disagree on who picks the UID, what serves an Ingress, or whether type: LoadBalancer ever gets an address. These facts are data in kubeport, one YAML file per target version, plus a profile for your own cluster.
| fact | k3s (default install) | Kubernetes (kubeadm, kind) | OpenShift |
|---|---|---|---|
| Pod security | PSA, nothing enforced | PSA, nothing enforced (Talos: restricted) | SCC restricted-v2 on every project |
| Who picks the UID | the image | the image | the project: an arbitrary UID from a range; fixed runAsUser is rejected |
| hostPath, privileged, host namespaces | allowed | allowed | refused without a custom SCC |
| Ingress controller | Traefik, class traefik | none | OpenShift Router (HAProxy), class openshift-default, plus Route |
type: LoadBalancer | ServiceLB binds node IPs | pending forever | works (platform LB) |
| Default StorageClass | local-path, RWO only | none | platform: thin-csi, gp3-csi, ODF |
| Extra APIs | traefik.io, helm.cattle.io | none | route, image, template, apps.openshift.io and more |
check finds it. translate fixes what is deterministic.
Rewrites are applied only when they preserve behaviour: Route to Ingress and back, StorageClass mapping, fixed UID removal, restricted-profile hardening, removed API versions, OpenShift Templates into plain objects, DeploymentConfig into Deployment. Everything else becomes a note for a human. Unchanged objects are written back byte for byte, comments included.
kubeport 0.1.0 · translate -> openshift:4.19/vsphere Deployment/api sec/allow-privilege-escalation set allowPrivilegeEscalation=false on api Deployment/api sec/capabilities added capabilities.drop [ALL] to api Deployment/api sec/fixed-uid removed pod.runAsUser, pod.fsGroup; kept runAsNonRoot=true (OpenShift assigns the UID). Make sure the image's writable paths are group-writable by GID 0. PersistentVolumeClaim/api-data stor/storageclass-missing spec: storageClassName local-path -> thin-csi Ingress/api net/ingress-on-openshift converted Ingress to route.openshift.io/v1 Route Route/api StatefulSet/uploads stor/storageclass-missing volumeClaimTemplates[0]: storageClassName local-path -> thin-csi manual follow-up: - Ingress/api [net/ingress-on-openshift]: Route uses the router's default certificate; to keep the certificate from Secret "api-tls", paste it into spec.tls.certificate/key or use cert-manager's openshift-routes integration - Ingress/api [net/ingress-on-openshift]: dropped controller annotations with no Route equivalent: traefik.ingress.kubernetes.io/router.entrypoints, traefik.ingress.kubernetes.io/router.middlewares. Re-express rewrites/timeouts with haproxy.router.openshift.io/* annotations --- a/deploy/deployment.yaml +++ b/deploy/deployment.yaml @@ -16,8 +16,7 @@ app: api spec: securityContext: - runAsUser: 1000 - fsGroup: 1000 + runAsNonRoot: true containers: - name: api image: ghcr.io/acme/api:latest @@ -28,6 +27,11 @@ mountPath: /data - name: docker mountPath: /var/run/docker.sock + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL volumes: - name: data persistentVolumeClaim: --- a/deploy/service.yaml +++ b/deploy/service.yaml @@ -11,29 +11,20 @@ port: 80 targetPort: 8080 --- -apiVersion: networking.k8s.io/v1 -kind: Ingress +apiVersion: route.openshift.io/v1 +kind: Route metadata: name: api - annotations: - traefik.ingress.kubernetes.io/router.middlewares: default-strip@kubernetescrd - traefik.ingress.kubernetes.io/router.entrypoints: websecure spec: - ingressClassName: traefik + host: api.example.com + to: + name: api + kind: Service + port: + targetPort: 80 tls: - - hosts: [api.example.com] - secretName: api-tls - rules: - - host: api.example.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: api - port: - number: 80 + termination: edge + insecureEdgeTerminationPolicy: Redirect --- apiVersion: traefik.io/v1alpha1 kind: Middleware (dry run: add --write to apply)
29 rules, each one about a difference between clusters.
If a check would fire the same way on every target, it belongs in kube-score, not here. Each rule links to the upstream documentation and says which field to change. kubeport explain sec/fixed-uid gives the long version. Rules marked fix have a safe rewrite.
Security context and admission
sec/allow-privilege-escalationfixallowPrivilegeEscalation not set to falsesec/capabilitiesfixCapabilities not dropped or dangerous capabilities addedsec/fixed-uidfixHard-coded runAsUser on an arbitrary-UID targetsec/host-namespacesHost network, PID or IPC namespacesec/hostpathhostPath volumesec/privilegedPrivileged containersec/psp-usagePodSecurityPolicy objectsec/run-as-rootfixContainer may run as rootsec/scc-annotationOpenShift SCC reference on a non-OpenShift targetsec/seccomp-profilefixNo seccomp profile
Networking
net/foreign-ingress-annotationsfixIngress annotations for a controller the target does not runnet/ingress-class-missingIngress without ingressClassName on a target with no default classnet/ingress-on-openshiftfixIngress that the OpenShift Router cannot honournet/network-policy-defaultNo NetworkPolicy on a default-deny targetnet/route-on-non-openshiftfixOpenShift Route on a target without the Route APInet/servicelb-assumptionService type LoadBalancer on a target with no LoadBalancer implementation
Storage
stor/rwx-unsupportedReadWriteMany on a class that cannot provide itstor/storageclass-missingfixStorageClass not available on the target
Images
img/imagestream-on-non-openshiftImageStream reference outside OpenShiftimg/latest-tagImage uses :latest or no tag
API versions
api/crd-missingCustom resource whose API group is not known on the targetapi/deprecatedAPI version deprecated on the target's Kubernetes releaseapi/removedfixAPI version removed on the target's Kubernetes release
OpenShift objects
ocp/deploymentconfigfixDeploymentConfigocp/service-caOpenShift service-ca annotationocp/template-objectfixOpenShift Template outside OpenShift
k3s add-ons
k3s/traefik-crdTraefik custom resource outside k3s
Resources
res/limits-missingContainer without resource requests or limits
Install
One static binary, no dependencies, no cluster access, no network calls. Helm charts and Kustomize overlays are rendered with the helm, kustomize, kubectl or oc already on your PATH.
brew install kubeport/tap/kubeport kubeport check --to openshift:4.19 ./deploy
go install github.com/kubeport/kubeport/cmd/kubeport@latest
kubectl krew install port kubectl port check --to k3s:1.31 ./overlays/edge oc port check --to openshift:4.19 -f ./chart
$v = (irm https://api.github.com/repos/kubeport/kubeport/releases/latest).tag_name
curl.exe -LO "https://github.com/kubeport/kubeport/releases/download/$v/kubeport_$($v.TrimStart('v'))_windows_amd64.zip"
Expand-Archive kubeport_*_windows_amd64.zip -DestinationPath $env:LOCALAPPDATA\kubeport
# add $env:LOCALAPPDATA\kubeport to PATHkubectl get deploy,sts,svc,ingress,pvc -n shop -o yaml | kubeport check --from k8s:1.32/eks --to openshift:4.19 -
kubeport check --to openshift:4.19/odf -f prod.yaml ./chart
# or render yourself
helm template shop ./chart | kubeport check --to k3s:1.31 -Releases are built by GitHub Actions with goreleaser, ship an SBOM, and their checksums are signed with cosign. Verification is in every release's notes.
In CI, before it reaches the cluster.
Exit 1 on errors (configurable), SARIF for GitHub code scanning, a Markdown compatibility matrix for your README, and a shields.io badge endpoint. Also a pre-commit hook and a Tekton task.
Your cluster as a target
Platform teams write a profile once: the StorageClasses, Ingress class, installed CRDs and admission level of the real cluster. Application teams point their pipelines at it. kubeport profile init writes the starter file; schema.
# .github/workflows/portability.yml - uses: kubeport/kubeport/action@v1 with: path: ./chart from: k3s:1.31 to: openshift:4.19/vsphere,k8s:1.32/eks fail-on: error - uses: github/codeql-action/upload-sarif@v3 with: sarif_file: kubeport.sarif
# prod-cluster.yaml extends: openshift:4.19 profile: prod storage: classes: [thin-csi, ocs-storagecluster-cephfs] default_class: thin-csi rwx_classes: [ocs-storagecluster-cephfs] mapping: {local-path: thin-csi, gp3: thin-csi} api_groups: [route.openshift.io, monitoring.coreos.com, cert-manager.io, external-secrets.io] quotas_enforced: true network_policy_default_deny: true $ kubeport check --to ./prod-cluster.yaml ./deploy