Skip to content

Commit f692f5c

Browse files
authored
Merge pull request kubernetes#88049 from mtaufen/provider-info-agnhost
Update agnhost to test OIDC validation of JWT tokens
2 parents 497a998 + 5ceecd3 commit f692f5c

File tree

6 files changed

+209
-11
lines changed

6 files changed

+209
-11
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ require (
3838
github.com/containerd/typeurl v0.0.0-20190228175220-2a93cfde8c20 // indirect
3939
github.com/containernetworking/cni v0.7.1
4040
github.com/coredns/corefile-migration v1.0.6
41+
github.com/coreos/go-oidc v2.1.0+incompatible
4142
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e
4243
github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea
4344
github.com/cpuguy83/go-md2man v1.0.10

test/images/agnhost/BUILD

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ go_library(
3232
"//test/images/agnhost/nettest:go_default_library",
3333
"//test/images/agnhost/no-snat-test:go_default_library",
3434
"//test/images/agnhost/no-snat-test-proxy:go_default_library",
35+
"//test/images/agnhost/openidmetadata:go_default_library",
3536
"//test/images/agnhost/pause:go_default_library",
3637
"//test/images/agnhost/port-forward-tester:go_default_library",
3738
"//test/images/agnhost/porter:go_default_library",
@@ -71,6 +72,7 @@ filegroup(
7172
"//test/images/agnhost/nettest:all-srcs",
7273
"//test/images/agnhost/no-snat-test:all-srcs",
7374
"//test/images/agnhost/no-snat-test-proxy:all-srcs",
75+
"//test/images/agnhost/openidmetadata:all-srcs",
7476
"//test/images/agnhost/pause:all-srcs",
7577
"//test/images/agnhost/port-forward-tester:all-srcs",
7678
"//test/images/agnhost/porter:all-srcs",

test/images/agnhost/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.11
1+
2.12

test/images/agnhost/agnhost.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,33 +22,34 @@ import (
2222
"github.com/spf13/cobra"
2323

2424
"k8s.io/klog"
25-
"k8s.io/kubernetes/test/images/agnhost/audit-proxy"
25+
auditproxy "k8s.io/kubernetes/test/images/agnhost/audit-proxy"
2626
"k8s.io/kubernetes/test/images/agnhost/connect"
27-
"k8s.io/kubernetes/test/images/agnhost/crd-conversion-webhook"
27+
crdconvwebhook "k8s.io/kubernetes/test/images/agnhost/crd-conversion-webhook"
2828
"k8s.io/kubernetes/test/images/agnhost/dns"
2929
"k8s.io/kubernetes/test/images/agnhost/entrypoint-tester"
3030
"k8s.io/kubernetes/test/images/agnhost/fakegitserver"
3131
"k8s.io/kubernetes/test/images/agnhost/guestbook"
3232
"k8s.io/kubernetes/test/images/agnhost/inclusterclient"
3333
"k8s.io/kubernetes/test/images/agnhost/liveness"
34-
"k8s.io/kubernetes/test/images/agnhost/logs-generator"
34+
logsgen "k8s.io/kubernetes/test/images/agnhost/logs-generator"
3535
"k8s.io/kubernetes/test/images/agnhost/mounttest"
3636
"k8s.io/kubernetes/test/images/agnhost/net"
3737
"k8s.io/kubernetes/test/images/agnhost/netexec"
3838
"k8s.io/kubernetes/test/images/agnhost/nettest"
39-
"k8s.io/kubernetes/test/images/agnhost/no-snat-test"
40-
"k8s.io/kubernetes/test/images/agnhost/no-snat-test-proxy"
39+
nosnat "k8s.io/kubernetes/test/images/agnhost/no-snat-test"
40+
nosnatproxy "k8s.io/kubernetes/test/images/agnhost/no-snat-test-proxy"
41+
"k8s.io/kubernetes/test/images/agnhost/openidmetadata"
4142
"k8s.io/kubernetes/test/images/agnhost/pause"
42-
"k8s.io/kubernetes/test/images/agnhost/port-forward-tester"
43+
portforwardtester "k8s.io/kubernetes/test/images/agnhost/port-forward-tester"
4344
"k8s.io/kubernetes/test/images/agnhost/porter"
44-
"k8s.io/kubernetes/test/images/agnhost/resource-consumer-controller"
45-
"k8s.io/kubernetes/test/images/agnhost/serve-hostname"
46-
"k8s.io/kubernetes/test/images/agnhost/test-webserver"
45+
resconsumerctrl "k8s.io/kubernetes/test/images/agnhost/resource-consumer-controller"
46+
servehostname "k8s.io/kubernetes/test/images/agnhost/serve-hostname"
47+
testwebserver "k8s.io/kubernetes/test/images/agnhost/test-webserver"
4748
"k8s.io/kubernetes/test/images/agnhost/webhook"
4849
)
4950

5051
func main() {
51-
rootCmd := &cobra.Command{Use: "app", Version: "2.11"}
52+
rootCmd := &cobra.Command{Use: "app", Version: "2.12"}
5253

5354
rootCmd.AddCommand(auditproxy.CmdAuditProxy)
5455
rootCmd.AddCommand(connect.CmdConnect)
@@ -75,6 +76,7 @@ func main() {
7576
rootCmd.AddCommand(servehostname.CmdServeHostname)
7677
rootCmd.AddCommand(testwebserver.CmdTestWebserver)
7778
rootCmd.AddCommand(webhook.CmdWebhook)
79+
rootCmd.AddCommand(openidmetadata.CmdTestServiceAccountIssuerDiscovery)
7880

7981
// NOTE(claudiub): Some tests are passing logging related flags, so we need to be able to
8082
// accept them. This will also include them in the printed help.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library")
2+
3+
go_library(
4+
name = "go_default_library",
5+
srcs = ["openidmetadata.go"],
6+
importpath = "k8s.io/kubernetes/test/images/agnhost/openidmetadata",
7+
visibility = ["//visibility:public"],
8+
deps = [
9+
"//staging/src/k8s.io/client-go/rest:go_default_library",
10+
"//vendor/github.com/coreos/go-oidc:go_default_library",
11+
"//vendor/github.com/spf13/cobra:go_default_library",
12+
"//vendor/golang.org/x/oauth2:go_default_library",
13+
"//vendor/gopkg.in/square/go-jose.v2/jwt:go_default_library",
14+
],
15+
)
16+
17+
filegroup(
18+
name = "package-srcs",
19+
srcs = glob(["**"]),
20+
tags = ["automanaged"],
21+
visibility = ["//visibility:private"],
22+
)
23+
24+
filegroup(
25+
name = "all-srcs",
26+
srcs = [":package-srcs"],
27+
tags = ["automanaged"],
28+
visibility = ["//visibility:public"],
29+
)
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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+
"io/ioutil"
25+
"log"
26+
"net/http"
27+
28+
oidc "github.com/coreos/go-oidc"
29+
"github.com/spf13/cobra"
30+
"golang.org/x/oauth2"
31+
"gopkg.in/square/go-jose.v2/jwt"
32+
"k8s.io/client-go/rest"
33+
)
34+
35+
// CmdTestServiceAccountIssuerDiscovery is used by agnhost Cobra.
36+
var CmdTestServiceAccountIssuerDiscovery = &cobra.Command{
37+
Use: "test-service-account-issuer-discovery",
38+
Short: "Tests the ServiceAccountIssuerDiscovery feature",
39+
Long: "Reads in a mounted token and attempts to verify it against the API server's " +
40+
"OIDC endpoints, using a third-party OIDC implementation.",
41+
Args: cobra.MaximumNArgs(0),
42+
Run: main,
43+
}
44+
45+
var (
46+
tokenPath string
47+
audience string
48+
inClusterDiscovery bool
49+
)
50+
51+
func init() {
52+
fs := CmdTestServiceAccountIssuerDiscovery.Flags()
53+
fs.StringVar(&tokenPath, "token-path", "", "Path to read service account token from.")
54+
fs.StringVar(&audience, "audience", "", "Audience to check on received token.")
55+
fs.BoolVar(&inClusterDiscovery, "in-cluster-discovery", false,
56+
"Includes the in-cluster bearer token in request headers. "+
57+
"Use when validating against API server's discovery endpoints, "+
58+
"which require authentication.")
59+
}
60+
61+
func main(cmd *cobra.Command, args []string) {
62+
ctx, err := withOAuth2Client(context.Background())
63+
if err != nil {
64+
log.Fatal(err)
65+
}
66+
67+
raw, err := gettoken()
68+
if err != nil {
69+
log.Fatal(err)
70+
}
71+
log.Print("OK: Got token")
72+
tok, err := jwt.ParseSigned(raw)
73+
if err != nil {
74+
log.Fatal(err)
75+
}
76+
var unsafeClaims claims
77+
if err := tok.UnsafeClaimsWithoutVerification(&unsafeClaims); err != nil {
78+
log.Fatal(err)
79+
}
80+
log.Printf("OK: got issuer %s", unsafeClaims.Issuer)
81+
log.Printf("Full, not-validated claims: \n%#v", unsafeClaims)
82+
83+
iss, err := oidc.NewProvider(ctx, unsafeClaims.Issuer)
84+
if err != nil {
85+
log.Fatal(err)
86+
}
87+
log.Printf("OK: Constructed OIDC provider for issuer %v", unsafeClaims.Issuer)
88+
89+
validTok, err := iss.Verifier(&oidc.Config{ClientID: audience}).Verify(ctx, raw)
90+
if err != nil {
91+
log.Fatal(err)
92+
}
93+
log.Print("OK: Validated signature on JWT")
94+
95+
var safeClaims claims
96+
if err := validTok.Claims(&safeClaims); err != nil {
97+
log.Fatal(err)
98+
}
99+
log.Print("OK: Got valid claims from token!")
100+
log.Printf("Full, validated claims: \n%#v", &safeClaims)
101+
}
102+
103+
type kubeName struct {
104+
Name string `json:"name"`
105+
UID string `json:"uid"`
106+
}
107+
108+
type kubeClaims struct {
109+
Namespace string `json:"namespace"`
110+
ServiceAccount kubeName `json:"serviceaccount"`
111+
}
112+
113+
type claims struct {
114+
jwt.Claims
115+
116+
Kubernetes kubeClaims `json:"kubernetes.io"`
117+
}
118+
119+
func (k *claims) String() string {
120+
return fmt.Sprintf("%s/%s for %s", k.Kubernetes.Namespace, k.Kubernetes.ServiceAccount.Name, k.Audience)
121+
}
122+
123+
func gettoken() (string, error) {
124+
b, err := ioutil.ReadFile(tokenPath)
125+
return string(b), err
126+
}
127+
128+
// withOAuth2Client returns a context that includes an HTTP Client, under the
129+
// oauth2.HTTPClient key. If --in-cluster-discovery is true, the client will
130+
// use the Kubernetes InClusterConfig. Otherwise it will use
131+
// http.DefaultTransport.
132+
// The `oidc` library respects the oauth2.HTTPClient context key; if it is set,
133+
// the library will use the provided http.Client rather than the default
134+
// HTTP client.
135+
// This allows us to ensure requests get routed to the API server for
136+
// --in-cluster-discovery, in a client configured with the appropriate CA.
137+
func withOAuth2Client(context.Context) (context.Context, error) {
138+
// TODO(mtaufen): Someday, might want to change this so that we can test
139+
// TokenProjection with an API audience set to the external provider with
140+
// requests against external endpoints (in which case we'd send
141+
// a different token with a non-Kubernetes audience).
142+
143+
// By default, use the default http transport with the system root bundle,
144+
// since it's validating against the external internet.
145+
rt := http.DefaultTransport
146+
if inClusterDiscovery {
147+
// If in-cluster discovery, then use the in-cluster config so we can
148+
// authenticate with the API server.
149+
cfg, err := rest.InClusterConfig()
150+
if err != nil {
151+
return nil, err
152+
}
153+
rt, err = rest.TransportFor(cfg)
154+
if err != nil {
155+
return nil, fmt.Errorf("could not get roundtripper: %v", err)
156+
}
157+
}
158+
159+
ctx := context.WithValue(context.Background(),
160+
oauth2.HTTPClient, &http.Client{
161+
Transport: rt,
162+
})
163+
return ctx, nil
164+
}

0 commit comments

Comments
 (0)