mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-10-31 13:50:01 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			389 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			389 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright 2016 The Kubernetes Authors.
 | |
| 
 | |
| Licensed under the Apache License, Version 2.0 (the "License");
 | |
| you may not use this file except in compliance with the License.
 | |
| You may obtain a copy of the License at
 | |
| 
 | |
|     http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
| Unless required by applicable law or agreed to in writing, software
 | |
| distributed under the License is distributed on an "AS IS" BASIS,
 | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| See the License for the specific language governing permissions and
 | |
| limitations under the License.
 | |
| */
 | |
| 
 | |
| package app
 | |
| 
 | |
| import (
 | |
| 	"crypto/ecdsa"
 | |
| 	"crypto/elliptic"
 | |
| 	"crypto/rand"
 | |
| 	"crypto/x509"
 | |
| 	"crypto/x509/pkix"
 | |
| 	"encoding/json"
 | |
| 	"encoding/pem"
 | |
| 	"io"
 | |
| 	"math/big"
 | |
| 	"net/http"
 | |
| 	"net/http/httptest"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"sync"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	certapi "k8s.io/api/certificates/v1"
 | |
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | |
| 	"k8s.io/apimachinery/pkg/runtime"
 | |
| 	"k8s.io/apimachinery/pkg/types"
 | |
| 	restclient "k8s.io/client-go/rest"
 | |
| 	certutil "k8s.io/client-go/util/cert"
 | |
| 	capihelper "k8s.io/kubernetes/pkg/apis/certificates/v1"
 | |
| 	"k8s.io/kubernetes/pkg/controller/certificates/authority"
 | |
| )
 | |
| 
 | |
| // Test_buildClientCertificateManager validates that we can build a local client cert
 | |
| // manager that will use the bootstrap client until we get a valid cert, then use our
 | |
| // provided identity on subsequent requests.
 | |
