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

At a glance.
  • 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.
Use it on dev or staging clusters you trust. Never expose the agent's port on a public network, and keep it away from production workloads.

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

  1. Set PLUG_ADMISSION: invite on the agent and redeploy it. The Kubernetes manifest and the service snippet carry the line, commented. The members are kept in the plug-members Secret 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.
  2. Connect first. The first key to connect becomes the first member, and that should be you:
    plug -p demo members
  3. 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:
    plug: 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, bob
    Any member runs the line, or plug -p demo requests to 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-app

The 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 members lists 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                  # ignored

Verifying 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.