k8s.io/kubernetes@v1.29.3/test/images/agnhost/openidmetadata/openidmetadata.go (about) 1 /* 2 Copyright 2020 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 openidmetadata tests the OIDC discovery endpoints which are part of 18 // the ServiceAccountIssuerDiscovery feature. 19 package openidmetadata 20 21 import ( 22 "context" 23 "fmt" 24 "log" 25 "net" 26 "net/http" 27 "net/url" 28 "os" 29 "runtime" 30 "time" 31 32 "github.com/coreos/go-oidc" 33 "github.com/spf13/cobra" 34 "golang.org/x/oauth2" 35 "gopkg.in/square/go-jose.v2/jwt" 36 "k8s.io/apimachinery/pkg/util/wait" 37 "k8s.io/client-go/rest" 38 ) 39 40 // CmdTestServiceAccountIssuerDiscovery is used by agnhost Cobra. 41 var CmdTestServiceAccountIssuerDiscovery = &cobra.Command{ 42 Use: "test-service-account-issuer-discovery", 43 Short: "Tests the ServiceAccountIssuerDiscovery feature", 44 Long: "Reads in a mounted token and attempts to verify it against the API server's " + 45 "OIDC endpoints, using a third-party OIDC implementation.", 46 Args: cobra.MaximumNArgs(0), 47 Run: main, 48 } 49 50 var ( 51 tokenPath string 52 audience string 53 ) 54 55 func init() { 56 fs := CmdTestServiceAccountIssuerDiscovery.Flags() 57 fs.StringVar(&tokenPath, "token-path", "", "Path to read service account token from.") 58 fs.StringVar(&audience, "audience", "", "Audience to check on received token.") 59 } 60 61 func main(cmd *cobra.Command, args []string) { 62 raw, err := gettoken() 63 if err != nil { 64 log.Fatal(err) 65 } 66 log.Print("OK: Got token") 67 68 /* 69 To support both in-cluster discovery and external (non kube-apiserver) 70 discovery: 71 1. Attempt with in-cluster discovery. Only trust Cluster CA. 72 If pass, exit early, successfully. This attempt includes the bearer 73 token, so we only trust the Cluster CA to avoid sending tokens to 74 some external endpoint by accident. 75 2. If in-cluster discovery doesn't pass, then try again assuming both 76 discovery doc and JWKS endpoints are external rather than being 77 served from kube-apiserver. This attempt does not pass the bearer 78 token at all. 79 */ 80 81 log.Print("validating with in-cluster discovery") 82 inClusterCtx, err := withInClusterOauth2Client(context.Background()) 83 if err != nil { 84 log.Fatal(err) 85 } 86 if err := validate(inClusterCtx, raw); err == nil { 87 os.Exit(0) 88 } else { 89 log.Print("failed to validate with in-cluster discovery: ", err) 90 } 91 92 log.Print("falling back to validating with external discovery") 93 externalCtx, err := withExternalOAuth2Client(context.Background()) 94 if err != nil { 95 log.Fatal(err) 96 } 97 if err := validate(externalCtx, raw); err != nil { 98 log.Fatal(err) 99 } 100 } 101 102 func validate(ctx context.Context, raw string) error { 103 tok, err := jwt.ParseSigned(raw) 104 if err != nil { 105 log.Fatal(err) 106 } 107 var unsafeClaims claims 108 if err := tok.UnsafeClaimsWithoutVerification(&unsafeClaims); err != nil { 109 log.Fatal(err) 110 } 111 log.Printf("OK: got issuer %s", unsafeClaims.Issuer) 112 log.Printf("Full, not-validated claims: \n%#v", unsafeClaims) 113 114 if runtime.GOOS == "windows" { 115 if err := ensureWindowsDNSAvailability(unsafeClaims.Issuer); err != nil { 116 log.Fatal(err) 117 } 118 } 119 120 iss, err := oidc.NewProvider(ctx, unsafeClaims.Issuer) 121 if err != nil { 122 return err 123 } 124 log.Printf("OK: Constructed OIDC provider for issuer %v", unsafeClaims.Issuer) 125 126 validTok, err := iss.Verifier(&oidc.Config{ 127 ClientID: audience, 128 SupportedSigningAlgs: []string{oidc.RS256, oidc.ES256}, 129 }).Verify(ctx, raw) 130 if err != nil { 131 return err 132 } 133 log.Print("OK: Validated signature on JWT") 134 135 var safeClaims claims 136 if err := validTok.Claims(&safeClaims); err != nil { 137 return err 138 } 139 log.Print("OK: Got valid claims from token!") 140 log.Printf("Full, validated claims: \n%#v", &safeClaims) 141 return nil 142 } 143 144 type kubeName struct { 145 Name string `json:"name"` 146 UID string `json:"uid"` 147 } 148 149 type kubeClaims struct { 150 Namespace string `json:"namespace"` 151 ServiceAccount kubeName `json:"serviceaccount"` 152 } 153 154 type claims struct { 155 jwt.Claims 156 157 Kubernetes kubeClaims `json:"kubernetes.io"` 158 } 159 160 func (k *claims) String() string { 161 return fmt.Sprintf("%s/%s for %s", k.Kubernetes.Namespace, k.Kubernetes.ServiceAccount.Name, k.Audience) 162 } 163 164 func gettoken() (string, error) { 165 b, err := os.ReadFile(tokenPath) 166 return string(b), err 167 } 168 169 func withExternalOAuth2Client(ctx context.Context) (context.Context, error) { 170 // Use the default http transport with the system root bundle, 171 // since it's validating against the external internet. 172 return context.WithValue(ctx, 173 // The `oidc` library respects the oauth2.HTTPClient context key; if it is set, 174 // the library will use the provided http.Client rather than the default HTTP client. 175 oauth2.HTTPClient, &http.Client{ 176 Transport: http.DefaultTransport, 177 }), nil 178 } 179 180 func withInClusterOauth2Client(ctx context.Context) (context.Context, error) { 181 // Use the in-cluster config so we can trust and authenticate with kube-apiserver 182 cfg, err := rest.InClusterConfig() 183 if err != nil { 184 return nil, err 185 } 186 187 rt, err := rest.TransportFor(cfg) 188 if err != nil { 189 return nil, fmt.Errorf("could not get roundtripper: %v", err) 190 } 191 192 return context.WithValue(ctx, 193 // The `oidc` library respects the oauth2.HTTPClient context key; if it is set, 194 // the library will use the provided http.Client rather than the default HTTP client. 195 oauth2.HTTPClient, &http.Client{ 196 Transport: rt, 197 }), nil 198 } 199 200 // DNS can be available sometime after the container starts due to the way 201 // networking is set up for Windows nodes with dockershim as the container runtime. 202 // In this case, we should make sure we are able to resolve the issuer before 203 // invoking oidc.NewProvider. 204 // See https://github.com/kubernetes/kubernetes/issues/99470 for more details. 205 func ensureWindowsDNSAvailability(issuer string) error { 206 log.Println("Ensuring Windows DNS availability") 207 208 u, err := url.Parse(issuer) 209 if err != nil { 210 return err 211 } 212 213 return wait.PollImmediate(5*time.Second, 20*time.Second, func() (bool, error) { 214 ips, err := net.LookupHost(u.Host) 215 if err != nil { 216 log.Println(err) 217 return false, nil 218 } 219 log.Printf("OK: Resolved host %s: %v", u.Host, ips) 220 return true, nil 221 }) 222 }