Run a local process as if it were inside your cluster: prefix any command with plug and it resolves the cluster DNS and reaches the cluster services like any workload of the stack - no code change, no proxy configuration in your app. One tiny agent container on the cluster side, one static CLI on yours. Docker, Swarm, Kubernetes and OpenShift.

Kubernetes and OpenShift

The same agent image runs on Kubernetes - a small pod running one static Go binary and nothing else, no shell, sitting inside the cluster and dialing services on the CLI's behalf. Nothing Kubernetes-specific is baked in; only the way you deploy and reach it differs.

Deploy

Apply deploy/plug-k8s.yaml in the target namespace. No subnet or CIDR is needed - the agent resolves service names via CoreDNS from inside the cluster, exactly like on Swarm.

The full manifest - copy it or download plug-k8s.yaml, then apply:

kubectl -n my-namespace apply -f plug-k8s.yaml

To let in only the developers a member accepts, uncomment PLUG_ADMISSION: invite in the manifest before applying it: the members live in the plug-members Secret the same file creates. How it works, and how to reset it, under admission by approval.

What the Role grants, rule by rule

One namespace-scoped Role, and every rule in it has a feature behind it. Apply the manifest as a whole, and re-apply it when you upgrade the agent: plug update moves the image and never the manifest, so a rule a newer agent needs is missing until you do - the agent says which at boot, and plug doctor -p <profile> says it from your terminal, with the one-line kubectl patch that grants that rule alone. If you deploy the agent through your own chart, mirror these rules there.

RuleWhySinceWithout it, doctor says
services get, list, create, delete, update, patch-s creates the Service carrying the name and deletes it after; a takeover repoints an existing one and restores it.alwaysthe agent refuses to start
endpoints get, create, update, deletea served name points at the ONE agent pod holding the session, through Endpoints the agent writes (no selector).2.12.0"endpoints grant"
discovery.k8s.io/endpointslices list, deletecollectiona taken-over Service keeps the EndpointSlice Kubernetes built for its pod; kube-proxy routes over every slice, so without this half the requests still reach the deployed pod. The agent deletes it at park.2.20.1"endpointslices grant", and "taken-over names" still doubled
apps/deployments get, list, patchplug update rolls the agent's own Deployment so the node re-pulls the tag.2.9update by hand
pods get, list · pods/exec get, createa plugged process inherits the parked pod's environment (exec cat /proc/1/environ) and its mounted secret files.2.16.0"exec grant"
secrets get, patch, on plug-members onlyan agent deployed with PLUG_ADMISSION=invite keeps its members in that Secret, which the manifest creates empty (admission by approval). Unused otherwise.2.26.0an invite agent refuses to start
pods create, delete · persistentvolumeclaims getthe live mount: a helper pod beside the workload serves its PersistentVolumeClaim over SMB for the session; the claim is read to refuse a ReadWriteOncePod one with the reason.2.20.0the mount answers "cannot create pods"

OpenShift and OKD

The same manifest and the same Role, admitted by the default restricted-v2 SCC with nothing to grant: the agent runs as the uid the cluster picks from the namespace's range, never root. It listens on 2222 inside its pod and keeps its state in a directory that uid can write, so it asks for no capability and pins no uid. CI runs it on MicroShift built from OKD at every commit, installs plug from it and opens sessions through it.

The mount works under the same SCC, with nothing particular to it: the helper has one shape on every cluster. It runs with the uid and gid of the workload whose volume it serves, which the agent reads on the workload's own process (exec cat /proc/1/status, the pods/exec rule above) or, failing that, in its securityContext; it carries the workload's fsGroup, no capability, no privilege escalation, the runtime's seccomp profile. The helper listens on 1445 and the helper's Service answers on 445, the port the client is told. Files are written under the workload's uid, so what you save is what it reads back. On OpenShift and OKD that uid is one of the namespace's range, which is all the SCC asks of a pod that names its own; the same pod passes Pod Security restricted elsewhere. Two cases keep two capabilities (SETUID, SETGID), which a restricted admission refuses: a workload that runs as root, and one whose uid could be read nowhere; the helper then writes as the owner of the volume's root.

Reaching it

  • NodePort - the manifest publishes one (default 32222); point the profile at any node's IP and that port.
  • kubectl port-forward - an RBAC-gated tunnel with no exposed port: access is governed by each developer's own kubeconfig, which also softens the no-auth trade-off.
# zero exposed port - the tunnel rides the API server, gated by your kubeconfig RBAC
kubectl -n my-namespace port-forward svc/plug 2222:2222
Same agent, same contract: plug reaches only what the agent's namespace can resolve and route. Deploy it where the dev services live, and the rest of the cluster stays out of reach - see Security model.

The name in the cluster

plug -s <name>:<cluster-port>:<local-port> <cmd> publishes the process in the cluster under a DNS name, for the lifetime of the session - no name pre-declared, no redeploy. On Kubernetes the name is a Service pointing at the agent pod that holds the session, and the agent creates and deletes it itself per session. It carries no selector: a selector would match every replica of the agent, while the session lives in exactly one pod, so the agent writes the Service's endpoints instead - one address, the right pod. The manifest above grants exactly what that needs (the rules are listed above, each with its reason); it is part of the one deploy, so -s works out of the box. Apply the manifest as a whole: an agent that cannot manage Services refuses to start, rather than come up looking healthy and fail on the first -s.

A dev runs plug -s service1:8081:4200 npm start - pods calling http://service1:8081 land on their machine's :4200, and the Service is gone when the session ends (leftovers from a crashed session are swept on agent restart). plug verifies the full path at startup; the port closes with the session. A Service name is unique, so unlike Swarm there is no DNS round-robin - if the real service1 is deployed it already owns the name, and plug takes it over: the existing Service is repointed at the agent (its original selector and ports saved in an annotation on the Service itself) and restored when the session ends - even across an agent restart. Note the pods themselves keep running (only the name is rerouted) - a parked k8s workload still consumes queues and runs its crons. A nice property of this design: the Service keeps its ClusterIP through park and restore, so cached DNS answers (a JVM caches ~30 s) stay valid across the switch - kube-proxy reroutes underneath, and new connections reach your session immediately. No stale-IP window, unlike Swarm where the address behind the name changes. The mirror-image caveat: since the parked pods keep running, a caller holding a keep-alive connection opened before the switch keeps reaching the old pod until that connection closes or idles out (typically under two minutes) - where Swarm's stopped container kills them outright. The manifest and the security model spell out exactly what the grant allows.

Cluster driven by Argo CD, Flux or Fleet? A controller that reconciles continuously undoes the takeover a few minutes in, so a name can stop pointing at your machine without anything failing loudly. One directive on the controller settles it - see CD & GitOps.

The image, tags, how it also serves the CLI, and the under-the-hood notes are identical on every platform - see Swarm for those.