Add alias field to ssh hosts for improved ux

This commit is contained in:
Tuan Dang
2025-04-26 18:04:21 -07:00
parent f93edbb37f
commit de63c8cb6c
16 changed files with 182 additions and 117 deletions
@@ -0,0 +1,23 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
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");
});
}
}
+2 -1
View File
@@ -16,7 +16,8 @@ export const SshHostsSchema = z.object({
userCertTtl: z.string(), userCertTtl: z.string(),
hostCertTtl: z.string(), hostCertTtl: z.string(),
userSshCaId: z.string().uuid(), userSshCaId: z.string().uuid(),
hostSshCaId: z.string().uuid() hostSshCaId: z.string().uuid(),
alias: z.string().nullable().optional()
}); });
export type TSshHosts = z.infer<typeof SshHostsSchema>; export type TSshHosts = z.infer<typeof SshHostsSchema>;
@@ -1,3 +1,4 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
@@ -96,10 +97,20 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
hostname: z hostname: z
.string() .string()
.min(1) .min(1)
.trim()
.refine((v) => isValidHostname(v), { .refine((v) => isValidHostname(v), {
message: "Hostname must be a valid hostname" message: "Hostname must be a valid hostname"
}) })
.describe(SSH_HOSTS.CREATE.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 userCertTtl: z
.string() .string()
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => ms(val) > 0, "TTL must be a positive number")
@@ -138,6 +149,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
metadata: { metadata: {
sshHostId: host.id, sshHostId: host.id,
hostname: host.hostname, hostname: host.hostname,
alias: host.alias ?? null,
userCertTtl: host.userCertTtl, userCertTtl: host.userCertTtl,
hostCertTtl: host.hostCertTtl, hostCertTtl: host.hostCertTtl,
loginMappings: host.loginMappings, loginMappings: host.loginMappings,
@@ -166,12 +178,22 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
hostname: z hostname: z
.string() .string()
.trim()
.min(1) .min(1)
.refine((v) => isValidHostname(v), { .refine((v) => isValidHostname(v), {
message: "Hostname must be a valid hostname" message: "Hostname must be a valid hostname"
}) })
.optional() .optional()
.describe(SSH_HOSTS.UPDATE.hostname), .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 userCertTtl: z
.string() .string()
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => ms(val) > 0, "TTL must be a positive number")
@@ -208,6 +230,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
metadata: { metadata: {
sshHostId: host.id, sshHostId: host.id,
hostname: host.hostname, hostname: host.hostname,
alias: host.alias,
userCertTtl: host.userCertTtl, userCertTtl: host.userCertTtl,
hostCertTtl: host.hostCertTtl, hostCertTtl: host.hostCertTtl,
loginMappings: host.loginMappings, loginMappings: host.loginMappings,
@@ -1494,6 +1494,7 @@ interface CreateSshHost {
metadata: { metadata: {
sshHostId: string; sshHostId: string;
hostname: string; hostname: string;
alias: string | null;
userCertTtl: string; userCertTtl: string;
hostCertTtl: string; hostCertTtl: string;
loginMappings: { loginMappings: {
@@ -1512,6 +1513,7 @@ interface UpdateSshHost {
metadata: { metadata: {
sshHostId: string; sshHostId: string;
hostname?: string; hostname?: string;
alias?: string | null;
userCertTtl?: string; userCertTtl?: string;
hostCertTtl?: string; hostCertTtl?: string;
loginMappings?: { loginMappings?: {
@@ -33,6 +33,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"), db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
db.ref("projectId").withSchema(TableName.SshHost), db.ref("projectId").withSchema(TableName.SshHost),
db.ref("hostname").withSchema(TableName.SshHost), db.ref("hostname").withSchema(TableName.SshHost),
db.ref("alias").withSchema(TableName.SshHost),
db.ref("userCertTtl").withSchema(TableName.SshHost), db.ref("userCertTtl").withSchema(TableName.SshHost),
db.ref("hostCertTtl").withSchema(TableName.SshHost), db.ref("hostCertTtl").withSchema(TableName.SshHost),
db.ref("loginUser").withSchema(TableName.SshHostLoginUser), db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
@@ -45,7 +46,8 @@ export const sshHostDALFactory = (db: TDbClient) => {
const grouped = groupBy(rows, (r) => r.sshHostId); const grouped = groupBy(rows, (r) => r.sshHostId);
return Object.values(grouped).map((hostRows) => { 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); const loginMappingGrouped = groupBy(hostRows, (r) => r.loginUser);
@@ -59,6 +61,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
return { return {
id: sshHostId, id: sshHostId,
hostname, hostname,
alias,
projectId, projectId,
userCertTtl, userCertTtl,
hostCertTtl, hostCertTtl,
@@ -87,6 +90,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"), db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
db.ref("projectId").withSchema(TableName.SshHost), db.ref("projectId").withSchema(TableName.SshHost),
db.ref("hostname").withSchema(TableName.SshHost), db.ref("hostname").withSchema(TableName.SshHost),
db.ref("alias").withSchema(TableName.SshHost),
db.ref("userCertTtl").withSchema(TableName.SshHost), db.ref("userCertTtl").withSchema(TableName.SshHost),
db.ref("hostCertTtl").withSchema(TableName.SshHost), db.ref("hostCertTtl").withSchema(TableName.SshHost),
db.ref("loginUser").withSchema(TableName.SshHostLoginUser), db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
@@ -99,7 +103,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
const hostsGrouped = groupBy(rows, (r) => r.sshHostId); const hostsGrouped = groupBy(rows, (r) => r.sshHostId);
return Object.values(hostsGrouped).map((hostRows) => { 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( const loginMappingGrouped = groupBy(
hostRows.filter((r) => r.loginUser), hostRows.filter((r) => r.loginUser),
@@ -116,6 +120,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
return { return {
id: sshHostId, id: sshHostId,
hostname, hostname,
alias,
projectId, projectId,
userCertTtl, userCertTtl,
hostCertTtl, hostCertTtl,
@@ -144,6 +149,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"), db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
db.ref("projectId").withSchema(TableName.SshHost), db.ref("projectId").withSchema(TableName.SshHost),
db.ref("hostname").withSchema(TableName.SshHost), db.ref("hostname").withSchema(TableName.SshHost),
db.ref("alias").withSchema(TableName.SshHost),
db.ref("userCertTtl").withSchema(TableName.SshHost), db.ref("userCertTtl").withSchema(TableName.SshHost),
db.ref("hostCertTtl").withSchema(TableName.SshHost), db.ref("hostCertTtl").withSchema(TableName.SshHost),
db.ref("loginUser").withSchema(TableName.SshHostLoginUser), db.ref("loginUser").withSchema(TableName.SshHostLoginUser),
@@ -155,7 +161,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
if (rows.length === 0) return null; 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( const loginMappingGrouped = groupBy(
rows.filter((r) => r.loginUser), rows.filter((r) => r.loginUser),
@@ -173,6 +179,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
id, id,
projectId, projectId,
hostname, hostname,
alias,
userCertTtl, userCertTtl,
hostCertTtl, hostCertTtl,
loginMappings, loginMappings,
@@ -6,6 +6,7 @@ export const sanitizedSshHost = SshHostsSchema.pick({
id: true, id: true,
projectId: true, projectId: true,
hostname: true, hostname: true,
alias: true,
userCertTtl: true, userCertTtl: true,
hostCertTtl: true, hostCertTtl: true,
userSshCaId: true, userSshCaId: true,
@@ -119,6 +119,7 @@ export const sshHostServiceFactory = ({
const createSshHost = async ({ const createSshHost = async ({
projectId, projectId,
hostname, hostname,
alias,
userCertTtl, userCertTtl,
hostCertTtl, hostCertTtl,
loginMappings, loginMappings,
@@ -192,6 +193,7 @@ export const sshHostServiceFactory = ({
{ {
projectId, projectId,
hostname, hostname,
alias,
userCertTtl, userCertTtl,
hostCertTtl, hostCertTtl,
userSshCaId, userSshCaId,
@@ -265,6 +267,7 @@ export const sshHostServiceFactory = ({
const updateSshHost = async ({ const updateSshHost = async ({
sshHostId, sshHostId,
hostname, hostname,
alias,
userCertTtl, userCertTtl,
hostCertTtl, hostCertTtl,
loginMappings, loginMappings,
@@ -297,6 +300,7 @@ export const sshHostServiceFactory = ({
sshHostId, sshHostId,
{ {
hostname, hostname,
alias,
userCertTtl, userCertTtl,
hostCertTtl hostCertTtl
}, },
@@ -4,6 +4,7 @@ export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
export type TCreateSshHostDTO = { export type TCreateSshHostDTO = {
hostname: string; hostname: string;
alias: string | null;
userCertTtl: string; userCertTtl: string;
hostCertTtl: string; hostCertTtl: string;
loginMappings: { loginMappings: {
@@ -19,6 +20,7 @@ export type TCreateSshHostDTO = {
export type TUpdateSshHostDTO = { export type TUpdateSshHostDTO = {
sshHostId: string; sshHostId: string;
hostname?: string; hostname?: string;
alias?: string | null;
userCertTtl?: string; userCertTtl?: string;
hostCertTtl?: string; hostCertTtl?: string;
loginMappings?: { loginMappings?: {
+2
View File
@@ -1387,6 +1387,7 @@ export const SSH_HOSTS = {
CREATE: { CREATE: {
projectId: "The ID of the project to create the SSH host in.", projectId: "The ID of the project to create the SSH host in.",
hostname: "The hostname of the SSH host.", 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.", userCertTtl: "The time to live for user certificates issued under this host.",
hostCertTtl: "The time to live for host 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')", loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
@@ -1401,6 +1402,7 @@ export const SSH_HOSTS = {
UPDATE: { UPDATE: {
sshHostId: "The ID of the SSH host to update.", sshHostId: "The ID of the SSH host to update.",
hostname: "The hostname of the SSH host to update to.", 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.", 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.", 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')", loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
+1 -1
View File
@@ -12,7 +12,7 @@ require (
github.com/fatih/semgroup v1.2.0 github.com/fatih/semgroup v1.2.0
github.com/gitleaks/go-gitdiff v0.8.0 github.com/gitleaks/go-gitdiff v0.8.0
github.com/h2non/filetype v1.1.3 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/infisical/infisical-kmip v0.3.5
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
+2 -2
View File
@@ -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/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 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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.92 h1:PoCnVndrd6Dbkipuxl9fFiwlD5vCKsabtQo09mo8lUE=
github.com/infisical/go-sdk v0.5.8/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= 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 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs= github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
+35 -17
View File
@@ -631,18 +631,18 @@ func sshConnect(cmd *cobra.Command, args []string) {
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
} }
writeHostCaToFile, err := cmd.Flags().GetBool("writeHostCaToFile") writeHostCaToFile, err := cmd.Flags().GetBool("write-host-ca-to-file")
if err != nil { 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 { if err != nil {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
hostname, _ := cmd.Flags().GetString("hostname") hostname, _ := cmd.Flags().GetString("hostname")
loginUser, _ := cmd.Flags().GetString("loginUser") loginUser, _ := cmd.Flags().GetString("login-user")
var outputDir, privateKeyPath, publicKeyPath, signedKeyPath string var outputDir, privateKeyPath, publicKeyPath, signedKeyPath string
if outFilePath != "" { if outFilePath != "" {
@@ -722,17 +722,24 @@ func sshConnect(cmd *cobra.Command, args []string) {
} else { } else {
hostNames := make([]string, len(hosts)) hostNames := make([]string, len(hosts))
for i, h := range 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{ hostPrompt := promptui.Select{
Label: "Select an SSH Host", Label: "Select an SSH Host",
Items: hostNames, Items: hostNames,
Size: 10, Size: 10,
} }
hostIdx, _, err := hostPrompt.Run() hostIdx, _, err := hostPrompt.Run()
if err != nil { if err != nil {
util.HandleError(err, "Prompt failed") util.HandleError(err, "Prompt failed")
} }
selectedHost = hosts[hostIdx] selectedHost = hosts[hostIdx]
} }
@@ -893,24 +900,33 @@ func sshAddHost(cmd *cobra.Command, args []string) {
util.PrintErrorMessageAndExit("You must provide --hostname") util.PrintErrorMessageAndExit("You must provide --hostname")
} }
writeUserCaToFile, err := cmd.Flags().GetBool("writeUserCaToFile") alias, err := cmd.Flags().GetString("alias")
if err != nil { 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 { 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 { 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 { 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") forceOverwrite, err := cmd.Flags().GetBool("force")
@@ -919,7 +935,7 @@ func sshAddHost(cmd *cobra.Command, args []string) {
} }
if configureSshd && (!writeUserCaToFile || !writeHostCertToFile) { 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 // Pre-check for file overwrites before proceeding
@@ -927,7 +943,7 @@ func sshAddHost(cmd *cobra.Command, args []string) {
if strings.HasPrefix(userCaOutFilePath, "~") { if strings.HasPrefix(userCaOutFilePath, "~") {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { 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) userCaOutFilePath = strings.Replace(userCaOutFilePath, "~", homeDir, 1)
} }
@@ -998,6 +1014,7 @@ func sshAddHost(cmd *cobra.Command, args []string) {
host, err := client.Ssh().AddSshHost(infisicalSdk.AddSshHostOptions{ host, err := client.Ssh().AddSshHost(infisicalSdk.AddSshHostOptions{
ProjectID: projectId, ProjectID: projectId,
Hostname: hostname, Hostname: hostname,
Alias: alias,
}) })
if err != nil { if err != nil {
util.HandleError(err, "Failed to register SSH host") 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("token", "", "Use a machine identity access token")
sshAddHostCmd.Flags().String("projectId", "", "Project ID the host belongs to (required)") sshAddHostCmd.Flags().String("projectId", "", "Project ID the host belongs to (required)")
sshAddHostCmd.Flags().String("hostname", "", "Hostname of the SSH host (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().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().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_<type>_key-cert.pub") sshAddHostCmd.Flags().Bool("write-host-cert-to-file", false, "Write SSH host certificate to /etc/ssh/ssh_host_<type>_key-cert.pub")
sshAddHostCmd.Flags().Bool("configure-sshd", false, "Update TrustedUserCAKeys, HostKey, and HostCertificate in the sshd_config file") 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 writeUserCaToFile and writeHostCertToFile") 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) sshCmd.AddCommand(sshAddHostCmd)
+35 -91
View File
@@ -22,15 +22,15 @@ This command enables you to obtain SSH credentials used to access a remote host.
<Accordion title="--hostname"> <Accordion title="--hostname">
The hostname of the SSH host to connect to. If not provided, you will be prompted to select from available hosts. The hostname of the SSH host to connect to. If not provided, you will be prompted to select from available hosts.
</Accordion> </Accordion>
<Accordion title="--loginUser"> <Accordion title="--login-user">
The login user for the SSH connection. If not provided, you will be prompted to select from available login users. The login user for the SSH connection. If not provided, you will be prompted to select from available login users.
</Accordion> </Accordion>
<Accordion title="--writeHostCaToFile"> <Accordion title="--write-host-ca-to-file">
Whether to write the Host CA public key to `~/.ssh/known_hosts` if it doesn't already exist. Whether to write the Host CA public key to `~/.ssh/known_hosts` if it doesn't already exist.
Default value: `true` Default value: `true`
</Accordion> </Accordion>
<Accordion title="--outFilePath"> <Accordion title="--out-file-path">
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. 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.
</Accordion> </Accordion>
<Accordion title="--token"> <Accordion title="--token">
@@ -39,108 +39,52 @@ This command enables you to obtain SSH credentials used to access a remote host.
</Accordion> </Accordion>
<Accordion title="infisical ssh issue-credentials"> <Accordion title="infisical ssh add-host">
This command is used to issue SSH credentials (SSH certificate, public key, and private key) against a certificate template. This command is used to register a new SSH host with Infisical.
We recommend using the `--addToAgent` flag to automatically load issued SSH credentials to the SSH agent.
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 ```bash
$ infisical ssh issue-credentials --certificateTemplateId=<certificate-template-id> --principals=<principals> --addToAgent $ infisical ssh add-host --projectId=<project-id> --hostname=<hostname>
``` ```
### Flags ### Flags
<Accordion title="--certificateTemplateId"> <Accordion title="--projectId">
The ID of the SSH certificate template to issue SSH credentials for. Project ID the host belongs to (required)
</Accordion> </Accordion>
<Accordion title="--principals"> <Accordion title="--hostname">
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)
</Accordion> </Accordion>
<Accordion title="--addToAgent"> <Accordion title="--write-user-ca-to-file">
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`
</Accordion>
<Accordion title="--user-ca-out-file-path">
Custom file path to write the User CA public key
Default value: `/etc/ssh/infisical_user_ca.pub`
</Accordion>
<Accordion title="--write-host-cert-to-file">
Write SSH host certificate to `/etc/ssh/ssh_host_<type>_key-cert.pub`
Default value: `false`
</Accordion>
<Accordion title="--configure-sshd">
Update `TrustedUserCAKeys`, `HostKey`, and `HostCertificate` in the `/etc/ssh/sshd_config` file
Default value: `false` 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
</Accordion> </Accordion>
<Accordion title="--outFilePath"> <Accordion title="--force">
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. Default value: `false`
</Accordion>
<Accordion title="--keyAlgorithm">
The key algorithm to issue SSH credentials for.
Default value: `RSA_2048`
Available options: `RSA_2048`, `RSA_4096`, `EC_prime256v1`, `EC_secp384r1`.
</Accordion>
<Accordion title="--certType">
The certificate type to issue SSH credentials for.
Default value: `user`
Available options: `user` or `host`
</Accordion>
<Accordion title="--ttl">
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.
</Accordion>
<Accordion title="--keyId">
A custom Key ID to issue SSH credentials for.
Defaults to the autogenerated Key ID by Infisical.
</Accordion> </Accordion>
<Accordion title="--token"> <Accordion title="--token">
An authenticated token to use to issue SSH credentials. Use a machine identity access token
</Accordion>
</Accordion>
<Accordion title="infisical ssh sign-key">
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=<certificate-template-id> --publicKey=<public-key> --principals=<principals> --outFilePath=<out-file-path>
```
<Accordion title="--certificateTemplateId">
The ID of the SSH certificate template to issue the SSH certificate for.
</Accordion>
<Accordion title="--publicKey">
The public key to sign.
Note that either the `--publicKey` or `--publicKeyFilePath` flag must be set for the sub-command to execute successfully.
</Accordion>
<Accordion title="--publicKeyFilePath">
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.
</Accordion>
<Accordion title="--principals">
A comma-separated list of principals (i.e. usernames like `ec2-user` or hostnames) to issue SSH credentials for.
</Accordion>
<Accordion title="--outFilePath">
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.
</Accordion>
<Accordion title="--certType">
The certificate type to issue SSH credentials for.
Default value: `user`
Available options: `user` or `host`
</Accordion>
<Accordion title="--ttl">
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.
</Accordion>
<Accordion title="--keyId">
A custom Key ID to issue SSH credentials for.
Defaults to the autogenerated Key ID by Infisical.
</Accordion>
<Accordion title="--token">
An authenticated token to use to issue SSH credentials.
</Accordion> </Accordion>
</Accordion> </Accordion>
+3
View File
@@ -2,6 +2,7 @@ export type TSshHost = {
id: string; id: string;
projectId: string; projectId: string;
hostname: string; hostname: string;
alias: string | null;
userCertTtl: string; userCertTtl: string;
hostCertTtl: string; hostCertTtl: string;
loginMappings: { loginMappings: {
@@ -15,6 +16,7 @@ export type TSshHost = {
export type TCreateSshHostDTO = { export type TCreateSshHostDTO = {
projectId: string; projectId: string;
hostname: string; hostname: string;
alias: string | null;
userCertTtl?: string; userCertTtl?: string;
hostCertTtl?: string; hostCertTtl?: string;
loginMappings: { loginMappings: {
@@ -28,6 +30,7 @@ export type TCreateSshHostDTO = {
export type TUpdateSshHostDTO = { export type TUpdateSshHostDTO = {
sshHostId: string; sshHostId: string;
hostname?: string; hostname?: string;
alias?: string | null;
userCertTtl?: string; userCertTtl?: string;
hostCertTtl?: string; hostCertTtl?: string;
loginMappings?: { loginMappings?: {
@@ -36,6 +36,7 @@ type Props = {
const schema = z const schema = z
.object({ .object({
hostname: z.string(), hostname: z.string(),
alias: z.string(),
userCertTtl: z userCertTtl: z
.string() .string()
.trim() .trim()
@@ -81,6 +82,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
hostname: "", hostname: "",
alias: "",
userCertTtl: "8h", userCertTtl: "8h",
loginMappings: [] loginMappings: []
} }
@@ -95,6 +97,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
if (sshHost) { if (sshHost) {
reset({ reset({
hostname: sshHost.hostname, hostname: sshHost.hostname,
alias: sshHost.alias ?? "",
userCertTtl: sshHost.userCertTtl, userCertTtl: sshHost.userCertTtl,
loginMappings: sshHost.loginMappings.map(({ loginUser, allowedPrincipals }) => ({ loginMappings: sshHost.loginMappings.map(({ loginUser, allowedPrincipals }) => ({
loginUser, loginUser,
@@ -108,13 +111,14 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
} else { } else {
reset({ reset({
hostname: "", hostname: "",
alias: "",
userCertTtl: "8h", userCertTtl: "8h",
loginMappings: [] loginMappings: []
}); });
} }
}, [sshHost]); }, [sshHost]);
const onFormSubmit = async ({ hostname, userCertTtl, loginMappings }: FormData) => { const onFormSubmit = async ({ hostname, alias, userCertTtl, loginMappings }: FormData) => {
try { try {
if (!projectId) return; if (!projectId) return;
@@ -122,7 +126,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
const existingHostnames = const existingHostnames =
sshHosts?.filter((h) => h.id !== sshHost?.id).map((h) => h.hostname) || []; sshHosts?.filter((h) => h.id !== sshHost?.id).map((h) => h.hostname) || [];
if (existingHostnames.includes(hostname)) { if (existingHostnames.includes(hostname.trim())) {
createNotification({ createNotification({
text: "A host with this hostname already exists.", text: "A host with this hostname already exists.",
type: "error" type: "error"
@@ -130,10 +134,28 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
return; 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) { if (sshHost) {
await updateMutateAsync({ await updateMutateAsync({
sshHostId: sshHost.id, sshHostId: sshHost.id,
hostname, hostname,
alias: processedAlias,
userCertTtl, userCertTtl,
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({ loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
loginUser, loginUser,
@@ -146,6 +168,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
await createMutateAsync({ await createMutateAsync({
projectId, projectId,
hostname, hostname,
alias: processedAlias,
userCertTtl, userCertTtl,
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({ loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
loginUser, loginUser,
@@ -209,6 +232,16 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
</FormControl> </FormControl>
)} )}
/> />
<Controller
control={control}
defaultValue=""
name="alias"
render={({ field, fieldState: { error } }) => (
<FormControl label="Alias" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="host" />
</FormControl>
)}
/>
<Controller <Controller
control={control} control={control}
defaultValue="" defaultValue=""
@@ -66,6 +66,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
<Table> <Table>
<THead> <THead>
<Tr> <Tr>
<Th>Alias</Th>
<Th>Hostname</Th> <Th>Hostname</Th>
<Th>Login User - Authorized Principals Mapping</Th> <Th>Login User - Authorized Principals Mapping</Th>
<Th /> <Th />
@@ -83,6 +84,7 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
className="h-10" className="h-10"
key={`ssh-host-${host.id}`} key={`ssh-host-${host.id}`}
> >
<Td>{host.alias ?? "-"}</Td>
<Td>{host.hostname}</Td> <Td>{host.hostname}</Td>
<Td> <Td>
{host.loginMappings.length === 0 ? ( {host.loginMappings.length === 0 ? (