Merge pull request #1503 from ericchiang/external-auth-providers
proposal: external client-go auth providers
This commit is contained in:
commit
405705a5f6
|
@ -0,0 +1,282 @@
|
||||||
|
# Out-of-tree client authentication providers
|
||||||
|
|
||||||
|
Author: @ericchiang
|
||||||
|
|
||||||
|
# Objective
|
||||||
|
|
||||||
|
This document describes a credential rotation strategy for client-go using an exec-based
|
||||||
|
plugin mechanism.
|
||||||
|
|
||||||
|
# Motivation
|
||||||
|
|
||||||
|
Kubernetes clients can provide three kinds of credentials: bearer tokens, TLS
|
||||||
|
client certs, and basic authentication username and password. Kubeconfigs can either
|
||||||
|
in-line the credential, load credentials from a file, or can use an `AuthProvider`
|
||||||
|
to actively fetch and rotate credentials. `AuthProviders` are compiled into client-go
|
||||||
|
and target specific providers (GCP, Keystone, Azure AD) or implement a specification
|
||||||
|
supported but a subset of vendors (OpenID Connect).
|
||||||
|
|
||||||
|
Long term, it's not practical to maintain custom code in kubectl for every provider. This
|
||||||
|
is in-line with other efforts around kubernetes/kubernetes to move integration with cloud
|
||||||
|
provider, or other non-standards-based systems, out of core in favor of extension points.
|
||||||
|
|
||||||
|
Credential rotation tools have to be called on a regular basis in case the current
|
||||||
|
credentials have expired, making [kubectl plugins](https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/),
|
||||||
|
kubectl's current extension point, unsuitable for credential rotation. It's easier
|
||||||
|
to wrap `kubectl` so the tool is invoked on every command. For example, the following
|
||||||
|
is a [real example](
|
||||||
|
https://github.com/heptio/authenticator#4-set-up-kubectl-to-use-heptio-authenticator-for-aws-tokens)
|
||||||
|
from Heptio's AWS authenticator:
|
||||||
|
|
||||||
|
```terminal
|
||||||
|
kubectl --kubeconfig /path/to/kubeconfig --token "$(heptio-authenticator-aws token -i CLUSTER_ID -r ROLE_ARN)" [...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Beside resulting in a long command, this potentially encourages distributions to
|
||||||
|
wrap or fork kubectl, changing the way that users interact with different
|
||||||
|
Kubernetes clusters.
|
||||||
|
|
||||||
|
# Proposal
|
||||||
|
|
||||||
|
This proposal builds off of earlier requests to [support exec-based plugins](
|
||||||
|
https://github.com/kubernetes/kubernetes/issues/35530#issuecomment-256170024), and
|
||||||
|
proposes that we should add this as a first-class feature of kubectl. Specifically,
|
||||||
|
client-go should be able to receive credentials by executing a command and reading
|
||||||
|
that command's stdout.
|
||||||
|
|
||||||
|
In fact, client-go already does this today. The GCP plugin can already be configured
|
||||||
|
to [call a command](
|
||||||
|
https://github.com/kubernetes/client-go/blob/kubernetes-1.8.5/plugin/pkg/client/auth/gcp/gcp.go#L228-L240)
|
||||||
|
other than `gcloud`.
|
||||||
|
|
||||||
|
## Plugin responsibilities
|
||||||
|
|
||||||
|
Plugins are exec'd through client-go and print credentials to stdout. Errors are
|
||||||
|
surfaced through stderr and a non-zero exit code. client-go will use structured APIs
|
||||||
|
to pass information to the plugin, and receive credentials from it.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ExecCredentials are credentials returned by the plugin.
|
||||||
|
type ExecCredentials struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
|
||||||
|
// Token is a bearer token used by the client for request authentication.
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
// Expiry indicates a unix time when the provided credentials expire.
|
||||||
|
Expiry int64 `json:"expiry,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response defines metadata about a failed request, including HTTP status code and
|
||||||
|
// response headers.
|
||||||
|
type Response struct {
|
||||||
|
// HTTP header returned by the server.
|
||||||
|
Header map[string][]string `json:"header,omitempty"`
|
||||||
|
// HTTP status code returned by the server.
|
||||||
|
Code int32 `json:"code,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecInfo is structed information passed to the plugin.
|
||||||
|
type ExecInfo struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
|
||||||
|
// Response is populated when the transport encounters HTTP status codes, such as 401,
|
||||||
|
// suggesting previous credentials were invalid.
|
||||||
|
// +optional
|
||||||
|
Response *Response `json:"response,omitempty"`
|
||||||
|
|
||||||
|
// Interactive is true when the transport detects the command is being called from an
|
||||||
|
// interactive prompt.
|
||||||
|
Interactive bool `json:"interactive,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To instruct client-go to use the bearer token `BEARER_TOKEN`, a plugin would print:
|
||||||
|
|
||||||
|
```terminal
|
||||||
|
$ ./kubectl-example-auth-plugin
|
||||||
|
{
|
||||||
|
"kind": "ExecCredentials",
|
||||||
|
"apiVersion":"client.authentication.k8s.io/v1alpha1",
|
||||||
|
"token":"BEARER_TOKEN"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To surface runtime-based information to the plugin, such as a request body for request
|
||||||
|
signing, client-go will set the environment variable `KUBERNETES_EXEC_INFO` to a JSON
|
||||||
|
serialized Kubernetes object when calling the plugin.
|
||||||
|
|
||||||
|
|
||||||
|
```terminal
|
||||||
|
KUBERNETES_EXEC_INFO='{
|
||||||
|
"kind":"ExecInfo",
|
||||||
|
"apiVersion":"client.authentication.k8s.io/v1alpha1",
|
||||||
|
"response": {
|
||||||
|
"code": 401,
|
||||||
|
"header": {
|
||||||
|
"WWW-Authenticate": ["Bearer realm=\"Access to the staging site\""]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"interactive": true
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caching
|
||||||
|
|
||||||
|
kubectl repeatedly [re-initializes transports](https://github.com/kubernetes/kubernetes/issues/37876)
|
||||||
|
while client-go transports are long lived over many requests. As a result naive auth
|
||||||
|
provider implementations that re-request credentials on every request have historically
|
||||||
|
been slow.
|
||||||
|
|
||||||
|
Plugins will be called on client-go initialization, and again when the API server returns
|
||||||
|
a 401 HTTP status code indicating expired credentials. Plugins can indicate their credentials
|
||||||
|
explicit expiry using the `Expiry` field on the returned `ExecCredentials` object, otherwise
|
||||||
|
credentials will be cached throughout the lifetime of a program.
|
||||||
|
|
||||||
|
## Kubeconfig changes
|
||||||
|
|
||||||
|
The current `AuthProviderConfig` uses `map[string]string` for configuration, which
|
||||||
|
makes it hard to express things like a list of arguments or list key/value environment
|
||||||
|
variables. As such, `AuthInfo` should add another field which expresses the `exec`
|
||||||
|
config. This has the benefit of a more natural structure, but the trade-off of not being
|
||||||
|
compatible with the existing `kubectl config set-credentials` implementation.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// AuthInfo contains information that describes identity information. This is use to tell the kubernetes cluster who you are.
|
||||||
|
type AuthInfo struct {
|
||||||
|
// Existing fields ...
|
||||||
|
|
||||||
|
// Exec is a command to execute which returns credentials to the transport to use.
|
||||||
|
// +optional
|
||||||
|
Exec *ExecAuthProviderConfig `json:"exec,omitempty"`
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecAuthProviderConfig struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
Args []string `json:"args"`
|
||||||
|
// Env defines additional environment variables to expose to the process. These
|
||||||
|
// are unioned with the host's environment, as well as variables client-go uses
|
||||||
|
// to pass argument to the plugin.
|
||||||
|
Env []ExecEnvVar `json:"env"`
|
||||||
|
|
||||||
|
// Prefered input version of the ExecInfo. The returned ExecCredentials MUST use
|
||||||
|
// the same encoding version as the input.
|
||||||
|
APIVersion string `json:"apiVersion,omitempty"`
|
||||||
|
|
||||||
|
// TODO: JSONPath options for filtering output.
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecEnvVar struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
|
||||||
|
// TODO: Load env vars from files or from other envs?
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This would allow a user block of a kubeconfig to declare the following:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
users:
|
||||||
|
- name: mmosley
|
||||||
|
user:
|
||||||
|
exec:
|
||||||
|
apiVersion: "client.authentication.k8s.io/v1alpha1"
|
||||||
|
command: /bin/kubectl-login
|
||||||
|
args: ["hello", "world"]
|
||||||
|
```
|
||||||
|
|
||||||
|
The AWS authenticator, modified to return structured output, would become:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
users:
|
||||||
|
- name: kubernetes-admin
|
||||||
|
user:
|
||||||
|
exec:
|
||||||
|
apiVersion: "client.authentication.k8s.io/v1alpha1"
|
||||||
|
command: heptio-authenticator-aws
|
||||||
|
# CLUSTER_ID and ROLE_ARN should be replaced with actual desired values.
|
||||||
|
args: ["token", "-i", "(CLUSTER_ID)", "-r", "(ROLE_ARN)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## TLS client certificate support
|
||||||
|
|
||||||
|
TLS client certificate support is orthogonal to bearer tokens, but something that
|
||||||
|
we should consider supporting in the future. Beyond requiring different command
|
||||||
|
output, it also requires changes to the client-go `AuthProvider` interface.
|
||||||
|
|
||||||
|
The current The auth provider interface doesn't let the user modify the dialer,
|
||||||
|
only wrap the transport.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type AuthProvider interface {
|
||||||
|
// WrapTransport allows the plugin to create a modified RoundTripper that
|
||||||
|
// attaches authorization headers (or other info) to requests.
|
||||||
|
WrapTransport(http.RoundTripper) http.RoundTripper
|
||||||
|
// Login allows the plugin to initialize its configuration. It must not
|
||||||
|
// require direct user interaction.
|
||||||
|
Login() error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Since this doesn't let a `AuthProvider` supply things like client certificates,
|
||||||
|
the signature of the `AuthProvider` should change too ([with corresponding changes
|
||||||
|
to `k8s.io/client-go-transport`](
|
||||||
|
https://gist.github.com/ericchiang/7f5804403b359ebdf79dcf76c4071bff)):
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"k8s.io/client-go/transport"
|
||||||
|
// ...
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthProvider interface {
|
||||||
|
// UpdateTransportConfig updates a config by adding a transport wrapper,
|
||||||
|
// setting a bearer token (should ignore if one is already set), or adding
|
||||||
|
// TLS client certificate credentials.
|
||||||
|
//
|
||||||
|
// This is called once on transport initialization. Providers that need to
|
||||||
|
// rotate credentials should use Config.WrapTransport to dynamically update
|
||||||
|
// credentials.
|
||||||
|
UpdateTransportConfig(c *transport.Config)
|
||||||
|
|
||||||
|
// Login() dropped, it was never used.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This would let auth transports supply TLS credentials, as well as instrument
|
||||||
|
transports with in-memory rotation code like the utilities implemented by
|
||||||
|
[`k8s.io/client-go/util/certificate`](https://godoc.org/k8s.io/client-go/util/certificate).
|
||||||
|
|
||||||
|
The `ExecCredentials` would then expand to provide TLS options.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ExecCredentials struct {
|
||||||
|
metav1.TypeMeta `json:",inline"`
|
||||||
|
|
||||||
|
// Token is a bearer token used by the client for request authentication.
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
// PEM encoded client certificate and key.
|
||||||
|
ClientCertificateData string `json:"clientCertificateData,omitempty"`
|
||||||
|
ClientKeyData string `json:"clientKeyData,omitempty"`
|
||||||
|
|
||||||
|
// Expiry indicates a unix time when the provided credentials expire.
|
||||||
|
Expiry int64 `json:"expiry,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `AuthProvider` then adds those credentials to the `transport.Config`.
|
||||||
|
|
||||||
|
## Login
|
||||||
|
|
||||||
|
Historically, `AuthProviders` have had a `Login()` method with the hope that it
|
||||||
|
could trigger bootstrapping into the cluster. While no providers implement this
|
||||||
|
method, the Azure `AuthProvider` can already prompt an [interactive auth flow](
|
||||||
|
https://github.com/kubernetes/client-go/blob/kubernetes-1.8.5/plugin/pkg/client/auth/azure/azure.go#L343).
|
||||||
|
This suggests that an exec'd tool should be able to trigger its own custom logins,
|
||||||
|
either by opening a browser, or performing a text based prompt.
|
||||||
|
|
||||||
|
We should take care that interactive stderr and stdin are correctly inherited by
|
||||||
|
the sub-process to enable this kind of interaction. The plugin will still be
|
||||||
|
responsible for prompting the user, receiving user feedback, and timeouts.
|
Loading…
Reference in New Issue