diff --git a/bootstrapper/internal/initserver/BUILD.bazel b/bootstrapper/internal/initserver/BUILD.bazel index 3ad04343c3..f45aad1e65 100644 --- a/bootstrapper/internal/initserver/BUILD.bazel +++ b/bootstrapper/internal/initserver/BUILD.bazel @@ -11,6 +11,7 @@ go_library( "//bootstrapper/internal/journald", "//internal/atls", "//internal/attestation", + "//internal/constants", "//internal/crypto", "//internal/file", "//internal/grpc/atlscredentials", @@ -26,6 +27,7 @@ go_library( "@org_golang_google_grpc//keepalive", "@org_golang_google_grpc//status", "@org_golang_x_crypto//bcrypt", + "@org_golang_x_crypto//ssh", ], ) diff --git a/bootstrapper/internal/initserver/initserver.go b/bootstrapper/internal/initserver/initserver.go index a65a5f8b7c..fad54abdaf 100644 --- a/bootstrapper/internal/initserver/initserver.go +++ b/bootstrapper/internal/initserver/initserver.go @@ -20,6 +20,7 @@ package initserver import ( "bufio" "context" + "crypto/ed25519" "errors" "fmt" "io" @@ -33,6 +34,7 @@ import ( "github.com/edgelesssys/constellation/v2/bootstrapper/internal/journald" "github.com/edgelesssys/constellation/v2/internal/atls" "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/atlscredentials" @@ -44,6 +46,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/role" "github.com/edgelesssys/constellation/v2/internal/versions/components" "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/ssh" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/keepalive" @@ -222,6 +225,28 @@ func (s *Server) Init(req *initproto.InitRequest, stream initproto.API_InitServe return err } + // Derive the emergency ssh CA key + key, err := cloudKms.GetDEK(stream.Context(), crypto.DEKPrefix+constants.SSHCAKeySuffix, ed25519.SeedSize) + if err != nil { + if e := s.sendLogsWithMessage(stream, status.Errorf(codes.Internal, "retrieving DEK for key derivation: %s", err)); e != nil { + err = errors.Join(err, e) + } + return err + } + ca, err := crypto.GenerateEmergencySSHCAKey(key) + if err != nil { + if e := s.sendLogsWithMessage(stream, status.Errorf(codes.Internal, "generating emergency SSH CA key: %s", err)); e != nil { + err = errors.Join(err, e) + } + return err + } + if err := s.fileHandler.Write(constants.SSHCAKeyPath, ssh.MarshalAuthorizedKey(ca.PublicKey()), file.OptMkdirAll); err != nil { + if e := s.sendLogsWithMessage(stream, status.Errorf(codes.Internal, "writing ssh CA pubkey: %s", err)); e != nil { + err = errors.Join(err, e) + } + return err + } + clusterName := req.ClusterName if clusterName == "" { clusterName = "constellation" diff --git a/bootstrapper/internal/joinclient/joinclient.go b/bootstrapper/internal/joinclient/joinclient.go index 37c9e9b8fc..5369069880 100644 --- a/bootstrapper/internal/joinclient/joinclient.go +++ b/bootstrapper/internal/joinclient/joinclient.go @@ -271,6 +271,10 @@ func (c *JoinClient) startNodeAndJoin(ticket *joinproto.IssueJoinTicketResponse, return fmt.Errorf("writing kubelet key: %w", err) } + if err := c.fileHandler.Write(constants.SSHCAKeyPath, ticket.AuthorizedCaPublicKey, file.OptMkdirAll); err != nil { + return fmt.Errorf("writing ssh ca key: %w", err) + } + state := nodestate.NodeState{ Role: c.role, MeasurementSalt: ticket.MeasurementSalt, diff --git a/bootstrapper/internal/joinclient/joinclient_test.go b/bootstrapper/internal/joinclient/joinclient_test.go index 6a0b89f4bc..e652c51653 100644 --- a/bootstrapper/internal/joinclient/joinclient_test.go +++ b/bootstrapper/internal/joinclient/joinclient_test.go @@ -50,6 +50,8 @@ func TestClient(t *testing.T) { {Role: role.ControlPlane, Name: "node-4", VPCIP: "192.0.2.2"}, {Role: role.ControlPlane, Name: "node-5", VPCIP: "192.0.2.3"}, } + caDerivationKey := make([]byte, 256) + respCaKey := &joinproto.IssueJoinTicketResponse{AuthorizedCaPublicKey: caDerivationKey} testCases := map[string]struct { role role.Role @@ -69,7 +71,7 @@ func TestClient(t *testing.T) { selfAnswer{err: assert.AnError}, selfAnswer{instance: workerSelf}, listAnswer{instances: peers}, - issueJoinTicketAnswer{}, + issueJoinTicketAnswer{resp: respCaKey}, }, clusterJoiner: &stubClusterJoiner{}, nodeLock: newFakeLock(), @@ -85,7 +87,7 @@ func TestClient(t *testing.T) { selfAnswer{instance: metadata.InstanceMetadata{Name: "node-1"}}, selfAnswer{instance: workerSelf}, listAnswer{instances: peers}, - issueJoinTicketAnswer{}, + issueJoinTicketAnswer{resp: respCaKey}, }, clusterJoiner: &stubClusterJoiner{}, nodeLock: newFakeLock(), @@ -101,7 +103,7 @@ func TestClient(t *testing.T) { listAnswer{err: assert.AnError}, listAnswer{err: assert.AnError}, listAnswer{instances: peers}, - issueJoinTicketAnswer{}, + issueJoinTicketAnswer{resp: respCaKey}, }, clusterJoiner: &stubClusterJoiner{}, nodeLock: newFakeLock(), @@ -117,7 +119,7 @@ func TestClient(t *testing.T) { listAnswer{}, listAnswer{}, listAnswer{instances: peers}, - issueJoinTicketAnswer{}, + issueJoinTicketAnswer{resp: respCaKey}, }, clusterJoiner: &stubClusterJoiner{}, nodeLock: newFakeLock(), @@ -134,7 +136,7 @@ func TestClient(t *testing.T) { listAnswer{instances: peers}, issueJoinTicketAnswer{err: assert.AnError}, listAnswer{instances: peers}, - issueJoinTicketAnswer{}, + issueJoinTicketAnswer{resp: respCaKey}, }, clusterJoiner: &stubClusterJoiner{}, nodeLock: newFakeLock(), @@ -151,7 +153,7 @@ func TestClient(t *testing.T) { listAnswer{instances: peers}, issueJoinTicketAnswer{err: assert.AnError}, listAnswer{instances: peers}, - issueJoinTicketAnswer{}, + issueJoinTicketAnswer{resp: respCaKey}, }, clusterJoiner: &stubClusterJoiner{}, nodeLock: newFakeLock(), @@ -164,7 +166,7 @@ func TestClient(t *testing.T) { apiAnswers: []any{ selfAnswer{instance: controlSelf}, listAnswer{instances: peers}, - issueJoinTicketAnswer{}, + issueJoinTicketAnswer{resp: respCaKey}, }, clusterJoiner: &stubClusterJoiner{numBadCalls: -1, joinClusterErr: assert.AnError}, nodeLock: newFakeLock(), @@ -177,7 +179,7 @@ func TestClient(t *testing.T) { apiAnswers: []any{ selfAnswer{instance: controlSelf}, listAnswer{instances: peers}, - issueJoinTicketAnswer{}, + issueJoinTicketAnswer{resp: respCaKey}, }, clusterJoiner: &stubClusterJoiner{numBadCalls: 1, joinClusterErr: assert.AnError}, nodeLock: newFakeLock(), @@ -191,7 +193,7 @@ func TestClient(t *testing.T) { apiAnswers: []any{ selfAnswer{instance: controlSelf}, listAnswer{instances: peers}, - issueJoinTicketAnswer{}, + issueJoinTicketAnswer{resp: respCaKey}, }, clusterJoiner: &stubClusterJoiner{}, nodeLock: lockedLock, diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 6baaf3f1ff..cd3f24647f 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -61,6 +61,7 @@ func NewRootCmd() *cobra.Command { rootCmd.AddCommand(cmd.NewIAMCmd()) rootCmd.AddCommand(cmd.NewVersionCmd()) rootCmd.AddCommand(cmd.NewInitCmd()) + rootCmd.AddCommand(cmd.NewSSHCmd()) rootCmd.AddCommand(cmd.NewMaaPatchCmd()) return rootCmd diff --git a/cli/internal/cmd/BUILD.bazel b/cli/internal/cmd/BUILD.bazel index 828a63d5b1..bc6a71a501 100644 --- a/cli/internal/cmd/BUILD.bazel +++ b/cli/internal/cmd/BUILD.bazel @@ -37,6 +37,7 @@ go_library( "miniup_linux_amd64.go", "recover.go", "spinner.go", + "ssh.go", "status.go", "terminate.go", "upgrade.go", @@ -116,6 +117,8 @@ go_library( "//internal/attestation/azure/tdx", "@com_github_google_go_sev_guest//proto/sevsnp", "@com_github_google_go_tpm_tools//proto/attest", + "@org_golang_x_crypto//ssh", + "//internal/kms/setup", ] + select({ "@io_bazel_rules_go//go/platform:android_amd64": [ "@org_golang_x_sys//unix", @@ -142,6 +145,7 @@ go_test( "maapatch_test.go", "recover_test.go", "spinner_test.go", + "ssh_test.go", "status_test.go", "terminate_test.go", "upgradeapply_test.go", @@ -201,6 +205,7 @@ go_test( "@org_golang_google_grpc//:grpc", "@org_golang_google_grpc//codes", "@org_golang_google_grpc//status", + "@org_golang_x_crypto//ssh", "@org_golang_x_mod//semver", "@org_uber_go_goleak//:goleak", ], diff --git a/cli/internal/cmd/ssh.go b/cli/internal/cmd/ssh.go new file mode 100644 index 0000000000..d65b85fc09 --- /dev/null +++ b/cli/internal/cmd/ssh.go @@ -0,0 +1,122 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package cmd + +import ( + "crypto/ed25519" + "crypto/rand" + "fmt" + "os" + "time" + + "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/kms/setup" + "github.com/edgelesssys/constellation/v2/internal/kms/uri" + "github.com/spf13/afero" + "github.com/spf13/cobra" + + "golang.org/x/crypto/ssh" +) + +// NewSSHCmd returns a new cobra.Command for the ssh command. +func NewSSHCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "ssh", + Short: "Prepare your cluster for emergency ssh access", + Long: "Prepare your cluster for emergency ssh access and sign a given key pair for authorization.", + Args: cobra.ExactArgs(0), + RunE: runSSH, + } + cmd.Flags().String("key", "", "the path to an existing ssh public key") + must(cmd.MarkFlagRequired("key")) + return cmd +} + +func runSSH(cmd *cobra.Command, _ []string) error { + fh := file.NewHandler(afero.NewOsFs()) + debugLogger, err := newDebugFileLogger(cmd, fh) + if err != nil { + return err + } + + keyPath, err := cmd.Flags().GetString("key") + if err != nil { + return fmt.Errorf("retrieving path to public key from flags: %s", err) + } + + return writeCertificateForKey(cmd, keyPath, fh, debugLogger) +} + +func writeCertificateForKey(cmd *cobra.Command, keyPath string, fh file.Handler, debugLogger debugLog) error { + _, err := fh.Stat(constants.TerraformWorkingDir) + if os.IsNotExist(err) { + return fmt.Errorf("directory %q does not exist", constants.TerraformWorkingDir) + } + if err != nil { + return err + } + + // NOTE(miampf): Since other KMS aren't fully implemented yet, this commands assumes that the cKMS is used and derives the key accordingly. + var mastersecret uri.MasterSecret + if err = fh.ReadJSON(constants.MasterSecretFilename, &mastersecret); err != nil { + return fmt.Errorf("reading master secret: %s", err) + } + + mastersecretURI := uri.MasterSecret{Key: mastersecret.Key, Salt: mastersecret.Salt} + kms, err := setup.KMS(cmd.Context(), uri.NoStoreURI, mastersecretURI.EncodeToURI()) + if err != nil { + return fmt.Errorf("setting up KMS: %s", err) + } + sshCAKeySeed, err := kms.GetDEK(cmd.Context(), crypto.DEKPrefix+constants.SSHCAKeySuffix, ed25519.SeedSize) + if err != nil { + return fmt.Errorf("retrieving key from KMS: %s", err) + } + + ca, err := crypto.GenerateEmergencySSHCAKey(sshCAKeySeed) + if err != nil { + return fmt.Errorf("generating ssh emergency CA key: %s", err) + } + + debugLogger.Debug("SSH CA KEY generated", "public-key", string(ssh.MarshalAuthorizedKey(ca.PublicKey()))) + + keyBuffer, err := fh.Read(keyPath) + if err != nil { + return fmt.Errorf("reading public key %q: %s", keyPath, err) + } + + pub, _, _, _, err := ssh.ParseAuthorizedKey(keyBuffer) + if err != nil { + return fmt.Errorf("parsing public key %q: %s", keyPath, err) + } + + certificate := ssh.Certificate{ + Key: pub, + CertType: ssh.UserCert, + ValidAfter: uint64(time.Now().Unix()), + ValidBefore: uint64(time.Now().Add(24 * time.Hour).Unix()), + ValidPrincipals: []string{"root"}, + Permissions: ssh.Permissions{ + Extensions: map[string]string{ + "permit-port-forwarding": "yes", + "permit-pty": "yes", + }, + }, + } + if err := certificate.SignCert(rand.Reader, ca); err != nil { + return fmt.Errorf("signing certificate: %s", err) + } + + debugLogger.Debug("Signed certificate", "certificate", string(ssh.MarshalAuthorizedKey(&certificate))) + if err := fh.Write(fmt.Sprintf("%s/ca_cert.pub", constants.TerraformWorkingDir), ssh.MarshalAuthorizedKey(&certificate), file.OptOverwrite); err != nil { + return fmt.Errorf("writing certificate: %s", err) + } + cmd.Printf("You can now connect to a node using 'ssh -F %s/ssh_config -i '.\nYou can obtain the private node IP via the web UI of your CSP.\n", constants.TerraformWorkingDir) + + return nil +} diff --git a/cli/internal/cmd/ssh_test.go b/cli/internal/cmd/ssh_test.go new file mode 100644 index 0000000000..7f5fe566df --- /dev/null +++ b/cli/internal/cmd/ssh_test.go @@ -0,0 +1,114 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package cmd + +import ( + "bytes" + "fmt" + "testing" + + "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/edgelesssys/constellation/v2/internal/file" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" +) + +func TestSSH(t *testing.T) { + someSSHPubKey := "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDA1yYg1PIJNjAGjyuv66r8AJtpfBDFLdp3u9lVwkgbVKv1AzcaeTF/NEw+nhNJOjuCZ61LTPj12LZ8Wy/oSm0A= motte@lolcatghost" + someSSHPubKeyPath := "some-key.pub" + someMasterSecret := ` + { + "key": "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAK", + "salt": "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAK" + } + ` + + newFsWithDirectory := func() file.Handler { + require := require.New(t) + fh := file.NewHandler(afero.NewMemMapFs()) + require.NoError(fh.MkdirAll(constants.TerraformWorkingDir)) + return fh + } + newFsNoDirectory := func() file.Handler { + fh := file.NewHandler(afero.NewMemMapFs()) + return fh + } + + testCases := map[string]struct { + fh file.Handler + pubKey string + masterSecret string + wantErr bool + }{ + "everything exists": { + fh: newFsWithDirectory(), + pubKey: someSSHPubKey, + masterSecret: someMasterSecret, + }, + "no public key": { + fh: newFsWithDirectory(), + masterSecret: someMasterSecret, + wantErr: true, + }, + "no master secret": { + fh: newFsWithDirectory(), + pubKey: someSSHPubKey, + wantErr: true, + }, + "malformed public key": { + fh: newFsWithDirectory(), + pubKey: "asdf", + masterSecret: someMasterSecret, + wantErr: true, + }, + "malformed master secret": { + fh: newFsWithDirectory(), + masterSecret: "asdf", + pubKey: someSSHPubKey, + wantErr: true, + }, + "directory does not exist": { + fh: newFsNoDirectory(), + pubKey: someSSHPubKey, + masterSecret: someMasterSecret, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + if tc.pubKey != "" { + require.NoError(tc.fh.Write(someSSHPubKeyPath, []byte(tc.pubKey))) + } + if tc.masterSecret != "" { + require.NoError(tc.fh.Write(constants.MasterSecretFilename, []byte(tc.masterSecret))) + } + + cmd := NewSSHCmd() + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + cmd.SetIn(&bytes.Buffer{}) + + err := writeCertificateForKey(cmd, someSSHPubKeyPath, tc.fh, logger.NewTest(t)) + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + cert, err := tc.fh.Read(fmt.Sprintf("%s/ca_cert.pub", constants.TerraformWorkingDir)) + require.NoError(err) + _, _, _, _, err = ssh.ParseAuthorizedKey(cert) + require.NoError(err) + } + }) + } +} diff --git a/docs/docs/reference/cli.md b/docs/docs/reference/cli.md index 99acef5203..06e823e6eb 100644 --- a/docs/docs/reference/cli.md +++ b/docs/docs/reference/cli.md @@ -39,6 +39,7 @@ Commands: * [apply](#constellation-iam-upgrade-apply): Apply an upgrade to an IAM profile * [version](#constellation-version): Display version of this CLI * [init](#constellation-init): Initialize the Constellation cluster +* [ssh](#constellation-ssh): Prepare your cluster for emergency ssh access ## constellation config @@ -842,3 +843,31 @@ constellation init [flags] -C, --workspace string path to the Constellation workspace ``` +## constellation ssh + +Prepare your cluster for emergency ssh access + +### Synopsis + +Prepare your cluster for emergency ssh access and sign a given key pair for authorization. + +``` +constellation ssh [flags] +``` + +### Options + +``` + -h, --help help for ssh + --key string the path to an existing ssh public key +``` + +### Options inherited from parent commands + +``` + --debug enable debug logging + --force disable version compatibility checks - might result in corrupted clusters + --tf-log string Terraform log level (default "NONE") + -C, --workspace string path to the Constellation workspace +``` + diff --git a/internal/constants/constants.go b/internal/constants/constants.go index c313b74a68..aecef23c8a 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -42,6 +42,10 @@ const ( DefaultWorkerGroupName = "worker_default" // CLIDebugLogFile is the name of the debug log file for constellation init/constellation apply. CLIDebugLogFile = "constellation-debug.log" + // SSHCAKeySuffix is the suffix used together with the DEKPrefix to derive an SSH CA key for emergency ssh access. + SSHCAKeySuffix = "ca_emergency_ssh" + // SSHCAKeyPath is the path to the emergency SSH CA key on the node. + SSHCAKeyPath = "/run/ssh/ssh_ca.pub" // // Ports. diff --git a/internal/crypto/BUILD.bazel b/internal/crypto/BUILD.bazel index 28131c022c..0b3e402d99 100644 --- a/internal/crypto/BUILD.bazel +++ b/internal/crypto/BUILD.bazel @@ -6,7 +6,10 @@ go_library( srcs = ["crypto.go"], importpath = "github.com/edgelesssys/constellation/v2/internal/crypto", visibility = ["//:__subpackages__"], - deps = ["@org_golang_x_crypto//hkdf"], + deps = [ + "@org_golang_x_crypto//hkdf", + "@org_golang_x_crypto//ssh", + ], ) go_test( diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go index 081e25d71b..0a88ec2f5f 100644 --- a/internal/crypto/crypto.go +++ b/internal/crypto/crypto.go @@ -9,6 +9,7 @@ package crypto import ( "bytes" + "crypto/ed25519" "crypto/rand" "crypto/sha256" "crypto/x509" @@ -18,6 +19,7 @@ import ( "math/big" "golang.org/x/crypto/hkdf" + "golang.org/x/crypto/ssh" ) const ( @@ -62,6 +64,19 @@ func GenerateRandomBytes(length int) ([]byte, error) { return nonce, nil } +// GenerateEmergencySSHCAKey creates a CA that is used to sign keys for emergency ssh access. +func GenerateEmergencySSHCAKey(seed []byte) (ssh.Signer, error) { + _, priv, err := ed25519.GenerateKey(bytes.NewReader(seed)) + if err != nil { + return nil, err + } + ca, err := ssh.NewSignerFromSigner(priv) + if err != nil { + return nil, err + } + return ca, nil +} + // PemToX509Cert takes a list of PEM-encoded certificates, parses the first one and returns it // as an x.509 certificate. func PemToX509Cert(raw []byte) (*x509.Certificate, error) { diff --git a/internal/crypto/crypto_test.go b/internal/crypto/crypto_test.go index 674ec4c84a..12c3bdc9cf 100644 --- a/internal/crypto/crypto_test.go +++ b/internal/crypto/crypto_test.go @@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only package crypto import ( + "crypto/ed25519" "crypto/x509" "testing" @@ -121,6 +122,47 @@ func TestGenerateRandomBytes(t *testing.T) { assert.Len(n3, 16) } +func TestGenerateEmergencySSHCAKey(t *testing.T) { + nullKey := make([]byte, ed25519.SeedSize) + + testCases := map[string]struct { + key []byte + wantErr bool + }{ + "key length = 0": { + key: make([]byte, 0), + wantErr: true, + }, + "valid key": { + key: nullKey, + }, + "nil input": { + key: nil, + wantErr: true, + }, + "long key": { + key: make([]byte, 256), + }, + "key too short": { + key: make([]byte, ed25519.SeedSize-1), + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + _, err := GenerateEmergencySSHCAKey(tc.key) + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + } + }) + } +} + func TestPemToX509Cert(t *testing.T) { testCases := map[string]struct { pemCert []byte diff --git a/joinservice/internal/server/BUILD.bazel b/joinservice/internal/server/BUILD.bazel index 7e29a733c9..eed06e663e 100644 --- a/joinservice/internal/server/BUILD.bazel +++ b/joinservice/internal/server/BUILD.bazel @@ -19,6 +19,7 @@ go_library( "@org_golang_google_grpc//codes", "@org_golang_google_grpc//credentials", "@org_golang_google_grpc//status", + "@org_golang_x_crypto//ssh", ], ) @@ -28,6 +29,7 @@ go_test( embed = [":server"], deps = [ "//internal/attestation", + "//internal/constants", "//internal/logger", "//internal/versions/components", "//joinservice/joinproto", diff --git a/joinservice/internal/server/server.go b/joinservice/internal/server/server.go index 21bb24d67c..e6fc82b95a 100644 --- a/joinservice/internal/server/server.go +++ b/joinservice/internal/server/server.go @@ -9,6 +9,7 @@ package server import ( "context" + "crypto/ed25519" "fmt" "log/slog" "net" @@ -21,6 +22,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/versions/components" "github.com/edgelesssys/constellation/v2/joinservice/joinproto" + "golang.org/x/crypto/ssh" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" @@ -100,6 +102,18 @@ func (s *Server) IssueJoinTicket(ctx context.Context, req *joinproto.IssueJoinTi return nil, status.Errorf(codes.Internal, "getting key for stateful disk: %s", err) } + log.Info("Requesting emergency SSH CA derivation key") + sshCAKeySeed, err := s.dataKeyGetter.GetDataKey(ctx, constants.SSHCAKeySuffix, ed25519.SeedSize) + if err != nil { + log.With(slog.Any("error", err)).Error("Failed to get seed material to derive SSH CA key") + return nil, status.Errorf(codes.Internal, "getting emergency SSH CA seed material: %s", err) + } + ca, err := crypto.GenerateEmergencySSHCAKey(sshCAKeySeed) + if err != nil { + log.With(slog.Any("error", err)).Error("Failed to derive ssh CA key from seed material") + return nil, status.Errorf(codes.Internal, "generating ssh emergency CA key: %s", err) + } + log.Info("Creating Kubernetes join token") kubeArgs, err := s.joinTokenGetter.GetJoinToken(constants.KubernetesJoinTokenTTL) if err != nil { @@ -167,6 +181,7 @@ func (s *Server) IssueJoinTicket(ctx context.Context, req *joinproto.IssueJoinTi KubeletCert: kubeletCert, ControlPlaneFiles: controlPlaneFiles, KubernetesComponents: components, + AuthorizedCaPublicKey: ssh.MarshalAuthorizedKey(ca.PublicKey()), }, nil } diff --git a/joinservice/internal/server/server_test.go b/joinservice/internal/server/server_test.go index ff11c7b57c..2834a46402 100644 --- a/joinservice/internal/server/server_test.go +++ b/joinservice/internal/server/server_test.go @@ -8,11 +8,13 @@ package server import ( "context" + "crypto/ed25519" "errors" "testing" "time" "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/versions/components" "github.com/edgelesssys/constellation/v2/joinservice/joinproto" @@ -29,6 +31,7 @@ func TestMain(m *testing.M) { func TestIssueJoinTicket(t *testing.T) { someErr := errors.New("error") testKey := []byte{0x1, 0x2, 0x3} + testCaKey := make([]byte, ed25519.SeedSize) testCert := []byte{0x4, 0x5, 0x6} measurementSecret := []byte{0x7, 0x8, 0x9} uuid := "uuid" @@ -62,6 +65,7 @@ func TestIssueJoinTicket(t *testing.T) { 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"}, @@ -71,6 +75,7 @@ func TestIssueJoinTicket(t *testing.T) { kms: stubKeyGetter{dataKeys: map[string][]byte{ uuid: testKey, attestation.MeasurementSecretContext: measurementSecret, + constants.SSHCAKeySuffix: testCaKey, }}, ca: stubCA{cert: testCert, nodeName: "node"}, kubeClient: stubKubeClient{getComponentsErr: someErr}, @@ -81,6 +86,7 @@ func TestIssueJoinTicket(t *testing.T) { kms: stubKeyGetter{dataKeys: map[string][]byte{ uuid: testKey, attestation.MeasurementSecretContext: measurementSecret, + constants.SSHCAKeySuffix: testCaKey, }}, ca: stubCA{cert: testCert, nodeName: "node", getNameErr: someErr}, kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"}, @@ -91,6 +97,7 @@ func TestIssueJoinTicket(t *testing.T) { kms: stubKeyGetter{dataKeys: map[string][]byte{ uuid: testKey, attestation.MeasurementSecretContext: measurementSecret, + constants.SSHCAKeySuffix: testCaKey, }}, ca: stubCA{cert: testCert, nodeName: "node"}, kubeClient: stubKubeClient{getComponentsVal: clusterComponents, addNodeToJoiningNodesErr: someErr, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"}, @@ -108,6 +115,7 @@ func TestIssueJoinTicket(t *testing.T) { 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"}, @@ -118,6 +126,7 @@ func TestIssueJoinTicket(t *testing.T) { kms: stubKeyGetter{dataKeys: map[string][]byte{ uuid: testKey, attestation.MeasurementSecretContext: measurementSecret, + constants.SSHCAKeySuffix: testCaKey, }}, ca: stubCA{getCertErr: someErr, nodeName: "node"}, kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"}, @@ -132,6 +141,7 @@ func TestIssueJoinTicket(t *testing.T) { 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"}, @@ -139,6 +149,28 @@ func TestIssueJoinTicket(t *testing.T) { "GetControlPlaneCertificateKey fails": { isControlPlane: true, kubeadm: stubTokenGetter{token: testJoinToken, certificateKeyErr: someErr}, + 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"}, + wantErr: true, + }, + "CA data key to short": { + kubeadm: stubTokenGetter{token: testJoinToken}, + kms: stubKeyGetter{dataKeys: map[string][]byte{ + uuid: testKey, + attestation.MeasurementSecretContext: measurementSecret, + constants.SSHCAKeySuffix: testKey, + }}, + ca: stubCA{cert: testCert, nodeName: "node"}, + kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"}, + wantErr: true, + }, + "CA data key doesn't exist": { + kubeadm: stubTokenGetter{token: testJoinToken}, kms: stubKeyGetter{dataKeys: map[string][]byte{ uuid: testKey, attestation.MeasurementSecretContext: measurementSecret, diff --git a/joinservice/joinproto/join.pb.go b/joinservice/joinproto/join.pb.go index bf8245d220..752640d683 100644 --- a/joinservice/joinproto/join.pb.go +++ b/joinservice/joinproto/join.pb.go @@ -97,6 +97,7 @@ type IssueJoinTicketResponse struct { ControlPlaneFiles []*ControlPlaneCertOrKey `protobuf:"bytes,8,rep,name=control_plane_files,json=controlPlaneFiles,proto3" json:"control_plane_files,omitempty"` KubernetesVersion string `protobuf:"bytes,9,opt,name=kubernetes_version,json=kubernetesVersion,proto3" json:"kubernetes_version,omitempty"` KubernetesComponents []*components.Component `protobuf:"bytes,10,rep,name=kubernetes_components,json=kubernetesComponents,proto3" json:"kubernetes_components,omitempty"` + AuthorizedCaPublicKey []byte `protobuf:"bytes,11,opt,name=authorized_ca_public_key,json=authorizedCaPublicKey,proto3" json:"authorized_ca_public_key,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -201,6 +202,13 @@ func (x *IssueJoinTicketResponse) GetKubernetesComponents() []*components.Compon return nil } +func (x *IssueJoinTicketResponse) GetAuthorizedCaPublicKey() []byte { + if x != nil { + return x.AuthorizedCaPublicKey + } + return nil +} + type ControlPlaneCertOrKey struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -366,7 +374,7 @@ var file_joinservice_joinproto_join_proto_rawDesc = []byte{ 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x69, 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69, 0x73, 0x43, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x50, 0x6c, 0x61, 0x6e, 0x65, 0x22, 0x8e, 0x04, 0x0a, 0x17, 0x49, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x50, 0x6c, 0x61, 0x6e, 0x65, 0x22, 0xc7, 0x04, 0x0a, 0x17, 0x49, 0x73, 0x73, 0x75, 0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, @@ -399,38 +407,41 @@ var file_joinservice_joinproto_join_proto_rawDesc = []byte{ 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x52, 0x14, 0x6b, 0x75, 0x62, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x65, - 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x43, 0x0a, 0x19, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x63, 0x65, 0x72, - 0x74, 0x5f, 0x6f, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, - 0x22, 0x37, 0x0a, 0x18, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x6a, 0x6f, 0x69, 0x6e, 0x54, - 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, - 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x64, 0x69, 0x73, 0x6b, 0x55, 0x75, 0x69, 0x64, 0x22, 0x70, 0x0a, 0x19, 0x49, 0x73, 0x73, + 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x37, 0x0a, 0x18, 0x61, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x63, 0x61, 0x5f, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x61, + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x43, 0x61, 0x50, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x4b, 0x65, 0x79, 0x22, 0x43, 0x0a, 0x19, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, + 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x5f, 0x6b, 0x65, + 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x37, 0x0a, 0x18, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x6a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, - 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x44, 0x69, 0x73, 0x6b, 0x4b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x12, - 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x32, 0xab, 0x01, 0x0a, 0x03, - 0x41, 0x50, 0x49, 0x12, 0x4e, 0x0a, 0x0f, 0x49, 0x73, 0x73, 0x75, 0x65, 0x4a, 0x6f, 0x69, 0x6e, - 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1c, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x2e, 0x49, 0x73, - 0x73, 0x75, 0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x2e, 0x49, 0x73, 0x73, 0x75, - 0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x11, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x6a, 0x6f, - 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1e, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x2e, - 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x6a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x2e, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x75, 0x75, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x69, 0x73, 0x6b, 0x55, 0x75, + 0x69, 0x64, 0x22, 0x70, 0x0a, 0x19, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x6a, 0x6f, 0x69, + 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x24, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x65, 0x44, 0x69, + 0x73, 0x6b, 0x4b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x12, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x11, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x32, 0xab, 0x01, 0x0a, 0x03, 0x41, 0x50, 0x49, 0x12, 0x4e, 0x0a, 0x0f, + 0x49, 0x73, 0x73, 0x75, 0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x12, + 0x1c, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x4a, 0x6f, 0x69, 0x6e, + 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, + 0x6a, 0x6f, 0x69, 0x6e, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x54, 0x69, + 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x11, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x6a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x64, 0x67, 0x65, 0x6c, 0x65, 0x73, 0x73, - 0x73, 0x79, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x65, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x2f, 0x76, 0x32, 0x2f, 0x6a, 0x6f, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x2f, 0x6a, 0x6f, 0x69, 0x6e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x74, 0x12, 0x1e, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, + 0x6a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1f, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, + 0x6a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x65, 0x64, 0x67, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x73, 0x79, 0x73, 0x2f, 0x63, 0x6f, 0x6e, + 0x73, 0x74, 0x65, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x32, 0x2f, 0x6a, 0x6f, + 0x69, 0x6e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x6a, 0x6f, 0x69, 0x6e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/joinservice/joinproto/join.proto b/joinservice/joinproto/join.proto index 2a910a0391..89c40b8a0b 100644 --- a/joinservice/joinproto/join.proto +++ b/joinservice/joinproto/join.proto @@ -45,6 +45,8 @@ message IssueJoinTicketResponse { string kubernetes_version = 9; // kubernetes_components is a list of components to install on the node. repeated components.Component kubernetes_components = 10; + // authorized_ca_public_key is an ssh ca key that can be used to connect to a node in case of an emergency. + bytes authorized_ca_public_key = 11; } message control_plane_cert_or_key {