diff --git a/go.mod b/go.mod index d60fbb6f373..6a5d3e48838 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/containerd/typeurl v0.0.0-20190228175220-2a93cfde8c20 // indirect github.com/containernetworking/cni v0.7.1 github.com/coredns/corefile-migration v1.0.6 + github.com/coreos/go-oidc v2.1.0+incompatible github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea github.com/cpuguy83/go-md2man v1.0.10 diff --git a/test/images/agnhost/BUILD b/test/images/agnhost/BUILD index 95559ea5e23..2dae6851da8 100644 --- a/test/images/agnhost/BUILD +++ b/test/images/agnhost/BUILD @@ -32,6 +32,7 @@ go_library( "//test/images/agnhost/nettest:go_default_library", "//test/images/agnhost/no-snat-test:go_default_library", "//test/images/agnhost/no-snat-test-proxy:go_default_library", + "//test/images/agnhost/openidmetadata:go_default_library", "//test/images/agnhost/pause:go_default_library", "//test/images/agnhost/port-forward-tester:go_default_library", "//test/images/agnhost/porter:go_default_library", @@ -71,6 +72,7 @@ filegroup( "//test/images/agnhost/nettest:all-srcs", "//test/images/agnhost/no-snat-test:all-srcs", "//test/images/agnhost/no-snat-test-proxy:all-srcs", + "//test/images/agnhost/openidmetadata:all-srcs", "//test/images/agnhost/pause:all-srcs", "//test/images/agnhost/port-forward-tester:all-srcs", "//test/images/agnhost/porter:all-srcs", diff --git a/test/images/agnhost/VERSION b/test/images/agnhost/VERSION index 6a5fe6e8977..3e162f02ea2 100644 --- a/test/images/agnhost/VERSION +++ b/test/images/agnhost/VERSION @@ -1 +1 @@ -2.11 +2.12 diff --git a/test/images/agnhost/agnhost.go b/test/images/agnhost/agnhost.go index ca82e93c2b8..7bbc9aa448e 100644 --- a/test/images/agnhost/agnhost.go +++ b/test/images/agnhost/agnhost.go @@ -22,33 +22,34 @@ import ( "github.com/spf13/cobra" "k8s.io/klog" - "k8s.io/kubernetes/test/images/agnhost/audit-proxy" + auditproxy "k8s.io/kubernetes/test/images/agnhost/audit-proxy" "k8s.io/kubernetes/test/images/agnhost/connect" - "k8s.io/kubernetes/test/images/agnhost/crd-conversion-webhook" + crdconvwebhook "k8s.io/kubernetes/test/images/agnhost/crd-conversion-webhook" "k8s.io/kubernetes/test/images/agnhost/dns" "k8s.io/kubernetes/test/images/agnhost/entrypoint-tester" "k8s.io/kubernetes/test/images/agnhost/fakegitserver" "k8s.io/kubernetes/test/images/agnhost/guestbook" "k8s.io/kubernetes/test/images/agnhost/inclusterclient" "k8s.io/kubernetes/test/images/agnhost/liveness" - "k8s.io/kubernetes/test/images/agnhost/logs-generator" + logsgen "k8s.io/kubernetes/test/images/agnhost/logs-generator" "k8s.io/kubernetes/test/images/agnhost/mounttest" "k8s.io/kubernetes/test/images/agnhost/net" "k8s.io/kubernetes/test/images/agnhost/netexec" "k8s.io/kubernetes/test/images/agnhost/nettest" - "k8s.io/kubernetes/test/images/agnhost/no-snat-test" - "k8s.io/kubernetes/test/images/agnhost/no-snat-test-proxy" + nosnat "k8s.io/kubernetes/test/images/agnhost/no-snat-test" + nosnatproxy "k8s.io/kubernetes/test/images/agnhost/no-snat-test-proxy" + "k8s.io/kubernetes/test/images/agnhost/openidmetadata" "k8s.io/kubernetes/test/images/agnhost/pause" - "k8s.io/kubernetes/test/images/agnhost/port-forward-tester" + portforwardtester "k8s.io/kubernetes/test/images/agnhost/port-forward-tester" "k8s.io/kubernetes/test/images/agnhost/porter" - "k8s.io/kubernetes/test/images/agnhost/resource-consumer-controller" - "k8s.io/kubernetes/test/images/agnhost/serve-hostname" - "k8s.io/kubernetes/test/images/agnhost/test-webserver" + resconsumerctrl "k8s.io/kubernetes/test/images/agnhost/resource-consumer-controller" + servehostname "k8s.io/kubernetes/test/images/agnhost/serve-hostname" + testwebserver "k8s.io/kubernetes/test/images/agnhost/test-webserver" "k8s.io/kubernetes/test/images/agnhost/webhook" ) func main() { - rootCmd := &cobra.Command{Use: "app", Version: "2.11"} + rootCmd := &cobra.Command{Use: "app", Version: "2.12"} rootCmd.AddCommand(auditproxy.CmdAuditProxy) rootCmd.AddCommand(connect.CmdConnect) @@ -75,6 +76,7 @@ func main() { rootCmd.AddCommand(servehostname.CmdServeHostname) rootCmd.AddCommand(testwebserver.CmdTestWebserver) rootCmd.AddCommand(webhook.CmdWebhook) + rootCmd.AddCommand(openidmetadata.CmdTestServiceAccountIssuerDiscovery) // NOTE(claudiub): Some tests are passing logging related flags, so we need to be able to // accept them. This will also include them in the printed help. diff --git a/test/images/agnhost/openidmetadata/BUILD b/test/images/agnhost/openidmetadata/BUILD new file mode 100644 index 00000000000..ae3f1dba4b2 --- /dev/null +++ b/test/images/agnhost/openidmetadata/BUILD @@ -0,0 +1,29 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["openidmetadata.go"], + importpath = "k8s.io/kubernetes/test/images/agnhost/openidmetadata", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/client-go/rest:go_default_library", + "//vendor/github.com/coreos/go-oidc:go_default_library", + "//vendor/github.com/spf13/cobra:go_default_library", + "//vendor/golang.org/x/oauth2:go_default_library", + "//vendor/gopkg.in/square/go-jose.v2/jwt:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/test/images/agnhost/openidmetadata/openidmetadata.go b/test/images/agnhost/openidmetadata/openidmetadata.go new file mode 100644 index 00000000000..9672873b0e0 --- /dev/null +++ b/test/images/agnhost/openidmetadata/openidmetadata.go @@ -0,0 +1,164 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package openidmetadata tests the OIDC discovery endpoints which are part of +// the ServiceAccountIssuerDiscovery feature. +package openidmetadata + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "net/http" + + oidc "github.com/coreos/go-oidc" + "github.com/spf13/cobra" + "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2/jwt" + "k8s.io/client-go/rest" +) + +// CmdTestServiceAccountIssuerDiscovery is used by agnhost Cobra. +var CmdTestServiceAccountIssuerDiscovery = &cobra.Command{ + Use: "test-service-account-issuer-discovery", + Short: "Tests the ServiceAccountIssuerDiscovery feature", + Long: "Reads in a mounted token and attempts to verify it against the API server's " + + "OIDC endpoints, using a third-party OIDC implementation.", + Args: cobra.MaximumNArgs(0), + Run: main, +} + +var ( + tokenPath string + audience string + inClusterDiscovery bool +) + +func init() { + fs := CmdTestServiceAccountIssuerDiscovery.Flags() + fs.StringVar(&tokenPath, "token-path", "", "Path to read service account token from.") + fs.StringVar(&audience, "audience", "", "Audience to check on received token.") + fs.BoolVar(&inClusterDiscovery, "in-cluster-discovery", false, + "Includes the in-cluster bearer token in request headers. "+ + "Use when validating against API server's discovery endpoints, "+ + "which require authentication.") +} + +func main(cmd *cobra.Command, args []string) { + ctx, err := withOAuth2Client(context.Background()) + if err != nil { + log.Fatal(err) + } + + raw, err := gettoken() + if err != nil { + log.Fatal(err) + } + log.Print("OK: Got token") + tok, err := jwt.ParseSigned(raw) + if err != nil { + log.Fatal(err) + } + var unsafeClaims claims + if err := tok.UnsafeClaimsWithoutVerification(&unsafeClaims); err != nil { + log.Fatal(err) + } + log.Printf("OK: got issuer %s", unsafeClaims.Issuer) + log.Printf("Full, not-validated claims: \n%#v", unsafeClaims) + + iss, err := oidc.NewProvider(ctx, unsafeClaims.Issuer) + if err != nil { + log.Fatal(err) + } + log.Printf("OK: Constructed OIDC provider for issuer %v", unsafeClaims.Issuer) + + validTok, err := iss.Verifier(&oidc.Config{ClientID: audience}).Verify(ctx, raw) + if err != nil { + log.Fatal(err) + } + log.Print("OK: Validated signature on JWT") + + var safeClaims claims + if err := validTok.Claims(&safeClaims); err != nil { + log.Fatal(err) + } + log.Print("OK: Got valid claims from token!") + log.Printf("Full, validated claims: \n%#v", &safeClaims) +} + +type kubeName struct { + Name string `json:"name"` + UID string `json:"uid"` +} + +type kubeClaims struct { + Namespace string `json:"namespace"` + ServiceAccount kubeName `json:"serviceaccount"` +} + +type claims struct { + jwt.Claims + + Kubernetes kubeClaims `json:"kubernetes.io"` +} + +func (k *claims) String() string { + return fmt.Sprintf("%s/%s for %s", k.Kubernetes.Namespace, k.Kubernetes.ServiceAccount.Name, k.Audience) +} + +func gettoken() (string, error) { + b, err := ioutil.ReadFile(tokenPath) + return string(b), err +} + +// withOAuth2Client returns a context that includes an HTTP Client, under the +// oauth2.HTTPClient key. If --in-cluster-discovery is true, the client will +// use the Kubernetes InClusterConfig. Otherwise it will use +// http.DefaultTransport. +// The `oidc` library respects the oauth2.HTTPClient context key; if it is set, +// the library will use the provided http.Client rather than the default +// HTTP client. +// This allows us to ensure requests get routed to the API server for +// --in-cluster-discovery, in a client configured with the appropriate CA. +func withOAuth2Client(context.Context) (context.Context, error) { + // TODO(mtaufen): Someday, might want to change this so that we can test + // TokenProjection with an API audience set to the external provider with + // requests against external endpoints (in which case we'd send + // a different token with a non-Kubernetes audience). + + // By default, use the default http transport with the system root bundle, + // since it's validating against the external internet. + rt := http.DefaultTransport + if inClusterDiscovery { + // If in-cluster discovery, then use the in-cluster config so we can + // authenticate with the API server. + cfg, err := rest.InClusterConfig() + if err != nil { + return nil, err + } + rt, err = rest.TransportFor(cfg) + if err != nil { + return nil, fmt.Errorf("could not get roundtripper: %v", err) + } + } + + ctx := context.WithValue(context.Background(), + oauth2.HTTPClient, &http.Client{ + Transport: rt, + }) + return ctx, nil +}