Skip to content

Cluster Access via SSH

Use SSH to reach a host that has kubectl and a valid kubeconfig. That host is usually the Kubernetes control plane or a native deploy runner. Once SSH works from your laptop, you can run cluster commands yourself and let tools such as Cursor execute the same commands on your behalf.

When to use this

Goal Approach
Debug deploy failures (kubectl get pods, logs, events) SSH + kubectl on cluster host
Let Cursor Agent inspect the cluster during chat SSH alias in ~/.ssh/config, agent runs ssh <host> kubectl ...
Run repo helper scripts (deploy-ci-runners-k8s.sh) SSH host k8s-master (or your alias)
CI deploy workflows Forgejo runners with label deploy-<env> — no SSH needed from laptop

Architecture

graph LR
    A["Your laptop<br/>(Cursor / terminal)"] -->|SSH| B["Cluster host<br/>control plane or deploy runner"]
    B -->|kubectl + kubeconfig| C["Kubernetes API"]
    C --> D["meshflows-lab"]
    C --> E["meshflows-dev"]
    C --> F["meshflows-tst"]

Typical hosts:

  • Control plane — has admin kubeconfig (/etc/kubernetes/admin.conf or ~/.kube/config)
  • Native deploy runner — registered with label deploy-dev, deploy-lab, etc. (CI runner setup)
  • In-cluster runners — jobs run inside the cluster; you do not SSH to them for day-2 ops

Environment namespaces follow the pattern meshflows-<env> (for example meshflows-dev, meshflows-lab).

1. Generate an SSH key (once per machine)

Linux / macOS:

ssh-keygen -t ed25519 -C "meshflows-dev-$(whoami)" -f ~/.ssh/meshflows_k8s
chmod 600 ~/.ssh/meshflows_k8s

Windows (PowerShell):

ssh-keygen -t ed25519 -C "meshflows-dev-$env:USERNAME" -f $env:USERPROFILE\.ssh\meshflows_k8s

Copy the public key to the cluster host (replace user and host):

ssh-copy-id -i ~/.ssh/meshflows_k8s.pub deploy@YOUR_CLUSTER_HOST

On Windows without ssh-copy-id, append the public key manually to ~/.ssh/authorized_keys on the server.

2. SSH config on your laptop

Add a host alias so scripts and Cursor can use a short name. The repo default for CI runner deployment is k8s-master.

Linux / macOS — ~/.ssh/config:

Host k8s-master
    HostName YOUR_CLUSTER_IP_OR_DNS
    User deploy
    IdentityFile ~/.ssh/meshflows_k8s
    IdentitiesOnly yes
    ServerAliveInterval 60

Windows — %USERPROFILE%\.ssh\config:

Host k8s-master
    HostName YOUR_CLUSTER_IP_OR_DNS
    User deploy
    IdentityFile C:\Users\YOUR_USER\.ssh\meshflows_k8s
    IdentitiesOnly yes
    ServerAliveInterval 60

Verify:

ssh k8s-master "hostname && kubectl cluster-info"
ssh k8s-master "kubectl get nodes"
ssh k8s-master "kubectl get pods -n meshflows-dev"

If kubectl is missing on the host, install it on the deploy runner:

sudo bash engine/scripts/install-native-runner.sh --help

That script installs kubectl, kustomize, and helm on the runner host.

3. Using SSH with Cursor Agent

Cursor runs shell commands in your workspace. It does not have direct network access to a private cluster unless you provide a path:

  1. Configure SSH as above on the same machine where Cursor runs.
  2. Ask the agent to run remote commands, for example:

    ssh k8s-master "kubectl get pods -n meshflows-dev -o wide"
    ssh k8s-master "kubectl logs -n meshflows-dev deploy/gateway --tail=80"
    ssh k8s-master "kubectl get events -n meshflows-dev --sort-by=.lastTimestamp | tail -20"
    
  3. Optional — Remote SSH in Cursor/VS Code: connect to k8s-master as the remote workspace. Then the agent runs kubectl locally on that host without wrapping every command in ssh.

Agent-friendly alias

Keep one stable SSH host name (k8s-master or meshflows-k8s). Mention that name in chat so the agent can reuse it consistently.

4. Optional — local kubectl via SSH tunnel

If you prefer local kubectl without prefixing every command with ssh:

# Terminal 1 — keep open
ssh -N -L 6443:127.0.0.1:6443 k8s-master

# Terminal 2 — copy kubeconfig from host once
scp k8s-master:~/.kube/config ./meshflows-kubeconfig

Edit the copied kubeconfig: set server: https://127.0.0.1:6443 and point certificate-authority-data / client certs as in the original file.

export KUBECONFIG="$PWD/meshflows-kubeconfig"
kubectl get pods -n meshflows-dev

Stop the tunnel when finished. Do not commit kubeconfig files to git.

5. Relation to CI secrets

Deploy workflows use Forgejo runners, not your laptop SSH key. For CI, store a base64 kubeconfig as repository secret CLUSTER_<ENV>:

bash engine/scripts/generate-cluster-secret.sh --env DEV --kube ~/.kube/config

See also engine/runner.secrets.env.example for CLUSTER_LAB, CLUSTER_DEV, etc.

Helper to apply a secret to Forgejo (optional):

bash engine/scripts/set-cluster-secret.sh --host https://code.meshflows.org \
  --repo owner/meshflows --env DEV --file ~/.kube/config

6. Repo scripts that expect SSH

Script Default SSH host Purpose
engine/scripts/deploy-ci-runners-k8s.sh k8s-master Deploy Forgejo build/deploy runners into meshflows-ci
engine/scripts/triage-control-plane.sh local kubectl Control-plane health (run on host or via SSH)

Example:

bash engine/scripts/deploy-ci-runners-k8s.sh --remote k8s-master

Override with environment variable:

REMOTE_HOST=meshflows-k8s bash engine/scripts/deploy-ci-runners-k8s.sh

7. Security

  • Use a dedicated Linux user on the cluster host (for example deploy), not root over SSH.
  • Restrict SSH to your IP or VPN where possible (firewall / AllowUsers).
  • Prefer ed25519 keys; protect private keys with a passphrase.
  • Never commit private keys, kubeconfig, or runner.secrets.env.
  • Read-only debugging: a kubeconfig with get, list, watch on meshflows-* namespaces is enough for most triage; use admin kubeconfig only when required.
  • Rotate keys when people leave the team or laptops are replaced.

8. Troubleshooting

Symptom Check
Permission denied (publickey) Public key on server? Correct IdentityFile in SSH config?
kubectl: command not found on host Run install-native-runner.sh or install kubectl package
Unable to connect to the server Is kubeconfig valid on the host? ssh k8s-master "kubectl cluster-info"
Agent says it cannot reach cluster SSH not configured on the Cursor machine; set up §1–2 first
Wrong namespace / empty pods Confirm env: meshflows-lab vs meshflows-dev vs meshflows-tst