Authenticating as the cluster
The operator presents the token Kubernetes already mints for its service account. kyvlt.sh checks it against your cluster's published signing keys and issues a credential that lasts ten minutes and is never written anywhere.
Nothing to store, nothing to rotate, nothing to find in a backup.
What your cluster must do
kyvlt.sh reads <issuer>/.well-known/openid-configuration over https.
EKS, GKE and AKS publish one. A self-managed cluster whose issuer is
https://kubernetes.default.svc does not, and needs --service-account-issuer
pointed somewhere reachable — a decision about your cluster, not about
kyvlt.sh.
If that is not something you want to do, service tokens renew themselves and the expiry cliff is gone either way.
Self-managed clusters, the whole recipe
On a managed cluster the issuer already works and you can skip to the steps below. On k3s (and most kubeadm clusters) three things stand between you and a working registration, and each one fails quietly on its own. Shown for k3s; the flags are plain kube-apiserver flags everywhere.
Give the issuer a reachable https name
In
/etc/rancher/k3s/config.yaml:kube-apiserver-arg: - service-account-issuer=https://k8s.example.com - service-account-issuer=https://kubernetes.default.svc.cluster.local - service-account-jwks-uri=https://k8s.example.com/openid/v1/jwksthen restart k3s. The first issuer is the one new tokens carry — and the one you register. Keeping the old issuer second means every token minted before the change still verifies, so running workloads do not notice.
Let an unauthenticated reader see the discovery documents
kyvlt.sh fetches
/.well-known/openid-configurationand the JWKS with no credential at all. That needs two things, and k3s clusters need both:kubectl create clusterrolebinding oidc-discovery \ --clusterrole=system:service-account-issuer-discovery \ --group=system:unauthenticatedand
anonymous-auth=trueadded to thekube-apiserver-arglist above. Upstream Kubernetes ships with anonymous auth on; k3s turns it off, and with it off the fetch gets401no matter what RBAC says. Turning it back on grants anonymous callers only what RBAC grants them — with the binding above, the discovery documents and nothing else.The test that settles it — no kubeconfig anywhere near it:
curl https://k8s.example.com/.well-known/openid-configurationPut a real certificate in front
kyvlt.sh will not accept your cluster CA — the fetch verifies against public roots, like every other https client. Any proxy that terminates TLS with a real certificate and forwards to the API server works. With Tailscale it is one line on the node:
tailscale serve --bg --https=443 https+insecure://127.0.0.1:6443(
https+insecurebecause the hop to127.0.0.1is verified by being local, not by its self-signed cert. HTTPS certificates must be enabled for your tailnet, and the name must resolve from wherever kyvlt.sh runs.)Caddy, nginx with certbot, or a cloud load balancer with ACM do the same job. Whatever the front door, the name it serves is the name in
service-account-issuer— exactly, scheme and all.
Find your issuer
kubectl get --raw /.well-known/openid-configuration | jq -r .issuerRegister the cluster
Integrations → Kubernetes, choose This cluster's identity, and paste the issuer. Owners only, and once per cluster.
The registration names three things and matches all of them exactly: that issuer, the audience
kyvlt.sh, and the subjectsystem:serviceaccount:kyvlt-operator:kyvlt-connector. There is no wildcard. Four clusters is four registrations.A registration grants nothing by itself. What the cluster may read is its assignments — one environment each, made by an Owner, each its own audited grant, any number of them on one registration. (Before assignments existed, the registration carried one environment; that ceiling is gone, and old registrations were migrated to a single equivalent grant.)
Install with identity on
helm install kyvlt-connector oci://ghcr.io/bluepawlabs/charts/kyvlt-connector \ --version 0.10.0 \ --namespace kyvlt-operator --create-namespace \ --set servedNamespaces='{default}' \ --set reload.enabled=true \ --set identity.enabled=trueValue Default identity.enabled false Mounts a projected service-account token for the operator. identity.audience kyvlt.sh Must match the trust. Never the API server's own, or the ambient token every pod mounts would be accepted. identity.expirationSeconds 3600 How often the kubelet rotates the file. The operator re-reads it each time. Ask for the environment
apiVersion: connectors.kyvlt.sh/v1alpha1 kind: KyvltSync metadata: name: kyvlt-secrets spec: # <your-project> / <your-environment> environmentId: <your-environment-id> apiBaseUrl: https://api.kyvlt.sh identity: {} intervalSeconds: 300identity: {}instead oftokenRef. Setting both is refused at apply time; setting neither is too.
What changes day to day
Nothing visible. The operator exchanges roughly every eight minutes, holds the
credential in memory, and reports no TokenExpiring condition — there is no
expiry anybody has to act on.
Every issuance is its own audit record, so a cluster's steady cadence is visible and so is an exchange that does not fit it — and the clusters view's "last seen" is derived from exactly these authenticated asks, never from anything a cluster says about itself.
Turning it off
Removing the registration revokes every credential it issued, on the next
request. The operator's next sync reports Unauthorised — and Secrets
already projected remain in the cluster: revoking access is not an
instruction to delete what the cluster holds. Removing an assignment is; see
assignments.