Passwordless Kubernetes Operator Setup in 15 Minutes
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
- A Kubernetes cluster you have admin on (kind, minikube, k3d, EKS, GKE, AKS all work). Kubernetes 1.24+ for projected token support.
kubectlinstalled and pointed at your cluster.- A CoreLink tenant with a tenant-admin role.
jq(or any JSON pretty-printer).- Network path: your pods must be able to reach your CoreLink endpoint. For a local cluster talking to a cloud-hosted CoreLink, that's just outbound HTTPS.
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:
- Name:
k8s-prod-apps - Attestation type:
kubernetes - Trusted issuer: the issuer URL from Step 1
- Expected audience:
corelink(you'll use this in the pod spec in Step 4) - Claim matchers:
kubernetes.io/namespaceequalsprodkubernetes.io/serviceaccount/nameprefixmyapp-
- Default scopes:
secrets:read - Max NHIs: 50 (cap on auto-registered workloads; tune as needed)
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:
- Applies the bundled RBAC manifest (Namespace
corelink-system, ServiceAccounttbcl-k8s-nhi-operator, ClusterRole granting onlypods.get/list/watchandserviceaccounts/token.create). - 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. - 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:
- 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/tokenand rotates it in the background. - 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 KubernetesTokenRequestAPI (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. - CoreLink fetched the cluster's JWKS, verified the JWT signature, issuer, and audience, extracted the claims, and matched them against the
k8s-prod-appsworkload template. - 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
- See the Kubernetes use case for the full architecture diagram and advanced configurations (multiple templates, custom scopes per pod, federation across clusters).
- Read Passwordless Workload Authentication: Closing the NHI Gap for the design context.
- For multi-cluster federation via SPIFFE trust bundles, see Tenant > Federation Peers.