cli: add ssh command to securely connect with nodes over ssh (#3568)

This commit is contained in:
miampf 2025-01-30 12:08:59 +00:00 committed by GitHub
parent e6048e093b
commit 706d1dff15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 471 additions and 41 deletions

View File

@ -11,6 +11,7 @@ go_library(
"//bootstrapper/internal/journald", "//bootstrapper/internal/journald",
"//internal/atls", "//internal/atls",
"//internal/attestation", "//internal/attestation",
"//internal/constants",
"//internal/crypto", "//internal/crypto",
"//internal/file", "//internal/file",
"//internal/grpc/atlscredentials", "//internal/grpc/atlscredentials",
@ -26,6 +27,7 @@ go_library(
"@org_golang_google_grpc//keepalive", "@org_golang_google_grpc//keepalive",
"@org_golang_google_grpc//status", "@org_golang_google_grpc//status",
"@org_golang_x_crypto//bcrypt", "@org_golang_x_crypto//bcrypt",
"@org_golang_x_crypto//ssh",
], ],
) )

View File

@ -20,6 +20,7 @@ package initserver
import ( import (
"bufio" "bufio"
"context" "context"
"crypto/ed25519"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -33,6 +34,7 @@ import (
"github.com/edgelesssys/constellation/v2/bootstrapper/internal/journald" "github.com/edgelesssys/constellation/v2/bootstrapper/internal/journald"
"github.com/edgelesssys/constellation/v2/internal/atls" "github.com/edgelesssys/constellation/v2/internal/atls"
"github.com/edgelesssys/constellation/v2/internal/attestation" "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/crypto"
"github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/grpc/atlscredentials" "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/role"
"github.com/edgelesssys/constellation/v2/internal/versions/components" "github.com/edgelesssys/constellation/v2/internal/versions/components"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/ssh"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/keepalive" "google.golang.org/grpc/keepalive"
@ -222,6 +225,28 @@ func (s *Server) Init(req *initproto.InitRequest, stream initproto.API_InitServe
return err 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 clusterName := req.ClusterName
if clusterName == "" { if clusterName == "" {
clusterName = "constellation" clusterName = "constellation"

View File

@ -271,6 +271,10 @@ func (c *JoinClient) startNodeAndJoin(ticket *joinproto.IssueJoinTicketResponse,
return fmt.Errorf("writing kubelet key: %w", err) 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{ state := nodestate.NodeState{
Role: c.role, Role: c.role,
MeasurementSalt: ticket.MeasurementSalt, MeasurementSalt: ticket.MeasurementSalt,

View File

@ -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-4", VPCIP: "192.0.2.2"},
{Role: role.ControlPlane, Name: "node-5", VPCIP: "192.0.2.3"}, {Role: role.ControlPlane, Name: "node-5", VPCIP: "192.0.2.3"},
} }
caDerivationKey := make([]byte, 256)
respCaKey := &joinproto.IssueJoinTicketResponse{AuthorizedCaPublicKey: caDerivationKey}
testCases := map[string]struct { testCases := map[string]struct {
role role.Role role role.Role
@ -69,7 +71,7 @@ func TestClient(t *testing.T) {
selfAnswer{err: assert.AnError}, selfAnswer{err: assert.AnError},
selfAnswer{instance: workerSelf}, selfAnswer{instance: workerSelf},
listAnswer{instances: peers}, listAnswer{instances: peers},
issueJoinTicketAnswer{}, issueJoinTicketAnswer{resp: respCaKey},
}, },
clusterJoiner: &stubClusterJoiner{}, clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(), nodeLock: newFakeLock(),
@ -85,7 +87,7 @@ func TestClient(t *testing.T) {
selfAnswer{instance: metadata.InstanceMetadata{Name: "node-1"}}, selfAnswer{instance: metadata.InstanceMetadata{Name: "node-1"}},
selfAnswer{instance: workerSelf}, selfAnswer{instance: workerSelf},
listAnswer{instances: peers}, listAnswer{instances: peers},
issueJoinTicketAnswer{}, issueJoinTicketAnswer{resp: respCaKey},
}, },
clusterJoiner: &stubClusterJoiner{}, clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(), nodeLock: newFakeLock(),
@ -101,7 +103,7 @@ func TestClient(t *testing.T) {
listAnswer{err: assert.AnError}, listAnswer{err: assert.AnError},
listAnswer{err: assert.AnError}, listAnswer{err: assert.AnError},
listAnswer{instances: peers}, listAnswer{instances: peers},
issueJoinTicketAnswer{}, issueJoinTicketAnswer{resp: respCaKey},
}, },
clusterJoiner: &stubClusterJoiner{}, clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(), nodeLock: newFakeLock(),
@ -117,7 +119,7 @@ func TestClient(t *testing.T) {
listAnswer{}, listAnswer{},
listAnswer{}, listAnswer{},
listAnswer{instances: peers}, listAnswer{instances: peers},
issueJoinTicketAnswer{}, issueJoinTicketAnswer{resp: respCaKey},
}, },
clusterJoiner: &stubClusterJoiner{}, clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(), nodeLock: newFakeLock(),
@ -134,7 +136,7 @@ func TestClient(t *testing.T) {
listAnswer{instances: peers}, listAnswer{instances: peers},
issueJoinTicketAnswer{err: assert.AnError}, issueJoinTicketAnswer{err: assert.AnError},
listAnswer{instances: peers}, listAnswer{instances: peers},
issueJoinTicketAnswer{}, issueJoinTicketAnswer{resp: respCaKey},
}, },
clusterJoiner: &stubClusterJoiner{}, clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(), nodeLock: newFakeLock(),
@ -151,7 +153,7 @@ func TestClient(t *testing.T) {
listAnswer{instances: peers}, listAnswer{instances: peers},
issueJoinTicketAnswer{err: assert.AnError}, issueJoinTicketAnswer{err: assert.AnError},
listAnswer{instances: peers}, listAnswer{instances: peers},
issueJoinTicketAnswer{}, issueJoinTicketAnswer{resp: respCaKey},
}, },
clusterJoiner: &stubClusterJoiner{}, clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(), nodeLock: newFakeLock(),
@ -164,7 +166,7 @@ func TestClient(t *testing.T) {
apiAnswers: []any{ apiAnswers: []any{
selfAnswer{instance: controlSelf}, selfAnswer{instance: controlSelf},
listAnswer{instances: peers}, listAnswer{instances: peers},
issueJoinTicketAnswer{}, issueJoinTicketAnswer{resp: respCaKey},
}, },
clusterJoiner: &stubClusterJoiner{numBadCalls: -1, joinClusterErr: assert.AnError}, clusterJoiner: &stubClusterJoiner{numBadCalls: -1, joinClusterErr: assert.AnError},
nodeLock: newFakeLock(), nodeLock: newFakeLock(),
@ -177,7 +179,7 @@ func TestClient(t *testing.T) {
apiAnswers: []any{ apiAnswers: []any{
selfAnswer{instance: controlSelf}, selfAnswer{instance: controlSelf},
listAnswer{instances: peers}, listAnswer{instances: peers},
issueJoinTicketAnswer{}, issueJoinTicketAnswer{resp: respCaKey},
}, },
clusterJoiner: &stubClusterJoiner{numBadCalls: 1, joinClusterErr: assert.AnError}, clusterJoiner: &stubClusterJoiner{numBadCalls: 1, joinClusterErr: assert.AnError},
nodeLock: newFakeLock(), nodeLock: newFakeLock(),
@ -191,7 +193,7 @@ func TestClient(t *testing.T) {
apiAnswers: []any{ apiAnswers: []any{
selfAnswer{instance: controlSelf}, selfAnswer{instance: controlSelf},
listAnswer{instances: peers}, listAnswer{instances: peers},
issueJoinTicketAnswer{}, issueJoinTicketAnswer{resp: respCaKey},
}, },
clusterJoiner: &stubClusterJoiner{}, clusterJoiner: &stubClusterJoiner{},
nodeLock: lockedLock, nodeLock: lockedLock,

View File

@ -61,6 +61,7 @@ func NewRootCmd() *cobra.Command {
rootCmd.AddCommand(cmd.NewIAMCmd()) rootCmd.AddCommand(cmd.NewIAMCmd())
rootCmd.AddCommand(cmd.NewVersionCmd()) rootCmd.AddCommand(cmd.NewVersionCmd())
rootCmd.AddCommand(cmd.NewInitCmd()) rootCmd.AddCommand(cmd.NewInitCmd())
rootCmd.AddCommand(cmd.NewSSHCmd())
rootCmd.AddCommand(cmd.NewMaaPatchCmd()) rootCmd.AddCommand(cmd.NewMaaPatchCmd())
return rootCmd return rootCmd

View File

@ -37,6 +37,7 @@ go_library(
"miniup_linux_amd64.go", "miniup_linux_amd64.go",
"recover.go", "recover.go",
"spinner.go", "spinner.go",
"ssh.go",
"status.go", "status.go",
"terminate.go", "terminate.go",
"upgrade.go", "upgrade.go",
@ -116,6 +117,8 @@ go_library(
"//internal/attestation/azure/tdx", "//internal/attestation/azure/tdx",
"@com_github_google_go_sev_guest//proto/sevsnp", "@com_github_google_go_sev_guest//proto/sevsnp",
"@com_github_google_go_tpm_tools//proto/attest", "@com_github_google_go_tpm_tools//proto/attest",
"@org_golang_x_crypto//ssh",
"//internal/kms/setup",
] + select({ ] + select({
"@io_bazel_rules_go//go/platform:android_amd64": [ "@io_bazel_rules_go//go/platform:android_amd64": [
"@org_golang_x_sys//unix", "@org_golang_x_sys//unix",
@ -142,6 +145,7 @@ go_test(
"maapatch_test.go", "maapatch_test.go",
"recover_test.go", "recover_test.go",
"spinner_test.go", "spinner_test.go",
"ssh_test.go",
"status_test.go", "status_test.go",
"terminate_test.go", "terminate_test.go",
"upgradeapply_test.go", "upgradeapply_test.go",
@ -201,6 +205,7 @@ go_test(
"@org_golang_google_grpc//:grpc", "@org_golang_google_grpc//:grpc",
"@org_golang_google_grpc//codes", "@org_golang_google_grpc//codes",
"@org_golang_google_grpc//status", "@org_golang_google_grpc//status",
"@org_golang_x_crypto//ssh",
"@org_golang_x_mod//semver", "@org_golang_x_mod//semver",
"@org_uber_go_goleak//:goleak", "@org_uber_go_goleak//:goleak",
], ],

122
cli/internal/cmd/ssh.go Normal file
View File

@ -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 <your private key> <node ip>'.\nYou can obtain the private node IP via the web UI of your CSP.\n", constants.TerraformWorkingDir)
return nil
}

View File

@ -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)
}
})
}
}

View File

@ -39,6 +39,7 @@ Commands:
* [apply](#constellation-iam-upgrade-apply): Apply an upgrade to an IAM profile * [apply](#constellation-iam-upgrade-apply): Apply an upgrade to an IAM profile
* [version](#constellation-version): Display version of this CLI * [version](#constellation-version): Display version of this CLI
* [init](#constellation-init): Initialize the Constellation cluster * [init](#constellation-init): Initialize the Constellation cluster
* [ssh](#constellation-ssh): Prepare your cluster for emergency ssh access
## constellation config ## constellation config
@ -842,3 +843,31 @@ constellation init [flags]
-C, --workspace string path to the Constellation workspace -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
```

View File

@ -42,6 +42,10 @@ const (
DefaultWorkerGroupName = "worker_default" DefaultWorkerGroupName = "worker_default"
// CLIDebugLogFile is the name of the debug log file for constellation init/constellation apply. // CLIDebugLogFile is the name of the debug log file for constellation init/constellation apply.
CLIDebugLogFile = "constellation-debug.log" 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. // Ports.

View File

@ -6,7 +6,10 @@ go_library(
srcs = ["crypto.go"], srcs = ["crypto.go"],
importpath = "github.com/edgelesssys/constellation/v2/internal/crypto", importpath = "github.com/edgelesssys/constellation/v2/internal/crypto",
visibility = ["//:__subpackages__"], visibility = ["//:__subpackages__"],
deps = ["@org_golang_x_crypto//hkdf"], deps = [
"@org_golang_x_crypto//hkdf",
"@org_golang_x_crypto//ssh",
],
) )
go_test( go_test(

View File

@ -9,6 +9,7 @@ package crypto
import ( import (
"bytes" "bytes"
"crypto/ed25519"
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
"crypto/x509" "crypto/x509"
@ -18,6 +19,7 @@ import (
"math/big" "math/big"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
"golang.org/x/crypto/ssh"
) )
const ( const (
@ -62,6 +64,19 @@ func GenerateRandomBytes(length int) ([]byte, error) {
return nonce, nil 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 // PemToX509Cert takes a list of PEM-encoded certificates, parses the first one and returns it
// as an x.509 certificate. // as an x.509 certificate.
func PemToX509Cert(raw []byte) (*x509.Certificate, error) { func PemToX509Cert(raw []byte) (*x509.Certificate, error) {

View File

@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
package crypto package crypto
import ( import (
"crypto/ed25519"
"crypto/x509" "crypto/x509"
"testing" "testing"
@ -121,6 +122,47 @@ func TestGenerateRandomBytes(t *testing.T) {
assert.Len(n3, 16) 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) { func TestPemToX509Cert(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
pemCert []byte pemCert []byte

View File

@ -19,6 +19,7 @@ go_library(
"@org_golang_google_grpc//codes", "@org_golang_google_grpc//codes",
"@org_golang_google_grpc//credentials", "@org_golang_google_grpc//credentials",
"@org_golang_google_grpc//status", "@org_golang_google_grpc//status",
"@org_golang_x_crypto//ssh",
], ],
) )
@ -28,6 +29,7 @@ go_test(
embed = [":server"], embed = [":server"],
deps = [ deps = [
"//internal/attestation", "//internal/attestation",
"//internal/constants",
"//internal/logger", "//internal/logger",
"//internal/versions/components", "//internal/versions/components",
"//joinservice/joinproto", "//joinservice/joinproto",

View File

@ -9,6 +9,7 @@ package server
import ( import (
"context" "context"
"crypto/ed25519"
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
@ -21,6 +22,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/versions/components" "github.com/edgelesssys/constellation/v2/internal/versions/components"
"github.com/edgelesssys/constellation/v2/joinservice/joinproto" "github.com/edgelesssys/constellation/v2/joinservice/joinproto"
"golang.org/x/crypto/ssh"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials" "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) 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") log.Info("Creating Kubernetes join token")
kubeArgs, err := s.joinTokenGetter.GetJoinToken(constants.KubernetesJoinTokenTTL) kubeArgs, err := s.joinTokenGetter.GetJoinToken(constants.KubernetesJoinTokenTTL)
if err != nil { if err != nil {
@ -167,6 +181,7 @@ func (s *Server) IssueJoinTicket(ctx context.Context, req *joinproto.IssueJoinTi
KubeletCert: kubeletCert, KubeletCert: kubeletCert,
ControlPlaneFiles: controlPlaneFiles, ControlPlaneFiles: controlPlaneFiles,
KubernetesComponents: components, KubernetesComponents: components,
AuthorizedCaPublicKey: ssh.MarshalAuthorizedKey(ca.PublicKey()),
}, nil }, nil
} }

View File

@ -8,11 +8,13 @@ package server
import ( import (
"context" "context"
"crypto/ed25519"
"errors" "errors"
"testing" "testing"
"time" "time"
"github.com/edgelesssys/constellation/v2/internal/attestation" "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/logger"
"github.com/edgelesssys/constellation/v2/internal/versions/components" "github.com/edgelesssys/constellation/v2/internal/versions/components"
"github.com/edgelesssys/constellation/v2/joinservice/joinproto" "github.com/edgelesssys/constellation/v2/joinservice/joinproto"
@ -29,6 +31,7 @@ func TestMain(m *testing.M) {
func TestIssueJoinTicket(t *testing.T) { func TestIssueJoinTicket(t *testing.T) {
someErr := errors.New("error") someErr := errors.New("error")
testKey := []byte{0x1, 0x2, 0x3} testKey := []byte{0x1, 0x2, 0x3}
testCaKey := make([]byte, ed25519.SeedSize)
testCert := []byte{0x4, 0x5, 0x6} testCert := []byte{0x4, 0x5, 0x6}
measurementSecret := []byte{0x7, 0x8, 0x9} measurementSecret := []byte{0x7, 0x8, 0x9}
uuid := "uuid" uuid := "uuid"
@ -62,6 +65,7 @@ func TestIssueJoinTicket(t *testing.T) {
kms: stubKeyGetter{dataKeys: map[string][]byte{ kms: stubKeyGetter{dataKeys: map[string][]byte{
uuid: testKey, uuid: testKey,
attestation.MeasurementSecretContext: measurementSecret, attestation.MeasurementSecretContext: measurementSecret,
constants.SSHCAKeySuffix: testCaKey,
}}, }},
ca: stubCA{cert: testCert, nodeName: "node"}, ca: stubCA{cert: testCert, nodeName: "node"},
kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"}, kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"},
@ -71,6 +75,7 @@ func TestIssueJoinTicket(t *testing.T) {
kms: stubKeyGetter{dataKeys: map[string][]byte{ kms: stubKeyGetter{dataKeys: map[string][]byte{
uuid: testKey, uuid: testKey,
attestation.MeasurementSecretContext: measurementSecret, attestation.MeasurementSecretContext: measurementSecret,
constants.SSHCAKeySuffix: testCaKey,
}}, }},
ca: stubCA{cert: testCert, nodeName: "node"}, ca: stubCA{cert: testCert, nodeName: "node"},
kubeClient: stubKubeClient{getComponentsErr: someErr}, kubeClient: stubKubeClient{getComponentsErr: someErr},
@ -81,6 +86,7 @@ func TestIssueJoinTicket(t *testing.T) {
kms: stubKeyGetter{dataKeys: map[string][]byte{ kms: stubKeyGetter{dataKeys: map[string][]byte{
uuid: testKey, uuid: testKey,
attestation.MeasurementSecretContext: measurementSecret, attestation.MeasurementSecretContext: measurementSecret,
constants.SSHCAKeySuffix: testCaKey,
}}, }},
ca: stubCA{cert: testCert, nodeName: "node", getNameErr: someErr}, ca: stubCA{cert: testCert, nodeName: "node", getNameErr: someErr},
kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"}, kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"},
@ -91,6 +97,7 @@ func TestIssueJoinTicket(t *testing.T) {
kms: stubKeyGetter{dataKeys: map[string][]byte{ kms: stubKeyGetter{dataKeys: map[string][]byte{
uuid: testKey, uuid: testKey,
attestation.MeasurementSecretContext: measurementSecret, attestation.MeasurementSecretContext: measurementSecret,
constants.SSHCAKeySuffix: testCaKey,
}}, }},
ca: stubCA{cert: testCert, nodeName: "node"}, ca: stubCA{cert: testCert, nodeName: "node"},
kubeClient: stubKubeClient{getComponentsVal: clusterComponents, addNodeToJoiningNodesErr: someErr, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"}, 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{ kms: stubKeyGetter{dataKeys: map[string][]byte{
uuid: testKey, uuid: testKey,
attestation.MeasurementSecretContext: measurementSecret, attestation.MeasurementSecretContext: measurementSecret,
constants.SSHCAKeySuffix: testCaKey,
}}, }},
ca: stubCA{cert: testCert, nodeName: "node"}, ca: stubCA{cert: testCert, nodeName: "node"},
kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"}, kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"},
@ -118,6 +126,7 @@ func TestIssueJoinTicket(t *testing.T) {
kms: stubKeyGetter{dataKeys: map[string][]byte{ kms: stubKeyGetter{dataKeys: map[string][]byte{
uuid: testKey, uuid: testKey,
attestation.MeasurementSecretContext: measurementSecret, attestation.MeasurementSecretContext: measurementSecret,
constants.SSHCAKeySuffix: testCaKey,
}}, }},
ca: stubCA{getCertErr: someErr, nodeName: "node"}, ca: stubCA{getCertErr: someErr, nodeName: "node"},
kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"}, kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"},
@ -132,6 +141,7 @@ func TestIssueJoinTicket(t *testing.T) {
kms: stubKeyGetter{dataKeys: map[string][]byte{ kms: stubKeyGetter{dataKeys: map[string][]byte{
uuid: testKey, uuid: testKey,
attestation.MeasurementSecretContext: measurementSecret, attestation.MeasurementSecretContext: measurementSecret,
constants.SSHCAKeySuffix: testCaKey,
}}, }},
ca: stubCA{cert: testCert, nodeName: "node"}, ca: stubCA{cert: testCert, nodeName: "node"},
kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"}, kubeClient: stubKubeClient{getComponentsVal: clusterComponents, getK8sComponentsRefFromNodeVersionCRDVal: "k8s-components-ref"},
@ -139,6 +149,28 @@ func TestIssueJoinTicket(t *testing.T) {
"GetControlPlaneCertificateKey fails": { "GetControlPlaneCertificateKey fails": {
isControlPlane: true, isControlPlane: true,
kubeadm: stubTokenGetter{token: testJoinToken, certificateKeyErr: someErr}, 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{ kms: stubKeyGetter{dataKeys: map[string][]byte{
uuid: testKey, uuid: testKey,
attestation.MeasurementSecretContext: measurementSecret, attestation.MeasurementSecretContext: measurementSecret,

View File

@ -98,6 +98,7 @@ type IssueJoinTicketResponse struct {
ControlPlaneFiles []*ControlPlaneCertOrKey `protobuf:"bytes,8,rep,name=control_plane_files,json=controlPlaneFiles,proto3" json:"control_plane_files,omitempty"` 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"` 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"` 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -202,6 +203,13 @@ func (x *IssueJoinTicketResponse) GetKubernetesComponents() []*components.Compon
return nil return nil
} }
func (x *IssueJoinTicketResponse) GetAuthorizedCaPublicKey() []byte {
if x != nil {
return x.AuthorizedCaPublicKey
}
return nil
}
type ControlPlaneCertOrKey struct { type ControlPlaneCertOrKey struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
@ -367,7 +375,7 @@ var file_joinservice_joinproto_join_proto_rawDesc = string([]byte{
0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 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, 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, 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, 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, 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, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c,
@ -400,38 +408,41 @@ var file_joinservice_joinproto_join_proto_rawDesc = string([]byte{
0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 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, 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, 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, 0x73, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x37, 0x0a, 0x18, 0x61,
0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x63, 0x65, 0x72, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x63, 0x61, 0x5f, 0x70, 0x75, 0x62,
0x74, 0x5f, 0x6f, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x61,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x43, 0x61, 0x50, 0x75, 0x62, 0x6c, 0x69,
0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x63, 0x4b, 0x65, 0x79, 0x22, 0x43, 0x0a, 0x19, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f,
0x22, 0x37, 0x0a, 0x18, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x6a, 0x6f, 0x69, 0x6e, 0x54, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x6f, 0x72, 0x5f, 0x6b, 0x65,
0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x64, 0x69, 0x73, 0x6b, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20,
0x08, 0x64, 0x69, 0x73, 0x6b, 0x55, 0x75, 0x69, 0x64, 0x22, 0x70, 0x0a, 0x19, 0x49, 0x73, 0x73, 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, 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, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x75, 0x75,
0x64, 0x69, 0x73, 0x6b, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x69, 0x73, 0x6b, 0x55, 0x75,
0x73, 0x74, 0x61, 0x74, 0x65, 0x44, 0x69, 0x73, 0x6b, 0x4b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x12, 0x69, 0x64, 0x22, 0x70, 0x0a, 0x19, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x6a, 0x6f, 0x69,
0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x24, 0x0a, 0x0e, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x6b, 0x5f, 0x6b, 0x65,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x32, 0xab, 0x01, 0x0a, 0x03, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x65, 0x44, 0x69,
0x41, 0x50, 0x49, 0x12, 0x4e, 0x0a, 0x0f, 0x49, 0x73, 0x73, 0x75, 0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x73, 0x6b, 0x4b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x12, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65,
0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1c, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x2e, 0x49, 0x73, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
0x73, 0x75, 0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x0c, 0x52, 0x11, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x63, 0x72, 0x65, 0x74, 0x32, 0xab, 0x01, 0x0a, 0x03, 0x41, 0x50, 0x49, 0x12, 0x4e, 0x0a, 0x0f,
0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x49, 0x73, 0x73, 0x75, 0x65, 0x4a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x12,
0x6e, 0x73, 0x65, 0x12, 0x54, 0x0a, 0x11, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x6a, 0x6f, 0x1c, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x4a, 0x6f, 0x69, 0x6e,
0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x1e, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x2e, 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, 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, 0x74, 0x12, 0x1e, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65,
0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65, 0x6a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x6a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x74, 0x1a, 0x1f, 0x2e, 0x6a, 0x6f, 0x69, 0x6e, 0x2e, 0x49, 0x73, 0x73, 0x75, 0x65, 0x52, 0x65,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x64, 0x67, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x6a, 0x6f, 0x69, 0x6e, 0x54, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x79, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x65, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x73, 0x65, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x6e, 0x2f, 0x76, 0x32, 0x2f, 0x6a, 0x6f, 0x69, 0x6e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x65, 0x64, 0x67, 0x65, 0x6c, 0x65, 0x73, 0x73, 0x73, 0x79, 0x73, 0x2f, 0x63, 0x6f, 0x6e,
0x2f, 0x6a, 0x6f, 0x69, 0x6e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x73, 0x74, 0x65, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x32, 0x2f, 0x6a, 0x6f,
0x6f, 0x33, 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 ( var (

View File

@ -45,6 +45,8 @@ message IssueJoinTicketResponse {
string kubernetes_version = 9; string kubernetes_version = 9;
// kubernetes_components is a list of components to install on the node. // kubernetes_components is a list of components to install on the node.
repeated components.Component kubernetes_components = 10; 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 { message control_plane_cert_or_key {