mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-08-03 12:36:09 -04:00
feat: use SSH host certificates (#3786)
This commit is contained in:
parent
95f17a6d06
commit
7ea5c41f9b
34 changed files with 706 additions and 117 deletions
|
@ -10,6 +10,7 @@ go_library(
|
|||
"//internal/attestation",
|
||||
"//internal/constants",
|
||||
"//internal/crypto",
|
||||
"//internal/file",
|
||||
"//internal/grpc/grpclog",
|
||||
"//internal/logger",
|
||||
"//internal/versions/components",
|
||||
|
@ -30,12 +31,15 @@ go_test(
|
|||
deps = [
|
||||
"//internal/attestation",
|
||||
"//internal/constants",
|
||||
"//internal/file",
|
||||
"//internal/logger",
|
||||
"//internal/versions/components",
|
||||
"//joinservice/joinproto",
|
||||
"@com_github_spf13_afero//:afero",
|
||||
"@com_github_stretchr_testify//assert",
|
||||
"@com_github_stretchr_testify//require",
|
||||
"@io_k8s_kubernetes//cmd/kubeadm/app/apis/kubeadm/v1beta3",
|
||||
"@org_golang_x_crypto//ssh",
|
||||
"@org_uber_go_goleak//:goleak",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -13,11 +13,13 @@ import (
|
|||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/file"
|
||||
"github.com/edgelesssys/constellation/v2/internal/grpc/grpclog"
|
||||
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||
"github.com/edgelesssys/constellation/v2/internal/versions/components"
|
||||
|
@ -40,6 +42,7 @@ type Server struct {
|
|||
dataKeyGetter dataKeyGetter
|
||||
ca certificateAuthority
|
||||
kubeClient kubeClient
|
||||
fileHandler file.Handler
|
||||
joinproto.UnimplementedAPIServer
|
||||
}
|
||||
|
||||
|
@ -47,6 +50,7 @@ type Server struct {
|
|||
func New(
|
||||
measurementSalt []byte, ca certificateAuthority,
|
||||
joinTokenGetter joinTokenGetter, dataKeyGetter dataKeyGetter, kubeClient kubeClient, log *slog.Logger,
|
||||
fileHandler file.Handler,
|
||||
) (*Server, error) {
|
||||
return &Server{
|
||||
measurementSalt: measurementSalt,
|
||||
|
@ -55,6 +59,7 @@ func New(
|
|||
dataKeyGetter: dataKeyGetter,
|
||||
ca: ca,
|
||||
kubeClient: kubeClient,
|
||||
fileHandler: fileHandler,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -114,6 +119,25 @@ func (s *Server) IssueJoinTicket(ctx context.Context, req *joinproto.IssueJoinTi
|
|||
return nil, status.Errorf(codes.Internal, "generating ssh emergency CA key: %s", err)
|
||||
}
|
||||
|
||||
principalList := req.HostCertificatePrincipals
|
||||
additionalPrincipals, err := s.fileHandler.Read(constants.SSHAdditionalPrincipalsPath)
|
||||
if err != nil {
|
||||
log.With(slog.Any("error", err)).Error("Failed to read additional principals file")
|
||||
return nil, status.Errorf(codes.Internal, "reading additional principals file: %s", err)
|
||||
}
|
||||
principalList = append(principalList, strings.Split(string(additionalPrincipals), ",")...)
|
||||
|
||||
publicKey, err := ssh.ParsePublicKey(req.HostPublicKey)
|
||||
if err != nil {
|
||||
log.With(slog.Any("error", err)).Error("Failed to parse host public key")
|
||||
return nil, status.Errorf(codes.Internal, "unmarshalling host public key: %s", err)
|
||||
}
|
||||
hostCertificate, err := crypto.GenerateSSHHostCertificate(principalList, publicKey, ca)
|
||||
if err != nil {
|
||||
log.With(slog.Any("error", err)).Error("Failed to generate and sign SSH host key")
|
||||
return nil, status.Errorf(codes.Internal, "generating and signing SSH host key: %s", err)
|
||||
}
|
||||
|
||||
log.Info("Creating Kubernetes join token")
|
||||
kubeArgs, err := s.joinTokenGetter.GetJoinToken(constants.KubernetesJoinTokenTTL)
|
||||
if err != nil {
|
||||
|
@ -182,6 +206,7 @@ func (s *Server) IssueJoinTicket(ctx context.Context, req *joinproto.IssueJoinTi
|
|||
ControlPlaneFiles: controlPlaneFiles,
|
||||
KubernetesComponents: components,
|
||||
AuthorizedCaPublicKey: ssh.MarshalAuthorizedKey(ca.PublicKey()),
|
||||
HostCertificate: ssh.MarshalAuthorizedKey(hostCertificate),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -15,12 +15,15 @@ import (
|
|||
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation"
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||
"github.com/edgelesssys/constellation/v2/internal/versions/components"
|
||||
"github.com/edgelesssys/constellation/v2/joinservice/joinproto"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
"golang.org/x/crypto/ssh"
|
||||
kubeadmv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
|
||||
)
|
||||
|
||||
|
@ -36,6 +39,11 @@ func TestIssueJoinTicket(t *testing.T) {
|
|||
measurementSecret := []byte{0x7, 0x8, 0x9}
|
||||
uuid := "uuid"
|
||||
|
||||
pubkey, _, err := ed25519.GenerateKey(nil)
|
||||
require.NoError(t, err)
|
||||
hostSSHPubKey, err := ssh.NewPublicKey(pubkey)
|
||||
require.NoError(t, err)
|
||||
|
||||
testJoinToken := &kubeadmv1.BootstrapTokenDiscovery{
|
||||
APIServerEndpoint: "192.0.2.1",
|
||||
CACertHashes: []string{"hash"},
|
||||
|
@ -52,13 +60,15 @@ func TestIssueJoinTicket(t *testing.T) {
|
|||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
isControlPlane bool
|
||||
kubeadm stubTokenGetter
|
||||
kms stubKeyGetter
|
||||
ca stubCA
|
||||
kubeClient stubKubeClient
|
||||
missingComponentsReferenceFile bool
|
||||
wantErr bool
|
||||
isControlPlane bool
|
||||
kubeadm stubTokenGetter
|
||||
kms stubKeyGetter
|
||||
ca stubCA
|
||||
kubeClient stubKubeClient
|
||||
missingComponentsReferenceFile bool
|
||||
missingAdditionalPrincipalsFile bool
|
||||
missingSSHHostKey bool
|
||||
wantErr bool
|
||||
}{
|
||||
"worker node": {
|
||||
kubeadm: stubTokenGetter{token: testJoinToken},
|
||||
|
@ -179,6 +189,30 @@ func TestIssueJoinTicket(t *testing.T) {
|
|||
kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"},
|
||||
wantErr: true,
|
||||
},
|
||||
"Additional principals file is missing": {
|
||||
kubeadm: stubTokenGetter{token: testJoinToken},
|
||||
kms: stubKeyGetter{dataKeys: map[string][]byte{
|
||||
uuid: testKey,
|
||||
attestation.MeasurementSecretContext: measurementSecret,
|
||||
constants.SSHCAKeySuffix: testCaKey,
|
||||
}},
|
||||
ca: stubCA{cert: testCert, nodeName: "node"},
|
||||
kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"},
|
||||
missingAdditionalPrincipalsFile: true,
|
||||
wantErr: true,
|
||||
},
|
||||
"Host pubkey is missing": {
|
||||
kubeadm: stubTokenGetter{token: testJoinToken},
|
||||
kms: stubKeyGetter{dataKeys: map[string][]byte{
|
||||
uuid: testKey,
|
||||
attestation.MeasurementSecretContext: measurementSecret,
|
||||
constants.SSHCAKeySuffix: testCaKey,
|
||||
}},
|
||||
ca: stubCA{cert: testCert, nodeName: "node"},
|
||||
kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"},
|
||||
missingSSHHostKey: true,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
|
@ -188,6 +222,11 @@ func TestIssueJoinTicket(t *testing.T) {
|
|||
|
||||
salt := []byte{0xA, 0xB, 0xC}
|
||||
|
||||
fh := file.NewHandler(afero.NewMemMapFs())
|
||||
if !tc.missingAdditionalPrincipalsFile {
|
||||
require.NoError(fh.Write(constants.SSHAdditionalPrincipalsPath, []byte("*"), file.OptMkdirAll))
|
||||
}
|
||||
|
||||
api := Server{
|
||||
measurementSalt: salt,
|
||||
ca: tc.ca,
|
||||
|
@ -195,11 +234,20 @@ func TestIssueJoinTicket(t *testing.T) {
|
|||
dataKeyGetter: tc.kms,
|
||||
kubeClient: &tc.kubeClient,
|
||||
log: logger.NewTest(t),
|
||||
fileHandler: fh,
|
||||
}
|
||||
|
||||
var keyToSend []byte
|
||||
if tc.missingSSHHostKey {
|
||||
keyToSend = nil
|
||||
} else {
|
||||
keyToSend = hostSSHPubKey.Marshal()
|
||||
}
|
||||
|
||||
req := &joinproto.IssueJoinTicketRequest{
|
||||
DiskUuid: "uuid",
|
||||
IsControlPlane: tc.isControlPlane,
|
||||
HostPublicKey: keyToSend,
|
||||
}
|
||||
resp, err := api.IssueJoinTicket(t.Context(), req)
|
||||
if tc.wantErr {
|
||||
|
@ -260,6 +308,7 @@ func TestIssueRejoinTicker(t *testing.T) {
|
|||
joinTokenGetter: stubTokenGetter{},
|
||||
dataKeyGetter: tc.keyGetter,
|
||||
log: logger.NewTest(t),
|
||||
fileHandler: file.NewHandler(afero.NewMemMapFs()),
|
||||
}
|
||||
|
||||
req := &joinproto.IssueRejoinTicketRequest{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue