1
16
17 package serving
18
19 import (
20 "context"
21 "crypto/tls"
22 "crypto/x509"
23 "fmt"
24 "io"
25 "net/http"
26 "os"
27 "path"
28 "strings"
29 "testing"
30
31 "k8s.io/apiserver/pkg/server"
32 "k8s.io/apiserver/pkg/server/options"
33 cloudprovider "k8s.io/cloud-provider"
34 cloudctrlmgrtesting "k8s.io/cloud-provider/app/testing"
35 "k8s.io/cloud-provider/fake"
36 "k8s.io/klog/v2/ktesting"
37 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
38 kubectrlmgrtesting "k8s.io/kubernetes/cmd/kube-controller-manager/app/testing"
39 kubeschedulertesting "k8s.io/kubernetes/cmd/kube-scheduler/app/testing"
40 "k8s.io/kubernetes/test/integration/framework"
41 )
42
43 type componentTester interface {
44 StartTestServer(ctx context.Context, customFlags []string) (*options.SecureServingOptionsWithLoopback, *server.SecureServingInfo, func(), error)
45 }
46
47 type kubeControllerManagerTester struct{}
48
49 func (kubeControllerManagerTester) StartTestServer(ctx context.Context, customFlags []string) (*options.SecureServingOptionsWithLoopback, *server.SecureServingInfo, func(), error) {
50
51 customFlags = append([]string{"--controllers="}, customFlags...)
52 gotResult, err := kubectrlmgrtesting.StartTestServer(ctx, customFlags)
53 if err != nil {
54 return nil, nil, nil, err
55 }
56 return gotResult.Options.SecureServing, gotResult.Config.SecureServing, gotResult.TearDownFn, err
57 }
58
59 type cloudControllerManagerTester struct{}
60
61 func (cloudControllerManagerTester) StartTestServer(ctx context.Context, customFlags []string) (*options.SecureServingOptionsWithLoopback, *server.SecureServingInfo, func(), error) {
62 gotResult, err := cloudctrlmgrtesting.StartTestServer(ctx, customFlags)
63 if err != nil {
64 return nil, nil, nil, err
65 }
66 return gotResult.Options.SecureServing, gotResult.Config.SecureServing, gotResult.TearDownFn, err
67 }
68
69 type kubeSchedulerTester struct{}
70
71 func (kubeSchedulerTester) StartTestServer(ctx context.Context, customFlags []string) (*options.SecureServingOptionsWithLoopback, *server.SecureServingInfo, func(), error) {
72 gotResult, err := kubeschedulertesting.StartTestServer(ctx, customFlags)
73 if err != nil {
74 return nil, nil, nil, err
75 }
76 return gotResult.Options.SecureServing, gotResult.Config.SecureServing, gotResult.TearDownFn, err
77 }
78
79 func TestComponentSecureServingAndAuth(t *testing.T) {
80 if !cloudprovider.IsCloudProvider("fake") {
81 cloudprovider.RegisterCloudProvider("fake", fakeCloudProviderFactory)
82 }
83
84
85
86 if len(os.Getenv("KUBERNETES_SERVICE_HOST")) > 0 {
87 t.Setenv("KUBERNETES_SERVICE_HOST", "")
88 }
89
90
91 token := "flwqkenfjasasdfmwerasd"
92 tokenFile, err := os.CreateTemp("", "kubeconfig")
93 if err != nil {
94 t.Fatal(err)
95 }
96 tokenFile.WriteString(fmt.Sprintf(`
97 %s,system:kube-controller-manager,system:kube-controller-manager,""
98 `, token))
99 tokenFile.Close()
100
101
102 server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{
103 "--token-auth-file", tokenFile.Name(),
104 "--authorization-mode", "RBAC",
105 }, framework.SharedEtcd())
106 defer server.TearDownFn()
107
108
109 apiserverConfig, err := os.CreateTemp("", "kubeconfig")
110 if err != nil {
111 t.Fatal(err)
112 }
113 apiserverConfig.WriteString(fmt.Sprintf(`
114 apiVersion: v1
115 kind: Config
116 clusters:
117 - cluster:
118 server: %s
119 certificate-authority: %s
120 name: integration
121 contexts:
122 - context:
123 cluster: integration
124 user: controller-manager
125 name: default-context
126 current-context: default-context
127 users:
128 - name: controller-manager
129 user:
130 token: %s
131 `, server.ClientConfig.Host, server.ServerOpts.SecureServing.ServerCert.CertKey.CertFile, token))
132 apiserverConfig.Close()
133
134
135 brokenApiserverConfig, err := os.CreateTemp("", "kubeconfig")
136 if err != nil {
137 t.Fatal(err)
138 }
139 brokenApiserverConfig.WriteString(fmt.Sprintf(`
140 apiVersion: v1
141 kind: Config
142 clusters:
143 - cluster:
144 server: %s
145 certificate-authority: %s
146 name: integration
147 contexts:
148 - context:
149 cluster: integration
150 user: controller-manager
151 name: default-context
152 current-context: default-context
153 users:
154 - name: controller-manager
155 user:
156 token: WRONGTOKEN
157 `, server.ClientConfig.Host, server.ServerOpts.SecureServing.ServerCert.CertKey.CertFile))
158 brokenApiserverConfig.Close()
159
160 tests := []struct {
161 name string
162 tester componentTester
163 extraFlags []string
164 }{
165 {"kube-controller-manager", kubeControllerManagerTester{}, nil},
166 {"cloud-controller-manager", cloudControllerManagerTester{}, []string{"--cloud-provider=fake", "--webhook-secure-port=0"}},
167 {"kube-scheduler", kubeSchedulerTester{}, nil},
168 }
169
170 for _, tt := range tests {
171 t.Run(tt.name, func(t *testing.T) {
172 testComponentWithSecureServing(t, tt.tester, apiserverConfig.Name(), brokenApiserverConfig.Name(), token, tt.extraFlags)
173 })
174 }
175 }
176
177 func testComponentWithSecureServing(t *testing.T, tester componentTester, kubeconfig, brokenKubeconfig, token string, extraFlags []string) {
178 tests := []struct {
179 name string
180 flags []string
181 path string
182 anonymous bool
183 wantErr bool
184 wantSecureCode *int
185 }{
186 {"no-flags", nil, "/healthz", false, true, nil},
187 {"/healthz without authn/authz", []string{
188 "--kubeconfig", kubeconfig,
189 "--leader-elect=false",
190 }, "/healthz", true, false, intPtr(http.StatusOK)},
191 {"/metrics without authn/authz", []string{
192 "--kubeconfig", kubeconfig,
193 "--leader-elect=false",
194 }, "/metrics", true, false, intPtr(http.StatusForbidden)},
195 {"authorization skipped for /healthz with authn/authz", []string{
196 "--authentication-kubeconfig", kubeconfig,
197 "--authorization-kubeconfig", kubeconfig,
198 "--kubeconfig", kubeconfig,
199 "--leader-elect=false",
200 }, "/healthz", false, false, intPtr(http.StatusOK)},
201 {"authorization skipped for /healthz with BROKEN authn/authz", []string{
202 "--authentication-skip-lookup",
203 "--authentication-kubeconfig", brokenKubeconfig,
204 "--authorization-kubeconfig", brokenKubeconfig,
205 "--kubeconfig", kubeconfig,
206 "--leader-elect=false",
207 }, "/healthz", false, false, intPtr(http.StatusOK)},
208 {"not authorized /metrics with BROKEN authn/authz", []string{
209 "--authentication-kubeconfig", kubeconfig,
210 "--authorization-kubeconfig", brokenKubeconfig,
211 "--kubeconfig", kubeconfig,
212 "--leader-elect=false",
213 }, "/metrics", false, false, intPtr(http.StatusInternalServerError)},
214 {"always-allowed /metrics with BROKEN authn/authz", []string{
215 "--authentication-skip-lookup",
216 "--authentication-kubeconfig", brokenKubeconfig,
217 "--authorization-kubeconfig", brokenKubeconfig,
218 "--authorization-always-allow-paths", "/healthz,/metrics",
219 "--kubeconfig", kubeconfig,
220 "--leader-elect=false",
221 }, "/metrics", false, false, intPtr(http.StatusOK)},
222 }
223 for _, tt := range tests {
224 t.Run(tt.name, func(t *testing.T) {
225 _, ctx := ktesting.NewTestContext(t)
226 secureOptions, secureInfo, tearDownFn, err := tester.StartTestServer(ctx, append(append([]string{}, tt.flags...), extraFlags...))
227 if tearDownFn != nil {
228 defer tearDownFn()
229 }
230 if (err != nil) != tt.wantErr {
231 t.Fatalf("StartTestServer() error = %v, wantErr %v", err, tt.wantErr)
232 }
233 if err != nil {
234 return
235 }
236
237 if want, got := tt.wantSecureCode != nil, secureInfo != nil; want != got {
238 t.Errorf("SecureServing enabled: expected=%v got=%v", want, got)
239 } else if want {
240 url := fmt.Sprintf("https://%s%s", secureInfo.Listener.Addr().String(), tt.path)
241 url = strings.Replace(url, "[::]", "127.0.0.1", -1)
242
243
244 pool := x509.NewCertPool()
245 serverCertPath := path.Join(secureOptions.ServerCert.CertDirectory, secureOptions.ServerCert.PairName+".crt")
246 serverCert, err := os.ReadFile(serverCertPath)
247 if err != nil {
248 t.Fatalf("Failed to read component server cert %q: %v", serverCertPath, err)
249 }
250 pool.AppendCertsFromPEM(serverCert)
251 tr := &http.Transport{
252 TLSClientConfig: &tls.Config{
253 RootCAs: pool,
254 },
255 }
256
257 client := &http.Client{Transport: tr}
258 req, err := http.NewRequest("GET", url, nil)
259 if err != nil {
260 t.Fatal(err)
261 }
262 if !tt.anonymous {
263 req.Header.Add("Authorization", fmt.Sprintf("Token %s", token))
264 }
265 r, err := client.Do(req)
266 if err != nil {
267 t.Fatalf("failed to GET %s from component: %v", tt.path, err)
268 }
269
270 body, err := io.ReadAll(r.Body)
271 if err != nil {
272 t.Fatalf("failed to read response body: %v", err)
273 }
274 defer r.Body.Close()
275 if got, expected := r.StatusCode, *tt.wantSecureCode; got != expected {
276 t.Fatalf("expected http %d at %s of component, got: %d %q", expected, tt.path, got, string(body))
277 }
278 }
279 })
280 }
281 }
282
283 func intPtr(x int) *int {
284 return &x
285 }
286
287 func fakeCloudProviderFactory(io.Reader) (cloudprovider.Interface, error) {
288 return &fake.Cloud{
289 DisableRoutes: true,
290 }, nil
291 }
292
View as plain text