Skip to content

Reach your Resource

In Provision a cloud Resource you stood a Node up. Now you reach it. plexsphere never hands you a standing SSH key or a long-lived kubeconfig; instead it brokers mediated sessions — each one short-lived, scoped to a single Resource, gated by ReBAC, and recorded in the audit log. You ask for access, the platform issues a one-time credential bound to a listener it controls, and that credential expires on its own.

This lesson takes about fifteen minutes.

A note on the lean dev stack. make dev provisions a simulator Node — there is no real sshd, kube-apiserver, or TCP service behind it. So the session issuance below is real and audited (a token and a listener endpoint come back), but the final connect has nothing to land on locally. Against a real cloud Node the same commands drop you at a shell, serve the Kubernetes API, or tunnel a port. The skill you are learning — issuing and reasoning about mediated sessions — is identical either way.

Before you start

You need a running, logged-in stack from Set up your local plexsphere, jq on your $PATH, and a provisioned Resource in Ready: run Provision a cloud Resource through Step 6 and do not run its deprovision step yet, so the Resource is still there to reach (you will tear it down at the end).

If you still have the provision lesson's shell, $PROJECT_ID and $RESOURCE_ID are already set. In a fresh terminal, re-derive them — if you have more than one Project or Resource, pick the pair you provisioned into rather than the first row:

bash
export PATH="$PWD/bin:$PATH"
export PLEXSPHERE_URL=http://localhost:8080

DOMAIN_ID=$(plexctl domain get acme-corp --output json | jq -r '.id')
PROJECT_ID=$(plexctl project list --domain "$DOMAIN_ID" --output json | jq -r '.items[0].id')
RESOURCE_ID=$(plexctl resource list --project-id "$PROJECT_ID" --output json | jq -r '.items[0].id')
echo "project=$PROJECT_ID resource=$RESOURCE_ID"
text
project=019ecc6b-1a2b-7c3d-8e4f-0a1b2c3d4e5f resource=019ecc6b-2b3c-7d4e-8f50-1a2b3c4d5e6f

Step 1 — Access is a grant, not a key

Reaching a Resource needs the act permission on it — resource#act. You hold it here without doing anything: a Domain admin's authority walks down the tenancy tree (Domain → Project → Resource), so the seeded admin@example.com already has act on every Resource in Acme Corp.

To let a teammate who is not a Domain admin reach this Resource, you would grant it explicitly — the same rebac tuple add you used for Groups and Labels, now with the act relation on the Resource:

bash
plexctl rebac tuple add \
  --project "$PROJECT_ID" \
  --resource "resource:$RESOURCE_ID" \
  --relation act \
  --subject "user:019ecc6b-3c4d-7e5f-9061-2b3c4d5e6f70"

Without act, issuing a session is refused with 403 and reason=rebac_denied — access is a relation you hold, never a key you keep. Everything below goes through this gate.

Step 2 — Emit a kubeconfig

The cleanest session to issue is a Kubernetes one. kubeconfig asks the platform for a mediated k8s session and writes a ready-to-use kubeconfig. Impersonate a cluster user and write it to a file — it embeds a bearer token, so it lands at mode 0600:

bash
plexctl kubeconfig \
  --project-id "$PROJECT_ID" \
  --resource-id "$RESOURCE_ID" \
  --impersonate-user ops \
  --output-file ./resource.kubeconfig

The file points kubectl at a listener plexsphere controls, with a one-time bearer token in place of any cluster credential. The server endpoint and the token are minted fresh on every issuance:

yaml
apiVersion: v1
kind: Config
clusters:
- name: plexsphere
  cluster:
    server: https://<session-listener-endpoint>
users:
- name: plexsphere
  user:
    token: <one-time-session-jwt>
contexts:
- name: plexsphere
  context:
    cluster: plexsphere
    user: plexsphere
current-context: plexsphere

kubectl --kubeconfig ./resource.kubeconfig get pods would now route through the mediated session — against a real Node. On the dev-stack simulator the kubeconfig is well-formed but the listener has no API server behind it, so the request has nowhere to land.

Step 3 — Open a TCP forward

For anything that is not SSH or Kubernetes, tcp-forward issues a generic mediated tunnel to a host and port reachable from the Node. It does not proxy bytes itself — it issues the session and prints the coordinates for you to point your own tunnel client at:

bash
plexctl tcp-forward \
  --project-id "$PROJECT_ID" \
  --resource-id "$RESOURCE_ID" \
  --host localhost \
  --port 5432
text
local address: 127.0.0.1:54213
listener endpoint: <session-listener-endpoint>

The local address is where your tunnel client binds; the listener endpoint is the mediated entry point on the platform side. As with the kubeconfig, the endpoint is real on a live Node and inert on the dev-stack simulator.

Step 4 — Open an SSH session

ssh issues a mediated SSH session and hands control straight to your local ssh client, passing the one-time token in the environment rather than on the command line:

bash
plexctl ssh \
  --project-id "$PROJECT_ID" \
  --resource-id "$RESOURCE_ID" \
  --login-user ops

On a real cloud Node this drops you at a shell as ops, every command in the session recorded against the audit log. On the dev-stack simulator the session is issued but the ssh client has no sshd to reach, so the connect does not complete — issuing it is as far as the lean stack goes.

Step 5 — Sessions are short-lived and audited

None of these grants is a credential you keep. Each session carries a TTL — 30 minutes by default, clamped to a 4-hour ceiling (request a shorter one with --ttl-seconds) — and an idle timeout of 15 minutes. A background sweeper reclaims sessions the moment they expire or go idle, and a revoked token is refused for the rest of its lifetime. Every issuance is written to the per-Domain audit log under the access.issue relation, the same hash-chained trail you read in the earlier lessons — so who reached what, when is a query, not a guess.

Stronger sessions can demand a stronger sign-in: a Domain can require step-up for a session kind, in which case issuance is refused with 401 and reason=step_up_required unless your token carries a sufficient acr and a recent auth_time. Re-authenticate and retry.

What you learned

  • Access is mediated, never standing. You never hold an SSH key or a durable kubeconfig; the platform issues a one-time, Resource-scoped credential bound to a listener it controls, and it expires on its own.
  • Access is a relation, gated by resource#act. A Domain admin inherits it down the tenancy tree; anyone else needs an explicit act grant on the Resource, and without it issuance is rebac_denied.
  • One door, three shapes. kubeconfig, tcp-forward, and ssh all issue the same kind of short-lived session against the same Resource id.
  • Every reach is auditable. Issuance lands in the per-Domain audit log under access.issue, and step-up lets a Domain demand a fresh, strong sign-in before high-stakes access.

Where to go next

  • Keep learning by doingTear down your local plexsphere removes the cluster cleanly when you are done, or resets it so you can run any lesson again from a clean baseline.

Or pick the quadrant that matches what you need now:

  • You want the exact contract — the plexctl ssh, plexctl kubeconfig, and plexctl tcp-forward references document every flag, exit code, and output shape.
  • You want to understand why mediated access is shaped this way — the access bounded context explains the session aggregate, the resource#act gate, the step-up contract, and the revocation sweeper.