From bf5e8d8c8b3e7f0bf4bc96d4b079f51db31d220e Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 7 Apr 2025 22:25:37 -0700 Subject: [PATCH] Add ssh host command to cli --- .../migrations/20250405185753_ssh-mgmt-v2.ts | 1 + backend/src/ee/routes/v1/ssh-host-router.ts | 4 +- cli/go.mod | 2 +- cli/go.sum | 4 +- cli/packages/cmd/ssh.go | 112 +++++++++++++++++- 5 files changed, 116 insertions(+), 7 deletions(-) diff --git a/backend/src/db/migrations/20250405185753_ssh-mgmt-v2.ts b/backend/src/db/migrations/20250405185753_ssh-mgmt-v2.ts index 50bf90c3b..18d110498 100644 --- a/backend/src/db/migrations/20250405185753_ssh-mgmt-v2.ts +++ b/backend/src/db/migrations/20250405185753_ssh-mgmt-v2.ts @@ -17,6 +17,7 @@ export async function up(knex: Knex): Promise { t.foreign("userSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE"); t.uuid("hostSshCaId").notNullable(); t.foreign("hostSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE"); + t.unique(["projectId", "hostname"]); }); await createOnUpdateTrigger(knex, TableName.SshHost); } diff --git a/backend/src/ee/routes/v1/ssh-host-router.ts b/backend/src/ee/routes/v1/ssh-host-router.ts index 3f1b58554..76e17cc48 100644 --- a/backend/src/ee/routes/v1/ssh-host-router.ts +++ b/backend/src/ee/routes/v1/ssh-host-router.ts @@ -101,7 +101,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { - description: "Create SSH Host", + description: "Add an SSH Host", body: z.object({ projectId: z.string().describe(SSH_HOSTS.CREATE.projectId), hostname: z @@ -311,7 +311,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { - description: "Issue SSH credentials (certificate + key)", + description: "Issue SSH certificate for user", params: z.object({ sshHostId: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.sshHostId) }), diff --git a/cli/go.mod b/cli/go.mod index b69fef2a2..b7003a6df 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -12,7 +12,7 @@ require ( github.com/fatih/semgroup v1.2.0 github.com/gitleaks/go-gitdiff v0.8.0 github.com/h2non/filetype v1.1.3 - github.com/infisical/go-sdk v0.5.4 + github.com/infisical/go-sdk v0.5.5 github.com/infisical/infisical-kmip v0.3.5 github.com/mattn/go-isatty v0.0.20 github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a diff --git a/cli/go.sum b/cli/go.sum index 273339abf..a0d6028e5 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -277,8 +277,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/infisical/go-sdk v0.5.4 h1:/Jbl9DLYLmYA3A9W8YB7Kqhm8vymL1WeoITvjXBCq8w= -github.com/infisical/go-sdk v0.5.4/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= +github.com/infisical/go-sdk v0.5.5 h1:A0KfqZvRWScjVj19dbh2uHH4wSsElj5cTAgcT1Adezs= +github.com/infisical/go-sdk v0.5.5/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE= github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= diff --git a/cli/packages/cmd/ssh.go b/cli/packages/cmd/ssh.go index 3b4b61778..ba6e73a95 100644 --- a/cli/packages/cmd/ssh.go +++ b/cli/packages/cmd/ssh.go @@ -56,6 +56,12 @@ var sshConnectCmd = &cobra.Command{ Run: sshConnect, } +var sshAddHostCmd = &cobra.Command{ + Use: "add-host", + Short: "Register a new SSH host with Infisical", + Run: sshAddHost, +} + var algoToFileName = map[infisicalSdkUtil.CertKeyAlgorithm]string{ infisicalSdkUtil.RSA2048: "id_rsa_2048", infisicalSdkUtil.RSA4096: "id_rsa_4096", @@ -689,7 +695,7 @@ func sshConnect(cmd *cobra.Command, args []string) { selectedLoginUser := selectedHost.LoginMappings[loginIdx].LoginUser // Issue SSH creds for host - creds, err := infisicalClient.Ssh().IssueCredentialsFromHost(selectedHost.ID, infisicalSdk.IssueSshCredsFromHostOptions{ + creds, err := infisicalClient.Ssh().IssueSshHostUserCert(selectedHost.ID, infisicalSdk.IssueSshHostUserCertOptions{ LoginUser: selectedLoginUser, }) if err != nil { @@ -718,7 +724,101 @@ func sshConnect(cmd *cobra.Command, args []string) { } } +func sshAddHost(cmd *cobra.Command, args []string) { + token, err := util.GetInfisicalToken(cmd) + if err != nil { + util.HandleError(err, "Unable to parse token") + } + + var infisicalToken string + if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) { + infisicalToken = token.Token + } else { + util.RequireLogin() + util.RequireLocalWorkspaceFile() + + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true) + if err != nil { + util.HandleError(err, "Unable to authenticate") + } + if loggedInUserDetails.LoginExpired { + util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login]") + } + infisicalToken = loggedInUserDetails.UserCredentials.JTWToken + } + + projectId, err := cmd.Flags().GetString("projectId") + if err != nil { + util.HandleError(err, "Unable to parse --projectId flag") + } + if projectId == "" { + util.PrintErrorMessageAndExit("You must provide --projectId") + } + + hostname, err := cmd.Flags().GetString("hostname") + if err != nil { + util.HandleError(err, "Unable to parse --hostname flag") + } + if hostname == "" { + util.PrintErrorMessageAndExit("You must provide --hostname") + } + + writeUserCaToFile, err := cmd.Flags().GetBool("writeUserCaToFile") + if err != nil { + util.HandleError(err, "Unable to parse --writeUserCaToFile flag") + } + + userCaOutFilePath, err := cmd.Flags().GetString("userCaOutFilePath") + if err != nil { + util.HandleError(err, "Unable to parse --userCaOutFilePath flag") + } + + customHeaders, err := util.GetInfisicalCustomHeadersMap() + if err != nil { + util.HandleError(err, "Unable to get custom headers") + } + + client := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{ + SiteUrl: config.INFISICAL_URL, + UserAgent: api.USER_AGENT, + AutoTokenRefresh: false, + CustomHeaders: customHeaders, + }) + client.Auth().SetAccessToken(infisicalToken) + + host, err := client.Ssh().AddSshHost(infisicalSdk.AddSshHostOptions{ + ProjectID: projectId, + Hostname: hostname, + }) + if err != nil { + util.HandleError(err, "Failed to register SSH host") + } + + fmt.Println("✅ Successfully registered host:", host.Hostname) + + publicKey, err := client.Ssh().GetSshHostUserCaPublicKey(host.ID) + if err != nil { + util.HandleError(err, "Failed to fetch associated User CA public key") + } + + if writeUserCaToFile { + // Expand ~ if used in file path + if strings.HasPrefix(userCaOutFilePath, "~") { + homeDir, err := os.UserHomeDir() + if err != nil { + util.HandleError(err, "Unable to resolve ~ in userCaOutFilePath") + } + userCaOutFilePath = strings.Replace(userCaOutFilePath, "~", homeDir, 1) + } + + if err := writeToFile(userCaOutFilePath, publicKey, 0644); err != nil { + util.HandleError(err, "Failed to write User CA public key to file") + } + + fmt.Println("📁 Wrote User CA public key to:", userCaOutFilePath) + } +} func init() { sshSignKeyCmd.Flags().String("token", "", "Issue SSH certificate using machine identity access token") sshSignKeyCmd.Flags().String("certificateTemplateId", "", "The ID of the SSH certificate template to issue the SSH certificate for") @@ -744,6 +844,14 @@ func init() { sshConnectCmd.Flags().String("token", "", "Use a machine identity access token") sshCmd.AddCommand(sshConnectCmd) - rootCmd.AddCommand(sshCmd) + sshAddHostCmd.Flags().String("token", "", "Use a machine identity access token") + sshAddHostCmd.Flags().String("projectId", "", "Project ID the host belongs to (required)") + sshAddHostCmd.Flags().String("hostname", "", "Hostname of the SSH host (required)") + sshAddHostCmd.Flags().Bool("writeUserCaToFile", false, "Write User CA public key to /etc/ssh/infisical_user_ca.pub") + sshAddHostCmd.Flags().String("userCaOutFilePath", "/etc/ssh/infisical_user_ca.pub", "Custom file path to write the User CA public key") + + sshCmd.AddCommand(sshAddHostCmd) + + rootCmd.AddCommand(sshCmd) }