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.

  1. 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/jwks

    then 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.

  2. Let an unauthenticated reader see the discovery documents

    kyvlt.sh fetches /.well-known/openid-configuration and 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:unauthenticated

    and anonymous-auth=true added to the kube-apiserver-arg list above. Upstream Kubernetes ships with anonymous auth on; k3s turns it off, and with it off the fetch gets 401 no 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-configuration
  3. Put 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+insecure because the hop to 127.0.0.1 is 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.

  1. Find your issuer

    kubectl get --raw /.well-known/openid-configuration | jq -r .issuer
  2. Register 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 subject system: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.)

  3. 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=true
    ValueDefault
    identity.enabledfalseMounts a projected service-account token for the operator.
    identity.audiencekyvlt.shMust match the trust. Never the API server's own, or the ambient token every pod mounts would be accepted.
    identity.expirationSeconds3600How often the kubelet rotates the file. The operator re-reads it each time.
  4. 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: 300

    identity: {} instead of tokenRef. 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.