From f97f98b2c3ea980c1aa6ee7efd2d85bcbf54683c Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 18 Dec 2024 23:27:11 -0800 Subject: [PATCH 1/2] Add ability to load retrieved ssh credentials into ssh agent with new addToAgent flag --- cli/go.mod | 10 +- cli/go.sum | 20 ++-- cli/packages/cmd/ssh.go | 143 ++++++++++++++++++++++------ docs/documentation/platform/ssh.mdx | 79 +++++---------- 4 files changed, 150 insertions(+), 102 deletions(-) diff --git a/cli/go.mod b/cli/go.mod index 82d529da4c..637bd904d3 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -23,8 +23,8 @@ require ( github.com/spf13/cobra v1.6.1 github.com/spf13/viper v1.8.1 github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.25.0 - golang.org/x/term v0.22.0 + golang.org/x/crypto v0.31.0 + golang.org/x/term v0.27.0 gopkg.in/yaml.v2 v2.4.0 ) @@ -93,9 +93,9 @@ require ( go.opentelemetry.io/otel/trace v1.24.0 // indirect golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/api v0.188.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect diff --git a/cli/go.sum b/cli/go.sum index be90a0f2fc..79fe1fd7f6 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -453,8 +453,8 @@ golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -564,8 +564,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -620,16 +620,16 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -643,8 +643,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/cli/packages/cmd/ssh.go b/cli/packages/cmd/ssh.go index b97d6572f7..2b3c4f9a82 100644 --- a/cli/packages/cmd/ssh.go +++ b/cli/packages/cmd/ssh.go @@ -6,9 +6,11 @@ package cmd import ( "context" "fmt" + "net" "os" "path/filepath" "strings" + "time" "github.com/Infisical/infisical-merge/packages/api" "github.com/Infisical/infisical-merge/packages/config" @@ -16,6 +18,8 @@ import ( infisicalSdk "github.com/infisical/go-sdk" infisicalSdkUtil "github.com/infisical/go-sdk/packages/util" "github.com/spf13/cobra" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" ) var sshCmd = &cobra.Command{ @@ -52,8 +56,8 @@ var algoToFileName = map[infisicalSdkUtil.CertKeyAlgorithm]string{ } func isValidKeyAlgorithm(algo infisicalSdkUtil.CertKeyAlgorithm) bool { - _, exists := algoToFileName[algo] - return exists + _, exists := algoToFileName[algo] + return exists } func isValidCertType(certType infisicalSdkUtil.SshCertType) bool { @@ -81,6 +85,71 @@ func writeToFile(filePath string, content string, perm os.FileMode) error { return nil } +func addCredentialsToAgent(privateKeyContent, certContent string) error { + // Parse the private key + privateKey, err := ssh.ParseRawPrivateKey([]byte(privateKeyContent)) + if err != nil { + return fmt.Errorf("failed to parse private key: %w", err) + } + + // Parse the certificate + pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certContent)) + if err != nil { + return fmt.Errorf("failed to parse certificate: %w", err) + } + + cert, ok := pubKey.(*ssh.Certificate) + if !ok { + return fmt.Errorf("parsed key is not a certificate") + } + // Calculate LifetimeSecs based on certificate's valid-to time + validUntil := time.Unix(int64(cert.ValidBefore), 0) + now := time.Now() + + // Handle ValidBefore as either a timestamp or an enumeration + // SSH certificates use ValidBefore as a timestamp unless set to 0 or ~0 + if cert.ValidBefore == ssh.CertTimeInfinity { + // If certificate never expires, set default lifetime to 1 year (can adjust as needed) + validUntil = now.Add(365 * 24 * time.Hour) + } + + // Calculate the duration until expiration + lifetime := validUntil.Sub(now) + if lifetime <= 0 { + return fmt.Errorf("certificate is already expired") + } + + // Convert duration to seconds + lifetimeSecs := uint32(lifetime.Seconds()) + + // Connect to the SSH agent + socket := os.Getenv("SSH_AUTH_SOCK") + if socket == "" { + return fmt.Errorf("SSH_AUTH_SOCK not set") + } + + conn, err := net.Dial("unix", socket) + if err != nil { + return fmt.Errorf("failed to connect to SSH agent: %w", err) + } + defer conn.Close() + + agentClient := agent.NewClient(conn) + + // Add the key with certificate to the agent + err = agentClient.Add(agent.AddedKey{ + PrivateKey: privateKey, + Certificate: cert, + Comment: "Added via Infisical CLI", + LifetimeSecs: lifetimeSecs, + }) + if err != nil { + return fmt.Errorf("failed to add key to agent: %w", err) + } + + return nil +} + func issueCredentials(cmd *cobra.Command, args []string) { token, err := util.GetInfisicalToken(cmd) @@ -173,14 +242,7 @@ func issueCredentials(cmd *cobra.Command, args []string) { signedKeyPath string ) - if outFilePath == "" { - // Use current working directory - cwd, err := os.Getwd() - if err != nil { - util.HandleError(err, "Failed to get current working directory") - } - outputDir = cwd - } else { + if outFilePath != "" { // Expand ~ to home directory if present if strings.HasPrefix(outFilePath, "~") { homeDir, err := os.UserHomeDir() @@ -264,34 +326,52 @@ func issueCredentials(cmd *cobra.Command, args []string) { util.HandleError(err, "Failed to issue SSH credentials") } - // If signedKeyPath wasn't set in the directory scenario, set it now - if signedKeyPath == "" { - fileName := algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)] - signedKeyPath = filepath.Join(outputDir, fileName+"-cert.pub") - } + if outFilePath != "" { + // If signedKeyPath wasn't set in the directory scenario, set it now + if signedKeyPath == "" { + fileName := algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)] + signedKeyPath = filepath.Join(outputDir, fileName+"-cert.pub") + } - if privateKeyPath == "" { - privateKeyPath = filepath.Join(outputDir, algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)]) - } - err = writeToFile(privateKeyPath, creds.PrivateKey, 0600) - if err != nil { - util.HandleError(err, "Failed to write Private Key to file") - } + if privateKeyPath == "" { + privateKeyPath = filepath.Join(outputDir, algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)]) + } + err = writeToFile(privateKeyPath, creds.PrivateKey, 0600) + if err != nil { + util.HandleError(err, "Failed to write Private Key to file") + } - if publicKeyPath == "" { - publicKeyPath = privateKeyPath + ".pub" - } - err = writeToFile(publicKeyPath, creds.PublicKey, 0644) - if err != nil { - util.HandleError(err, "Failed to write Public Key to file") + if publicKeyPath == "" { + publicKeyPath = privateKeyPath + ".pub" + } + err = writeToFile(publicKeyPath, creds.PublicKey, 0644) + if err != nil { + util.HandleError(err, "Failed to write Public Key to file") + } + + err = writeToFile(signedKeyPath, creds.SignedKey, 0644) + if err != nil { + util.HandleError(err, "Failed to write Signed Key to file") + } + + fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath) } - err = writeToFile(signedKeyPath, creds.SignedKey, 0644) + // Check if we need to add the key to the SSH agent + addToAgent, err := cmd.Flags().GetBool("addToAgent") if err != nil { - util.HandleError(err, "Failed to write Signed Key to file") + util.HandleError(err, "Unable to parse addToAgent flag") } - fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath) + if addToAgent { + // Call the helper function to handle add-to-agent flow + err := addCredentialsToAgent(creds.PrivateKey, creds.SignedKey) + if err != nil { + util.HandleError(err, "Failed to add keys to SSH agent") + } else { + fmt.Println("The SSH key and certificate have been successfully added to your ssh-agent.") + } + } } func signKey(cmd *cobra.Command, args []string) { @@ -519,6 +599,7 @@ func init() { sshIssueCredentialsCmd.Flags().String("ttl", "", "The ttl to issue SSH credentials for") sshIssueCredentialsCmd.Flags().String("keyId", "", "The keyId to issue SSH credentials for") sshIssueCredentialsCmd.Flags().String("outFilePath", "", "The path to write the SSH credentials to such as ~/.ssh, ./some_folder, ./some_folder/id_rsa-cert.pub. If not provided, the credentials will be saved to the current working directory") + sshIssueCredentialsCmd.Flags().Bool("addToAgent", false, "Whether to add issued SSH credentials to the SSH agent") sshCmd.AddCommand(sshIssueCredentialsCmd) rootCmd.AddCommand(sshCmd) } diff --git a/docs/documentation/platform/ssh.mdx b/docs/documentation/platform/ssh.mdx index 92321fb3c3..1bcad484a2 100644 --- a/docs/documentation/platform/ssh.mdx +++ b/docs/documentation/platform/ssh.mdx @@ -159,7 +159,19 @@ as part of the SSH operation. ## Guide to Using Infisical SSH to Access a Host -We show how to obtain a SSH certificate (and optionally a new SSH key pair) for a client to access a host via CLI: +We show how to obtain a SSH certificate and use it for a client to access a host via CLI: + + + The subsequent guide assumes the following prerequisites: + +- SSH Agent is running: The `ssh-agent` must be actively running on the host machine. +- OpenSSH is installed: The system should have OpenSSH installed; this includes + both the `ssh` client and `ssh-agent`. +- `SSH_AUTH_SOCK` environment variable + is set; the `SSH_AUTH_SOCK` variable should point to the UNIX socket that + `ssh-agent` uses for communication. + + @@ -169,70 +181,25 @@ infisical login ``` - - Depending on the use-case, a client may either request a SSH certificate along with a new SSH key pair or obtain a SSH certificate for an existing SSH key pair to access a host. - - - - If you wish to obtain a new SSH key pair in conjunction with the SSH certificate, then you can use the `infisical ssh issue-credentials` command. - - ```bash - infisical ssh issue-credentials --certificateTemplateId= --principals= - ``` - - The following flags may be relevant: - - - `certificateTemplateId`: The ID of the certificate template to use for issuing the SSH certificate. - - `principals`: The comma-delimited username(s) or hostname(s) to include in the SSH certificate. - - `outFilePath` (optional): The path to the file to write the SSH certificate to. - - - If `outFilePath` is not specified, the SSH certificate will be written to the current working directory where the command is run. - - - - - If you have an existing SSH key pair, then you can use the `infisical ssh sign-key` command with either - the `--publicKey` flag or the `--publicKeyFilePath` flag to obtain a SSH certificate corresponding to - the existing credential. - - ```bash - infisical ssh sign-key --publicKeyFilePath= --certificateTemplateId= --principals= - ``` - - The following flags may be relevant: - - - `publicKey`: The public key to sign. - - `publicKeyFilePath`: The path to the public key file to sign. - - `certificateTemplateId`: The ID of the certificate template to use for issuing the SSH certificate. - - `principals`: The comma-delimited username(s) or hostname(s) to include in the SSH certificate. - - `outFilePath` (optional): The path to the file to write the SSH certificate to. - - - If `outFilePath` is not specified but `publicKeyFilePath` is then the SSH certificate will be written to the directory of the public key file; if the public key file is called `id_rsa.pub`, then the file containing the SSH certificate will be called `id_rsa-cert.pub`. + + Run the `infisical ssh issue-credentials` command, specifying the `--addToAgent` flag to automatically load the SSH certificate into the SSH agent. + ```bash + infisical ssh issue-credentials --certificateTemplateId= --principals= --addToAgent + ``` - Otherwise, if `outFilePath` is not specified, the SSH certificate will be written to the current working directory where the command is run. - + Here's some guidance on each flag: - - + - `certificateTemplateId`: The ID of the certificate template to use for issuing the SSH certificate. + - `principals`: The comma-delimited username(s) or hostname(s) to include in the SSH certificate. - Once you have obtained the SSH certificate, you can use it to SSH into the desired host. + Finally, SSH into the desired host; the SSH operation will be performed using the SSH certificate loaded into the SSH agent. ```bash - ssh -i /path/to/private_key.pem \ - -o CertificateFile=/path/to/ssh-cert.pub \ - username@hostname + ssh username@hostname ``` - - We recommend setting up aliases so you can more easily SSH into the desired host. - - For example, you may set up an SSH alias using the SSH client configuration file (usually `~/.ssh/config`), defining a host alias including the file path to the issued SSH credential(s). - - From f5a064167170c78bce75ba3ed752e33386b51814 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 19 Dec 2024 11:55:44 -0800 Subject: [PATCH 2/2] Add requirement for ssh issue credentials command to include either outFilePath or addToAgent flag --- cli/packages/cmd/ssh.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/cli/packages/cmd/ssh.go b/cli/packages/cmd/ssh.go index 2b3c4f9a82..d7c1f1e269 100644 --- a/cli/packages/cmd/ssh.go +++ b/cli/packages/cmd/ssh.go @@ -235,6 +235,15 @@ func issueCredentials(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse flag") } + addToAgent, err := cmd.Flags().GetBool("addToAgent") + if err != nil { + util.HandleError(err, "Unable to parse addToAgent flag") + } + + if outFilePath == "" && addToAgent == false { + util.PrintErrorMessageAndExit("You must provide either --outFilePath or --addToAgent flag to use this command") + } + var ( outputDir string privateKeyPath string @@ -357,12 +366,7 @@ func issueCredentials(cmd *cobra.Command, args []string) { fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath) } - // Check if we need to add the key to the SSH agent - addToAgent, err := cmd.Flags().GetBool("addToAgent") - if err != nil { - util.HandleError(err, "Unable to parse addToAgent flag") - } - + // Add SSH credentials to the SSH agent if needed if addToAgent { // Call the helper function to handle add-to-agent flow err := addCredentialsToAgent(creds.PrivateKey, creds.SignedKey)