diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 6baaf3f1f..cd3f24647 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 828a63d5b..483a84dfc 100644 --- a/cli/internal/cmd/BUILD.bazel +++ b/cli/internal/cmd/BUILD.bazel @@ -46,6 +46,7 @@ go_library( "validargs.go", "verify.go", "version.go", + "ssh.go", ], importpath = "github.com/edgelesssys/constellation/v2/cli/internal/cmd", visibility = ["//cli:__subpackages__"], @@ -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//hkdf", + "@org_golang_x_crypto//ssh", ] + select({ "@io_bazel_rules_go//go/platform:android_amd64": [ "@org_golang_x_sys//unix", diff --git a/cli/internal/cmd/ssh.go b/cli/internal/cmd/ssh.go new file mode 100644 index 000000000..b1cdb5093 --- /dev/null +++ b/cli/internal/cmd/ssh.go @@ -0,0 +1,108 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package cmd + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "fmt" + "time" + + "github.com/edgelesssys/constellation/v2/internal/constants" + "github.com/edgelesssys/constellation/v2/internal/file" + "github.com/spf13/afero" + "github.com/spf13/cobra" + + "golang.org/x/crypto/hkdf" + "golang.org/x/crypto/ssh" +) + +type secret struct { + Key []byte `json:"key,omitempty"` + Salt []byte `json:"salt,omitempty"` +} + +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.") + 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 + } + + var mastersecret secret + if err = fh.ReadJSON(fmt.Sprintf("%s.json", constants.ConstellationMasterSecretStoreName), &mastersecret); err != nil { + return err + } + + hkdf := hkdf.New(sha256.New, mastersecret.Key, mastersecret.Salt, []byte("ssh-ca")) + _, priv, err := ed25519.GenerateKey(hkdf) + if err != nil { + return err + } + + ca, err := ssh.NewSignerFromSigner(priv) + if err != nil { + return err + } + + debugLogger.Debug("CA KEY generated", "key", string(ssh.MarshalAuthorizedKey(ca.PublicKey()))) + + key_path, err := cmd.Flags().GetString("key") + if err != nil { + return err + } + + key_buf, err := fh.Read(key_path) + if err != nil { + return err + } + + pub, _, _, _, err := ssh.ParseAuthorizedKey(key_buf) + if err != nil { + return 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 err + } + + debugLogger.Debug("Signed certificate", "certificate", string(ssh.MarshalAuthorizedKey(&certificate))) + fh.Write(fmt.Sprintf("%s/ca_cert.pub", constants.TerraformWorkingDir), ssh.MarshalAuthorizedKey(&certificate), file.OptOverwrite, file.OptMkdirAll) + fmt.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 +}