| func Test_buildClientCertificateManager(t *testing.T) {
 | |
| 	testDir, err := os.MkdirTemp("", "kubeletcert")
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	defer func() { os.RemoveAll(testDir) }()
 | |
| 
 | |
| 	serverPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	serverCA, err := certutil.NewSelfSignedCACert(certutil.Config{
 | |
| 		CommonName: "the-test-framework",
 | |
| 	}, serverPrivateKey)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	server := &csrSimulator{
 | |
| 		t:                t,
 | |
| 		serverPrivateKey: serverPrivateKey,
 | |
| 		serverCA:         serverCA,
 | |
| 	}
 | |
| 	s := httptest.NewServer(server)
 | |
| 	defer s.Close()
 | |
| 
 | |
| 	config1 := &restclient.Config{
 | |
| 		UserAgent: "FirstClient",
 | |
| 		Host:      s.URL,
 | |
| 	}
 | |
| 	config2 := &restclient.Config{
 | |
| 		UserAgent: "SecondClient",
 | |
| 		Host:      s.URL,
 | |
| 	}
 | |
| 
 | |
| 	nodeName := types.NodeName("test")
 | |
| 	m, err := buildClientCertificateManager(config1, config2, testDir, nodeName)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	defer m.Stop()
 | |
| 	r := m.(rotater)
 | |
| 
 | |
| 	// get an expired CSR (simulating historical output)
 | |
| 	server.backdate = 2 * time.Hour
 | |
| 	server.SetExpectUserAgent("FirstClient")
 | |
| 	ok, err := r.RotateCerts()
 | |
| 	if !ok || err != nil {
 | |
| 		t.Fatalf("unexpected rotation err: %t %v", ok, err)
 | |
| 	}
 | |
| 	if cert := m.Current(); cert != nil {
 | |
| 		t.Fatalf("Unexpected cert, should be expired: %#v", cert)
 | |
| 	}
 | |
| 	fi := getFileInfo(testDir)
 | |
| 	if len(fi) != 2 {
 | |
| 		t.Fatalf("Unexpected directory contents: %#v", fi)
 | |
| 	}
 | |
| 
 | |
| 	// if m.Current() == nil, then we try again and get a valid
 | |
| 	// client
 | |
| 	server.backdate = 0
 | |
| 	server.SetExpectUserAgent("FirstClient")
 | |
| 	if ok, err := r.RotateCerts(); !ok || err != nil {
 | |
| 		t.Fatalf("unexpected rotation err: %t %v", ok, err)
 | |
| 	}
 | |
| 	if cert := m.Current(); cert == nil {
 | |
| 		t.Fatalf("Unexpected cert, should be valid: %#v", cert)
 | |
| 	}
 | |
| 	fi = getFileInfo(testDir)
 | |
| 	if len(fi) != 2 {
 | |
| 		t.Fatalf("Unexpected directory contents: %#v", fi)
 | |
| 	}
 | |
| 
 | |
| 	// if m.Current() != nil, then we should use the second client
 | |
| 	server.SetExpectUserAgent("SecondClient")
 | |
| 	if ok, err := r.RotateCerts(); !ok || err != nil {
 | |
| 		t.Fatalf("unexpected rotation err: %t %v", ok, err)
 | |
| 	}
 | |
| 	if cert := m.Current(); cert == nil {
 | |
| 		t.Fatalf("Unexpected cert, should be valid: %#v", cert)
 | |
| 	}
 | |
| 	fi = getFileInfo(testDir)
 | |
| 	if len(fi) != 2 {
 | |
| 		t.Fatalf("Unexpected directory contents: %#v", fi)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func Test_buildClientCertificateManager_populateCertDir(t *testing.T) {
 | |
| 	testDir, err := os.MkdirTemp("", "kubeletcert")
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	defer func() { os.RemoveAll(testDir) }()
 | |
| 
 | |
| 	// when no cert is provided, write nothing to disk
 | |
| 	config1 := &restclient.Config{
 | |
| 		UserAgent: "FirstClient",
 | |
| 		Host:      "http://localhost",
 | |
| 	}
 | |
| 	config2 := &restclient.Config{
 | |
| 		UserAgent: "SecondClient",
 | |
| 		Host:      "http://localhost",
 | |
| 	}
 | |
| 	nodeName := types.NodeName("test")
 | |
| 	if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	fi := getFileInfo(testDir)
 | |
| 	if len(fi) != 0 {
 | |
| 		t.Fatalf("Unexpected directory contents: %#v", fi)
 | |
| 	}
 | |
| 
 | |
| 	// an invalid cert should be ignored
 | |
| 	config2.CertData = []byte("invalid contents")
 | |
| 	config2.KeyData = []byte("invalid contents")
 | |
| 	if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err == nil {
 | |
| 		t.Fatal("unexpected non error")
 | |
| 	}
 | |
| 	fi = getFileInfo(testDir)
 | |
| 	if len(fi) != 0 {
 | |
| 		t.Fatalf("Unexpected directory contents: %#v", fi)
 | |
| 	}
 | |
| 
 | |
| 	// an expired client certificate should be written to disk, because the cert manager can
 | |
| 	// use config1 to refresh it and the cert manager won't return it for clients.
 | |
| 	config2.CertData, config2.KeyData = genClientCert(t, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour))
 | |
| 	if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	fi = getFileInfo(testDir)
 | |
| 	if len(fi) != 2 {
 | |
| 		t.Fatalf("Unexpected directory contents: %#v", fi)
 | |
| 	}
 | |
| 
 | |
| 	// a valid, non-expired client certificate should be written to disk
 | |
| 	config2.CertData, config2.KeyData = genClientCert(t, time.Now().Add(-time.Hour), time.Now().Add(24*time.Hour))
 | |
| 	if _, err := buildClientCertificateManager(config1, config2, testDir, nodeName); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	fi = getFileInfo(testDir)
 | |
| 	if len(fi) != 2 {
 | |
| 		t.Fatalf("Unexpected directory contents: %#v", fi)
 | |
| 	}
 | |
| 
 | |
| }
 | |
| 
 | |
| func getFileInfo(dir string) map[string]os.FileInfo {
 | |
| 	fi := make(map[string]os.FileInfo)
 | |
| 	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
 | |
| 		if path == dir {
 | |
| 			return nil
 | |
| 		}
 | |
| 		fi[path] = info
 | |
| 		if !info.IsDir() {
 | |
| 			os.Remove(path)
 | |
| 		}
 | |
| 		return nil
 | |
| 	})
 | |
