From 8c9c5160b65807e6e9d4692bc6a371869c9d5f5c Mon Sep 17 00:00:00 2001 From: Eric Chiang Date: Fri, 21 Oct 2016 23:07:38 -0700 Subject: [PATCH] storage/kubernetes: guess namespace from the service account token The in cluster kubernetes client currently requires using the downward API to determine its namespace. However this value can be determine by inspecting the service account token mounted into the pod. As a fallback, use this to guess the current namespace. --- storage/kubernetes/client.go | 41 +++++++++++++++++---- storage/kubernetes/client_test.go | 60 +++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 storage/kubernetes/client_test.go diff --git a/storage/kubernetes/client.go b/storage/kubernetes/client.go index bfdc9151..2d742008 100644 --- a/storage/kubernetes/client.go +++ b/storage/kubernetes/client.go @@ -322,6 +322,32 @@ func loadKubeConfig(kubeConfigPath string) (cluster k8sapi.Cluster, user k8sapi. return } +func namespaceFromServiceAccountJWT(s string) (string, error) { + // The service account token is just a JWT. Parse it as such. + parts := strings.Split(s, ".") + if len(parts) < 2 { + // It's extremely important we don't log the actual service account token. + return "", fmt.Errorf("malformed service account token: expected 3 parts got %d", len(parts)) + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + return "", fmt.Errorf("malformed service account token: %v", err) + } + var data struct { + // The claim Kubernetes uses to identify which namespace a service account belongs to. + // + // See: https://github.com/kubernetes/kubernetes/blob/v1.4.3/pkg/serviceaccount/jwt.go#L42 + Namespace string `json:"kubernetes.io/serviceaccount/namespace"` + } + if err := json.Unmarshal(payload, &data); err != nil { + return "", fmt.Errorf("malformed service account token: %v", err) + } + if data.Namespace == "" { + return "", errors.New(`jwt claim "kubernetes.io/serviceaccount/namespace" not found`) + } + return data.Namespace, nil +} + func inClusterConfig() (cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string, err error) { host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT") if len(host) == 0 || len(port) == 0 { @@ -332,17 +358,20 @@ func inClusterConfig() (cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace Server: "https://" + host + ":" + port, CertificateAuthority: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", } - - if namespace = os.Getenv("KUBERNETES_POD_NAMESPACE"); namespace == "" { - err = fmt.Errorf("unable to load in-cluster configuration, KUBERNETES_POD_NAMESPACE must be defined") - return - } - token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") if err != nil { return } user = k8sapi.AuthInfo{Token: string(token)} + + if namespace = os.Getenv("KUBERNETES_POD_NAMESPACE"); namespace == "" { + namespace, err = namespaceFromServiceAccountJWT(user.Token) + if err != nil { + err = fmt.Errorf("failed to inspect service account token: %v", err) + return + } + } + return } diff --git a/storage/kubernetes/client_test.go b/storage/kubernetes/client_test.go new file mode 100644 index 00000000..92ae204a --- /dev/null +++ b/storage/kubernetes/client_test.go @@ -0,0 +1,60 @@ +package kubernetes + +import "testing" + +func TestNamespaceFromServiceAccountJWT(t *testing.T) { + namespace, err := namespaceFromServiceAccountJWT(serviceAccountToken) + if err != nil { + t.Fatal(err) + } + wantNamespace := "dex-test-namespace" + if namespace != wantNamespace { + t.Errorf("expected namespace %q got %q", wantNamespace, namespace) + } +} + +var serviceAccountToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZXgtdGVzdC1uYW1lc3BhY2UiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlY3JldC5uYW1lIjoiZG90aGVyb2JvdC1zZWNyZXQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZG90aGVyb2JvdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjQyYjJhOTRmLTk4MjAtMTFlNi1iZDc0LTJlZmQzOGYxMjYxYyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZXgtdGVzdC1uYW1lc3BhY2U6ZG90aGVyb2JvdCJ9.KViBpPwCiBwxDvAjYUUXoVvLVwqV011aLlYQpNtX12Bh8M-QAFch-3RWlo_SR00bcdFg_nZo9JKACYlF_jHMEsf__PaYms9r7vEaSg0jPfkqnL2WXZktzQRyLBr0n-bxeUrbwIWsKOAC0DfFB5nM8XoXljRmq8yAx8BAdmQp7MIFb4EOV9nYthhua6pjzYyaFSiDiYTjw7HtXOvoL8oepodJ3-37pUKS8vdBvnvUoqC4M1YAhkO5L36JF6KV_RfmG8GPEdNQfXotHcsR-3jKi1n8S5l7Xd-rhrGOhSGQizH3dORzo9GvBAhYeqbq1O-NLzm2EQUiMQayIUx7o4g3Kw" + +// The following program was used to generate the example token. Since we don't want to +// import Kubernetes, just leave it as a comment. + +/* +package main + +import ( + "crypto/rand" + "crypto/rsa" + "fmt" + "log" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/serviceaccount" + "k8s.io/kubernetes/pkg/util/uuid" +) + +func main() { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + log.Fatal(err) + } + sa := api.ServiceAccount{ + ObjectMeta: api.ObjectMeta{ + Namespace: "dex-test-namespace", + Name: "dotherobot", + UID: uuid.NewUUID(), + }, + } + secret := api.Secret{ + ObjectMeta: api.ObjectMeta{ + Namespace: "dex-test-namespace", + Name: "dotherobot-secret", + UID: uuid.NewUUID(), + }, + } + token, err := serviceaccount.JWTTokenGenerator(key).GenerateToken(sa, secret) + if err != nil { + log.Fatal(err) + } + fmt.Println(token) +} +*/