Skip to content
Identity strength

Identity strength

Every write records how well the caller’s credential was bound. kslate derives it from the credential itself, stores it beside the author, and shows it in history and blame:

v1   08:17:56  actions_taken   sa:kagent/kslate-remediator append APPLIED
      identity: sa:kagent/kslate-remediator (pod-bound)

A class can require a minimum, per field. Then a credential that can be copied cannot change a field that matters.

The four levels

levelwhat it means
pod-boundA projected ServiceAccount token that Kubernetes bound to a pod object. The token stops working when the pod goes away.
sa-boundA ServiceAccount token kslate verified, on a cluster that does not report the pod binding. Valid until it expires, wherever it is replayed from.
issuer-declaredAn OIDC token or a client certificate. Only the operator knows what a given issuer’s tokens or a given CA’s keys are worth, so the strength is set per issuer or per CA in the gateway’s own configuration.
shared-secretAn API key. A string with no binding at all: whoever holds a copy is indistinguishable from you.

They are ordered as listed: pod-bound is the strongest and shared-secret the weakest.

A write made before this existed has no strength. history and blame print unknown for it. That is the absence of a value, not a weak one. A floor applies to new writes only.

You cannot set your own strength

There is no request field for it. kslate works it out from the credential you presented:

  • an API key is always shared-secret
  • a ServiceAccount token is pod-bound when TokenReview reports the pod it was bound to, and sa-bound when it does not
  • an OIDC token takes the strength its issuer was configured with, never a claim inside the token. An issuer cannot be configured above issuer-declared: a JWT is a bearer credential whoever minted it
  • a client certificate takes the strength its CA was configured with

A request that includes identity_strength is ignored. That is what lets a class gate a write on it.

Not every cluster reports the pod binding. The gateway says which one yours does, once, the first time it verifies a ServiceAccount token:

INFO identity: this cluster returns TokenReview's pod-binding extras;
     projected ServiceAccount tokens resolve as pod-bound

If yours does not, every ServiceAccount token is sa-bound and a pod-bound floor cannot be satisfied. Check that line before writing one.

Setting a floor

minIdentityStrength on the class is the default. Any field can override it. The floor is a property of the field, not of the role. A role says who may write a field. The floor says how well that writer must be authenticated.

spec:
  minIdentityStrength: sa-bound      # nothing weaker may change this class
  fields:
    scratch:
      type: string
      mode: soft
      minIdentityStrength: shared-secret   # a note is not worth a strong credential
    irreversible:
      type: string
      mode: versioned
      evidence: required
      minIdentityStrength: pod-bound       # this one is

A write below the floor is refused with WEAK_IDENTITY. The body names the floor and what you presented:

{
  "error": "WEAK_IDENTITY",
  "record": "incidents/inc-42",
  "field": "irreversible",
  "required": "pod-bound",
  "presented": "sa-bound"
}

It is a separate error from SCOPE_DENIED because the fix is different. SCOPE_DENIED means ask for more permissions. WEAK_IDENTITY means present a better credential. That is usually a deployment change (move the writer, mount a different token), not a policy change. Both are recorded in history, each with its own outcome, so an auditor can tell afterwards which it was.

A floor naming a level that does not exist is refused when the class loads, not when a write arrives. Otherwise a typo would reject every write to the field and look like an outage.

The floor hangs off the class and the field rather than off a role. A role answers who may touch a field; the floor answers how sure kslate has to be before that field changes, which is a property of the data.

Putting it on a role would also break the way roles combine. An identity that matches two roles gets the permissions of both. Add floors to that and combining two roles would mean taking the lower floor, which is the opposite of how every other part of scope adds up.

Strength is not provenance

Strength says how hard your credential would be to steal and replay. It says nothing about who decided the thing you wrote.

A reconciler driving an agent presents its own credential. A strong strength tells you the write came from that reconciler. It does not tell you which agent, or which call, produced the decision. For that, a writer can attach an invocation object. blame labels it writer-asserted because nothing verifies it.

The two lines answer different questions:

identity: sa:kagent/kslate-remediator (pod-bound)        <- kslate verified this
invocation (writer-asserted): agent=kagent:kagent/remediator
    task_id=01a1245d-d1ce-7047-bfed-d09ccd907278          <- the writer says this