k8s.io/kubernetes@v1.29.3/test/integration/apiserver/admissionwebhook/client_auth_test.go (about)

     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  	"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  // TestWebhookClientAuthWithAggregatorRouting ensures client auth is used for requests to URL backends
    53  func TestWebhookClientAuthWithAggregatorRouting(t *testing.T) {
    54  	testWebhookClientAuth(t, true)
    55  }
    56  
    57  // TestWebhookClientAuthWithoutAggregatorRouting ensures client auth is used for requests to URL backends
    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  	// Configure a client with a distinct user name so that it is easy to distinguish requests
   146  	// made by the client from requests made by controllers. We use this to filter out requests
   147  	// before recording them to ensure we don't accidentally mistake requests from controllers
   148  	// as requests made by the client.
   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  	// wait until new webhook is called
   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  // Reset zeros out all counts and returns a channel that is closed when the first admission of the
   223  // marker object is received.
   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  			// skip requests not originating from this integration test's client
   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  		// When resetting between tests, a marker object is patched until this webhook
   282  		// observes it, at which point it is considered ready.
   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  }