Skip to content

Commit 3933d3a

Browse files
Merge pull request #161 from Infisical/feat/add-pam-ssh-exec-and-proxy
feat: add pam ssh exec and proxy
2 parents 97e5070 + 63c87ca commit 3933d3a

7 files changed

Lines changed: 712 additions & 120 deletions

File tree

packages/api/model.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,7 @@ type UploadSessionLogEntry struct {
887887
type UploadTerminalEvent struct {
888888
Timestamp time.Time `json:"timestamp"`
889889
EventType string `json:"eventType"`
890+
ChannelType string `json:"channelType,omitempty"`
890891
Data []byte `json:"data"`
891892
ElapsedTime float64 `json:"elapsedTime"`
892893
}

packages/cmd/pam.go

Lines changed: 107 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -108,66 +108,113 @@ var pamSshCmd = &cobra.Command{
108108

109109
var pamSshAccessCmd = &cobra.Command{
110110
Use: "access",
111-
Short: "Start SSH session to PAM account",
112-
Long: "Start an SSH session to a PAM-managed SSH account. This command automatically launches an SSH client connected through the Infisical Gateway.",
113-
Example: "infisical pam ssh access --resource prod-servers --account root --project-id b38bef10-2685-43c4-9a2c-635206d60bec --duration 1h",
111+
Short: "Start interactive SSH session to PAM account",
112+
Long: "Start an interactive SSH session to a PAM-managed SSH account. This command automatically launches an SSH client connected through the Infisical Gateway.",
113+
Example: "infisical pam ssh access --resource prod-servers --account root --project-id <project-uuid> --duration 1h",
114114
DisableFlagsInUseLine: true,
115115
Args: cobra.NoArgs,
116116
Run: func(cmd *cobra.Command, args []string) {
117-
util.RequireLogin()
117+
runSSHCommand(cmd, args, pam.SSHAccessOptions{})
118+
},
119+
}
118120

119-
resourceName, _ := cmd.Flags().GetString("resource")
120-
accountName, _ := cmd.Flags().GetString("account")
121+
var pamSshExecCmd = &cobra.Command{
122+
Use: "exec [command]",
123+
Short: "Execute a command on a PAM SSH account",
124+
Long: `Execute a single command on a PAM-managed SSH account and return the output.
125+
This is useful for CI/CD pipelines and scripting where interactive sessions are not needed.`,
126+
Example: ` # Run a command and get output
127+
infisical pam ssh exec "ls -la /var/log" --resource prod-servers --account root --project-id <project-uuid>
121128
122-
if resourceName == "" || accountName == "" {
123-
util.PrintErrorMessageAndExit("Both --resource and --account flags are required")
124-
}
129+
# Use in a script
130+
OUTPUT=$(infisical pam ssh exec "cat /etc/hostname" --resource prod-servers --account root --project-id <project-uuid>)`,
131+
DisableFlagsInUseLine: true,
132+
Args: cobra.ExactArgs(1),
133+
Run: func(cmd *cobra.Command, args []string) {
134+
runSSHCommand(cmd, args, pam.SSHAccessOptions{
135+
ExecCommand: args[0],
136+
})
137+
},
138+
}
125139

126-
durationStr, err := cmd.Flags().GetString("duration")
127-
if err != nil {
128-
util.HandleError(err, "Unable to parse duration flag")
129-
}
140+
var pamSshProxyCmd = &cobra.Command{
141+
Use: "proxy",
142+
Short: "Start SSH proxy for SCP, SFTP, or rsync",
143+
Long: `Start an SSH proxy without launching an interactive session.
144+
This is useful for file transfers using SCP, SFTP, rsync, or other SSH-based tools.
145+
The proxy prints connection details and waits until terminated with Ctrl+C.`,
146+
Example: ` # Start the proxy
147+
infisical pam ssh proxy --resource prod-servers --account root --project-id <project-uuid>
130148
131-
_, err = time.ParseDuration(durationStr)
132-
if err != nil {
133-
util.HandleError(err, "Invalid duration format. Use formats like '1h', '30m', '2h30m'")
134-
}
149+
# Then in another terminal, use SCP:
150+
scp -P <port> -o StrictHostKeyChecking=no local-file.txt root@127.0.0.1:/remote/path/
135151
136-
projectID, err := cmd.Flags().GetString("project-id")
137-
if err != nil {
138-
util.HandleError(err, "Unable to parse project-id flag")
139-
}
152+
# Or use rsync:
153+
rsync -e "ssh -p <port> -o StrictHostKeyChecking=no" local-dir/ root@127.0.0.1:/remote/path/`,
154+
DisableFlagsInUseLine: true,
155+
Args: cobra.NoArgs,
156+
Run: func(cmd *cobra.Command, args []string) {
157+
runSSHCommand(cmd, args, pam.SSHAccessOptions{
158+
ProxyOnly: true,
159+
})
160+
},
161+
}
140162

141-
if projectID == "" {
142-
workspaceFile, err := util.GetWorkSpaceFromFile()
143-
if err != nil {
144-
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --project-id flag")
145-
}
146-
projectID = workspaceFile.WorkspaceId
147-
}
163+
// runSSHCommand is the shared implementation for all SSH subcommands
164+
func runSSHCommand(cmd *cobra.Command, args []string, options pam.SSHAccessOptions) {
165+
util.RequireLogin()
148166

149-
log.Debug().Msg("PAM SSH Access: Trying to fetch credentials using logged in details")
167+
resourceName, _ := cmd.Flags().GetString("resource")
168+
accountName, _ := cmd.Flags().GetString("account")
150169

151-
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
152-
isConnected := util.ValidateInfisicalAPIConnection()
170+
if resourceName == "" || accountName == "" {
171+
util.PrintErrorMessageAndExit("Both --resource and --account flags are required")
172+
}
153173

154-
if isConnected {
155-
log.Debug().Msg("PAM SSH Access: Connected to Infisical instance, checking logged in creds")
156-
}
174+
durationStr, err := cmd.Flags().GetString("duration")
175+
if err != nil {
176+
util.HandleError(err, "Unable to parse duration flag")
177+
}
178+
179+
_, err = time.ParseDuration(durationStr)
180+
if err != nil {
181+
util.HandleError(err, "Invalid duration format. Use formats like '1h', '30m', '2h30m'")
182+
}
183+
184+
projectID, err := cmd.Flags().GetString("project-id")
185+
if err != nil {
186+
util.HandleError(err, "Unable to parse project-id flag")
187+
}
157188

189+
if projectID == "" {
190+
workspaceFile, err := util.GetWorkSpaceFromFile()
158191
if err != nil {
159-
util.HandleError(err, "Unable to get logged in user details")
192+
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --project-id flag")
160193
}
194+
projectID = workspaceFile.WorkspaceId
195+
}
161196

162-
if isConnected && loggedInUserDetails.LoginExpired {
163-
loggedInUserDetails = util.EstablishUserLoginSession()
164-
}
197+
log.Debug().Msg("PAM SSH: Trying to fetch credentials using logged in details")
165198

166-
pam.StartSSHLocalProxy(loggedInUserDetails.UserCredentials.JTWToken, pam.PAMAccessParams{
167-
ResourceName: resourceName,
168-
AccountName: accountName,
169-
}, projectID, durationStr)
170-
},
199+
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
200+
isConnected := util.ValidateInfisicalAPIConnection()
201+
202+
if isConnected {
203+
log.Debug().Msg("PAM SSH: Connected to Infisical instance, checking logged in creds")
204+
}
205+
206+
if err != nil {
207+
util.HandleError(err, "Unable to get logged in user details")
208+
}
209+
210+
if isConnected && loggedInUserDetails.LoginExpired {
211+
loggedInUserDetails = util.EstablishUserLoginSession()
212+
}
213+
214+
pam.StartSSHLocalProxy(loggedInUserDetails.UserCredentials.JTWToken, pam.PAMAccessParams{
215+
ResourceName: resourceName,
216+
AccountName: accountName,
217+
}, projectID, durationStr, options)
171218
}
172219

173220
// ==================== Kubernetes Commands ====================
@@ -340,14 +387,24 @@ func init() {
340387
pamDbAccessCmd.MarkFlagRequired("resource")
341388
pamDbAccessCmd.MarkFlagRequired("account")
342389

343-
// SSH commands
390+
// SSH commands - shared flags helper
391+
addSSHFlags := func(cmd *cobra.Command) {
392+
cmd.Flags().String("resource", "", "Name of the PAM resource to access")
393+
cmd.Flags().String("account", "", "Name of the account within the resource")
394+
cmd.Flags().String("duration", "1h", "Duration for SSH access session (e.g., '1h', '30m', '2h30m')")
395+
cmd.Flags().String("project-id", "", "Project ID of the account to access")
396+
cmd.MarkFlagRequired("resource")
397+
cmd.MarkFlagRequired("account")
398+
}
399+
344400
pamSshCmd.AddCommand(pamSshAccessCmd)
345-
pamSshAccessCmd.Flags().String("resource", "", "Name of the PAM resource to access")
346-
pamSshAccessCmd.Flags().String("account", "", "Name of the account within the resource")
347-
pamSshAccessCmd.Flags().String("duration", "1h", "Duration for SSH access session (e.g., '1h', '30m', '2h30m')")
348-
pamSshAccessCmd.Flags().String("project-id", "", "Project ID of the account to access")
349-
pamSshAccessCmd.MarkFlagRequired("resource")
350-
pamSshAccessCmd.MarkFlagRequired("account")
401+
addSSHFlags(pamSshAccessCmd)
402+
403+
pamSshCmd.AddCommand(pamSshExecCmd)
404+
addSSHFlags(pamSshExecCmd)
405+
406+
pamSshCmd.AddCommand(pamSshProxyCmd)
407+
addSSHFlags(pamSshProxyCmd)
351408

352409
// Kubernetes commands
353410
pamKubernetesCmd.AddCommand(pamKubernetesAccessCmd)

0 commit comments

Comments
 (0)