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 35 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6b009fe
add ssh subcommand
miampf Jan 2, 2025
e4fd076
fixed json unmarshal
miampf Jan 2, 2025
b0dfc36
corrected description of `key` flag
miampf Jan 2, 2025
dfa9cfb
clarified description
miampf Jan 2, 2025
92bca2c
write to file
miampf Jan 2, 2025
2eed25d
use a better logger
miampf Jan 2, 2025
1a01475
refactor key derivation into own function
miampf Jan 2, 2025
b1b275f
added logic to update the cluster
miampf Jan 2, 2025
56da13e
removed functionality to apply terraform
miampf Jan 7, 2025
2f4a88a
added `emergency_ca_key` parameter to `IssueJoinTicketResponse`
miampf Jan 7, 2025
3ef0733
adjusted keyservice proto + regenerated go code
miampf Jan 7, 2025
03cb622
generated docs
miampf Jan 7, 2025
4a49874
implemented keyservice key derivation logic
miampf Jan 7, 2025
7badf21
adjusted client side key derivation
miampf Jan 7, 2025
b70218d
added clarifying comment in `ssh` command code
miampf Jan 7, 2025
df984c4
write CA key to file in joinclient
miampf Jan 7, 2025
ba85d21
use correct file name
miampf Jan 7, 2025
f509331
add sensible error messages to CLI
miampf Jan 9, 2025
a4eb979
use existing `MasterSecret` type
miampf Jan 9, 2025
515096c
check if directory constellation-terraform exists
miampf Jan 9, 2025
78c5699
fix autoformatting
miampf Jan 9, 2025
304bf51
use suffix for emergency ssh DEK key
miampf Jan 9, 2025
e7f7f7c
adjusted key derivation logic to happen in the join client
miampf Jan 9, 2025
444546b
regenerated protobuf definitions
miampf Jan 9, 2025
3f812dc
fixed tests
miampf Jan 9, 2025
f1fbdbf
make key path a constant
miampf Jan 9, 2025
cff202b
also derive the key on the control plane nodes
miampf Jan 9, 2025
dc8765f
remove unneeded TODO comment
miampf Jan 9, 2025
421961e
refactored CA key generation into own function
miampf Jan 9, 2025
9330e1c
added doc comment
miampf Jan 9, 2025
1648202
key -> public-key in debug message
miampf Jan 9, 2025
5567dba
fmt
miampf Jan 9, 2025
0c8b42d
please bazel check
miampf Jan 9, 2025
7d193b1
added test for CA generation + use SeedSize constant
miampf Jan 9, 2025
c2379ef
better variable naming
miampf Jan 14, 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)
}

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

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

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: 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