diff --git a/storage/kubernetes/client.go b/storage/kubernetes/client.go index 8e666178..04ee9cae 100644 --- a/storage/kubernetes/client.go +++ b/storage/kubernetes/client.go @@ -320,6 +320,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 { @@ -330,17 +356,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) +} +*/