Skip to content

Commit e734c70

Browse files
committed
Add integration test for webhook client auth
1 parent d127042 commit e734c70

File tree

3 files changed

+301
-1
lines changed

3 files changed

+301
-1
lines changed

staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/testing/authentication_info_resolver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func (a *authenticationInfoResolver) ClientConfigFor(hostPort string) (*rest.Con
5757
return a.restConfig, nil
5858
}
5959

60-
func (a *authenticationInfoResolver) ClientConfigForService(serviceName, serviceNamespace string) (*rest.Config, error) {
60+
func (a *authenticationInfoResolver) ClientConfigForService(serviceName, serviceNamespace string, servicePort int) (*rest.Config, error) {
6161
atomic.AddInt32(a.cacheMisses, 1)
6262
return a.restConfig, nil
6363
}

test/integration/apiserver/admissionwebhook/BUILD

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

0 commit comments

Comments
 (0)