2 Copyright 2018 The Kubernetes Authors.
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
8 http://www.apache.org/licenses/LICENSE-2.0
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
34 "golang.org/x/crypto/ssh/terminal"
35 "k8s.io/apimachinery/pkg/apis/meta/v1"
36 "k8s.io/apimachinery/pkg/runtime"
37 "k8s.io/apimachinery/pkg/runtime/schema"
38 "k8s.io/apimachinery/pkg/runtime/serializer"
39 utilruntime "k8s.io/apimachinery/pkg/util/runtime"
40 "k8s.io/client-go/pkg/apis/clientauthentication"
41 "k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1"
42 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
43 "k8s.io/client-go/tools/clientcmd/api"
44 "k8s.io/client-go/transport"
45 "k8s.io/client-go/util/connrotation"
49 const execInfoEnv = "KUBERNETES_EXEC_INFO"
51 var scheme = runtime.NewScheme()
52 var codecs = serializer.NewCodecFactory(scheme)
55 v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
56 utilruntime.Must(v1alpha1.AddToScheme(scheme))
57 utilruntime.Must(v1beta1.AddToScheme(scheme))
58 utilruntime.Must(clientauthentication.AddToScheme(scheme))
62 // Since transports can be constantly re-initialized by programs like kubectl,
63 // keep a cache of initialized authenticators keyed by a hash of their config.
64 globalCache = newCache()
65 // The list of API versions we accept.
66 apiVersions = map[string]schema.GroupVersion{
67 v1alpha1.SchemeGroupVersion.String(): v1alpha1.SchemeGroupVersion,
68 v1beta1.SchemeGroupVersion.String(): v1beta1.SchemeGroupVersion,
72 func newCache() *cache {
73 return &cache{m: make(map[string]*Authenticator)}
76 func cacheKey(c *api.ExecConfig) string {
77 return fmt.Sprintf("%#v", c)
82 m map[string]*Authenticator
85 func (c *cache) get(s string) (*Authenticator, bool) {
92 // put inserts an authenticator into the cache. If an authenticator is already
93 // associated with the key, the first one is returned instead.
94 func (c *cache) put(s string, a *Authenticator) *Authenticator {
97 existing, ok := c.m[s]
105 // GetAuthenticator returns an exec-based plugin for providing client credentials.
106 func GetAuthenticator(config *api.ExecConfig) (*Authenticator, error) {
107 return newAuthenticator(globalCache, config)
110 func newAuthenticator(c *cache, config *api.ExecConfig) (*Authenticator, error) {
111 key := cacheKey(config)
112 if a, ok := c.get(key); ok {
116 gv, ok := apiVersions[config.APIVersion]
118 return nil, fmt.Errorf("exec plugin: invalid apiVersion %q", config.APIVersion)
128 interactive: terminal.IsTerminal(int(os.Stdout.Fd())),
133 for _, env := range config.Env {
134 a.env = append(a.env, env.Name+"="+env.Value)
137 return c.put(key, a), nil
140 // Authenticator is a client credential provider that rotates credentials by executing a plugin.
141 // The plugin input and output are defined by the API group client.authentication.k8s.io.
142 type Authenticator struct {
146 group schema.GroupVersion
149 // Stubbable for testing
154 environ func() []string
158 // The mutex also guards calling the plugin. Since the plugin could be
159 // interactive we want to make sure it's only called once.
161 cachedCreds *credentials
167 type credentials struct {
169 cert *tls.Certificate
172 // UpdateTransportConfig updates the transport.Config to use credentials
173 // returned by the plugin.
174 func (a *Authenticator) UpdateTransportConfig(c *transport.Config) error {
175 wt := c.WrapTransport
176 c.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
180 return &roundTripper{a, rt}
183 if c.TLS.GetCert != nil {
184 return errors.New("can't add TLS certificate callback: transport.Config.TLS.GetCert already set")
186 c.TLS.GetCert = a.cert
188 var dial func(ctx context.Context, network, addr string) (net.Conn, error)
192 dial = (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext
194 d := connrotation.NewDialer(dial)
195 a.onRotate = d.CloseAll
196 c.Dial = d.DialContext
201 type roundTripper struct {
203 base http.RoundTripper
206 func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
207 // If a user has already set credentials, use that. This makes commands like
208 // "kubectl get --token (token) pods" work.
209 if req.Header.Get("Authorization") != "" {
210 return r.base.RoundTrip(req)
213 creds, err := r.a.getCreds()
215 return nil, fmt.Errorf("getting credentials: %v", err)
217 if creds.token != "" {
218 req.Header.Set("Authorization", "Bearer "+creds.token)
221 res, err := r.base.RoundTrip(req)
225 if res.StatusCode == http.StatusUnauthorized {
226 resp := &clientauthentication.Response{
228 Code: int32(res.StatusCode),
230 if err := r.a.maybeRefreshCreds(creds, resp); err != nil {
231 klog.Errorf("refreshing credentials: %v", err)
237 func (a *Authenticator) credsExpired() bool {
241 return a.now().After(a.exp)
244 func (a *Authenticator) cert() (*tls.Certificate, error) {
245 creds, err := a.getCreds()
249 return creds.cert, nil
252 func (a *Authenticator) getCreds() (*credentials, error) {
255 if a.cachedCreds != nil && !a.credsExpired() {
256 return a.cachedCreds, nil
259 if err := a.refreshCredsLocked(nil); err != nil {
262 return a.cachedCreds, nil
265 // maybeRefreshCreds executes the plugin to force a rotation of the
266 // credentials, unless they were rotated already.
267 func (a *Authenticator) maybeRefreshCreds(creds *credentials, r *clientauthentication.Response) error {
271 // Since we're not making a new pointer to a.cachedCreds in getCreds, no
272 // need to do deep comparison.
273 if creds != a.cachedCreds {
274 // Credentials already rotated.
278 return a.refreshCredsLocked(r)
281 // refreshCredsLocked executes the plugin and reads the credentials from
282 // stdout. It must be called while holding the Authenticator's mutex.
283 func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) error {
284 cred := &clientauthentication.ExecCredential{
285 Spec: clientauthentication.ExecCredentialSpec{
287 Interactive: a.interactive,
291 env := append(a.environ(), a.env...)
292 if a.group == v1alpha1.SchemeGroupVersion {
293 // Input spec disabled for beta due to lack of use. Possibly re-enable this later if
294 // someone wants it back.
296 // See: https://github.com/kubernetes/kubernetes/issues/61796
297 data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred)
299 return fmt.Errorf("encode ExecCredentials: %v", err)
301 env = append(env, fmt.Sprintf("%s=%s", execInfoEnv, data))
304 stdout := &bytes.Buffer{}
305 cmd := exec.Command(a.cmd, a.args...)
307 cmd.Stderr = a.stderr
313 if err := cmd.Run(); err != nil {
314 return fmt.Errorf("exec: %v", err)
317 _, gvk, err := codecs.UniversalDecoder(a.group).Decode(stdout.Bytes(), nil, cred)
319 return fmt.Errorf("decoding stdout: %v", err)
321 if gvk.Group != a.group.Group || gvk.Version != a.group.Version {
322 return fmt.Errorf("exec plugin is configured to use API version %s, plugin returned version %s",
323 a.group, schema.GroupVersion{Group: gvk.Group, Version: gvk.Version})
326 if cred.Status == nil {
327 return fmt.Errorf("exec plugin didn't return a status field")
329 if cred.Status.Token == "" && cred.Status.ClientCertificateData == "" && cred.Status.ClientKeyData == "" {
330 return fmt.Errorf("exec plugin didn't return a token or cert/key pair")
332 if (cred.Status.ClientCertificateData == "") != (cred.Status.ClientKeyData == "") {
333 return fmt.Errorf("exec plugin returned only certificate or key, not both")
336 if cred.Status.ExpirationTimestamp != nil {
337 a.exp = cred.Status.ExpirationTimestamp.Time
342 newCreds := &credentials{
343 token: cred.Status.Token,
345 if cred.Status.ClientKeyData != "" && cred.Status.ClientCertificateData != "" {
346 cert, err := tls.X509KeyPair([]byte(cred.Status.ClientCertificateData), []byte(cred.Status.ClientKeyData))
348 return fmt.Errorf("failed parsing client key/certificate: %v", err)
350 newCreds.cert = &cert
353 oldCreds := a.cachedCreds
354 a.cachedCreds = newCreds
355 // Only close all connections when TLS cert rotates. Token rotation doesn't
356 // need the extra noise.
357 if a.onRotate != nil && oldCreds != nil && !reflect.DeepEqual(oldCreds.cert, a.cachedCreds.cert) {