mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-05-05 07:45:27 -04:00
Create internal package for joinservice
This commit is contained in:
parent
43eb94b6dc
commit
2083d37b11
10 changed files with 25 additions and 33 deletions
148
joinservice/internal/server/server.go
Normal file
148
joinservice/internal/server/server.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
attestationtypes "github.com/edgelesssys/constellation/internal/attestation/types"
|
||||
"github.com/edgelesssys/constellation/internal/constants"
|
||||
"github.com/edgelesssys/constellation/internal/file"
|
||||
"github.com/edgelesssys/constellation/internal/grpc/grpclog"
|
||||
"github.com/edgelesssys/constellation/internal/logger"
|
||||
"github.com/edgelesssys/constellation/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"
|
||||
)
|
||||
|
||||
// Server implements the core logic of Constellation's node activation service.
|
||||
type Server struct {
|
||||
log *logger.Logger
|
||||
file file.Handler
|
||||
joinTokenGetter joinTokenGetter
|
||||
dataKeyGetter dataKeyGetter
|
||||
ca certificateAuthority
|
||||
joinproto.UnimplementedAPIServer
|
||||
}
|
||||
|
||||
// New initializes a new Server.
|
||||
func New(log *logger.Logger, fileHandler file.Handler, ca certificateAuthority, joinTokenGetter joinTokenGetter, dataKeyGetter dataKeyGetter) *Server {
|
||||
return &Server{
|
||||
log: log,
|
||||
file: fileHandler,
|
||||
joinTokenGetter: joinTokenGetter,
|
||||
dataKeyGetter: dataKeyGetter,
|
||||
ca: ca,
|
||||
}
|
||||
}
|
||||
|
||||
// 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(),
|
||||
)
|
||||
|
||||
joinproto.RegisterAPIServer(grpcServer, s)
|
||||
|
||||
lis, err := net.Listen("tcp", net.JoinHostPort("", port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to listen: %s", err)
|
||||
}
|
||||
s.log.Infof("Starting activation service on %s", lis.Addr().String())
|
||||
return grpcServer.Serve(lis)
|
||||
}
|
||||
|
||||
// IssueJoinTicket handles activation requests of Constellation nodes.
|
||||
// A node will receive:
|
||||
// - stateful disk encryption key.
|
||||
// - Kubernetes join token.
|
||||
// - cluster and owner ID to taint the node as initialized.
|
||||
// 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) (resp *joinproto.IssueJoinTicketResponse, retErr error) {
|
||||
s.log.Infof("IssueJoinTicket called")
|
||||
|
||||
defer func() {
|
||||
if retErr != nil {
|
||||
s.log.Errorf("IssueJoinTicket failed: %s", retErr)
|
||||
retErr = fmt.Errorf("IssueJoinTicket failed: %w", retErr)
|
||||
}
|
||||
}()
|
||||
|
||||
log := s.log.With(zap.String("peerAddress", grpclog.PeerAddrFromContext(ctx)))
|
||||
log.Infof("Loading IDs")
|
||||
var id attestationtypes.ID
|
||||
if err := s.file.ReadJSON(filepath.Join(constants.ServiceBasePath, constants.IDFilename), &id); err != nil {
|
||||
log.With(zap.Error(err)).Errorf("Unable to load IDs")
|
||||
return nil, status.Errorf(codes.Internal, "unable to load IDs: %s", err)
|
||||
}
|
||||
|
||||
log.Infof("Requesting disk encryption key")
|
||||
stateDiskKey, err := s.dataKeyGetter.GetDataKey(ctx, req.DiskUuid, constants.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("Creating Kubernetes join token")
|
||||
kubeArgs, err := s.joinTokenGetter.GetJoinToken(constants.KubernetesJoinTokenTTL)
|
||||
if err != nil {
|
||||
log.With(zap.Error(err)).Errorf("Unable to generate Kubernetes join arguments")
|
||||
return nil, status.Errorf(codes.Internal, "unable to generate Kubernetes join arguments: %s", err)
|
||||
}
|
||||
|
||||
log.Infof("Creating signed kubelet certificate")
|
||||
kubeletCert, kubeletKey, err := s.ca.GetCertificate(req.NodeName)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "unable to generate kubelet certificate: %s", err)
|
||||
}
|
||||
|
||||
var certKey string
|
||||
if req.IsControlPlane {
|
||||
log.Infof("Creating control plane certificate key")
|
||||
certKey, err = s.joinTokenGetter.GetControlPlaneCertificateKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ActivateControlPlane failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
s.log.Infof("IssueJoinTicket successful")
|
||||
return &joinproto.IssueJoinTicketResponse{
|
||||
StateDiskKey: stateDiskKey,
|
||||
ClusterId: id.Cluster,
|
||||
OwnerId: id.Owner,
|
||||
ApiServerEndpoint: kubeArgs.APIServerEndpoint,
|
||||
Token: kubeArgs.Token,
|
||||
DiscoveryTokenCaCertHash: kubeArgs.CACertHashes[0],
|
||||
KubeletCert: kubeletCert,
|
||||
KubeletKey: kubeletKey,
|
||||
CertificateKey: certKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// joinTokenGetter returns Kubernetes bootstrap (join) tokens.
|
||||
type joinTokenGetter interface {
|
||||
// GetJoinToken returns a bootstrap (join) token.
|
||||
GetJoinToken(ttl time.Duration) (*kubeadmv1.BootstrapTokenDiscovery, error)
|
||||
GetControlPlaneCertificateKey() (string, 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(nodeName string) (kubeletCert []byte, kubeletKey []byte, err error)
|
||||
}
|
194
joinservice/internal/server/server_test.go
Normal file
194
joinservice/internal/server/server_test.go
Normal file
|
@ -0,0 +1,194 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
attestationtypes "github.com/edgelesssys/constellation/internal/attestation/types"
|
||||
"github.com/edgelesssys/constellation/internal/constants"
|
||||
"github.com/edgelesssys/constellation/internal/file"
|
||||
"github.com/edgelesssys/constellation/internal/logger"
|
||||
"github.com/edgelesssys/constellation/joinservice/joinproto"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
kubeadmv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func TestIssueJoinTicket(t *testing.T) {
|
||||
someErr := errors.New("error")
|
||||
testKey := []byte{0x1, 0x2, 0x3}
|
||||
testCert := []byte{0x4, 0x5, 0x6}
|
||||
testID := attestationtypes.ID{
|
||||
Owner: []byte{0x4, 0x5, 0x6},
|
||||
Cluster: []byte{0x7, 0x8, 0x9},
|
||||
}
|
||||
testJoinToken := &kubeadmv1.BootstrapTokenDiscovery{
|
||||
APIServerEndpoint: "192.0.2.1",
|
||||
CACertHashes: []string{"hash"},
|
||||
Token: "token",
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
isControlPlane bool
|
||||
kubeadm stubTokenGetter
|
||||
kms stubKeyGetter
|
||||
ca stubCA
|
||||
id []byte
|
||||
wantErr bool
|
||||
}{
|
||||
"worker node": {
|
||||
kubeadm: stubTokenGetter{token: testJoinToken},
|
||||
kms: stubKeyGetter{dataKey: testKey},
|
||||
ca: stubCA{cert: testCert, key: testKey},
|
||||
id: mustMarshalID(testID),
|
||||
},
|
||||
"GetDataKey fails": {
|
||||
kubeadm: stubTokenGetter{token: testJoinToken},
|
||||
kms: stubKeyGetter{getDataKeyErr: someErr},
|
||||
ca: stubCA{cert: testCert, key: testKey},
|
||||
id: mustMarshalID(testID),
|
||||
wantErr: true,
|
||||
},
|
||||
"loading IDs fails": {
|
||||
kubeadm: stubTokenGetter{token: testJoinToken},
|
||||
kms: stubKeyGetter{dataKey: testKey},
|
||||
ca: stubCA{cert: testCert, key: testKey},
|
||||
id: []byte{0x1, 0x2, 0x3},
|
||||
wantErr: true,
|
||||
},
|
||||
"no ID file": {
|
||||
kubeadm: stubTokenGetter{token: testJoinToken},
|
||||
kms: stubKeyGetter{dataKey: testKey},
|
||||
ca: stubCA{cert: testCert, key: testKey},
|
||||
wantErr: true,
|
||||
},
|
||||
"GetJoinToken fails": {
|
||||
kubeadm: stubTokenGetter{getJoinTokenErr: someErr},
|
||||
kms: stubKeyGetter{dataKey: testKey},
|
||||
ca: stubCA{cert: testCert, key: testKey},
|
||||
id: mustMarshalID(testID),
|
||||
wantErr: true,
|
||||
},
|
||||
"GetCertificate fails": {
|
||||
kubeadm: stubTokenGetter{token: testJoinToken},
|
||||
kms: stubKeyGetter{dataKey: testKey},
|
||||
ca: stubCA{getCertErr: someErr},
|
||||
id: mustMarshalID(testID),
|
||||
wantErr: true,
|
||||
},
|
||||
"control plane": {
|
||||
isControlPlane: true,
|
||||
kubeadm: stubTokenGetter{token: testJoinToken, certificateKey: "test"},
|
||||
kms: stubKeyGetter{dataKey: testKey},
|
||||
ca: stubCA{cert: testCert, key: testKey},
|
||||
id: mustMarshalID(testID),
|
||||
},
|
||||
"GetControlPlaneCertificateKey fails": {
|
||||
isControlPlane: true,
|
||||
kubeadm: stubTokenGetter{token: testJoinToken, certificateKeyErr: someErr},
|
||||
kms: stubKeyGetter{dataKey: testKey},
|
||||
ca: stubCA{cert: testCert, key: testKey},
|
||||
id: mustMarshalID(testID),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
file := file.NewHandler(afero.NewMemMapFs())
|
||||
if len(tc.id) > 0 {
|
||||
require.NoError(file.Write(filepath.Join(constants.ServiceBasePath, constants.IDFilename), tc.id, 0o644))
|
||||
}
|
||||
api := New(
|
||||
logger.NewTest(t),
|
||||
file,
|
||||
tc.ca,
|
||||
tc.kubeadm,
|
||||
tc.kms,
|
||||
)
|
||||
|
||||
req := &joinproto.IssueJoinTicketRequest{
|
||||
DiskUuid: "uuid",
|
||||
NodeName: "test",
|
||||
IsControlPlane: tc.isControlPlane,
|
||||
}
|
||||
resp, err := api.IssueJoinTicket(context.Background(), req)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
var expectedIDs attestationtypes.ID
|
||||
require.NoError(json.Unmarshal(tc.id, &expectedIDs))
|
||||
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.kms.dataKey, resp.StateDiskKey)
|
||||
assert.Equal(expectedIDs.Cluster, resp.ClusterId)
|
||||
assert.Equal(expectedIDs.Owner, resp.OwnerId)
|
||||
assert.Equal(tc.kubeadm.token.APIServerEndpoint, resp.ApiServerEndpoint)
|
||||
assert.Equal(tc.kubeadm.token.CACertHashes[0], resp.DiscoveryTokenCaCertHash)
|
||||
assert.Equal(tc.kubeadm.token.Token, resp.Token)
|
||||
assert.Equal(tc.ca.cert, resp.KubeletCert)
|
||||
assert.Equal(tc.ca.key, resp.KubeletKey)
|
||||
|
||||
if tc.isControlPlane {
|
||||
assert.Equal(tc.kubeadm.certificateKey, resp.CertificateKey)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mustMarshalID(id attestationtypes.ID) []byte {
|
||||
b, err := json.Marshal(id)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
type stubTokenGetter struct {
|
||||
token *kubeadmv1.BootstrapTokenDiscovery
|
||||
getJoinTokenErr error
|
||||
certificateKey string
|
||||
certificateKeyErr error
|
||||
}
|
||||
|
||||
func (f stubTokenGetter) GetJoinToken(time.Duration) (*kubeadmv1.BootstrapTokenDiscovery, error) {
|
||||
return f.token, f.getJoinTokenErr
|
||||
}
|
||||
|
||||
func (f stubTokenGetter) GetControlPlaneCertificateKey() (string, error) {
|
||||
return f.certificateKey, f.certificateKeyErr
|
||||
}
|
||||
|
||||
type stubKeyGetter struct {
|
||||
dataKey []byte
|
||||
getDataKeyErr error
|
||||
}
|
||||
|
||||
func (f stubKeyGetter) GetDataKey(context.Context, string, int) ([]byte, error) {
|
||||
return f.dataKey, f.getDataKeyErr
|
||||
}
|
||||
|
||||
type stubCA struct {
|
||||
cert []byte
|
||||
key []byte
|
||||
getCertErr error
|
||||
}
|
||||
|
||||
func (f stubCA) GetCertificate(string) ([]byte, []byte, error) {
|
||||
return f.cert, f.key, f.getCertErr
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue