SXAccessSXAccessDocs
Open dashboard

Kubernetes and CI

Give engineers kubectl access to a private cluster, and CI jobs narrow, short-lived access, without exposing the API server.

What you will achieve

Engineers run kubectl against a private cluster from their laptops, and CI jobs reach only what they deploy to.

The API server stays off the public internet. Runners come and go without filling the device list.

When to use this

This page covers two jobs, in this order:

  • Reaching the cluster - engineers need the API server, and perhaps a few Services. Steps 1 to 4.
  • Giving CI access - runners need the API server for minutes, then disappear. Steps 5 and 6.

Keep the two apart. Never give CI the cluster's key: every runner would join the routing device's group and start routing traffic for the cluster.

What you need

The rows marked Required are enough to reach the cluster. CI also needs the Optional row.

WhatWhy
RequiredThe Owner or Admin role in the dashboardFor the enrollment keys and your member page
Requiredkubectl access to the cluster as you reach it today, with rights to create and label a namespace and to deploy into itSteps 1 and 2 use it before SXAccess reaches the API server. The agent needs the privileged level of Pod Security Admission, for NET_ADMIN and NET_RAW
RequiredThe image shown under Add device › DockerIt starts the agent and enrols it
RequiredA 1 GiB ReadWriteOnce volumeIt keeps the device's identity across restarts
RequiredThe server: line of your kubeconfigThe API server is published under that host and port
RequiredA laptop with the SXAccess app or the CLI, signed in, and kubectlTo test from
OptionalCI jobs that run as root or with sudo, on VMs or in containers allowed NET_ADMIN and NET_RAWSteps 5 and 6

Before you start

One pod speaks for the cluster.

How a private cluster is reachedLaptops in the platform group and temporary CI runners connect over encrypted tunnels to a single agent pod running in the cluster. That pod reaches the API server, and any Services you publish one by one. The pod CIDR is never published, because pod addresses change all the time.KUBERNETES CLUSTERPublished one by oneAPI serverServicesPod CIDRLaptopSXAccess appCI runnertemporary deviceAgent podone Deployment,one replicaencrypted tunnelcluster trafficnever publishedOne routing device per cluster, not per nodeHow a private cluster is reachedLaptops in the platform group and temporary CI runners connect over encrypted tunnels to a single agent pod running in the cluster. That pod reaches the API server, and any Services you publish one by one. The pod CIDR is never published, because pod addresses change all the time.KUBERNETES CLUSTERPublished one by oneAPI serverServicesPod CIDRLaptopSXAccess appCI runnertemporarydeviceAgent podone Deployment,one replicaencrypted tunnelcluster trafficnever publishedOne routing device per cluster, not per node

Why one Deployment rather than a DaemonSet, and Services rather than the pod CIDR: see Advanced configuration.

