Skip to content

Commit 5c4b652

Browse files
authored
Merge pull request kubernetes#77613 from mikedanese/fixinclusterconfig
BoundServiceAccountTokenVolume: fix InClusterConfig
2 parents 4990c5e + 96ed93d commit 5c4b652

File tree

11 files changed

+310
-3
lines changed

11 files changed

+310
-3
lines changed

staging/src/k8s.io/apiserver/pkg/util/webhook/authentication.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ func restConfigFromKubeconfig(configAuthInfo *clientcmdapi.AuthInfo) (*rest.Conf
171171
// blindly overwrite existing values based on precedence
172172
if len(configAuthInfo.Token) > 0 {
173173
config.BearerToken = configAuthInfo.Token
174+
config.BearerTokenFile = configAuthInfo.TokenFile
174175
} else if len(configAuthInfo.TokenFile) > 0 {
175176
tokenBytes, err := ioutil.ReadFile(configAuthInfo.TokenFile)
176177
if err != nil {

staging/src/k8s.io/client-go/rest/transport.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ func (c *Config) TransportConfig() (*transport.Config, error) {
7474
KeyFile: c.KeyFile,
7575
KeyData: c.KeyData,
7676
},
77-
Username: c.Username,
78-
Password: c.Password,
79-
BearerToken: c.BearerToken,
77+
Username: c.Username,
78+
Password: c.Password,
79+
BearerToken: c.BearerToken,
80+
BearerTokenFile: c.BearerTokenFile,
8081
Impersonate: transport.ImpersonationConfig{
8182
UserName: c.Impersonate.UserName,
8283
Groups: c.Impersonate.Groups,

staging/src/k8s.io/client-go/tools/clientcmd/client_config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthI
228228
// blindly overwrite existing values based on precedence
229229
if len(configAuthInfo.Token) > 0 {
230230
mergedConfig.BearerToken = configAuthInfo.Token
231+
mergedConfig.BearerTokenFile = configAuthInfo.TokenFile
231232
} else if len(configAuthInfo.TokenFile) > 0 {
232233
tokenBytes, err := ioutil.ReadFile(configAuthInfo.TokenFile)
233234
if err != nil {
@@ -491,6 +492,7 @@ func (config *inClusterClientConfig) ClientConfig() (*restclient.Config, error)
491492
}
492493
if token := config.overrides.AuthInfo.Token; len(token) > 0 {
493494
icc.BearerToken = token
495+
icc.BearerTokenFile = ""
494496
}
495497
if certificateAuthorityFile := config.overrides.ClusterInfo.CertificateAuthority; len(certificateAuthorityFile) > 0 {
496498
icc.TLSClientConfig.CAFile = certificateAuthorityFile

test/e2e/auth/service_accounts.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package auth
1919
import (
2020
"fmt"
2121
"path"
22+
"regexp"
23+
"strings"
2224
"time"
2325

2426
authenticationv1 "k8s.io/api/authentication/v1"
@@ -38,6 +40,7 @@ import (
3840
)
3941

4042
var mountImage = imageutils.GetE2EImage(imageutils.Mounttest)
43+
var inClusterClientImage = imageutils.GetE2EImage(imageutils.InClusterClient)
4144

4245
var _ = SIGDescribe("ServiceAccounts", func() {
4346
f := framework.NewDefaultFramework("svcaccounts")
@@ -410,4 +413,138 @@ var _ = SIGDescribe("ServiceAccounts", func() {
410413
}
411414
}
412415
})
416+
417+
ginkgo.It("should support InClusterConfig with token rotation [Slow] [Feature:TokenRequestProjection]", func() {
418+
cfg, err := framework.LoadConfig()
419+
framework.ExpectNoError(err)
420+
421+
if _, err := f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(&v1.ConfigMap{
422+
ObjectMeta: metav1.ObjectMeta{
423+
Name: "kube-root-ca.crt",
424+
},
425+
Data: map[string]string{
426+
"ca.crt": string(cfg.TLSClientConfig.CAData),
427+
},
428+
}); err != nil && !apierrors.IsAlreadyExists(err) {
429+
framework.Failf("Unexpected err creating kube-ca-crt: %v", err)
430+
}
431+
432+
tenMin := int64(10 * 60)
433+
pod := &v1.Pod{
434+
ObjectMeta: metav1.ObjectMeta{Name: "inclusterclient"},
435+
Spec: v1.PodSpec{
436+
Containers: []v1.Container{{
437+
Name: "inclusterclient",
438+
Image: inClusterClientImage,
439+
VolumeMounts: []v1.VolumeMount{{
440+
MountPath: "/var/run/secrets/kubernetes.io/serviceaccount",
441+
Name: "kube-api-access-e2e",
442+
ReadOnly: true,
443+
}},
444+
}},
445+
RestartPolicy: v1.RestartPolicyNever,
446+
ServiceAccountName: "default",
447+
Volumes: []v1.Volume{{
448+
Name: "kube-api-access-e2e",
449+
VolumeSource: v1.VolumeSource{
450+
Projected: &v1.ProjectedVolumeSource{
451+
Sources: []v1.VolumeProjection{
452+
{
453+
ServiceAccountToken: &v1.ServiceAccountTokenProjection{
454+
Path: "token",
455+
ExpirationSeconds: &tenMin,
456+
},
457+
},
458+
{
459+
ConfigMap: &v1.ConfigMapProjection{
460+
LocalObjectReference: v1.LocalObjectReference{
461+
Name: "kube-root-ca.crt",
462+
},
463+
Items: []v1.KeyToPath{
464+
{
465+
Key: "ca.crt",
466+
Path: "ca.crt",
467+
},
468+
},
469+
},
470+
},
471+
{
472+
DownwardAPI: &v1.DownwardAPIProjection{
473+
Items: []v1.DownwardAPIVolumeFile{
474+
{
475+
Path: "namespace",
476+
FieldRef: &v1.ObjectFieldSelector{
477+
APIVersion: "v1",
478+
FieldPath: "metadata.namespace",
479+
},
480+
},
481+
},
482+
},
483+
},
484+
},
485+
},
486+
},
487+
}},
488+
},
489+
}
490+
pod, err = f.ClientSet.CoreV1().Pods(f.Namespace.Name).Create(pod)
491+
framework.ExpectNoError(err)
492+
493+
framework.Logf("created pod")
494+
if !framework.CheckPodsRunningReady(f.ClientSet, f.Namespace.Name, []string{pod.Name}, time.Minute) {
495+
framework.Failf("pod %q in ns %q never became ready", pod.Name, f.Namespace.Name)
496+
}
497+
498+
framework.Logf("pod is ready")
499+
500+
var logs string
501+
if err := wait.Poll(1*time.Minute, 20*time.Minute, func() (done bool, err error) {
502+
framework.Logf("polling logs")
503+
logs, err = framework.GetPodLogs(f.ClientSet, f.Namespace.Name, "inclusterclient", "inclusterclient")
504+
if err != nil {
505+
framework.Logf("Error pulling logs: %v", err)
506+
return false, nil
507+
}
508+
tokenCount, err := parseInClusterClientLogs(logs)
509+
if err != nil {
510+
return false, fmt.Errorf("inclusterclient reported an error: %v", err)
511+
}
512+
if tokenCount < 2 {
513+
framework.Logf("Retrying. Still waiting to see more unique tokens: got=%d, want=2", tokenCount)
514+
return false, nil
515+
}
516+
return true, nil
517+
}); err != nil {
518+
framework.Failf("Unexpected error: %v\n%s", err, logs)
519+
}
520+
})
413521
})
522+
523+
var reportLogsParser = regexp.MustCompile("([a-zA-Z0-9-_]*)=([a-zA-Z0-9-_]*)$")
524+
525+
func parseInClusterClientLogs(logs string) (int, error) {
526+
seenTokens := map[string]struct{}{}
527+
528+
lines := strings.Split(logs, "\n")
529+
for _, line := range lines {
530+
parts := reportLogsParser.FindStringSubmatch(line)
531+
if len(parts) != 3 {
532+
continue
533+
}
534+
535+
key, value := parts[1], parts[2]
536+
switch key {
537+
case "authz_header":
538+
if value == "<empty>" {
539+
return 0, fmt.Errorf("saw empty Authorization header")
540+
}
541+
seenTokens[value] = struct{}{}
542+
case "status":
543+
if value == "failed" {
544+
return 0, fmt.Errorf("saw status=failed")
545+
}
546+
}
547+
}
548+
549+
return len(seenTokens), nil
550+
}

test/images/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ filegroup(
1818
"//test/images/echoserver:all-srcs",
1919
"//test/images/entrypoint-tester:all-srcs",
2020
"//test/images/fakegitserver:all-srcs",
21+
"//test/images/inclusterclient:all-srcs",
2122
"//test/images/liveness:all-srcs",
2223
"//test/images/logs-generator:all-srcs",
2324
"//test/images/metadata-concealment:all-srcs",

test/images/inclusterclient/BUILD

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
2+
3+
go_library(
4+
name = "go_default_library",
5+
srcs = ["main.go"],
6+
importpath = "k8s.io/kubernetes/test/images/inclusterclient",
7+
visibility = ["//visibility:private"],
8+
deps = [
9+
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
10+
"//staging/src/k8s.io/client-go/rest:go_default_library",
11+
"//staging/src/k8s.io/component-base/logs:go_default_library",
12+
"//vendor/k8s.io/klog:go_default_library",
13+
],
14+
)
15+
16+
go_binary(
17+
name = "inclusterconfig",
18+
embed = [":go_default_library"],
19+
visibility = ["//visibility:public"],
20+
)
21+
22+
filegroup(
23+
name = "package-srcs",
24+
srcs = glob(["**"]),
25+
tags = ["automanaged"],
26+
visibility = ["//visibility:private"],
27+
)
28+
29+
filegroup(
30+
name = "all-srcs",
31+
srcs = [":package-srcs"],
32+
tags = ["automanaged"],
33+
visibility = ["//visibility:public"],
34+
)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2019 The Kubernetes Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
FROM gcr.io/distroless/static:latest
16+
17+
ADD inclusterclient /inclusterclient
18+
ENTRYPOINT ["/inclusterclient"]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2019 The Kubernetes Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
SRCS = inclusterclient
16+
ARCH ?= amd64
17+
TARGET ?= $(CURDIR)
18+
GOLANG_VERSION ?= latest
19+
SRC_DIR = $(notdir $(shell pwd))
20+
export
21+
22+
bin:
23+
../image-util.sh bin $(SRCS)
24+
25+
.PHONY: bin
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1.0
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
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
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
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.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"crypto/sha256"
21+
"encoding/base64"
22+
"flag"
23+
"fmt"
24+
"log"
25+
"net/http"
26+
"time"
27+
28+
"k8s.io/client-go/kubernetes"
29+
"k8s.io/client-go/rest"
30+
"k8s.io/component-base/logs"
31+
"k8s.io/klog"
32+
)
33+
34+
func main() {
35+
logs.InitLogs()
36+
defer logs.FlushLogs()
37+
38+
pollInterval := flag.Int("poll-interval", 30, "poll interval of call to /healhtz in seconds")
39+
flag.Set("logtostderr", "true")
40+
flag.Parse()
41+
42+
klog.Infof("started")
43+
44+
cfg, err := rest.InClusterConfig()
45+
if err != nil {
46+
log.Fatalf("err: %v", err)
47+
}
48+
49+
cfg.Wrap(func(rt http.RoundTripper) http.RoundTripper {
50+
return &debugRt{
51+
rt: rt,
52+
}
53+
})
54+
55+
c := kubernetes.NewForConfigOrDie(cfg).RESTClient()
56+
57+
t := time.Tick(time.Duration(*pollInterval) * time.Second)
58+
for {
59+
<-t
60+
klog.Infof("calling /healthz")
61+
b, err := c.Get().AbsPath("/healthz").Do().Raw()
62+
if err != nil {
63+
klog.Errorf("status=failed")
64+
klog.Errorf("error checking /healthz: %v\n%s\n", err, string(b))
65+
}
66+
}
67+
}
68+
69+
type debugRt struct {
70+
rt http.RoundTripper
71+
}
72+
73+
func (rt *debugRt) RoundTrip(req *http.Request) (*http.Response, error) {
74+
authHeader := req.Header.Get("Authorization")
75+
if len(authHeader) != 0 {
76+
authHash := sha256.Sum256([]byte(fmt.Sprintf("%s|%s", "salt", authHeader)))
77+
klog.Infof("authz_header=%s", base64.RawURLEncoding.EncodeToString(authHash[:]))
78+
} else {
79+
klog.Errorf("authz_header=<empty>")
80+
}
81+
return rt.rt.RoundTrip(req)
82+
}
83+
84+
func (rt *debugRt) WrappedRoundTripper() http.RoundTripper { return rt.rt }

0 commit comments

Comments
 (0)