Repository navigation
kubectl ate: authenticate with ExecCredential plugins named by --credential-plugin - #2851
Max Thompson (thompsonmax) wants to merge 4 commits into
Conversation
f8db639 to
cb06553
Compare
cb06553 to
56c6bf0
Compare
9cb63fd to
a2892f6
Compare
Benjamin Elder (BenTheElder)
left a comment
There was a problem hiding this comment.
AI-assisted first pass, inline comments are mostly small. Two bigger comments:
-
Can we split this up? WhoAmI and
kubectl ate credential/wrapcould each be their own PR, this is a lot to review at once. Or at least one PR with more commits, they don't even all have to build, since the merge queue only squash merges. The latest AGENTS.md has some guidance for this, if you're generating this you can ask your agent something like "please pull the latest upstream AGENTS.md and apply it to the PR". -
I'm not sure we need ATE_REFRESH_CREDENTIAL or the client cert path yet, can we start with the ServiceAccount token default (shared or per-user via --service-account) + client-go's expiry handling, and add the rest when we have a case for it?
We should try hard to not require non-portable flags / dedicated environment variables to get a working client.
nit: please note AI use in the PR description per the PR template
(either way, so far this has been a useful quality signal WRT agents that are not respecting AGENTS.md / docs or humans that are ignoring the PR template ...)
| } | ||
|
|
||
| // PrincipalKind is the authentication method that established a principal. | ||
| enum PrincipalKind { |
There was a problem hiding this comment.
cc Julian Gutierrez Oschmann (@juli4n) Tim Hockin (@thockin) , we were discussing only using strings instead of proto enums for compat reasons. We should decide on that ASAP #1378
There was a problem hiding this comment.
Let me know if/when there's a decision there and I'll change this ASAP.
There was a problem hiding this comment.
That's bonkers. Why?
|
d762059 to
de5dc63
Compare
2decbb9 to
9a2eb79
Compare
I'm not sure we want the default experience to require connecting to the k8s cluster and minting a random service account token that erases your human identity. I (sort of) think that we should always make people pass an --exec-credential-plugin flag to kubectl ate for now. |
| string id = 1; | ||
|
|
||
| // How the caller authenticated. | ||
| PrincipalKind kind = 2; |
There was a problem hiding this comment.
Let's model this the way kubernetes does --- a single string credential ID. For JWTs, it would be a string like JTI=${jti-claim-from-jwt}. For certificates it would be a string like X509SHA256=${sha-256-of-the-certficate}
There was a problem hiding this comment.
Or, if you want to include the issuer, have a oneof that wraps a JWTInfo and SPIFFECertificateInfo struct.
There was a problem hiding this comment.
Moved WhoAmI to #3261. I think ideally we include the issuer, so we can model this like:
message WhoAmIResponse {
// The principal ID that authorization policies name.
string id = 1;
// How the caller authenticated. Exactly one is set.
// +k8s:optional
// +k8s:unionMember
JWTCredential jwt = 2;
// +k8s:optional
// +k8s:unionMember
SPIFFECertificateCredential spiffe_certificate = 3;
}
message JWTCredential {
string issuer = 1;
}
message SPIFFECertificateCredential {}
| } | ||
|
|
||
| // PrincipalKind is the authentication method that established a principal. | ||
| enum PrincipalKind { |
There was a problem hiding this comment.
That's bonkers. Why?
| ) | ||
|
|
||
| var credentialCmd = &cobra.Command{ | ||
| Use: "credential", |
There was a problem hiding this comment.
If I understand correctly, this command is a credential plugin that:
- Authenticates to a k8s cluster (using your credentials configured in a .kubeconfig file)
- Mints a kubernetes service account JWT for a named service account.
If so, it needs to be named something much more specific than "credential" --- I think, as it is, it will be confusing to users, who might think this is the one blessed way to get a credential for a substrate install.
There was a problem hiding this comment.
What about kubectl ate service-account-token?
There was a problem hiding this comment.
Went with that name, LMK if you're not a fan.
| } | ||
|
|
||
| // An existing file runs with no arguments, so its path may hold spaces. | ||
| command, args := credentialPlugin, []string(nil) |
There was a problem hiding this comment.
I feel like instead of doing this parsing, we should take the information in a structured way, like:
kubectl ate foo --credential-plugin="binary" --credential-plugin-arg="--foo" --credential-plugin-arg="--bar"
There was a problem hiding this comment.
Done, separated into structured --credential-plugin and --credential-plugin-arg flags.
We'll definitely want to clean this up post-GA.
|
|
||
| // execCreds runs a kubeconfig exec credential plugin through client-go's | ||
| // authenticator, which caches the credential until its expirationTimestamp. | ||
| // That authenticator is built for net/http, so execCreds drives it with |
There was a problem hiding this comment.
OK, there has to be a better way to do this. It would be better to fork the k8s exec credential code for now than to start up a magical local http server just for getting the credential.
There was a problem hiding this comment.
Or just write our own code for invoking the exec plugin. The important thing we need from k8s is the protocol, so that people don't have to write new credential plugins for substrate. The actual code seems to be more trouble than it's worth
There was a problem hiding this comment.
Done, ateclient now runs the plugin itself by setting KUBERNETES_EXEC_INFO and reads the v1 ExecCredential the plugin prints.
9a2eb79 to
3be4c10
Compare
The client minted one hour-long token for the shared ate-client ServiceAccount when it connected and never replaced it, so long-running clients failed after an hour. It now mints on first use and again a minute before expiry, and keeps the current token until it expires if re-minting fails. NewClient now wraps Dial and DialOptions, which merge the direct and port-forward paths so the credential is set up in one place. Signed-off-by: Max Thompson <[email protected]>
3be4c10 to
5ea9878
Compare
--credential-plugin names the command of a kubectl credential plugin, and each --credential-plugin-arg passes it one argument. kubectl ate passes the plugin its --kubeconfig and --context as $ATE_KUBECONFIG and $ATE_KUBE_CONTEXT, and its stdin when stdin is a terminal. Passing both --token-file and --credential-plugin is an error. ateclient runs the plugin itself and reads the v1 ExecCredential it prints. It reuses the bearer token until a minute before the token's expirationTimestamp. A plugin that prints no token is an error. Signed-off-by: Max Thompson <[email protected]>
kubectl ate service-account-token mints a token for ate-api for the ServiceAccount named with --service-account and prints it as an ExecCredential for --credential-plugin, or only the token with -o token. With no --kubeconfig or --context it uses $ATE_KUBECONFIG and $ATE_KUBE_CONTEXT, so it mints through the same cluster as the kubectl ate command that ran it. Signed-off-by: Max Thompson <[email protected]>
Describe --credential-plugin and kubectl ate service-account-token, how to give a user a ServiceAccount of their own and the RBAC a non-admin needs, what deleting a ServiceAccount does not revoke, and in-cluster workloads. Signed-off-by: Max Thompson <[email protected]>
5ea9878 to
ac9d542
Compare
…gent-substrate#3119) Part of agent-substrate#1464. - Rewrites `docs/guides/authentication.md` around who is calling ate-api: a person running `kubectl ate`, a pod in the Substrate cluster or another cluster, or a CI job, so people stop leaning on the shared `ate-client` token. - Adds pod certificates as an option for client pods (the benchmarks use them), the `ate.dev/api-access` namespace label the ate-system NetworkPolicy requires, and the Kubernetes RBAC `kubectl ate` needs to reach ate-api. - Warns where an identity is weaker than it looks: another cluster's issuer can mint `ate-client`, GKE's Kubernetes Engine Developer and basic Editor roles can act as any ServiceAccount, ate-system port-forward rights reach loopback-only Envoy admin APIs that can stop the data plane, and GitHub and Google tokens authenticate anyone. - Moves the per-user ServiceAccount recipe and the signing-in and install-time-provider gaps out of `authorization.md`, and drops its claim that a Secret-based token works there (ate-api rejects its issuer). The recipe and RBAC use agent-substrate#2851's names (`ate-users`, `ate-api-client`). For reviewers: agent-substrate#2851 (`--credential-plugin`) rewrites the same sections, and its paragraph calling GitHub and Google tokens unsafe needs to match this one. Testing: checked each claim against the code on main (GitHub, GKE and Kubernetes behavior against their docs or source) and that the relative links resolve. None of the commands or manifests ran on a cluster. AI use: AI-assisted. - [x] Tests pass - [x] Docs updated --------- Signed-off-by: Max Thompson <[email protected]>
…3119) Part of #1464. - Rewrites `docs/guides/authentication.md` around who is calling ate-api: a person running `kubectl ate`, a pod in the Substrate cluster or another cluster, or a CI job, so people stop leaning on the shared `ate-client` token. - Adds pod certificates as an option for client pods (the benchmarks use them), the `ate.dev/api-access` namespace label the ate-system NetworkPolicy requires, and the Kubernetes RBAC `kubectl ate` needs to reach ate-api. - Warns where an identity is weaker than it looks: another cluster's issuer can mint `ate-client`, GKE's Kubernetes Engine Developer and basic Editor roles can act as any ServiceAccount, ate-system port-forward rights reach loopback-only Envoy admin APIs that can stop the data plane, and GitHub and Google tokens authenticate anyone. - Moves the per-user ServiceAccount recipe and the signing-in and install-time-provider gaps out of `authorization.md`, and drops its claim that a Secret-based token works there (ate-api rejects its issuer). The recipe and RBAC use #2851's names (`ate-users`, `ate-api-client`). For reviewers: #2851 (`--credential-plugin`) rewrites the same sections, and its paragraph calling GitHub and Google tokens unsafe needs to match this one. Testing: checked each claim against the code on main (GitHub, GKE and Kubernetes behavior against their docs or source) and that the relative links resolve. None of the commands or manifests ran on a cluster. AI use: AI-assisted. - [x] Tests pass - [x] Docs updated --------- Signed-off-by: Max Thompson <[email protected]> (cherry picked from commit f1973c1) Signed-off-by: Eitan Yarmush <[email protected]>
Part of #1464
Support ExecCredential
kubectl ateauthentication.--credential-pluginnames a kubectl credential plugin's command, and each--credential-plugin-argpasses it one argument. kubectl ate passes the plugin its--kubeconfigand--contextas$ATE_KUBECONFIGand$ATE_KUBE_CONTEXT.internal/ateclientruns the plugin itself and reuses its token until a minute before it expires. The default ServiceAccount token now refreshes a minute before it expires, which fixes [P1] ateclient mints a single 1-hour bearer token and never refreshes it — long-lived clients go permanently Unauthenticated #644.kubectl ate service-account-tokenmints a token for the ServiceAccount named with--service-account.Here's how each authentication option looks after this change:
--token-file: you supply the token yourself (Already existed before this change)kubectl ate sends whatever token the file holds. It re-reads the file on every RPC, so a rotated file is picked up automatically, but it never runs anything to get a new token.
--credential-plugin: kubectl ate runs a command that produces the credential (What we're introducing here)The command must print an ExecCredential (v1) as JSON. kubectl ate runs the command when it first needs a token, and again a minute before the token's expirationTimestamp.
Rules that apply to both
Reviewer notes: the commits split the PR by feature (the #644 refresh,
--credential-plugin,kubectl ate service-account-token, docs) and can be reviewed in order.kubectl ate whoamimoved to the follow-up #3261. Plugins must print aclient.authentication.k8s.io/v1ExecCredential, so plugins that only answer v1beta1 fail.Testing: unit tests; e2e checks that a client with a credential plugin (
catof an ExecCredential) is allowed and denied as the plugin's ServiceAccount.AI use: AI-assisted (Claude Code) for code, tests, and docs. I reviewed and tested it.
Fixes #644