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..d7c1f1e269 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)
@@ -166,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
@@ -173,14 +251,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 +335,47 @@ 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")
+ 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)
}
- fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath)
+ // 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)
+ 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 +603,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).
-
-