Tutorials

Passwordless Kubernetes Operator Setup in 15 Minutes

Outcome. By the end of this tutorial a pod in your Kubernetes cluster will authenticate to CoreLink using only its projected service-account JWT — no API keys, no bootstrap tokens, no secrets stored in the pod spec. CoreLink will auto-register the workload as an NHI on first contact via a workload template you defined.

The operator you'll deploy in Step 3 has two reconcilers: identity (this tutorial) and secrets-sync (mirror CoreLink secrets into native K8s Secret objects). Both are on by default. Set OPERATOR_MODE=identity in the operator's config if you only want the attestation flow this tutorial demonstrates.

Prerequisites

Step 1 — Get your cluster's issuer URL

CoreLink verifies the pod's service-account JWT against the cluster's OIDC JWKS. You need the issuer URL, which is the base for that JWKS discovery:

kubectl get --raw /.well-known/openid-configuration | jq .issuer
# "https://kubernetes.default.svc.cluster.local"    # (cluster-internal, won't work for CoreLink)
# OR
# "https://oidc.eks.us-east-1.amazonaws.com/id/1234..."  # (EKS, publicly reachable)

If the issuer is cluster-internal, you'll need to configure your cluster to expose a public issuer (standard on EKS/GKE; requires --service-account-issuer=https://your-public-url and --service-account-jwks-uri= flags on kubelet for self-managed).

Step 2 — Create a workload template in CoreLink

In the CoreLink UI, open Tenant > Workload Templates and click New Template. Fill in:

Save. The template ID appears in the detail view — you won't need it for this tutorial (workload templates match implicitly via claims) but note it for later. While you're here, also copy your Tenant ID from Tenant > Settings — the operator needs it for JIT registration in Step 3.

Step 3 — Install the operator in your cluster

The operator runs as a Deployment in corelink-system. The published manifest creates the Namespace, ServiceAccount, ClusterRole/Binding, ConfigMap, and Deployment in one apply. The image is hosted publicly on Azure Container Registry — no imagePullSecrets, no private registry setup, no docker build:

# 1. Download the manifest
curl -O https://usecorelink.com/static/downloads/k8s/tbcl-k8s-nhi-operator.yaml

# 2. Edit tbcl-config to set your endpoint and tenant_id (the rest of the
#    file is generic -- you can leave it alone)
$EDITOR tbcl-k8s-nhi-operator.yaml

# 3. Apply
kubectl apply -f tbcl-k8s-nhi-operator.yaml

# 4. Verify
kubectl -n corelink-system rollout status deploy/tbcl-k8s-operator
kubectl -n corelink-system logs deploy/tbcl-k8s-operator --tail=20
# level=INFO msg="starting tbcl-k8s-nhi-operator" mode=both endpoint=... auth=in-cluster

The tbcl-config ConfigMap inside the manifest is the only thing you need to change:

data:
  endpoint:  "https://usecorelink.com"
  tenant_id: "<your-tenant-uuid>"        # from Tenant > Settings
  audience:  "corelink"
  mode:      "both"                       # identity | secrets | both

The operator authenticates to the Kubernetes API as its own ServiceAccount (tbcl-k8s-nhi-operator in corelink-system) using the in-pod token, and to CoreLink by self-attesting through the same /api/v1/nhi-agent/connect flow that user pods will use in Step 4. No CoreLink token, API key, or other pre-shared secret is stored anywhere.

Running outside the cluster (airgap, jumphost, validation)

If you can't run the operator in-cluster — airgapped registries, no admin access to apply Deployments cluster-wide, or you want to validate the flow from your workstation before deploying — the same binary runs as a host-level daemon using a kubeconfig. The installer applies the same RBAC and mints a least-privilege ServiceAccount kubeconfig for the daemon to use.

# Linux / macOS
curl -fsSL https://usecorelink.com/static/downloads/k8s/tbcl-k8s-nhi-operator-installer.tar.gz | tar -xz
sudo k8s-nhi-operator/install.sh \
  --endpoint  https://usecorelink.com \
  --tenant-id <your-tenant-uuid>

# Windows (elevated PowerShell)
Invoke-WebRequest -Uri https://usecorelink.com/static/downloads/k8s/tbcl-k8s-nhi-operator-installer.zip -OutFile installer.zip
Expand-Archive installer.zip
.\installer\k8s-nhi-operator\install.ps1 `
  -Endpoint https://usecorelink.com `
  -TenantId <your-tenant-uuid>

Both installers register a platform-native service: systemd on Linux/macOS (running as corelink-nhi), Scheduled Task on Windows (running at boot under SYSTEM). Functionally equivalent to the in-cluster Pod — just running off-cluster.

The installer:

  1. Applies the bundled RBAC manifest (Namespace corelink-system, ServiceAccount tbcl-k8s-nhi-operator, ClusterRole granting only pods.get/list/watch and serviceaccounts/token.create).
  2. Mints a long-lived token for that ServiceAccount and writes /etc/corelink-nhi/kubeconfig — the daemon authenticates as the SA, not as your human kubectl user.
  3. Drops the binary at /opt/corelink-nhi/bin/, env file at /etc/corelink-nhi/operator.env, and a hardened systemd unit, then starts the service.

Verify:

systemctl status tbcl-k8s-nhi-operator
# Active: active (running)

journalctl -u tbcl-k8s-nhi-operator --since="2 min ago" | head -5
# level=INFO msg="starting tbcl-k8s-nhi-operator" endpoint=https://usecorelink.com audience=corelink auth=kubeconfig:/etc/corelink-nhi/kubeconfig ...

The daemon does not require a pre-shared CoreLink token. It uses the cluster-side ServiceAccount to talk to the Kubernetes API (read pods, mint per-pod ServiceAccount tokens) and presents those minted tokens to CoreLink for attestation.

Prefer running the operator inside the cluster as a Pod?

That pattern is also supported — an in-cluster manifest is published at /static/downloads/k8s/tbcl-k8s-nhi-operator.yaml. It does exactly what the host daemon does, just packaged as a Deployment with its own ServiceAccount and RBAC. The host-daemon flow is the recommended default because it requires no container registry to publish images to, and the daemon's outbound traffic is easier to audit at the host level.

Step 4 — Deploy a workload that matches the template

Create the namespace, service account, and a pod. The critical parts are the namespace (prod), the SA name (myapp-web — matches the myapp- prefix), and the projected token with audience corelink. The audience is the operator's opt-in signal — it only attests pods that mount a projected SA token with the configured audience:

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
  name: prod
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp-web
  namespace: prod
---
apiVersion: v1
kind: Pod
metadata:
  name: myapp-web
  namespace: prod
spec:
  serviceAccountName: myapp-web
  containers:
    - name: app
      image: nginx:alpine
      volumeMounts:
        - name: corelink-token
          mountPath: /var/run/secrets/corelink
          readOnly: true
  volumes:
    - name: corelink-token
      projected:
        sources:
          - serviceAccountToken:
              path: token
              audience: corelink
              expirationSeconds: 3600
EOF

Step 5 — Verify

Within one poll interval (default 30s) the operator should detect the pod and establish a session. Tail the operator logs:

kubectl -n corelink-system logs deploy/tbcl-k8s-operator -f
# level=INFO msg="connecting workload" namespace=prod sa=myapp-web pod=myapp-web
# level=INFO msg="nhi session issued" namespace=prod sa=myapp-web nhi_id=... session_id=... ttl=1h0m0s

# Running off-cluster instead?
#   Linux/macOS: journalctl -u tbcl-k8s-nhi-operator -f
#   Windows:     Task Scheduler > CoreLink-NHI-Operator > History (or rerun install.ps1 -Foreground)

If you see msg="nhi session pending approval" instead, your workload template is configured to require manual approval — review and approve the request under Workspace > NHI.

In the CoreLink UI, open the workspace's NHI page (sidebar entry, or directly at /tenant/workspaces/<workspace>/nhi). You'll see a new auto-registered identity with attestation type kubernetes, source template k8s-prod-apps, and a last-connect timestamp from just now.

From inside the pod, the projected token is mounted and refreshed by the kubelet:

kubectl -n prod exec myapp-web -- cat /var/run/secrets/corelink/token
# eyJhbGci...  (JWT)

The operator establishes the NHI session on the workload's behalf. To make API calls from the pod, your application uses the CoreLink SDK or sets X-NHI-Session: <session-id> on requests — the session ID is returned by the operator's most recent connect (visible in the operator logs and in the workspace NHI detail view).

Step 6 (optional) — Sync CoreLink secrets into K8s Secrets

The same operator can also mirror CoreLink secrets into native Kubernetes Secret objects so legacy apps that read env vars or files can consume them without using an SDK. To enable, create a corelink-secrets-config ConfigMap in corelink-system listing the secrets you want synced:

apiVersion: v1
kind: ConfigMap
metadata:
  name: corelink-secrets-config
  namespace: corelink-system
data:
  config: |
    {
      "secrets": [
        {
          "corelink_secret_id": "<uuid-from-corelink>",
          "k8s_secret_name":    "myapp-db",
          "k8s_namespace":      "prod",
          "sync_interval":      "5m",
          "secret_type":        "Opaque"
        }
      ]
    }

The operator self-attests using its own ServiceAccount (the cluster RBAC the installer applied already grants it secrets get/list/create/update/patch), establishes its own NHI session, and uses that session to fetch each secret via GET /api/v1/nhi-agent/secrets/{id}. Each value is written to the named K8s Secret with annotations recording the CoreLink secret ID, last-sync timestamp, and a SHA-256 hash of the value (used to skip writes when the value hasn't changed).

Just like the identity flow, no CORELINK_TOKEN is stored anywhere — the operator authenticates via a fresh ServiceAccount JWT minted at runtime. Configure a workload template scoped to namespace=corelink-system, serviceaccount=tbcl-k8s-nhi-operator with default scope secrets:read so the operator's own NHI auto-registers on first connect.

What just happened

Four things:

  1. The pod requested a projected ServiceAccount token from kubelet with audience corelink. Kubelet writes a fresh JWT signed by the cluster OIDC issuer to /var/run/secrets/corelink/token and rotates it in the background.
  2. The operator (running as a host systemd daemon) listed pods cluster-wide via the kubeconfig the installer wrote, saw a projected token volume requesting audience corelink, and minted its own per-pod JWT for the same ServiceAccount via the Kubernetes TokenRequest API (bound to the pod UID, so it's invalidated when the pod is deleted). The operator then POSTed that JWT to /api/v1/nhi-agent/connect.
  3. CoreLink fetched the cluster's JWKS, verified the JWT signature, issuer, and audience, extracted the claims, and matched them against the k8s-prod-apps workload template.
  4. Because no NHI existed for this SA yet, CoreLink auto-registered one with the template's default scopes and returned a short-lived session.

No secret ever existed in the pod spec, the image, or the operator config. If the pod is compromised, the attacker gets a JWT that expires within the hour and is bound to the pod and the cluster issuer — it can't be used anywhere CoreLink doesn't trust. Rotation is automatic: kubelet rotates the projected token in the background and the operator re-establishes sessions on its session_refresh cadence (default 10 minutes).

Next steps