Setup

  1. 1

    Create the cluster's key and store it as a Secret

    Access › Enrollment keys › Create enrollment key:

    • Name - k8s-prod-router.
    • Make this key reusable - on, so a pod that lost its volume can enrol again.
    • Expires in - empty, so the key never expires.
    • Usage limit - 5. Only a new device uses one, not a restart.
    • Temporary devices, Allow Extra DNS Labels - off. A routing device must stay listed while its pod is rescheduled.
    • Auto-assigned groups - type k8s-routers, press Enter.

    Press Create enrollment key and copy the key: it is shown only once.

    Run the block below, like step 2, from wherever kubectl reaches the cluster today. It stores the key as a Secret, in a namespace that allows the agent's capabilities, and keeps it out of your shell history and manifests. Run the block in bash or zsh (macOS, Linux, WSL). Paste the whole block and press Enter once. When Enrollment key: appears, paste the key, and only the key, and press Enter. The key is not shown, and an empty line asks again.

    You see: secret/sxaccess-key created.

    shell
    kubectl create namespace sxaccesskubectl label namespace sxaccess pod-security.kubernetes.io/enforce=privileged{  KEY=''  until [ -n "$KEY" ]; do printf 'Enrollment key: '; read -rs KEY || break; echo; done  [ -n "$KEY" ] && kubectl -n sxaccess create secret generic sxaccess-key --from-literal=enrollment-key="$KEY"  unset KEY}
  2. 2

    Run the agent as a single Deployment

    Save the manifest below as sxaccess-router.yaml, replace the placeholder with the image from Add device › Docker, and run kubectl apply -f sxaccess-router.yaml. If that command sets other variables with -e, add them under env too.

    The image starts the agent and runs sxaccess start with the key from SX_ENROLLMENT_KEY and the name from SX_HOSTNAME. BPF is optional. strategy: Recreate stops the old pod before the new one starts, so two pods never share the identity kept on the volume.

    You see: after a minute, kubectl -n sxaccess exec deploy/sxaccess-router -- sxaccess info starts with Connected, and k8s-router-prod is under Devices in k8s-routers. Running alone does not prove the pod enrolled.

    shell
    apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: sxaccess-state  namespace: sxaccessspec:  accessModes: ["ReadWriteOnce"]  resources:    requests:      storage: 1Gi---apiVersion: apps/v1kind: Deploymentmetadata:  name: sxaccess-router  namespace: sxaccessspec:  replicas: 1  strategy:    type: Recreate  selector:    matchLabels: { app: sxaccess-router }  template:    metadata:      labels: { app: sxaccess-router }    spec:      containers:        - name: sxaccess          image: IMAGE-FROM-ADD-DEVICE-DOCKER   # copy it from Add device > Docker          securityContext:            capabilities:              add: ["NET_ADMIN", "NET_RAW"]     # "BPF" is optional          env:            - name: SX_ENROLLMENT_KEY              valueFrom:                secretKeyRef:                  name: sxaccess-key                  key: enrollment-key            - name: SX_HOSTNAME              value: k8s-router-prod          volumeMounts:            - name: state              mountPath: /var/lib/sxaccess      volumes:        - name: state          persistentVolumeClaim:            claimName: sxaccess-state
  3. 3

    Put yourself in the platform group

    The rule in step 4 lets the group platform reach the API server. Create it with your laptop in it: People › Members › your name › Access, type platform in Auto-assigned groups, press Enter, then Save Changes. Repeat for each engineer. Your laptop joins at once: group propagation is on by default.

    You see: platform next to your laptop under Devices.

  4. 4

    Publish the API server

    Networks › Internal networks › Add internal network: Network Name k8s-prod, then Add Network. Three forms follow:

    • Add Resource - Name Kubernetes API, which the apps and sxaccess reach list show. Address: the host from your kubeconfig's server: line, without https:// or port. Destination Groups: type k8s-prod-api, press Enter. Then Add Resource.
    • Create access rule - the destination is already Kubernetes API. Pick platform as the source, then TCP. Click the ports field, type the port from the server: line (443 if it has none) and press Enter: the field shows it instead of All. Then Create access rule.
    • Add Routing Device - on Device group, pick k8s-routers, then Continue. Leave Masquerade on and press Add Routing Device. The group also covers a pod that enrols again.

    You see: the k8s-prod row shows 1 resource, 1 device and platform. To publish a Service as well, see Advanced configuration.

  5. 5

    Give CI its own key and access rule

    A second key, for runners, in Access › Enrollment keys › Create enrollment key:

    • Name - ci-runners. Make this key reusable - on.
    • Expires in - 90. Replace the key before then: new key, new CI secret, old key revoked.
    • Usage limit - empty. Every job uses one, so a limit would stop CI.
    • Temporary devices - on: a finished runner is removed about 10 minutes after it goes offline. It cannot be switched on later. Allow Extra DNS Labels - off.
    • Auto-assigned groups - type ci-runners, press Enter.

    Press Create enrollment key and store the key, shown once, as a masked CI secret variable named CI_SXACCESS_KEY. Then click k8s-prod in the networks list and press Add Access rule in the Kubernetes API row. Pick ci-runners, TCP and the same port, then Create access rule. Grant runners nothing else: a build runs code from pull requests.

    You see: 2 in the Access rules column of the Kubernetes API row, and ci-runners next to platform on the k8s-prod row of the networks list.

  6. 6

    Connect each job, and disconnect it at the end

    On a VM runner, where the job has root or sudo, use the block below. The start lines go first in the job; sxaccess stop goes in the step your CI system runs after every job, passed or failed. CI_JOB_ID stands for your CI system's job number, so each runner gets its own name.

    A job in a container has no service manager, so use the block after these steps: it starts the agent itself, then waits for it to answer. It needs root, NET_ADMIN and NET_RAW; where your CI cannot grant them, as on most shared container runners, use a VM runner.

    You see: while the job runs, the runner is under Devices as ci- plus the job number. It is gone about 10 minutes after the job ends.

    shell
    # Start of the job, on a VM runner.curl -fsSL https://pkgs.sxaccess.com/install.sh | shSX_ENROLLMENT_KEY="$CI_SXACCESS_KEY" sxaccess start --hostname "ci-$CI_JOB_ID"# End of the job, passed or failed: your CI system's after-job step.sxaccess stop
shell - a CI job in a container, instead of the block in step 6
# Start of a job in a container: as root, with NET_ADMIN and NET_RAW.# The script can fail at the service step, after installing sxaccess.curl -fsSL https://pkgs.sxaccess.com/install.sh | sh || command -v sxaccess# Start the agent unless one answers already, then wait up to a minute for it.sxaccess info > /dev/null 2>&1 || { sxaccess agent serve & }n=0; until sxaccess info > /dev/null 2>&1 || [ "$n" -ge 15 ]; do n=$((n + 1)); sleep 1; doneSX_ENROLLMENT_KEY="$CI_SXACCESS_KEY" sxaccess start --hostname "ci-$CI_JOB_ID"# End of the job, passed or failed.sxaccess stop
The Enrollment keys tab on the Access page, listing keys with their status, uses against their limit, the group each joins and the expiry date.
Each key's status, uses against its limit, the group it joins and its expiry. A key that has expired or used up its limit enrols nothing, so look here first when a pod or a runner cannot join.

Verify

Test from your laptop in platform, then from a device outside it, then from a CI job.

Expected result

kubectl get nodes works from the laptop, with no bastion and no public endpoint. The SXAccess app lists Kubernetes API under Resources; with the CLI agent, sxaccess reach list shows it with state: Selected.

Then check what should fail

On a device in neither platform nor ci-runners, kubectl get nodes fails: it times out, or the name does not resolve. No second device? Take platform off your member page, try, then put it back.

Then check CI

A job that runs kubectl reaches the API server, and its runner leaves Devices about ten minutes after the job ends.

shell - on your laptop, in platform
kubectl get nodes

Troubleshooting

The first three are specific to running inside a cluster.

SymptomLikely causeWhat to do
No pod is ever createdPod Security Admission rejects the capabilities; the ReplicaSet's events say so.Label the namespace privileged (step 1). kubectl -n sxaccess describe replicaset
The pod restarts in a loop (CrashLoopBackOff), and its device is listed but offlineA capability is missing at runtime, typically NET_RAW: the agent enrols, then cannot bring up its interface.Add NET_ADMIN and NET_RAW. kubectl -n sxaccess logs deploy/sxaccess-router
A new device appears at every restartNo persistent volume, so the pod enrols again at every restart and uses up the key.Mount a PersistentVolumeClaim at /var/lib/sxaccess
kubectl times out, though Kubernetes API is listedThe server: line names another host or port than the resource and its rule.Make them match. Step 4
A cluster DNS name does not resolveThe routing device resolves domain resources through the cluster's DNS.kubectl -n sxaccess exec deploy/sxaccess-router -- nslookup <name>
A CI job stops at the install line, or says cannot reach the SXAccess agentThe job runs in a container with no service manager.Use the container block after step 6
Runners pile up under DevicesThe CI key lacks Temporary devices, or the jobs ended less than 10 minutes ago.Create a new key with it on, update the CI secret, revoke the old key
Still stuck?Take the connection apart one stage at a time.Trace why a connection is blocked

Advanced configuration

One Deployment, and Services rather than pods

A DaemonSet would enrol a device for every node it scales to; the cluster needs one routing device. Pod addresses are reassigned constantly, so a published pod CIDR soon points at the wrong pods: publish Services, or the whole Service CIDR, and put a Service in front of any pod you need.

To publish a Service, click k8s-prod in the networks list and press Add Resource. Give it a Name and, as Address, the Service's cluster IP or <service>.<namespace>.svc.cluster.local, which the routing device resolves. Then press Add Access rule in the new resource's Access rules column.

Surviving a node failure

With one replica, access returns once the pod is rescheduled and its volume has followed. A ReadWriteOnce volume on zonal storage only attaches in its own zone, so the pod can only come back there. For faster failover, run a second Deployment with its own volume and hostname in another zone: it joins k8s-routers with the same key.

Gating cluster access on the laptop

Attach a device requirement to the platform rule: cluster admin is the access most worth tying to a compliant laptop.

More clusters, and the VPC around them

Give each cluster its own network, key, group and hostname. A VPC you also want reached is a separate network with its own routing device: see AWS EC2 and cloud VPCs.

Related