Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement key derivation for RFC 16 #3568

Draft
wants to merge 34 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0d7b9cf
add ssh subcommand
miampf Jan 2, 2025
8597e07
fixed json unmarshal
miampf Jan 2, 2025
114ee3f
corrected description of `key` flag
miampf Jan 2, 2025
42daf63
clarified description
miampf Jan 2, 2025
f19c39b
write to file
miampf Jan 2, 2025
6ab3b43
use a better logger
miampf Jan 2, 2025
f1d5f1a
refactor key derivation into own function
miampf Jan 2, 2025
d1f5d82
added logic to update the cluster
miampf Jan 2, 2025
af528fd
removed functionality to apply terraform
miampf Jan 7, 2025
89a99c4
added `emergency_ca_key` parameter to `IssueJoinTicketResponse`
miampf Jan 7, 2025
bb2a2f8
adjusted keyservice proto + regenerated go code
miampf Jan 7, 2025
cbdec49
generated docs
miampf Jan 7, 2025
1290eaa
implemented keyservice key derivation logic
miampf Jan 7, 2025
f7176a1
adjusted client side key derivation
miampf Jan 7, 2025
bcd7247
added clarifying comment in `ssh` command code
miampf Jan 7, 2025
b13a1cf
write CA key to file in joinclient
miampf Jan 7, 2025
74fe17d
use correct file name
miampf Jan 7, 2025
ee0b19d
add sensible error messages to CLI
miampf Jan 9, 2025
f4724a7
use existing `MasterSecret` type
miampf Jan 9, 2025
58fedfe
check if directory constellation-terraform exists
miampf Jan 9, 2025
99289e0
fix autoformatting
miampf Jan 9, 2025
f4161ab
use suffix for emergency ssh DEK key
miampf Jan 9, 2025
d1c0d47
adjusted key derivation logic to happen in the join client
miampf Jan 9, 2025
ced42d5
regenerated protobuf definitions
miampf Jan 9, 2025
7401ba9
fixed tests
miampf Jan 9, 2025
37f537b
make key path a constant
miampf Jan 9, 2025
a67b141
also derive the key on the control plane nodes
miampf Jan 9, 2025
62b0c66
remove unneeded TODO comment
miampf Jan 9, 2025
fa57092
refactored CA key generation into own function
miampf Jan 9, 2025
5faa800
added doc comment
miampf Jan 9, 2025
cd0b518
key -> public-key in debug message
miampf Jan 9, 2025
7c9bc1c
fmt
miampf Jan 9, 2025
3afa72e
please bazel check
miampf Jan 9, 2025
0cf04f0
added test for CA generation + use SeedSize constant
miampf Jan 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bootstrapper/internal/initserver/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ go_library(
"//bootstrapper/internal/journald",
"//internal/atls",
"//internal/attestation",
"//internal/constants",
"//internal/crypto",
"//internal/file",
"//internal/grpc/atlscredentials",
Expand All @@ -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",
],
)