| 	return fi
 | |
| }
 | |
| 
 | |
| type rotater interface {
 | |
| 	RotateCerts() (bool, error)
 | |
| }
 | |
| 
 | |
| func getCSR(req *http.Request) (*certapi.CertificateSigningRequest, error) {
 | |
| 	if req.Body == nil {
 | |
| 		return nil, nil
 | |
| 	}
 | |
| 	body, err := io.ReadAll(req.Body)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	csr := &certapi.CertificateSigningRequest{}
 | |
| 	if err := json.Unmarshal(body, csr); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return csr, nil
 | |
| }
 | |
| 
 | |
| func mustMarshal(obj interface{}) []byte {
 | |
| 	data, err := json.Marshal(obj)
 | |
| 	if err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| 	return data
 | |
| }
 | |
| 
 | |
| type csrSimulator struct {
 | |
| 	t *testing.T
 | |
| 
 | |
| 	serverPrivateKey *ecdsa.PrivateKey
 | |
| 	serverCA         *x509.Certificate
 | |
| 	backdate         time.Duration
 | |
| 
 | |
| 	userAgentLock   sync.Mutex
 | |
| 	expectUserAgent string
 | |
| 
 | |
| 	lock sync.Mutex
 | |
| 	csr  *certapi.CertificateSigningRequest
 | |
| }
 | |
| 
 | |
| func (s *csrSimulator) SetExpectUserAgent(a string) {
 | |
| 	s.userAgentLock.Lock()
 | |
| 	defer s.userAgentLock.Unlock()
 | |
| 	s.expectUserAgent = a
 | |
| }
 | |
| func (s *csrSimulator) ExpectUserAgent() string {
 | |
| 	s.userAgentLock.Lock()
 | |
| 	defer s.userAgentLock.Unlock()
 | |
| 	return s.expectUserAgent
 | |
| }
 | |
| 
 | |
| func (s *csrSimulator) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 | |
| 	s.lock.Lock()
 | |
| 	defer s.lock.Unlock()
 | |
| 	t := s.t
 | |
| 
 | |
| 	// filter out timeouts as csrSimulator don't support them
 | |
| 	q := req.URL.Query()
 | |
| 	q.Del("timeout")
 | |
| 	q.Del("timeoutSeconds")
 | |
| 	q.Del("allowWatchBookmarks")
 | |
| 	req.URL.RawQuery = q.Encode()
 | |
| 
 | |
| 	t.Logf("Request %q %q %q", req.Method, req.URL, req.UserAgent())
 | |
| 
 | |
| 	if a := s.ExpectUserAgent(); len(a) > 0 && req.UserAgent() != a {
 | |
| 		t.Errorf("Unexpected user agent: %s", req.UserAgent())
 | |
| 	}
 | |
| 
 | |
