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.
| Rule | Why | Since | Without 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. | always | the agent refuses to start |
endpoints get, create, update, delete | a 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, deletecollection | a 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, patch | plug update rolls the agent's own Deployment so the node re-pulls the tag. | 2.9 | update by hand |
pods get, list · pods/exec get, create | a 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 only | an agent deployed with PLUG_ADMISSION=invite keeps its members in that Secret, which the manifest creates empty (admission by approval). Unused otherwise. | 2.26.0 | an invite agent refuses to start |
pods create, delete · persistentvolumeclaims get | the 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.0 | the 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:2222The 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.
The image, tags, how it also serves the CLI, and the under-the-hood notes are identical on every platform - see Swarm for those.