Expand Down
25 changes: 25 additions & 0 deletions bootstrapper/internal/initserver/initserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package initserver
import (
"bufio"
"context"
"crypto/ed25519"
"errors"
"fmt"
"io"
Expand All @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions bootstrapper/internal/joinclient/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ go_library(
"//internal/attestation",
"//internal/cloud/metadata",
"//internal/constants",
"//internal/crypto",
"//internal/file",
"//internal/nodestate",
"//internal/role",
Expand All @@ -21,6 +22,7 @@ go_library(
"@io_k8s_kubernetes//cmd/kubeadm/app/constants",
"@io_k8s_utils//clock",
"@org_golang_google_grpc//:grpc",
"@org_golang_x_crypto//ssh",
],
)

Expand Down
11 changes: 11 additions & 0 deletions bootstrapper/internal/joinclient/joinclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation"
"github.com/edgelesssys/constellation/v2/internal/cloud/metadata"
"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/nodestate"
"github.com/edgelesssys/constellation/v2/internal/role"
"github.com/edgelesssys/constellation/v2/internal/versions/components"
"github.com/edgelesssys/constellation/v2/joinservice/joinproto"
"github.com/spf13/afero"
"golang.org/x/crypto/ssh"
"google.golang.org/grpc"
kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
kubeconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
Expand Down Expand Up @@ -271,6 +273,15 @@ func (c *JoinClient) startNodeAndJoin(ticket *joinproto.IssueJoinTicketResponse,
return fmt.Errorf("writing kubelet key: %w", err)
}

ca, err := crypto.GenerateEmergencySSHCAKey(ticket.EmergencyCaKey)
if err != nil {
return fmt.Errorf("generating emergency SSH CA key: %s", err)
}

if err := c.fileHandler.Write(constants.SSHCAKeyPath, ssh.MarshalAuthorizedKey(ca.PublicKey()), file.OptMkdirAll); err != nil {
return fmt.Errorf("writing ca key: %w", err)
}

state := nodestate.NodeState{
Role: c.role,
MeasurementSalt: ticket.MeasurementSalt,
Expand Down
51 changes: 42 additions & 9 deletions bootstrapper/internal/joinclient/joinclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ 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)
for i := range caDerivationKey {
caDerivationKey[i] = 0x0
}
respCaKey := &joinproto.IssueJoinTicketResponse{EmergencyCaKey: caDerivationKey}

testCases := map[string]struct {
role role.Role
Expand All @@ -69,7 +74,7 @@ func TestClient(t *testing.T) {
selfAnswer{err: assert.AnError},
selfAnswer{instance: workerSelf},
listAnswer{instances: peers},
issueJoinTicketAnswer{},
issueJoinTicketAnswer{resp: respCaKey},
},
clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(),
Expand All @@ -85,7 +90,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(),
Expand All @@ -101,7 +106,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(),
Expand All @@ -117,7 +122,7 @@ func TestClient(t *testing.T) {
listAnswer{},
listAnswer{},
listAnswer{instances: peers},
issueJoinTicketAnswer{},
issueJoinTicketAnswer{resp: respCaKey},
},
clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(),
Expand All @@ -134,14 +139,30 @@ func TestClient(t *testing.T) {
listAnswer{instances: peers},
issueJoinTicketAnswer{err: assert.AnError},
listAnswer{instances: peers},
issueJoinTicketAnswer{},
issueJoinTicketAnswer{resp: respCaKey},
},
clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(),
disk: &stubDisk{},
wantJoin: true,
wantLock: true,
},
"on worker: no CA derivation key is given": {
role: role.Worker,
apiAnswers: []any{
selfAnswer{instance: workerSelf},
listAnswer{instances: peers},
issueJoinTicketAnswer{err: assert.AnError},
listAnswer{instances: peers},
issueJoinTicketAnswer{err: assert.AnError},
listAnswer{instances: peers},
issueJoinTicketAnswer{},
},
clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(),
disk: &stubDisk{},
wantLock: true,
},
"on control plane: issueJoinTicket errors": {
role: role.ControlPlane,
apiAnswers: []any{
Expand All @@ -151,7 +172,7 @@ func TestClient(t *testing.T) {
listAnswer{instances: peers},
issueJoinTicketAnswer{err: assert.AnError},
listAnswer{instances: peers},
issueJoinTicketAnswer{},
issueJoinTicketAnswer{resp: respCaKey},
},
clusterJoiner: &stubClusterJoiner{},
nodeLock: newFakeLock(),
Expand All @@ -164,7 +185,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(),
Expand All @@ -177,7 +198,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(),
Expand All @@ -186,13 +207,25 @@ func TestClient(t *testing.T) {
wantLock: true,
wantNumJoins: 2,
},
"on control plane: node already locked": {
"on control plane: no CA derivation key is given": {
role: role.ControlPlane,
apiAnswers: []any{
selfAnswer{instance: controlSelf},
listAnswer{instances: peers},
issueJoinTicketAnswer{},
},
clusterJoiner: &stubClusterJoiner{numBadCalls: 1, joinClusterErr: assert.AnError},
nodeLock: newFakeLock(),
disk: &stubDisk{},
wantLock: true,
},
"on control plane: node already locked": {
role: role.ControlPlane,
apiAnswers: []any{
selfAnswer{instance: controlSelf},
listAnswer{instances: peers},
issueJoinTicketAnswer{resp: respCaKey},
},
clusterJoiner: &stubClusterJoiner{},
nodeLock: lockedLock,
disk: &stubDisk{},
Expand Down
1 change: 1 addition & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions cli/internal/cmd/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ go_library(
"miniup_linux_amd64.go",
"recover.go",
"spinner.go",
"ssh.go",
"status.go",
"terminate.go",
"upgrade.go",
Expand Down Expand Up @@ -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",
Expand Down
120 changes: 120 additions & 0 deletions cli/internal/cmd/ssh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
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"
)

var permissions = ssh.Permissions{
Extensions: map[string]string{
"permit-port-forwarding": "yes",
"permit-pty": "yes",
},
}

// 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
}

_, 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(fmt.Sprintf("%s.json", constants.ConstellationMasterSecretStoreName), &mastersecret); err != nil {
return fmt.Errorf("reading master secret: %s", err)
}

mastersecret_uri := uri.MasterSecret{Key: mastersecret.Key, Salt: mastersecret.Salt}
kms, err := setup.KMS(cmd.Context(), uri.NoStoreURI, mastersecret_uri.EncodeToURI())
if err != nil {
return fmt.Errorf("setting up KMS: %s", err)
}
key, 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(key)
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())))

key_path, err := cmd.Flags().GetString("key")
if err != nil {
return fmt.Errorf("retrieving path to public key from flags: %s", err)
}

key_buf, err := fh.Read(key_path)
if err != nil {
return fmt.Errorf("reading public key %q: %s", key_path, err)
}

pub, _, _, _, err := ssh.ParseAuthorizedKey(key_buf)
if err != nil {
return fmt.Errorf("parsing public key %q: %s", key_path, 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: permissions,
}
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)
}
fmt.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)
miampf marked this conversation as resolved.
Show resolved Hide resolved

return nil
}
Loading
Loading