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.
Security model
- By default the agent authenticates no person: whoever reaches its port gets in. Admission by approval, or a gateway, changes that (below).
- A session reaches what the agent reaches: the networks it is attached to, and for any workload there, its environment, secret files, volumes and name.
- On Docker, Compose and Swarm the agent needs the Docker socket, which is root on the host. Kubernetes and OpenShift use a namespace-scoped Role.
- The CLI is installed over SSH from the agent, without checking its host key; plug pins the key afterwards and verifies the signature of every binary before granting privilege.
Who may open a session on a cluster is decided in one of three ways. Pick the one that fits who can reach the agent.
Open
the default- Who gets in
- Whoever reaches the agent's port.
- Why that holds
- The tunnel key ships in every plug binary: it proves the software, not a person. Behind an office network or a VPN, reaching the port already means being one of the team, and a colleague is at work a minute after the install, with no key to hand out and no account to create.
- For
- A dev cluster on a network you trust.
- To set up
- Nothing: it is how the agent starts.
By approval
PLUG_ADMISSION=invite- Who gets in
- The members. The first key to connect becomes the first member; any other is refused and kept as a request until a member accepts it.
- Why
- Reaching the port is no longer enough. A stranger who reaches it gets a request in a queue, and nothing else.
- For
- A network you do not fully trust, or with more people on it than should use the cluster.
- To set up
- One variable on the agent.
Hosted
with Meerkat- Who gets in
- The people a gateway knows. Each one registers a key there, under their own name.
- Why
- Rights are given and withdrawn per person, and every session carries a name: which service is plugged, and by whom.
- For
- A cluster several teams share, or where every session must be attributed.
- To set up
- The agent runs inside the gateway.
In all three, the traffic is encrypted by SSH, and what a session can do once in is the same (below). The mode only decides who gets that far.
Moving from one to another
From open to by approval
-
Set
PLUG_ADMISSION: inviteon the agent and redeploy it. The Kubernetes manifest and the service snippet carry the line, commented. The members are kept in theplug-membersSecret the manifest creates on Kubernetes, in a config the agent makes by itself on Swarm, and on Compose in a volume to mount at/var/lib/plug/members, without which the agent does not start. -
Connect first. The first key to connect becomes the first member, and that
should be you:
plug -p demo members -
Let the others in. Their next plug command gives their machine a key of its
own, records their request, and tells them what to ask:
Any member runs the line, orplug: demo admits its members by approval. Your request is registered. Ask a member to run: plug -p demo accept 9kcQ2m7xT4bW or to pick you in: plug -p demo requests Members: alice, bobplug -p demo requeststo pick from the waiting list.
From by approval back to open
Remove the variable and redeploy. The members stay where they are, unread: set the variable again and they are the members again. To start over with a new first member, delete the Secret, the config or the volume first.
To hosted
Hosted is not a setting of the agent you deployed: the agent runs inside the gateway, which serves its own CLI. Each developer installs plug with the command the gateway shows, gives the machine its key, and registers the key it prints in their gateway profile:
plug keygen -p my-app
plug pubkey -p my-appThe members of an agent by approval do not carry over: the gateway decides who is in. Once everyone has moved, remove the agent you deployed. More on the Meerkat page.
What a session can do once in
The same in every mode:
- Reach the networks the agent is attached to, names included.
- For any workload in reach (the agent's namespace on Kubernetes, the attached stack on Docker and Swarm): read its environment and its mounted secret and config files, mount its data volumes read-write, and point its name at the developer's machine.
- Redeploy the agent at another tag of its image, an older one included.
There is no shell: the agent answers a fixed set of verbs, with their arguments checked. What bounds a session is what the agent reaches, so attach it only to the networks developers need, and deploy it in a namespace no production workload shares.
Admission by approval in detail
- The first member is named. The agent's log says who it is
(
first member: <name> SHA256:… from <ip>), and anyone who arrives later sees the members' names, so a key that got there first does not go unnoticed. - Only a member accepts or revokes.
plug -p demo accept <key>takes the first 12 characters of the newcomer's fingerprint, too many to forge a second key that starts the same.plug -p demo memberslists the members and revokes the ones picked (plug -p demo revoke <key>in a script). A revoked member's open sessions end at once: their plug stops the command and says who revoked them, rather than reconnecting into a refusal. The last member cannot be revoked: the next key would become the first. - Members are equal. Every member can accept and revoke. Telling who may invite from who may only use is a matter of identity, which is what hosted brings.
- A request is forgotten after 7 days without a new attempt, and at most 20 wait at once, the oldest dropped first: anyone who reaches the port can add to the list, and it stays one a member can read. A machine that tries again keeps its one request.
- An offered key is never recorded. A request is written only once the client has signed with the key, so nobody holding a colleague's public half can put it on the list.
- An agent that cannot read its members does not start. It never falls back to letting anyone in. Deleting the Secret, the config or the volume resets the members, and the next key to connect becomes the first: that is also the way back when every member has lost their key.
Details
The port, in open mode
Open, reaching the port is the whole authorization, so the port is the boundary. On Kubernetes
the manifest publishes a NodePort (32222 by default) on every node: the boundary
is whatever can reach the nodes. kubectl -n my-namespace port-forward svc/plug 2222:2222
reaches the same agent with no exposed port at all, each developer's kubeconfig gating it;
drop the NodePort from the manifest if that is your model. On Docker and Swarm, firewall the
published port to your office or VPN range.
What the agent is granted on the cluster
Names, environments and mounts need a broad grant. On Docker and Swarm the agent needs the
Docker socket, which is root on the host: it creates and removes signposts, reads a workload's
environment, starts the mount helper and refreshes its own deployment, but the capability it
holds is the whole API. On Kubernetes the Role is namespace-scoped, and includes
pods/exec and pods create: in effect, every secret the namespace's
pods have resolved. The Kubernetes page says which feature
needs which rule. Neither grant is optional, so mount the socket or apply the Role on a dev
cluster only.
Serving a local port (-s)
The reverse direction opens one local port to the cluster's workloads, for the session's lifetime, never general access to the developer's machine. The listener is on the agent, so any workload that reaches the agent reaches it: the name is the intended path, not an ACL.
The agent's host key
plug pins the agent's host key on first use (~/.plug/known_hosts). The agent makes
a new one on every start, so a changed key is the normal case after a restart: plug pins the
new one and prints a one-line notice, worth a glance when you did not restart the agent.
PLUG_STRICT_HOSTKEY=1 turns the notice into a refusal, for an agent whose key you
keep stable.
The download user (get)
The agent serves the CLI on the same port through a passwordless user, get, which
can only print the version, send a binary with its digest, or emit the install script. It has
no shell (the image holds none) and cannot open the tunnel. There is nothing behind it but
public binaries.
ssh -p 2222 get@<host> install # returns the installer
ssh -p 2222 get@<host> cat /etc/shadow # ignoredVerifying the image
Every published image is signed with cosign, with no
key to fetch: the signature carries the identity of the workflow that pushed the image,
ci.yml of softwarity/plug on main, and that is what you
check.
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github\.com/softwarity/plug/\.github/workflows/ci\.yml@refs/heads/main$' \
docker.io/softwarity/plug:latest
The same command verifies every tag, -hosted included. What is signed is the
digest of the manifest list and of each per-architecture image, so a node verifies what it
actually runs. The CLI binaries inside are signed too, with plug's release key: a launcher
checks that signature before it runs a core it downloaded from the agent, an update before
it replaces plug, and the installer before it grants plug its privilege (the WinTUN driver
included, on Windows). A binary that does not carry it is not installed.
The rules
- Never publish the agent's port on a public network, whatever the mode.
- On a network you do not fully trust, deploy the agent by approval.
- Attach the agent only to the networks developers need: the networks list in the stack descriptor is the scoping tool.
- On Kubernetes, deploy the agent where the dev services live, never in a namespace a production workload shares.
- Production clusters: don't. Use a dedicated staging cluster.