From de63c8cb6cf253da14af8e8190d25459d5c9e578 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sat, 26 Apr 2025 18:04:21 -0700 Subject: [PATCH] Add alias field to ssh hosts for improved ux --- .../20250426044605_ssh-host-alias.ts | 23 ++++ backend/src/db/schemas/ssh-hosts.ts | 3 +- backend/src/ee/routes/v1/ssh-host-router.ts | 23 ++++ .../ee/services/audit-log/audit-log-types.ts | 2 + .../src/ee/services/ssh-host/ssh-host-dal.ts | 13 +- .../ee/services/ssh-host/ssh-host-schema.ts | 1 + .../ee/services/ssh-host/ssh-host-service.ts | 4 + .../ee/services/ssh-host/ssh-host-types.ts | 2 + backend/src/lib/api-docs/constants.ts | 2 + cli/go.mod | 2 +- cli/go.sum | 4 +- cli/packages/cmd/ssh.go | 52 +++++--- docs/cli/commands/ssh.mdx | 126 +++++------------- frontend/src/hooks/api/sshHost/types.ts | 3 + .../SshHostsPage/components/SshHostModal.tsx | 37 ++++- .../SshHostsPage/components/SshHostsTable.tsx | 2 + 16 files changed, 182 insertions(+), 117 deletions(-) create mode 100644 backend/src/db/migrations/20250426044605_ssh-host-alias.ts diff --git a/backend/src/db/migrations/20250426044605_ssh-host-alias.ts b/backend/src/db/migrations/20250426044605_ssh-host-alias.ts new file mode 100644 index 000000000..a6b1f1e4d --- /dev/null +++ b/backend/src/db/migrations/20250426044605_ssh-host-alias.ts @@ -0,0 +1,23 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasAliasColumn = await knex.schema.hasColumn(TableName.SshHost, "alias"); + if (!hasAliasColumn) { + await knex.schema.alterTable(TableName.SshHost, (t) => { + t.string("alias").nullable(); + t.unique(["projectId", "alias"]); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasAliasColumn = await knex.schema.hasColumn(TableName.SshHost, "alias"); + if (hasAliasColumn) { + await knex.schema.alterTable(TableName.SshHost, (t) => { + t.dropUnique(["projectId", "alias"]); + t.dropColumn("alias"); + }); + } +} diff --git a/backend/src/db/schemas/ssh-hosts.ts b/backend/src/db/schemas/ssh-hosts.ts index 7577e065b..54b36a6bd 100644 --- a/backend/src/db/schemas/ssh-hosts.ts +++ b/backend/src/db/schemas/ssh-hosts.ts @@ -16,7 +16,8 @@ export const SshHostsSchema = z.object({ userCertTtl: z.string(), hostCertTtl: z.string(), userSshCaId: z.string().uuid(), - hostSshCaId: z.string().uuid() + hostSshCaId: z.string().uuid(), + alias: z.string().nullable().optional() }); export type TSshHosts = z.infer; diff --git a/backend/src/ee/routes/v1/ssh-host-router.ts b/backend/src/ee/routes/v1/ssh-host-router.ts index 1dab5dd2f..aae79ad35 100644 --- a/backend/src/ee/routes/v1/ssh-host-router.ts +++ b/backend/src/ee/routes/v1/ssh-host-router.ts @@ -1,3 +1,4 @@ +import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; @@ -96,10 +97,20 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { hostname: z .string() .min(1) + .trim() .refine((v) => isValidHostname(v), { message: "Hostname must be a valid hostname" }) .describe(SSH_HOSTS.CREATE.hostname), + alias: z + .string() + .trim() + .nullable() + .default(null) + .refine((v) => v == null || slugify(v) === v, { + message: "Alias must be a valid slug" + }) + .describe(SSH_HOSTS.CREATE.alias), userCertTtl: z .string() .refine((val) => ms(val) > 0, "TTL must be a positive number") @@ -138,6 +149,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { metadata: { sshHostId: host.id, hostname: host.hostname, + alias: host.alias ?? null, userCertTtl: host.userCertTtl, hostCertTtl: host.hostCertTtl, loginMappings: host.loginMappings, @@ -166,12 +178,22 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { body: z.object({ hostname: z .string() + .trim() .min(1) .refine((v) => isValidHostname(v), { message: "Hostname must be a valid hostname" }) .optional() .describe(SSH_HOSTS.UPDATE.hostname), + alias: z + .string() + .trim() + .nullable() + .refine((v) => v == null || slugify(v) === v, { + message: "Alias must be a valid slug" + }) + .optional() + .describe(SSH_HOSTS.CREATE.alias), userCertTtl: z .string() .refine((val) => ms(val) > 0, "TTL must be a positive number") @@ -208,6 +230,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => { metadata: { sshHostId: host.id, hostname: host.hostname, + alias: host.alias, userCertTtl: host.userCertTtl, hostCertTtl: host.hostCertTtl, loginMappings: host.loginMappings, diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index f85fbd6a3..247bdfacb 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -1494,6 +1494,7 @@ interface CreateSshHost { metadata: { sshHostId: string; hostname: string; + alias: string | null; userCertTtl: string; hostCertTtl: string; loginMappings: { @@ -1512,6 +1513,7 @@ interface UpdateSshHost { metadata: { sshHostId: string; hostname?: string; + alias?: string | null; userCertTtl?: string; hostCertTtl?: string; loginMappings?: { diff --git a/backend/src/ee/services/ssh-host/ssh-host-dal.ts b/backend/src/ee/services/ssh-host/ssh-host-dal.ts index 4baeca503..3c9755e65 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-dal.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-dal.ts @@ -33,6 +33,7 @@ export const sshHostDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.SshHost).as("sshHostId"), db.ref("projectId").withSchema(TableName.SshHost), db.ref("hostname").withSchema(TableName.SshHost), + db.ref("alias").withSchema(TableName.SshHost), db.ref("userCertTtl").withSchema(TableName.SshHost), db.ref("hostCertTtl").withSchema(TableName.SshHost), db.ref("loginUser").withSchema(TableName.SshHostLoginUser), @@ -45,7 +46,8 @@ export const sshHostDALFactory = (db: TDbClient) => { const grouped = groupBy(rows, (r) => r.sshHostId); return Object.values(grouped).map((hostRows) => { - const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } = hostRows[0]; + const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } = + hostRows[0]; const loginMappingGrouped = groupBy(hostRows, (r) => r.loginUser); @@ -59,6 +61,7 @@ export const sshHostDALFactory = (db: TDbClient) => { return { id: sshHostId, hostname, + alias, projectId, userCertTtl, hostCertTtl, @@ -87,6 +90,7 @@ export const sshHostDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.SshHost).as("sshHostId"), db.ref("projectId").withSchema(TableName.SshHost), db.ref("hostname").withSchema(TableName.SshHost), + db.ref("alias").withSchema(TableName.SshHost), db.ref("userCertTtl").withSchema(TableName.SshHost), db.ref("hostCertTtl").withSchema(TableName.SshHost), db.ref("loginUser").withSchema(TableName.SshHostLoginUser), @@ -99,7 +103,7 @@ export const sshHostDALFactory = (db: TDbClient) => { const hostsGrouped = groupBy(rows, (r) => r.sshHostId); return Object.values(hostsGrouped).map((hostRows) => { - const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0]; + const { sshHostId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0]; const loginMappingGrouped = groupBy( hostRows.filter((r) => r.loginUser), @@ -116,6 +120,7 @@ export const sshHostDALFactory = (db: TDbClient) => { return { id: sshHostId, hostname, + alias, projectId, userCertTtl, hostCertTtl, @@ -144,6 +149,7 @@ export const sshHostDALFactory = (db: TDbClient) => { db.ref("id").withSchema(TableName.SshHost).as("sshHostId"), db.ref("projectId").withSchema(TableName.SshHost), db.ref("hostname").withSchema(TableName.SshHost), + db.ref("alias").withSchema(TableName.SshHost), db.ref("userCertTtl").withSchema(TableName.SshHost), db.ref("hostCertTtl").withSchema(TableName.SshHost), db.ref("loginUser").withSchema(TableName.SshHostLoginUser), @@ -155,7 +161,7 @@ export const sshHostDALFactory = (db: TDbClient) => { if (rows.length === 0) return null; - const { sshHostId: id, projectId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0]; + const { sshHostId: id, projectId, hostname, alias, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0]; const loginMappingGrouped = groupBy( rows.filter((r) => r.loginUser), @@ -173,6 +179,7 @@ export const sshHostDALFactory = (db: TDbClient) => { id, projectId, hostname, + alias, userCertTtl, hostCertTtl, loginMappings, diff --git a/backend/src/ee/services/ssh-host/ssh-host-schema.ts b/backend/src/ee/services/ssh-host/ssh-host-schema.ts index 4eeb90881..a9b674991 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-schema.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-schema.ts @@ -6,6 +6,7 @@ export const sanitizedSshHost = SshHostsSchema.pick({ id: true, projectId: true, hostname: true, + alias: true, userCertTtl: true, hostCertTtl: true, userSshCaId: true, diff --git a/backend/src/ee/services/ssh-host/ssh-host-service.ts b/backend/src/ee/services/ssh-host/ssh-host-service.ts index 69807431a..f1b14a9cb 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-service.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-service.ts @@ -119,6 +119,7 @@ export const sshHostServiceFactory = ({ const createSshHost = async ({ projectId, hostname, + alias, userCertTtl, hostCertTtl, loginMappings, @@ -192,6 +193,7 @@ export const sshHostServiceFactory = ({ { projectId, hostname, + alias, userCertTtl, hostCertTtl, userSshCaId, @@ -265,6 +267,7 @@ export const sshHostServiceFactory = ({ const updateSshHost = async ({ sshHostId, hostname, + alias, userCertTtl, hostCertTtl, loginMappings, @@ -297,6 +300,7 @@ export const sshHostServiceFactory = ({ sshHostId, { hostname, + alias, userCertTtl, hostCertTtl }, diff --git a/backend/src/ee/services/ssh-host/ssh-host-types.ts b/backend/src/ee/services/ssh-host/ssh-host-types.ts index 0c7cb25e1..58f4db9fb 100644 --- a/backend/src/ee/services/ssh-host/ssh-host-types.ts +++ b/backend/src/ee/services/ssh-host/ssh-host-types.ts @@ -4,6 +4,7 @@ export type TListSshHostsDTO = Omit; export type TCreateSshHostDTO = { hostname: string; + alias: string | null; userCertTtl: string; hostCertTtl: string; loginMappings: { @@ -19,6 +20,7 @@ export type TCreateSshHostDTO = { export type TUpdateSshHostDTO = { sshHostId: string; hostname?: string; + alias?: string | null; userCertTtl?: string; hostCertTtl?: string; loginMappings?: { diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index b5181ae81..26301040e 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1387,6 +1387,7 @@ export const SSH_HOSTS = { CREATE: { projectId: "The ID of the project to create the SSH host in.", hostname: "The hostname of the SSH host.", + alias: "The alias for the SSH host.", userCertTtl: "The time to live for user certificates issued under this host.", hostCertTtl: "The time to live for host certificates issued under this host.", loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')", @@ -1401,6 +1402,7 @@ export const SSH_HOSTS = { UPDATE: { sshHostId: "The ID of the SSH host to update.", hostname: "The hostname of the SSH host to update to.", + alias: "The alias for the SSH host to update to.", userCertTtl: "The time to live for user certificates issued under this host to update to.", hostCertTtl: "The time to live for host certificates issued under this host to update to.", loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')", diff --git a/cli/go.mod b/cli/go.mod index c713417e2..52cb79f38 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.8 + github.com/infisical/go-sdk v0.5.92 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 68bce9cd3..49566f1cc 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.8 h1:bCetYLp7HWt8DnU9KPh1n8n3z5pjmunkGDB4bA3lEFs= -github.com/infisical/go-sdk v0.5.8/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= +github.com/infisical/go-sdk v0.5.92 h1:PoCnVndrd6Dbkipuxl9fFiwlD5vCKsabtQo09mo8lUE= +github.com/infisical/go-sdk v0.5.92/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 a11e4da4c..7f74d8ee6 100644 --- a/cli/packages/cmd/ssh.go +++ b/cli/packages/cmd/ssh.go @@ -631,18 +631,18 @@ func sshConnect(cmd *cobra.Command, args []string) { infisicalToken = loggedInUserDetails.UserCredentials.JTWToken } - writeHostCaToFile, err := cmd.Flags().GetBool("writeHostCaToFile") + writeHostCaToFile, err := cmd.Flags().GetBool("write-host-ca-to-file") if err != nil { - util.HandleError(err, "Unable to parse --writeHostCaToFile flag") + util.HandleError(err, "Unable to parse --write-host-ca-to-file flag") } - outFilePath, err := cmd.Flags().GetString("outFilePath") + outFilePath, err := cmd.Flags().GetString("out-file-path") if err != nil { util.HandleError(err, "Unable to parse flag") } hostname, _ := cmd.Flags().GetString("hostname") - loginUser, _ := cmd.Flags().GetString("loginUser") + loginUser, _ := cmd.Flags().GetString("login-user") var outputDir, privateKeyPath, publicKeyPath, signedKeyPath string if outFilePath != "" { @@ -722,17 +722,24 @@ func sshConnect(cmd *cobra.Command, args []string) { } else { hostNames := make([]string, len(hosts)) for i, h := range hosts { - hostNames[i] = h.Hostname + if h.Alias != "" { + hostNames[i] = h.Alias + } else { + hostNames[i] = h.Hostname + } } + hostPrompt := promptui.Select{ Label: "Select an SSH Host", Items: hostNames, Size: 10, } + hostIdx, _, err := hostPrompt.Run() if err != nil { util.HandleError(err, "Prompt failed") } + selectedHost = hosts[hostIdx] } @@ -893,24 +900,33 @@ func sshAddHost(cmd *cobra.Command, args []string) { util.PrintErrorMessageAndExit("You must provide --hostname") } - writeUserCaToFile, err := cmd.Flags().GetBool("writeUserCaToFile") + alias, err := cmd.Flags().GetString("alias") if err != nil { - util.HandleError(err, "Unable to parse --writeUserCaToFile flag") + util.HandleError(err, "Unable to parse --alias flag") + } + + // if alias == "" { + // util.PrintErrorMessageAndExit("You must provide --alias") + // } + + writeUserCaToFile, err := cmd.Flags().GetBool("write-user-ca-to-file") + if err != nil { + util.HandleError(err, "Unable to parse --write-user-ca-to-file flag") } - userCaOutFilePath, err := cmd.Flags().GetString("userCaOutFilePath") + userCaOutFilePath, err := cmd.Flags().GetString("user-ca-out-file-path") if err != nil { - util.HandleError(err, "Unable to parse --userCaOutFilePath flag") + util.HandleError(err, "Unable to parse --user-ca-out-file-path flag") } - writeHostCertToFile, err := cmd.Flags().GetBool("writeHostCertToFile") + writeHostCertToFile, err := cmd.Flags().GetBool("write-host-cert-to-file") if err != nil { - util.HandleError(err, "Unable to parse --writeHostCertToFile flag") + util.HandleError(err, "Unable to parse --write-host-cert-to-file flag") } - configureSshd, err := cmd.Flags().GetBool("configureSshd") + configureSshd, err := cmd.Flags().GetBool("configure-sshd") if err != nil { - util.HandleError(err, "Unable to parse --configureSshd flag") + util.HandleError(err, "Unable to parse --configure-sshd flag") } forceOverwrite, err := cmd.Flags().GetBool("force") @@ -919,7 +935,7 @@ func sshAddHost(cmd *cobra.Command, args []string) { } if configureSshd && (!writeUserCaToFile || !writeHostCertToFile) { - util.PrintErrorMessageAndExit("--configureSshd requires both --writeUserCaToFile and --writeHostCertToFile to also be set") + util.PrintErrorMessageAndExit("--configure-sshd requires both --write-user-ca-to-file and --write-host-cert-to-file to also be set") } // Pre-check for file overwrites before proceeding @@ -927,7 +943,7 @@ func sshAddHost(cmd *cobra.Command, args []string) { if strings.HasPrefix(userCaOutFilePath, "~") { homeDir, err := os.UserHomeDir() if err != nil { - util.HandleError(err, "Unable to resolve ~ in userCaOutFilePath") + util.HandleError(err, "Unable to resolve ~ in user-ca-out-file-path") } userCaOutFilePath = strings.Replace(userCaOutFilePath, "~", homeDir, 1) } @@ -998,6 +1014,7 @@ func sshAddHost(cmd *cobra.Command, args []string) { host, err := client.Ssh().AddSshHost(infisicalSdk.AddSshHostOptions{ ProjectID: projectId, Hostname: hostname, + Alias: alias, }) if err != nil { util.HandleError(err, "Failed to register SSH host") @@ -1112,11 +1129,12 @@ func init() { 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().String("alias", "", "Alias for the SSH host") sshAddHostCmd.Flags().Bool("write-user-ca-to-file", false, "Write User CA public key to /etc/ssh/infisical_user_ca.pub") sshAddHostCmd.Flags().String("user-ca-out-file-path", "/etc/ssh/infisical_user_ca.pub", "Custom file path to write the User CA public key") sshAddHostCmd.Flags().Bool("write-host-cert-to-file", false, "Write SSH host certificate to /etc/ssh/ssh_host__key-cert.pub") - sshAddHostCmd.Flags().Bool("configure-sshd", false, "Update TrustedUserCAKeys, HostKey, and HostCertificate in the sshd_config file") - sshAddHostCmd.Flags().Bool("force", false, "Force overwrite of existing certificate files as part of writeUserCaToFile and writeHostCertToFile") + sshAddHostCmd.Flags().Bool("configure-sshd", false, "Update `TrustedUserCAKeys`, `HostKey`, and `HostCertificate` in the `/etc/ssh/sshd_config` file") + sshAddHostCmd.Flags().Bool("force", false, "Force overwrite of existing certificate files as part of `--write-user-ca-to-file` and `--write-host-cert-to-file`") sshCmd.AddCommand(sshAddHostCmd) diff --git a/docs/cli/commands/ssh.mdx b/docs/cli/commands/ssh.mdx index d99a69dda..b0988afca 100644 --- a/docs/cli/commands/ssh.mdx +++ b/docs/cli/commands/ssh.mdx @@ -22,15 +22,15 @@ This command enables you to obtain SSH credentials used to access a remote host. The hostname of the SSH host to connect to. If not provided, you will be prompted to select from available hosts. - + The login user for the SSH connection. If not provided, you will be prompted to select from available login users. - + Whether to write the Host CA public key to `~/.ssh/known_hosts` if it doesn't already exist. Default value: `true` - + 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 added to the SSH agent and used to establish an interactive SSH connection. @@ -39,108 +39,52 @@ This command enables you to obtain SSH credentials used to access a remote host. - - This command is used to issue SSH credentials (SSH certificate, public key, and private key) against a certificate template. - - We recommend using the `--addToAgent` flag to automatically load issued SSH credentials to the SSH agent. + + This command is used to register a new SSH host with Infisical. + This command can be used with the `--write-user-ca-to-file`, `--write-host-cert-to-file`, and `--configure-sshd` flags + to also configure the host's SSH daemon with the necessary certificate authority and host certificate settings. + ```bash - $ infisical ssh issue-credentials --certificateTemplateId= --principals= --addToAgent + $ infisical ssh add-host --projectId= --hostname= ``` ### Flags - - The ID of the SSH certificate template to issue SSH credentials for. + + Project ID the host belongs to (required) - - A comma-separated list of principals (i.e. usernames like `ec2-user` or hostnames) to issue SSH credentials for. + + Hostname of the SSH host (required) - - Whether to add issued SSH credentials to the SSH agent. + + Write User CA public key to `/etc/ssh/infisical_user_ca.pub` + + Default value: `false` + + + Custom file path to write the User CA public key + + Default value: `/etc/ssh/infisical_user_ca.pub` + + + Write SSH host certificate to `/etc/ssh/ssh_host__key-cert.pub` + + Default value: `false` + + + Update `TrustedUserCAKeys`, `HostKey`, and `HostCertificate` in the `/etc/ssh/sshd_config` file Default value: `false` - Note that either the `--outFilePath` or `--addToAgent` flag must be set for the sub-command to execute successfully. + Note: This flag requires both --write-user-ca-to-file and --write-host-cert-to-file to be set - - 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 where the command is run. + + Force overwrite of existing certificate files as part of `--write-user-ca-to-file` and `--write-host-cert-to-file` - Note that either the `--outFilePath` or `--addToAgent` flag must be set for the sub-command to execute successfully. - - - The key algorithm to issue SSH credentials for. - - Default value: `RSA_2048` - - Available options: `RSA_2048`, `RSA_4096`, `EC_prime256v1`, `EC_secp384r1`. - - - The certificate type to issue SSH credentials for. - - Default value: `user` - - Available options: `user` or `host` - - - The time-to-live (TTL) for the issued SSH certificate (e.g. `2 days`, `1d`, `2h`, `1y`). - - Defaults to the Default TTL value set in the certificate template. - - - A custom Key ID to issue SSH credentials for. - - Defaults to the autogenerated Key ID by Infisical. + Default value: `false` - An authenticated token to use to issue SSH credentials. - - - - - - This command is used to sign an existing SSH public key against a certificate template; the command outputs the corresponding signed SSH certificate. - - ```bash - $ infisical ssh sign-key --certificateTemplateId= --publicKey= --principals= --outFilePath= - ``` - - The ID of the SSH certificate template to issue the SSH certificate for. - - - The public key to sign. - - Note that either the `--publicKey` or `--publicKeyFilePath` flag must be set for the sub-command to execute successfully. - - - The path to the public key file to sign. - - Note that either the `--publicKey` or `--publicKeyFilePath` flag must be set for the sub-command to execute successfully. - - - A comma-separated list of principals (i.e. usernames like `ec2-user` or hostnames) to issue SSH credentials for. - - - The path to write the SSH certificate to such as `~/.ssh/id_rsa-cert.pub`; the specified file must have the `.pub` extension. If not provided, the credentials will be saved to the directory of the specified `--publicKeyFilePath` or the current working directory where the command is run. - - - The certificate type to issue SSH credentials for. - - Default value: `user` - - Available options: `user` or `host` - - - The time-to-live (TTL) for the issued SSH certificate (e.g. `2 days`, `1d`, `2h`, `1y`). - - Defaults to the Default TTL value set in the certificate template. - - - A custom Key ID to issue SSH credentials for. - - Defaults to the autogenerated Key ID by Infisical. - - - An authenticated token to use to issue SSH credentials. + Use a machine identity access token diff --git a/frontend/src/hooks/api/sshHost/types.ts b/frontend/src/hooks/api/sshHost/types.ts index 4bb61008c..6997d7c60 100644 --- a/frontend/src/hooks/api/sshHost/types.ts +++ b/frontend/src/hooks/api/sshHost/types.ts @@ -2,6 +2,7 @@ export type TSshHost = { id: string; projectId: string; hostname: string; + alias: string | null; userCertTtl: string; hostCertTtl: string; loginMappings: { @@ -15,6 +16,7 @@ export type TSshHost = { export type TCreateSshHostDTO = { projectId: string; hostname: string; + alias: string | null; userCertTtl?: string; hostCertTtl?: string; loginMappings: { @@ -28,6 +30,7 @@ export type TCreateSshHostDTO = { export type TUpdateSshHostDTO = { sshHostId: string; hostname?: string; + alias?: string | null; userCertTtl?: string; hostCertTtl?: string; loginMappings?: { diff --git a/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx b/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx index 249fc1b35..fd0737e32 100644 --- a/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx +++ b/frontend/src/pages/ssh/SshHostsPage/components/SshHostModal.tsx @@ -36,6 +36,7 @@ type Props = { const schema = z .object({ hostname: z.string(), + alias: z.string(), userCertTtl: z .string() .trim() @@ -81,6 +82,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => { resolver: zodResolver(schema), defaultValues: { hostname: "", + alias: "", userCertTtl: "8h", loginMappings: [] } @@ -95,6 +97,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => { if (sshHost) { reset({ hostname: sshHost.hostname, + alias: sshHost.alias ?? "", userCertTtl: sshHost.userCertTtl, loginMappings: sshHost.loginMappings.map(({ loginUser, allowedPrincipals }) => ({ loginUser, @@ -108,13 +111,14 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => { } else { reset({ hostname: "", + alias: "", userCertTtl: "8h", loginMappings: [] }); } }, [sshHost]); - const onFormSubmit = async ({ hostname, userCertTtl, loginMappings }: FormData) => { + const onFormSubmit = async ({ hostname, alias, userCertTtl, loginMappings }: FormData) => { try { if (!projectId) return; @@ -122,7 +126,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => { const existingHostnames = sshHosts?.filter((h) => h.id !== sshHost?.id).map((h) => h.hostname) || []; - if (existingHostnames.includes(hostname)) { + if (existingHostnames.includes(hostname.trim())) { createNotification({ text: "A host with this hostname already exists.", type: "error" @@ -130,10 +134,28 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => { return; } + const processedAlias = alias.trim() || null; + + // check if there is already a different host with the same non-null alias + if (processedAlias) { + const existingAliases = + sshHosts?.filter((h) => h.id !== sshHost?.id && h.alias !== null).map((h) => h.alias) || + []; + + if (existingAliases.includes(processedAlias)) { + createNotification({ + text: "A host with this alias already exists.", + type: "error" + }); + return; + } + } + if (sshHost) { await updateMutateAsync({ sshHostId: sshHost.id, hostname, + alias: processedAlias, userCertTtl, loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({ loginUser, @@ -146,6 +168,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => { await createMutateAsync({ projectId, hostname, + alias: processedAlias, userCertTtl, loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({ loginUser, @@ -209,6 +232,16 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => { )} /> + ( + + + + )} + /> { +
Alias Hostname Login User - Authorized Principals Mapping @@ -83,6 +84,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => { className="h-10" key={`ssh-host-${host.id}`} > + {host.alias ?? "-"} {host.hostname} {host.loginMappings.length === 0 ? (