| 	switch {
 | |
| 	case req.Method == "POST" && req.URL.Path == "/apis/certificates.k8s.io/v1/certificatesigningrequests":
 | |
| 		csr, err := getCSR(req)
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 		if csr.Name == "" {
 | |
| 			csr.Name = "test-csr"
 | |
| 		}
 | |
| 
 | |
| 		csr.UID = types.UID("1")
 | |
| 		csr.ResourceVersion = "1"
 | |
| 		data := mustMarshal(csr)
 | |
| 		w.Header().Set("Content-Type", "application/json")
 | |
| 		w.Write(data)
 | |
| 
 | |
| 		csr = csr.DeepCopy()
 | |
| 		csr.ResourceVersion = "2"
 | |
| 		ca := &authority.CertificateAuthority{
 | |
| 			Certificate: s.serverCA,
 | |
| 			PrivateKey:  s.serverPrivateKey,
 | |
| 		}
 | |
| 		cr, err := capihelper.ParseCSR(csr.Spec.Request)
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 		der, err := ca.Sign(cr.Raw, authority.PermissiveSigningPolicy{
 | |
| 			TTL:      time.Hour,
 | |
| 			Backdate: s.backdate,
 | |
| 		})
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 		csr.Status.Certificate = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
 | |
| 		csr.Status.Conditions = []certapi.CertificateSigningRequestCondition{
 | |
| 			{Type: certapi.CertificateApproved},
 | |
| 		}
 | |
| 		s.csr = csr
 | |
| 
 | |
| 	case req.Method == "GET" && req.URL.Path == "/apis/certificates.k8s.io/v1/certificatesigningrequests" && (req.URL.RawQuery == "fieldSelector=metadata.name%3Dtest-csr&limit=500&resourceVersion=0" || req.URL.RawQuery == "fieldSelector=metadata.name%3Dtest-csr"):
 | |
| 		if s.csr == nil {
 | |
| 			t.Fatalf("no csr")
 | |
| 		}
 | |
| 		csr := s.csr.DeepCopy()
 | |
| 
 | |
| 		data := mustMarshal(&certapi.CertificateSigningRequestList{
 | |
| 			ListMeta: metav1.ListMeta{
 | |
| 				ResourceVersion: "2",
 | |
| 			},
 | |
| 			Items: []certapi.CertificateSigningRequest{
 | |
| 				*csr,
 | |
| 			},
 | |
| 		})
 | |
| 		w.Header().Set("Content-Type", "application/json")
 | |
| 		w.Write(data)
 | |
| 
 | |
| 	case req.Method == "GET" && req.URL.Path == "/apis/certificates.k8s.io/v1/certificatesigningrequests" && req.URL.RawQuery == "fieldSelector=metadata.name%3Dtest-csr&resourceVersion=2&watch=true":
 | |
| 		if s.csr == nil {
 | |
| 			t.Fatalf("no csr")
 | |
| 		}
 | |
| 		csr := s.csr.DeepCopy()
 | |
| 
 | |
| 		data := mustMarshal(&metav1.WatchEvent{
 | |
| 			Type: "ADDED",
 | |
| 			Object: runtime.RawExtension{
 | |
| 				Raw: mustMarshal(csr),
 | |
| 			},
 | |
| 		})
 | |
| 		w.Header().Set("Content-Type", "application/json")
 | |
| 		w.Write(data)
 | |
| 
 | |
| 	default:
 | |
| 		t.Fatalf("unexpected request: %s %s", req.Method, req.URL)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // genClientCert generates an x509 certificate for testing. Certificate and key
 | |
| // are returned in PEM encoding.
 | |
| func genClientCert(t *testing.T, from, to time.Time) ([]byte, []byte) {
 | |
| 	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	keyRaw, err := x509.MarshalECPrivateKey(key)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
 | |
| 	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	cert := &x509.Certificate{
 | |
| 		SerialNumber: serialNumber,
 | |
| 		Subject:      pkix.Name{Organization: []string{"Acme Co"}},
 | |
| 		NotBefore:    from,
 | |
| 		NotAfter:     to,
 | |
| 
 | |
| 		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
 | |
| 		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
 | |
| 		BasicConstraintsValid: true,
 | |
| 	}
 | |
| 	certRaw, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}),
 | |
| 		pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyRaw})
 | |
| }
 |