#1760

[Feature]: Kubernetes group impersonation to preserve per-user identity at the API server

Openjullianowopened 2mo ago

Is your feature request related to a problem?

A Kubernetes connection authenticates to the cluster with a single credential. Every user who reaches that cluster through hoop therefore arrives at the API server as the same subject.

Two things follow from that, and neither can be worked around from the hoop side:

Kubernetes cannot authorize per user or per group. Its authorization model is keyed on the identity presented to the API server. When that identity is one shared credential, RBAC has nothing to distinguish callers by — everyone reaching a given connection has exactly the permissions of that credential, no more and no less.

The cluster's audit log cannot attribute an action to a person. Every entry names the shared credential. The distinction between users exists only in hoop's own session records, so anyone reading the cluster's logs — during an incident, or an audit — cannot tell who did what.

Guardrails do not address either point. They express prohibitions that apply to everyone on a connection, not permissions that vary by caller.

Describe the solution you'd like

Allow a Kubernetes connection to send the standard impersonation headers, derived from the identity hoop has already authenticated:

Impersonate-User:  <hoop user identity>
Impersonate-Group: <group(s) mapped from the user's hoop groups>

The connection credential would need the impersonate verb on users and groups, which is the usual pattern for a proxy doing this.

With that in place, Kubernetes decides authorization using the identity it is given, and the audit log records the actual user — while hoop keeps doing what it does at the session layer.

Describe alternatives you've considered

  • A separate connection per permission level, each with its own credential. Works, but multiplies connections by levels times clusters, and still leaves the audit log naming a credential rather than a person.
  • Impersonating only the user, without groups. Restores audit attribution, but not authorization, whenever permissions are assigned to groups rather than to individuals.
  • Expressing the restrictions as guardrails. A different mechanism — it blocks patterns for everyone on a connection, and cannot vary by caller.

Activity

2
github-actions[bot]2mo ago

🔗 Synced to Linear: SUP-225

jullianow2mo ago

Thanks for looking at this.

On the attribution point, I don't think the seam is there: the API server records both subjects in the same audit event. Event.user is the authenticated identity — the connection credential — and Event.impersonatedUser is the impersonated one (audit API reference). A single entry therefore already reads "credential X acted as user Y", which also covers the second edge you raise: the upstream credential is retained next to the impersonated subject, natively, with no extra bookkeeping.

And because the request derives Impersonate-User from the identity hoop has already authenticated, the impersonated subject is the user — there is no synthetic subject to translate, so there is no hoop-side mapping to persist. If we do want to tie a request to a specific hoop session, which is useful on its own, Kubernetes already carries it: Impersonate-Extra-<key> headers surface under impersonatedUser.extra, so an Impersonate-Extra-Session-Id puts the correlation id straight into the cluster's own log.

Agreed that group sets must be per user rather than a static list — that is what the request specifies (Impersonate-Group mapped from the user's hoop groups), and it is exactly why impersonating only the user was listed as insufficient.

The part I would rather see settled is the privilege shape on the connection side: granting the connection credential impersonate on users and groups is itself an escalation path, since unrestricted it can present as any subject, system:masters included. Constraining it — RBAC resourceNames, or the constrained impersonation mode that authenticationMetadata.impersonationConstraint points at in recent versions — looks like the real design decision here.