mirror of https://github.com/kubernetes/kops.git
				
				
				
			
		
			
				
	
	
		
			218 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			218 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			Go
		
	
	
	
| /*
 | |
| Copyright 2023 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 bootstrap
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"crypto/sha256"
 | |
| 	"crypto/tls"
 | |
| 	"crypto/x509"
 | |
| 	"crypto/x509/pkix"
 | |
| 	"encoding/hex"
 | |
| 	"encoding/pem"
 | |
| 	"fmt"
 | |
| 	"net"
 | |
| 	"sync"
 | |
| 
 | |
| 	"google.golang.org/grpc"
 | |
| 	"google.golang.org/grpc/codes"
 | |
| 	"google.golang.org/grpc/credentials"
 | |
| 	"google.golang.org/grpc/peer"
 | |
| 	"google.golang.org/grpc/status"
 | |
| 
 | |
| 	"k8s.io/klog/v2"
 | |
| 	"k8s.io/kops/pkg/apis/nodeup"
 | |
| 	pb "k8s.io/kops/proto/kops/bootstrap/v1"
 | |
| )
 | |
| 
 | |
| type ChallengeServer struct {
 | |
| 	tlsConfig *tls.Config
 | |
| 	servingCA []byte
 | |
| 
 | |
| 	mutex      sync.Mutex
 | |
| 	challenges map[string]*Challenge
 | |
| 
 | |
| 	RequiredSubject pkix.Name
 | |
| 
 | |
| 	pb.UnimplementedCallbackServiceServer
 | |
| }
 | |
| 
 | |
| func NewChallengeServer(clusterName string, caBundle []byte) (*ChallengeServer, error) {
 | |
| 	serverCertificate, err := BuildChallengeServerCertificate(clusterName)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	tlsConfig := &tls.Config{
 | |
| 		Certificates: []tls.Certificate{*serverCertificate},
 | |
| 	}
 | |
| 
 | |
| 	var servingCA bytes.Buffer
 | |
| 	for _, cert := range serverCertificate.Certificate {
 | |
| 		if err := pem.Encode(&servingCA, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	clientCAs := x509.NewCertPool()
 | |
| 	if !clientCAs.AppendCertsFromPEM(caBundle) {
 | |
| 		return nil, fmt.Errorf("unable to build client-cert CA pools")
 | |
| 	}
 | |
| 	tlsConfig.ClientCAs = clientCAs
 | |
| 	tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
 | |
| 
 | |
| 	return &ChallengeServer{
 | |
| 		RequiredSubject: challengeKopsControllerSubject(clusterName),
 | |
| 		tlsConfig:       tlsConfig,
 | |
| 		servingCA:       servingCA.Bytes(),
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| type Challenge struct {
 | |
| 	ChallengeID     string
 | |
| 	ChallengeSecret []byte
 | |
| }
 | |
| 
 | |
| func (s *ChallengeServer) createChallenge() *Challenge {
 | |
| 	c := &Challenge{}
 | |
| 	c.ChallengeID = hex.EncodeToString(randomBytes(16))
 | |
| 	c.ChallengeSecret = randomBytes(16)
 | |
| 
 | |
| 	s.mutex.Lock()
 | |
| 	defer s.mutex.Unlock()
 | |
| 	if s.challenges == nil {
 | |
| 		s.challenges = make(map[string]*Challenge)
 | |
| 	}
 | |
| 	s.challenges[c.ChallengeID] = c
 | |
| 	return c
 | |
| }
 | |
| 
 | |
| type ChallengeListener struct {
 | |
| 	endpoint   string
 | |
| 	server     *ChallengeServer
 | |
| 	grpcServer *grpc.Server
 | |
| }
 | |
| 
 | |
| func (s *ChallengeListener) CreateChallenge() *nodeup.ChallengeRequest {
 | |
| 	challenge := s.server.createChallenge()
 | |
| 
 | |
| 	return &nodeup.ChallengeRequest{
 | |
| 		Endpoint:        s.Endpoint(),
 | |
| 		ChallengeID:     challenge.ChallengeID,
 | |
| 		ChallengeSecret: challenge.ChallengeSecret,
 | |
| 		ServerCA:        s.server.servingCA,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (s *ChallengeListener) Stop() {
 | |
| 	s.grpcServer.Stop()
 | |
| }
 | |
| 
 | |
| func (s *ChallengeListener) Endpoint() string {
 | |
| 	return s.endpoint
 | |
| }
 | |
| 
 | |
| func (s *ChallengeServer) NewListener(ctx context.Context, listen string) (*ChallengeListener, error) {
 | |
| 	var opts []grpc.ServerOption
 | |
| 
 | |
| 	opts = append(opts, grpc.Creds(credentials.NewTLS(s.tlsConfig)))
 | |
| 	grpcServer := grpc.NewServer(opts...)
 | |
| 	pb.RegisterCallbackServiceServer(grpcServer, s)
 | |
| 
 | |
| 	lis, err := net.Listen("tcp", listen)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("error listening on %q: %w", listen, err)
 | |
| 	}
 | |
| 
 | |
| 	grpcListener := &ChallengeListener{
 | |
| 		server:     s,
 | |
| 		grpcServer: grpcServer,
 | |
| 		endpoint:   lis.Addr().String(),
 | |
| 	}
 | |
| 
 | |
| 	go func() {
 | |
| 		klog.Infof("starting node-challenge listener on %v", lis.Addr())
 | |
| 		if err := grpcServer.Serve(lis); err != nil {
 | |
| 			lis.Close()
 | |
| 
 | |
| 			klog.Warningf("error serving GRPC: %v", err)
 | |
| 		}
 | |
| 	}()
 | |
| 
 | |
| 	return grpcListener, nil
 | |
| }
 | |
| 
 | |
| // Answers challenges to cross-check bootstrap requests.
 | |
| func (s *ChallengeServer) Challenge(ctx context.Context, req *pb.ChallengeRequest) (*pb.ChallengeResponse, error) {
 | |
| 	klog.Infof("got node-challenge request")
 | |
| 	// Explicitly authenticate the username for safety
 | |
| 	peerInfo, ok := peer.FromContext(ctx)
 | |
| 	if !ok {
 | |
| 		klog.Warningf("no peer in context")
 | |
| 		return nil, status.Error(codes.Unauthenticated, "peer was nil")
 | |
| 	}
 | |
| 
 | |
| 	tlsInfo, ok := peerInfo.AuthInfo.(credentials.TLSInfo)
 | |
| 	if !ok {
 | |
| 		klog.Warningf("peer.AuthInfo was of unexpected type %T", peerInfo.AuthInfo)
 | |
| 		return nil, status.Error(codes.Unauthenticated, "unexpected peer transport credentials")
 | |
| 	}
 | |
| 
 | |
| 	if len(tlsInfo.State.VerifiedChains) == 0 || len(tlsInfo.State.VerifiedChains[0]) == 0 {
 | |
| 		klog.Warningf("no VerifiedChains in TLSInfo")
 | |
| 		return nil, status.Error(codes.Unauthenticated, "verified chains were empty")
 | |
| 	}
 | |
| 
 | |
| 	if got, want := tlsInfo.State.VerifiedChains[0][0].Subject, s.RequiredSubject; !subjectsMatch(got, want) {
 | |
| 		klog.Warningf("certificate subjects did not match expected; got %q, want %q", got, want)
 | |
| 		return nil, status.Error(codes.Unauthenticated, "certificate subjects did not match")
 | |
| 	}
 | |
| 
 | |
| 	s.mutex.Lock()
 | |
| 	defer s.mutex.Unlock()
 | |
| 
 | |
| 	key := req.ChallengeId
 | |
| 	if key == "" {
 | |
| 		return nil, status.Errorf(codes.InvalidArgument, "challenge_id is required")
 | |
| 	}
 | |
| 
 | |
| 	challenge := s.challenges[key]
 | |
| 	if challenge == nil {
 | |
| 		return nil, status.Errorf(codes.NotFound, "challenge was not found")
 | |
| 	}
 | |
| 	// Prevent replay attacks
 | |
| 	delete(s.challenges, key)
 | |
| 
 | |
| 	hash := buildChallengeResponse(challenge.ChallengeSecret, req.GetChallengeRandom())
 | |
| 	response := &pb.ChallengeResponse{
 | |
| 		ChallengeResponse: hash,
 | |
| 	}
 | |
| 
 | |
| 	return response, nil
 | |
| }
 | |
| 
 | |
| func buildChallengeResponse(nodeNonce []byte, kopsControllerNonde []byte) []byte {
 | |
| 	// Arguably this is overkill because the TLS handshake is stronger and everything is encrypted.
 | |
| 	hasher := sha256.New()
 | |
| 	hasher.Sum(nodeNonce)
 | |
| 	hasher.Sum(kopsControllerNonde)
 | |
| 
 | |
| 	hash := hasher.Sum(nil)
 | |
| 
 | |
| 	return hash
 | |
| }
 |