constellation/joinservice/internal/server/server.go

232 lines
9.1 KiB
Go
Raw Normal View History

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
// Package server implements the gRPC endpoint of Constellation's node join service.
package server
import (
"context"
"fmt"
"net"
"time"
2022-09-21 07:47:57 -04:00
"github.com/edgelesssys/constellation/v2/internal/attestation"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/crypto"
"github.com/edgelesssys/constellation/v2/internal/grpc/grpclog"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/versions/components"
2022-09-21 07:47:57 -04:00
"github.com/edgelesssys/constellation/v2/joinservice/joinproto"
"go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/status"
kubeadmv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
)
2022-07-05 08:13:19 -04:00
// Server implements the core logic of Constellation's node join service.
type Server struct {
measurementSalt []byte
log *logger.Logger
joinTokenGetter joinTokenGetter
dataKeyGetter dataKeyGetter
ca certificateAuthority
kubeClient kubeClient
2022-07-05 05:41:31 -04:00
joinproto.UnimplementedAPIServer
}
// New initializes a new Server.
func New(
measurementSalt []byte, ca certificateAuthority,
joinservice: cache certificates for Azure SEV-SNP attestation (#2336) * add ASK caching in joinservice Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * use cached ASK in Azure SEV-SNP attestation Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * update test charts Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix linter Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix typ Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * make caching mechanism less provider-specific Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * update buildfiles Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add `omitempty` flag Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * frontload certificate getter Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * rename frontloaded function Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * pass cached certificates to constructor Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix race condition Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix marshalling of empty certs Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix validator usage Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * [wip] add certcache tests Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add certcache tests Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * tidy Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix validator test Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * remove unused fields in validator Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix certificate precedence Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * use separate context Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * tidy Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * linter fixes Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * linter fixes Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * Remove unnecessary comment Co-authored-by: Thomas Tendyck <51411342+thomasten@users.noreply.github.com> * use background context Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * Use error format directive Co-authored-by: Thomas Tendyck <51411342+thomasten@users.noreply.github.com> * `azure` -> `Azure` Co-authored-by: Thomas Tendyck <51411342+thomasten@users.noreply.github.com> * improve error messages Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add x509 -> PEM util function Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * use crypto util functions Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix certificate replacement logic Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * only require ASK from certcache Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * tidy Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix comment typo Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> --------- Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> Co-authored-by: Thomas Tendyck <51411342+thomasten@users.noreply.github.com>
2023-09-29 08:29:50 -04:00
joinTokenGetter joinTokenGetter, dataKeyGetter dataKeyGetter, kubeClient kubeClient, log *logger.Logger,
) (*Server, error) {
return &Server{
measurementSalt: measurementSalt,
log: log,
joinTokenGetter: joinTokenGetter,
dataKeyGetter: dataKeyGetter,
ca: ca,
kubeClient: kubeClient,
}, nil
}
// Run starts the gRPC server on the given port, using the provided tlsConfig.
func (s *Server) Run(creds credentials.TransportCredentials, port string) error {
s.log.WithIncreasedLevel(zap.WarnLevel).Named("gRPC").ReplaceGRPCLogger()
grpcServer := grpc.NewServer(
grpc.Creds(creds),
s.log.Named("gRPC").GetServerUnaryInterceptor(),
)
2022-07-05 05:41:31 -04:00
joinproto.RegisterAPIServer(grpcServer, s)
lis, err := net.Listen("tcp", net.JoinHostPort("", port))
if err != nil {
return fmt.Errorf("failed to listen: %s", err)
}
2022-07-05 08:13:19 -04:00
s.log.Infof("Starting join service on %s", lis.Addr().String())
return grpcServer.Serve(lis)
}
2022-07-05 08:13:19 -04:00
// IssueJoinTicket handles join requests of Constellation nodes.
2022-07-05 05:41:31 -04:00
// A node will receive:
// - stateful disk encryption key.
// - Kubernetes join token.
// - measurement salt and secret, to mark the node as initialized.
2022-07-05 05:41:31 -04:00
// In addition, control plane nodes receive:
// - a decryption key for CA certificates uploaded to the Kubernetes cluster.
func (s *Server) IssueJoinTicket(ctx context.Context, req *joinproto.IssueJoinTicketRequest) (*joinproto.IssueJoinTicketResponse, error) {
log := s.log.With(zap.String("peerAddress", grpclog.PeerAddrFromContext(ctx)))
log.Infof("IssueJoinTicket called")
log.Infof("Requesting measurement secret")
measurementSecret, err := s.dataKeyGetter.GetDataKey(ctx, attestation.MeasurementSecretContext, crypto.DerivedKeyLengthDefault)
if err != nil {
log.With(zap.Error(err)).Errorf("Failed to get measurement secret")
return nil, status.Errorf(codes.Internal, "getting measurement secret: %s", err)
}
log.Infof("Requesting disk encryption key")
stateDiskKey, err := s.dataKeyGetter.GetDataKey(ctx, req.DiskUuid, crypto.StateDiskKeyLength)
if err != nil {
log.With(zap.Error(err)).Errorf("Failed to get key for stateful disk")
return nil, status.Errorf(codes.Internal, "getting key for stateful disk: %s", err)
}
log.Infof("Creating Kubernetes join token")
kubeArgs, err := s.joinTokenGetter.GetJoinToken(constants.KubernetesJoinTokenTTL)
if err != nil {
log.With(zap.Error(err)).Errorf("Failed to generate Kubernetes join arguments")
return nil, status.Errorf(codes.Internal, "generating Kubernetes join arguments: %s", err)
}
log.Infof("Querying NodeVersion custom resource for components ConfigMap name")
componentsConfigMapName, err := s.getK8sComponentsConfigMapName(ctx)
if err != nil {
log.With(zap.Error(err)).Errorf("Failed getting components ConfigMap name")
return nil, status.Errorf(codes.Internal, "getting components ConfigMap name: %s", err)
}
log.Infof("Querying %s ConfigMap for components", componentsConfigMapName)
components, err := s.kubeClient.GetComponents(ctx, componentsConfigMapName)
if err != nil {
log.With(zap.Error(err)).Errorf("Failed getting components from ConfigMap")
return nil, status.Errorf(codes.Internal, "getting components: %s", err)
}
log.Infof("Creating signed kubelet certificate")
kubeletCert, err := s.ca.GetCertificate(req.CertificateRequest)
if err != nil {
log.With(zap.Error(err)).Errorf("Failed generating kubelet certificate")
return nil, status.Errorf(codes.Internal, "Generating kubelet certificate: %s", err)
}
var controlPlaneFiles []*joinproto.ControlPlaneCertOrKey
2022-07-05 05:41:31 -04:00
if req.IsControlPlane {
log.Infof("Loading control plane certificates and keys")
filesMap, err := s.joinTokenGetter.GetControlPlaneCertificatesAndKeys()
2022-07-05 05:41:31 -04:00
if err != nil {
log.With(zap.Error(err)).Errorf("Failed to load control plane certificates and keys")
return nil, status.Errorf(codes.Internal, "loading control-plane certificates and keys: %s", err)
2022-07-05 05:41:31 -04:00
}
for k, v := range filesMap {
controlPlaneFiles = append(controlPlaneFiles, &joinproto.ControlPlaneCertOrKey{
Name: k,
Data: v,
})
}
2022-07-05 05:41:31 -04:00
}
nodeName, err := s.ca.GetNodeNameFromCSR(req.CertificateRequest)
if err != nil {
log.With(zap.Error(err)).Errorf("Failed getting node name from CSR")
return nil, status.Errorf(codes.Internal, "getting node name from CSR: %s", err)
}
if err := s.kubeClient.AddNodeToJoiningNodes(ctx, nodeName, componentsConfigMapName, req.IsControlPlane); err != nil {
log.With(zap.Error(err)).Errorf("Failed adding node to joining nodes")
return nil, status.Errorf(codes.Internal, "adding node to joining nodes: %s", err)
}
log.Infof("IssueJoinTicket successful")
2022-07-05 05:41:31 -04:00
return &joinproto.IssueJoinTicketResponse{
StateDiskKey: stateDiskKey,
MeasurementSalt: s.measurementSalt,
MeasurementSecret: measurementSecret,
2022-07-05 05:41:31 -04:00
ApiServerEndpoint: kubeArgs.APIServerEndpoint,
Token: kubeArgs.Token,
DiscoveryTokenCaCertHash: kubeArgs.CACertHashes[0],
KubeletCert: kubeletCert,
ControlPlaneFiles: controlPlaneFiles,
KubernetesComponents: components,
2022-07-05 05:41:31 -04:00
}, nil
}
// IssueRejoinTicket issues a ticket for nodes to rejoin cluster.
func (s *Server) IssueRejoinTicket(ctx context.Context, req *joinproto.IssueRejoinTicketRequest) (*joinproto.IssueRejoinTicketResponse, error) {
log := s.log.With(zap.String("peerAddress", grpclog.PeerAddrFromContext(ctx)))
log.Infof("IssueRejoinTicket called")
log.Infof("Requesting measurement secret")
measurementSecret, err := s.dataKeyGetter.GetDataKey(ctx, attestation.MeasurementSecretContext, crypto.DerivedKeyLengthDefault)
if err != nil {
log.With(zap.Error(err)).Errorf("Unable to get measurement secret")
return nil, status.Errorf(codes.Internal, "unable to get measurement secret: %s", err)
}
log.Infof("Requesting disk encryption key")
stateDiskKey, err := s.dataKeyGetter.GetDataKey(ctx, req.DiskUuid, crypto.StateDiskKeyLength)
if err != nil {
log.With(zap.Error(err)).Errorf("Unable to get key for stateful disk")
return nil, status.Errorf(codes.Internal, "unable to get key for stateful disk: %s", err)
}
log.Infof("IssueRejoinTicket successful")
return &joinproto.IssueRejoinTicketResponse{
StateDiskKey: stateDiskKey,
MeasurementSecret: measurementSecret,
}, nil
}
// getK8sComponentsConfigMapName reads the k8s components config map name from a VolumeMount that is backed by the k8s-version ConfigMap.
func (s *Server) getK8sComponentsConfigMapName(ctx context.Context) (string, error) {
k8sComponentsRef, err := s.kubeClient.GetK8sComponentsRefFromNodeVersionCRD(ctx, constants.NodeVersionResourceName)
if err != nil {
return "", fmt.Errorf("could not get k8s components config map name: %w", err)
}
return k8sComponentsRef, nil
}
// joinTokenGetter returns Kubernetes bootstrap (join) tokens.
type joinTokenGetter interface {
// GetJoinToken returns a bootstrap (join) token.
GetJoinToken(ttl time.Duration) (*kubeadmv1.BootstrapTokenDiscovery, error)
GetControlPlaneCertificatesAndKeys() (map[string][]byte, error)
}
// dataKeyGetter interacts with Constellation's key management system to retrieve keys.
type dataKeyGetter interface {
// GetDataKey returns a key derived from Constellation's KMS.
GetDataKey(ctx context.Context, uuid string, length int) ([]byte, error)
}
type certificateAuthority interface {
// GetCertificate returns a certificate and private key, signed by the issuer.
GetCertificate(certificateRequest []byte) (kubeletCert []byte, err error)
// GetNodeNameFromCSR returns the node name from the CSR.
GetNodeNameFromCSR(csr []byte) (string, error)
}
type kubeClient interface {
GetK8sComponentsRefFromNodeVersionCRD(ctx context.Context, nodeName string) (string, error)
GetComponents(ctx context.Context, configMapName string) (components.Components, error)
AddNodeToJoiningNodes(ctx context.Context, nodeName string, componentsHash string, isControlPlane bool) error
}