1
16
17 package admissionwebhook
18
19 import (
20 "context"
21 "crypto/tls"
22 "crypto/x509"
23 "encoding/json"
24 "fmt"
25 "io"
26 "net/http"
27 "net/http/httptest"
28 "net/url"
29 "os"
30 "sync"
31 "testing"
32 "time"
33
34 utiltesting "k8s.io/client-go/util/testing"
35
36 "k8s.io/api/admission/v1beta1"
37 admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
38 corev1 "k8s.io/api/core/v1"
39 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
40 "k8s.io/apimachinery/pkg/types"
41 "k8s.io/apimachinery/pkg/util/wait"
42 clientset "k8s.io/client-go/kubernetes"
43 "k8s.io/client-go/rest"
44 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
45 "k8s.io/kubernetes/test/integration/framework"
46 )
47
48 const (
49 testClientAuthClientUsername = "webhook-client-auth-integration-client"
50 )
51
52
53 func TestWebhookClientAuthWithAggregatorRouting(t *testing.T) {
54 testWebhookClientAuth(t, true)
55 }
56
57
58 func TestWebhookClientAuthWithoutAggregatorRouting(t *testing.T) {
59 testWebhookClientAuth(t, false)
60 }
61
62 func testWebhookClientAuth(t *testing.T, enableAggregatorRouting bool) {
63
64 roots := x509.NewCertPool()
65 if !roots.AppendCertsFromPEM(localhostCert) {
66 t.Fatal("Failed to append Cert from PEM")
67 }
68 cert, err := tls.X509KeyPair(localhostCert, localhostKey)
69 if err != nil {
70 t.Fatalf("Failed to build cert with error: %+v", err)
71 }
72
73 recorder := &clientAuthRecorder{}
74 webhookServer := httptest.NewUnstartedServer(newClientAuthWebhookHandler(t, recorder))
75 webhookServer.TLS = &tls.Config{
76
77 RootCAs: roots,
78 Certificates: []tls.Certificate{cert},
79 }
80 webhookServer.StartTLS()
81 defer webhookServer.Close()
82
83 webhookServerURL, err := url.Parse(webhookServer.URL)
84 if err != nil {
85 t.Fatal(err)
86 }
87
88 kubeConfigFile, err := os.CreateTemp("", "admission-config.yaml")
89 if err != nil {
90 t.Fatal(err)
91 }
92 defer utiltesting.CloseAndRemove(t, kubeConfigFile)
93
94 if err := os.WriteFile(kubeConfigFile.Name(), []byte(`
95 apiVersion: v1
96 kind: Config
97 users:
98 - name: "`+webhookServerURL.Host+`"
99 user:
100 token: "localhost-match-with-port"
101 - name: "`+webhookServerURL.Hostname()+`"
102 user:
103 token: "localhost-match-without-port"
104 - name: "*.localhost"
105 user:
106 token: "localhost-prefix"
107 - name: "*"
108 user:
109 token: "fallback"
110 `), os.FileMode(0755)); err != nil {
111 t.Fatal(err)
112 }
113
114 admissionConfigFile, err := os.CreateTemp("", "admission-config.yaml")
115 if err != nil {
116 t.Fatal(err)
117 }
118 defer utiltesting.CloseAndRemove(t, admissionConfigFile)
119
120 if err := os.WriteFile(admissionConfigFile.Name(), []byte(`
121 apiVersion: apiserver.k8s.io/v1alpha1
122 kind: AdmissionConfiguration
123 plugins:
124 - name: ValidatingAdmissionWebhook
125 configuration:
126 apiVersion: apiserver.config.k8s.io/v1alpha1
127 kind: WebhookAdmission
128 kubeConfigFile: "`+kubeConfigFile.Name()+`"
129 - name: MutatingAdmissionWebhook
130 configuration:
131 apiVersion: apiserver.config.k8s.io/v1alpha1
132 kind: WebhookAdmission
133 kubeConfigFile: "`+kubeConfigFile.Name()+`"
134 `), os.FileMode(0755)); err != nil {
135 t.Fatal(err)
136 }
137
138 s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{
139 "--disable-admission-plugins=ServiceAccount",
140 fmt.Sprintf("--enable-aggregator-routing=%v", enableAggregatorRouting),
141 "--admission-control-config-file=" + admissionConfigFile.Name(),
142 }, framework.SharedEtcd())
143 defer s.TearDownFn()
144
145
146
147
148
149 clientConfig := rest.CopyConfig(s.ClientConfig)
150 clientConfig.Impersonate.UserName = testClientAuthClientUsername
151 clientConfig.Impersonate.Groups = []string{"system:masters", "system:authenticated"}
152 client, err := clientset.NewForConfig(clientConfig)
153 if err != nil {
154 t.Fatalf("unexpected error: %v", err)
155 }
156
157 _, err = client.CoreV1().Pods("default").Create(context.TODO(), clientAuthMarkerFixture, metav1.CreateOptions{})
158 if err != nil {
159 t.Fatal(err)
160 }
161
162 upCh := recorder.Reset()
163 ns := "load-balance"
164 _, err = client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns}}, metav1.CreateOptions{})
165 if err != nil {
166 t.Fatal(err)
167 }
168
169 fail := admissionregistrationv1.Fail
170 mutatingCfg, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), &admissionregistrationv1.MutatingWebhookConfiguration{
171 ObjectMeta: metav1.ObjectMeta{Name: "admission.integration.test"},
172 Webhooks: []admissionregistrationv1.MutatingWebhook{{
173 Name: "admission.integration.test",
174 ClientConfig: admissionregistrationv1.WebhookClientConfig{
175 URL: &webhookServer.URL,
176 CABundle: localhostCert,
177 },
178 Rules: []admissionregistrationv1.RuleWithOperations{{
179 Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
180 Rule: admissionregistrationv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}},
181 }},
182 FailurePolicy: &fail,
183 AdmissionReviewVersions: []string{"v1beta1"},
184 SideEffects: &noSideEffects,
185 }},
186 }, metav1.CreateOptions{})
187 if err != nil {
188 t.Fatal(err)
189 }
190 defer func() {
191 err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Delete(context.TODO(), mutatingCfg.GetName(), metav1.DeleteOptions{})
192 if err != nil {
193 t.Fatal(err)
194 }
195 }()
196
197
198 if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) {
199 _, err = client.CoreV1().Pods("default").Patch(context.TODO(), clientAuthMarkerFixture.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{})
200 if t.Failed() {
201 return true, nil
202 }
203 select {
204 case <-upCh:
205 return true, nil
206 default:
207 t.Logf("Waiting for webhook to become effective, getting marker object: %v", err)
208 return false, nil
209 }
210 }); err != nil {
211 t.Fatal(err)
212 }
213
214 }
215
216 type clientAuthRecorder struct {
217 mu sync.Mutex
218 upCh chan struct{}
219 upOnce sync.Once
220 }
221
222
223
224 func (i *clientAuthRecorder) Reset() chan struct{} {
225 i.mu.Lock()
226 defer i.mu.Unlock()
227 i.upCh = make(chan struct{})
228 i.upOnce = sync.Once{}
229 return i.upCh
230 }
231
232 func (i *clientAuthRecorder) MarkerReceived() {
233 i.mu.Lock()
234 defer i.mu.Unlock()
235 i.upOnce.Do(func() {
236 close(i.upCh)
237 })
238 }
239
240 func newClientAuthWebhookHandler(t *testing.T, recorder *clientAuthRecorder) http.Handler {
241 allow := func(w http.ResponseWriter) {
242 w.Header().Set("Content-Type", "application/json")
243 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
244 Response: &v1beta1.AdmissionResponse{
245 Allowed: true,
246 },
247 })
248 }
249 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
250 defer r.Body.Close()
251 data, err := io.ReadAll(r.Body)
252 if err != nil {
253 http.Error(w, err.Error(), http.StatusBadRequest)
254 }
255 review := v1beta1.AdmissionReview{}
256 if err := json.Unmarshal(data, &review); err != nil {
257 http.Error(w, err.Error(), http.StatusBadRequest)
258 }
259 if review.Request.UserInfo.Username != testClientAuthClientUsername {
260
261 allow(w)
262 return
263 }
264
265 if authz := r.Header.Get("Authorization"); authz != "Bearer localhost-match-with-port" {
266 t.Errorf("unexpected authz header: %q", authz)
267 http.Error(w, "Invalid auth", http.StatusUnauthorized)
268 return
269 }
270
271 if len(review.Request.Object.Raw) == 0 {
272 http.Error(w, err.Error(), http.StatusBadRequest)
273 return
274 }
275 pod := &corev1.Pod{}
276 if err := json.Unmarshal(review.Request.Object.Raw, pod); err != nil {
277 http.Error(w, err.Error(), http.StatusBadRequest)
278 return
279 }
280
281
282
283 if pod.Namespace == clientAuthMarkerFixture.Namespace && pod.Name == clientAuthMarkerFixture.Name {
284 recorder.MarkerReceived()
285 allow(w)
286 return
287 }
288 })
289 }
290
291 var clientAuthMarkerFixture = &corev1.Pod{
292 ObjectMeta: metav1.ObjectMeta{
293 Namespace: "default",
294 Name: "marker",
295 },
296 Spec: corev1.PodSpec{
297 Containers: []corev1.Container{{
298 Name: "fake-name",
299 Image: "fakeimage",
300 }},
301 },
302 }
303
View as plain text