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
| level | what it means |
|---|---|
pod-bound | A projected ServiceAccount token that Kubernetes bound to a pod object. The token stops working when the pod goes away. |
sa-bound | A ServiceAccount token kslate verified, on a cluster that does not report the pod binding. Valid until it expires, wherever it is replayed from. |
issuer-declared | An 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-secret | An 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-boundwhenTokenReviewreports the pod it was bound to, andsa-boundwhen 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-boundIf 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 isA 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