Add ssh host endpoint for issuing ssh host cert

This commit is contained in:
Tuan Dang
2025-04-07 20:47:52 -07:00
parent eb4816fd29
commit 99aa567a6f
39 changed files with 686 additions and 258 deletions
@@ -47,6 +47,14 @@ export async function up(knex: Knex): Promise<void> {
}); });
await createOnUpdateTrigger(knex, TableName.ProjectSshConfig); await createOnUpdateTrigger(knex, TableName.ProjectSshConfig);
} }
const hasColumn = await knex.schema.hasColumn(TableName.SshCertificate, "sshHostId");
if (!hasColumn) {
await knex.schema.alterTable(TableName.SshCertificate, (t) => {
t.uuid("sshHostId").nullable();
t.foreign("sshHostId").references("id").inTable(TableName.SshHost).onDelete("SET NULL");
});
}
} }
export async function down(knex: Knex): Promise<void> { export async function down(knex: Knex): Promise<void> {
@@ -58,4 +66,11 @@ export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.SshHost); await knex.schema.dropTableIfExists(TableName.SshHost);
await dropOnUpdateTrigger(knex, TableName.SshHost); await dropOnUpdateTrigger(knex, TableName.SshHost);
const hasColumn = await knex.schema.hasColumn(TableName.SshCertificate, "sshHostId");
if (hasColumn) {
await knex.schema.alterTable(TableName.SshCertificate, (t) => {
t.dropColumn("sshHostId");
});
}
} }
+2 -1
View File
@@ -18,7 +18,8 @@ export const SshCertificatesSchema = z.object({
principals: z.string().array(), principals: z.string().array(),
keyId: z.string(), keyId: z.string(),
notBefore: z.date(), notBefore: z.date(),
notAfter: z.date() notAfter: z.date(),
sshHostId: z.string().uuid().nullable().optional()
}); });
export type TSshCertificates = z.infer<typeof SshCertificatesSchema>; export type TSshCertificates = z.infer<typeof SshCertificatesSchema>;
+180 -99
View File
@@ -1,13 +1,16 @@
import { z } from "zod"; import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
import { sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema"; import { sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
import { isValidHostname } from "@app/ee/services/ssh-host/ssh-host-validators"; import { isValidHostname } from "@app/ee/services/ssh-host/ssh-host-validators";
import { SSH_HOSTS } from "@app/lib/api-docs"; import { SSH_HOSTS } from "@app/lib/api-docs";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { publicSshCaLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
export const registerSshHostRouter = async (server: FastifyZodProvider) => { export const registerSshHostRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -38,18 +41,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId
}); });
// TODO: consider adding audit log
// TODO: audit log
// await server.services.auditLog.createAuditLog({
// ...req.auditLogInfo,
// projectId: certificateTemplate.projectId,
// event: {
// type: EventType.GET_SSH_CERTIFICATE_TEMPLATE,
// metadata: {
// certificateTemplateId: certificateTemplate.id
// }
// }
// });
return hosts; return hosts;
} }
@@ -86,17 +78,17 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId
}); });
// TODO: audit log await server.services.auditLog.createAuditLog({
// await server.services.auditLog.createAuditLog({ ...req.auditLogInfo,
// ...req.auditLogInfo, projectId: host.projectId,
// projectId: certificateTemplate.projectId, event: {
// event: { type: EventType.GET_SSH_HOST,
// type: EventType.GET_SSH_CERTIFICATE_TEMPLATE, metadata: {
// metadata: { sshHostId: host.id,
// certificateTemplateId: certificateTemplate.id hostname: host.hostname
// } }
// } }
// }); });
return host; return host;
} }
@@ -161,26 +153,22 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId
}); });
// TODO: audit log await server.services.auditLog.createAuditLog({
// await server.services.auditLog.createAuditLog({ ...req.auditLogInfo,
// ...req.auditLogInfo, projectId: host.projectId,
// projectId: ca.projectId, event: {
// event: { type: EventType.CREATE_SSH_HOST,
// type: EventType.CREATE_SSH_CERTIFICATE_TEMPLATE, metadata: {
// metadata: { sshHostId: host.id,
// certificateTemplateId: certificateTemplate.id, hostname: host.hostname,
// sshCaId: ca.id, userCertTtl: host.userCertTtl,
// name: certificateTemplate.name, hostCertTtl: host.hostCertTtl,
// ttl: certificateTemplate.ttl, loginMappings: host.loginMappings,
// maxTTL: certificateTemplate.maxTTL, userSshCaId: host.userSshCaId,
// allowedUsers: certificateTemplate.allowedUsers, hostSshCaId: host.hostSshCaId
// allowedHosts: certificateTemplate.allowedHosts, }
// allowUserCertificates: certificateTemplate.allowUserCertificates, }
// allowHostCertificates: certificateTemplate.allowHostCertificates, });
// allowCustomKeyIds: certificateTemplate.allowCustomKeyIds
// }
// }
// });
return host; return host;
} }
@@ -205,16 +193,17 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
.refine((v) => isValidHostname(v), { .refine((v) => isValidHostname(v), {
message: "Hostname must be a valid hostname" message: "Hostname must be a valid hostname"
}) })
.optional()
.describe(SSH_HOSTS.UPDATE.hostname), .describe(SSH_HOSTS.UPDATE.hostname),
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")
.default("1h") .optional()
.describe(SSH_HOSTS.UPDATE.userCertTtl), .describe(SSH_HOSTS.UPDATE.userCertTtl),
hostCertTtl: z hostCertTtl: 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")
.default("1y") .optional()
.describe(SSH_HOSTS.UPDATE.hostCertTtl), .describe(SSH_HOSTS.UPDATE.hostCertTtl),
loginMappings: z loginMappings: z
.object({ .object({
@@ -246,19 +235,22 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
...req.body ...req.body
}); });
// TODO: audit log await server.services.auditLog.createAuditLog({
// await server.services.auditLog.createAuditLog({ ...req.auditLogInfo,
// ...req.auditLogInfo, projectId: host.projectId,
// projectId: ca.projectId, event: {
// event: { type: EventType.UPDATE_SSH_HOST,
// type: EventType.UPDATE_SSH_CA, metadata: {
// metadata: { sshHostId: host.id,
// sshCaId: ca.id, hostname: host.hostname,
// friendlyName: ca.friendlyName, userCertTtl: host.userCertTtl,
// status: ca.status as SshCaStatus hostCertTtl: host.hostCertTtl,
// } loginMappings: host.loginMappings,
// } userSshCaId: host.userSshCaId,
// }); hostSshCaId: host.hostSshCaId
}
}
});
return host; return host;
} }
@@ -295,17 +287,17 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId
}); });
// TODO: audit log await server.services.auditLog.createAuditLog({
// await server.services.auditLog.createAuditLog({ ...req.auditLogInfo,
// ...req.auditLogInfo, projectId: host.projectId,
// projectId: certificateTemplate.projectId, event: {
// event: { type: EventType.DELETE_SSH_HOST,
// type: EventType.DELETE_SSH_CERTIFICATE_TEMPLATE, metadata: {
// metadata: { sshHostId: host.id,
// certificateTemplateId: certificateTemplate.id hostname: host.hostname
// } }
// } }
// }); });
return host; return host;
} }
@@ -313,7 +305,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "POST", method: "POST",
url: "/:sshHostId/issue", url: "/:sshHostId/issue-user-cert",
config: { config: {
rateLimit: writeLimit rateLimit: writeLimit
}, },
@@ -321,7 +313,10 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
description: "Issue SSH credentials (certificate + key)", description: "Issue SSH credentials (certificate + key)",
params: z.object({ params: z.object({
sshHostId: z.string().describe(SSH_HOSTS.DELETE.sshHostId) sshHostId: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.sshHostId)
}),
body: z.object({
loginUser: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.loginUser)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -334,41 +329,41 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
} }
}, },
handler: async (req) => { handler: async (req) => {
const { serialNumber, signedPublicKey, privateKey, publicKey, keyAlgorithm } = const { serialNumber, signedPublicKey, privateKey, publicKey, keyAlgorithm, host, principals } =
await server.services.sshHost.issueSshCredsFromHost({ await server.services.sshHost.issueSshHostUserCert({
sshHostId: req.params.sshHostId, sshHostId: req.params.sshHostId,
loginUser: req.body.loginUser,
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId
}); });
// TODO: add audit log await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.ISSUE_SSH_HOST_USER_CERT,
metadata: {
sshHostId: req.params.sshHostId,
hostname: host.hostname,
loginUser: req.body.loginUser,
principals,
ttl: host.userCertTtl
}
}
});
// await server.services.auditLog.createAuditLog({ await server.services.telemetry.sendPostHogEvents({
// ...req.auditLogInfo, event: PostHogEventTypes.IssueSshHostUserCert,
// orgId: req.permission.orgId, distinctId: getTelemetryDistinctId(req),
// event: { properties: {
// type: EventType.ISSUE_SSH_CREDS, sshHostId: req.params.sshHostId,
// metadata: { hostname: host.hostname,
// certificateTemplateId: certificateTemplate.id, principals,
// keyAlgorithm: req.body.keyAlgorithm, ...req.auditLogInfo
// certType: req.body.certType, }
// principals: req.body.principals, });
// ttl: String(ttl),
// keyId
// }
// }
// });
// await server.services.telemetry.sendPostHogEvents({
// event: PostHogEventTypes.IssueSshCreds,
// distinctId: getTelemetryDistinctId(req),
// properties: {
// certificateTemplateId: req.body.certificateTemplateId,
// principals: req.body.principals,
// ...req.auditLogInfo
// }
// });
return { return {
serialNumber, serialNumber,
@@ -379,4 +374,90 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
}; };
} }
}); });
server.route({
method: "POST",
url: "/:sshHostId/issue-host-cert",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Issue SSH certificate for host",
params: z.object({
sshHostId: z.string().describe(SSH_HOSTS.DELETE.sshHostId)
}),
body: z.object({
publicKey: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.publicKey)
}),
response: {
200: z.object({
serialNumber: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.serialNumber),
signedKey: z.string().describe(SSH_HOSTS.ISSUE_HOST_CERT.signedKey)
})
}
},
handler: async (req) => {
const { host, principals, serialNumber, signedPublicKey } = await server.services.sshHost.issueSshHostHostCert({
sshHostId: req.params.sshHostId,
publicKey: req.body.publicKey,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.ISSUE_SSH_HOST_HOST_CERT,
metadata: {
sshHostId: req.params.sshHostId,
hostname: host.hostname,
principals,
serialNumber,
ttl: host.hostCertTtl
}
}
});
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.IssueSshHostHostCert,
distinctId: getTelemetryDistinctId(req),
properties: {
sshHostId: req.params.sshHostId,
hostname: host.hostname,
principals,
...req.auditLogInfo
}
});
return {
serialNumber,
signedKey: signedPublicKey
};
}
});
server.route({
method: "GET",
url: "/:sshHostId/user-ca-public-key",
config: {
rateLimit: publicSshCaLimit
},
schema: {
description: "Get public key of the user SSH CA linked to the host",
params: z.object({
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.sshHostId)
}),
response: {
200: z.string()
}
},
handler: async (req) => {
const publicKey = await server.services.sshHost.getSshHostUserCaPk(req.params.sshHostId);
return publicKey;
}
});
}; };
@@ -181,6 +181,12 @@ export enum EventType {
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template", UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template", DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template", GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
CREATE_SSH_HOST = "create-ssh-host",
UPDATE_SSH_HOST = "update-ssh-host",
DELETE_SSH_HOST = "delete-ssh-host",
GET_SSH_HOST = "get-ssh-host",
ISSUE_SSH_HOST_USER_CERT = "issue-ssh-host-user-cert",
ISSUE_SSH_HOST_HOST_CERT = "issue-ssh-host-host-cert",
CREATE_CA = "create-certificate-authority", CREATE_CA = "create-certificate-authority",
GET_CA = "get-certificate-authority", GET_CA = "get-certificate-authority",
UPDATE_CA = "update-certificate-authority", UPDATE_CA = "update-certificate-authority",
@@ -1455,6 +1461,76 @@ interface DeleteSshCertificateTemplate {
}; };
} }
interface CreateSshHost {
type: EventType.CREATE_SSH_HOST;
metadata: {
sshHostId: string;
hostname: string;
userCertTtl: string;
hostCertTtl: string;
loginMappings: {
loginUser: string;
allowedPrincipals: string[];
}[];
userSshCaId: string;
hostSshCaId: string;
};
}
interface UpdateSshHost {
type: EventType.UPDATE_SSH_HOST;
metadata: {
sshHostId: string;
hostname?: string;
userCertTtl?: string;
hostCertTtl?: string;
loginMappings?: {
loginUser: string;
allowedPrincipals: string[];
}[];
userSshCaId?: string;
hostSshCaId?: string;
};
}
interface DeleteSshHost {
type: EventType.DELETE_SSH_HOST;
metadata: {
sshHostId: string;
hostname: string;
};
}
interface GetSshHost {
type: EventType.GET_SSH_HOST;
metadata: {
sshHostId: string;
hostname: string;
};
}
interface IssueSshHostUserCert {
type: EventType.ISSUE_SSH_HOST_USER_CERT;
metadata: {
sshHostId: string;
hostname: string;
loginUser: string;
principals: string[];
ttl: string;
};
}
interface IssueSshHostHostCert {
type: EventType.ISSUE_SSH_HOST_HOST_CERT;
metadata: {
sshHostId: string;
hostname: string;
serialNumber: string;
principals: string[];
ttl: string;
};
}
interface CreateCa { interface CreateCa {
type: EventType.CREATE_CA; type: EventType.CREATE_CA;
metadata: { metadata: {
@@ -2409,6 +2485,12 @@ export type Event =
| UpdateSshCertificateTemplate | UpdateSshCertificateTemplate
| GetSshCertificateTemplate | GetSshCertificateTemplate
| DeleteSshCertificateTemplate | DeleteSshCertificateTemplate
| CreateSshHost
| UpdateSshHost
| DeleteSshHost
| GetSshHost
| IssueSshHostUserCert
| IssueSshHostHostCert
| CreateCa | CreateCa
| GetCa | GetCa
| UpdateCa | UpdateCa
@@ -5,5 +5,7 @@ export const sanitizedSshHost = SshHostsSchema.pick({
projectId: true, projectId: true,
hostname: true, hostname: true,
userCertTtl: true, userCertTtl: true,
hostCertTtl: true hostCertTtl: true,
userSshCaId: true,
hostSshCaId: true
}); });
@@ -5,23 +5,31 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal"; import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
import { TSshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal";
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types"; import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal"; import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
import { TSshHostLoginMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-mapping-dal"; import { TSshHostLoginMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-mapping-dal";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal"; import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { convertActorToPrincipals, createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns"; import {
convertActorToPrincipals,
createSshCert,
createSshKeyPair,
getSshPublicKey
} from "../ssh/ssh-certificate-authority-fns";
import { SshCertType } from "../ssh/ssh-certificate-authority-types"; import { SshCertType } from "../ssh/ssh-certificate-authority-types";
import { import {
TCreateSshHostDTO, TCreateSshHostDTO,
TDeleteSshHostDTO, TDeleteSshHostDTO,
TGetSshHostDTO, TGetSshHostDTO,
TIssueSshCredsFromHostDTO, TIssueSshHostHostCertDTO,
TIssueSshHostUserCertDTO,
TListSshHostsDTO, TListSshHostsDTO,
TUpdateSshHostDTO TUpdateSshHostDTO
} from "./ssh-host-types"; } from "./ssh-host-types";
@@ -32,6 +40,8 @@ type TSshCertificateAuthorityServiceFactoryDep = {
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">; projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">;
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findById">; sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findById">;
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">; sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">;
sshCertificateDAL: Pick<TSshCertificateDALFactory, "create" | "transaction">;
sshCertificateBodyDAL: Pick<TSshCertificateBodyDALFactory, "create">;
sshHostDAL: Pick< sshHostDAL: Pick<
TSshHostDALFactory, TSshHostDALFactory,
| "transaction" | "transaction"
@@ -64,6 +74,8 @@ export const sshHostServiceFactory = ({
projectSshConfigDAL, projectSshConfigDAL,
sshCertificateAuthorityDAL, sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL, sshCertificateAuthoritySecretDAL,
sshCertificateDAL,
sshCertificateBodyDAL,
sshHostDAL, sshHostDAL,
sshHostLoginMappingDAL, sshHostLoginMappingDAL,
permissionService, permissionService,
@@ -104,15 +116,13 @@ export const sshHostServiceFactory = ({
} }
} }
// const principals = await convertActorToPrincipals({ const principals = await convertActorToPrincipals({
// actor, actor,
// actorId, actorId,
// userDAL userDAL
// }); });
const hosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects(projectIdsWithAccess, [ const hosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects(projectIdsWithAccess, principals);
"[email protected]" // hardcode for now
]);
return hosts; return hosts;
}; };
@@ -336,13 +346,14 @@ export const sshHostServiceFactory = ({
* *
* Note: Used for issuing SSH credentials as part of request against a specific SSH Host. * Note: Used for issuing SSH credentials as part of request against a specific SSH Host.
*/ */
const issueSshCredsFromHost = async ({ const issueSshHostUserCert = async ({
sshHostId, sshHostId,
loginUser,
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TIssueSshCredsFromHostDTO) => { }: TIssueSshHostUserCertDTO) => {
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId); const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
if (!host) { if (!host) {
throw new NotFoundError({ throw new NotFoundError({
@@ -376,16 +387,29 @@ export const sshHostServiceFactory = ({
cipherTextBlob: sshCaSecret.encryptedPrivateKey cipherTextBlob: sshCaSecret.encryptedPrivateKey
}); });
// create user key pair // (dangtony98): will support more algorithms in the future
const keyAlgorithm = SshCertKeyAlgorithm.ED25519; // (dangtony98): will support more algorithms in the future const keyAlgorithm = SshCertKeyAlgorithm.ED25519;
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm); const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
const principals = await convertActorToPrincipals({ const internalPrincipals = await convertActorToPrincipals({
actor, actor,
actorId, actorId,
userDAL userDAL
}); });
const mapping = host.loginMappings.find(
(m) => m.loginUser === loginUser && m.allowedPrincipals.some((allowed) => internalPrincipals.includes(allowed))
);
if (!mapping) {
throw new UnauthorizedError({
message: `You are not allowed to login as ${loginUser} on this host`
});
}
// (dangtony98): include the loginUser as a principal on the issued certificate
const principals = [...internalPrincipals, loginUser];
const { serialNumber, signedPublicKey, ttl } = await createSshCert({ const { serialNumber, signedPublicKey, ttl } = await createSshCert({
caPrivateKey: decryptedCaPrivateKey.toString("utf8"), caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
clientPublicKey: publicKey, clientPublicKey: publicKey,
@@ -395,23 +419,169 @@ export const sshHostServiceFactory = ({
certType: SshCertType.USER certType: SshCertType.USER
}); });
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: host.projectId
});
const encryptedCertificate = secretManagerEncryptor({
plainText: Buffer.from(signedPublicKey, "utf8")
}).cipherTextBlob;
await sshCertificateDAL.transaction(async (tx) => {
const cert = await sshCertificateDAL.create(
{
sshCaId: host.hostSshCaId,
sshHostId: host.id,
serialNumber,
certType: SshCertType.USER,
principals,
keyId,
notBefore: new Date(),
notAfter: new Date(Date.now() + ttl * 1000)
},
tx
);
await sshCertificateBodyDAL.create(
{
sshCertId: cert.id,
encryptedCertificate
},
tx
);
});
return { return {
host,
principals,
serialNumber, serialNumber,
signedPublicKey, signedPublicKey,
privateKey, privateKey,
publicKey, publicKey,
ttl, ttl,
keyId,
keyAlgorithm keyAlgorithm
}; };
}; };
const issueSshHostHostCert = async ({
sshHostId,
publicKey,
actor,
actorId,
actorAuthMethod,
actorOrgId
}: TIssueSshHostHostCertDTO) => {
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
if (!host) {
throw new NotFoundError({
message: `SSH host with ID ${sshHostId} not found`
});
}
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId: host.projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SSH
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts);
// TODO: update permissions
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.hostSshCaId });
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: host.projectId
});
const decryptedCaPrivateKey = secretManagerDecryptor({
cipherTextBlob: sshCaSecret.encryptedPrivateKey
});
const principals = [host.hostname];
const keyId = `host:${host.hostname}`;
const { serialNumber, signedPublicKey, ttl } = await createSshCert({
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
clientPublicKey: publicKey,
keyId,
principals,
requestedTtl: host.hostCertTtl,
certType: SshCertType.HOST
});
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: host.projectId
});
const encryptedCertificate = secretManagerEncryptor({
plainText: Buffer.from(signedPublicKey, "utf8")
}).cipherTextBlob;
await sshCertificateDAL.transaction(async (tx) => {
const cert = await sshCertificateDAL.create(
{
sshCaId: host.hostSshCaId,
sshHostId: host.id,
serialNumber,
certType: SshCertType.HOST,
principals,
keyId,
notBefore: new Date(),
notAfter: new Date(Date.now() + ttl * 1000)
},
tx
);
await sshCertificateBodyDAL.create(
{
sshCertId: cert.id,
encryptedCertificate
},
tx
);
});
return { host, principals, serialNumber, signedPublicKey };
};
const getSshHostUserCaPk = async (sshHostId: string) => {
const host = await sshHostDAL.findById(sshHostId);
if (!host) {
throw new NotFoundError({
message: `SSH host with ID ${sshHostId} not found`
});
}
const sshCaSecret = await sshCertificateAuthoritySecretDAL.findOne({ sshCaId: host.userSshCaId });
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: host.projectId
});
const decryptedCaPrivateKey = secretManagerDecryptor({
cipherTextBlob: sshCaSecret.encryptedPrivateKey
});
const publicKey = await getSshPublicKey(decryptedCaPrivateKey.toString("utf-8"));
return publicKey;
};
return { return {
listSshHosts, listSshHosts,
createSshHost, createSshHost,
updateSshHost, updateSshHost,
deleteSshHost, deleteSshHost,
getSshHost, getSshHost,
issueSshCredsFromHost issueSshHostUserCert,
issueSshHostHostCert,
getSshHostUserCaPk
}; };
}; };
@@ -33,6 +33,12 @@ export type TDeleteSshHostDTO = {
sshHostId: string; sshHostId: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TIssueSshCredsFromHostDTO = { export type TIssueSshHostUserCertDTO = {
sshHostId: string; sshHostId: string;
loginUser: string;
} & Omit<TProjectPermission, "projectId">;
export type TIssueSshHostHostCertDTO = {
sshHostId: string;
publicKey: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -300,7 +300,12 @@ export const validateSshCertificateTtl = (template: TSshCertificateTemplates, tt
* that it only contains alphanumeric characters with no spaces. * that it only contains alphanumeric characters with no spaces.
*/ */
export const validateSshCertificateKeyId = (keyId: string) => { export const validateSshCertificateKeyId = (keyId: string) => {
const regex = characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen]); const regex = characterValidator([
CharacterType.AlphaNumeric,
CharacterType.Hyphen,
CharacterType.Colon,
CharacterType.Period
]);
if (!regex(keyId)) { if (!regex(keyId)) {
throw new BadRequestError({ throw new BadRequestError({
message: message:
+10
View File
@@ -1348,11 +1348,21 @@ export const SSH_HOSTS = {
sshHostId: "The ID of the SSH host to delete." sshHostId: "The ID of the SSH host to delete."
}, },
ISSUE_SSH_CREDENTIALS: { ISSUE_SSH_CREDENTIALS: {
sshHostId: "The ID of the SSH host to issue the SSH credentials for.",
loginUser: "The login user to issue the SSH credentials for.",
keyAlgorithm: "The type of public key algorithm and size, in bits, of the key pair for the SSH host.", keyAlgorithm: "The type of public key algorithm and size, in bits, of the key pair for the SSH host.",
serialNumber: "The serial number of the issued SSH certificate.", serialNumber: "The serial number of the issued SSH certificate.",
signedKey: "The SSH certificate or signed SSH public key.", signedKey: "The SSH certificate or signed SSH public key.",
privateKey: "The private key corresponding to the issued SSH certificate.", privateKey: "The private key corresponding to the issued SSH certificate.",
publicKey: "The public key of the issued SSH certificate." publicKey: "The public key of the issued SSH certificate."
},
ISSUE_HOST_CERT: {
publicKey: "The SSH public key to issue the SSH certificate for.",
serialNumber: "The serial number of the issued SSH certificate.",
signedKey: "The SSH certificate or signed SSH public key."
},
GET_USER_CA_PUBLIC_KEY: {
sshHostId: "The ID of the SSH host to get the user SSH CA public key for."
} }
}; };
+7
View File
@@ -93,3 +93,10 @@ export const userEngagementLimit: RateLimitOptions = {
max: 5, max: 5,
keyGenerator: (req) => req.realIp keyGenerator: (req) => req.realIp
}; };
export const publicSshCaLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
hook: "preValidation",
max: 30, // conservative default
keyGenerator: (req) => req.realIp
};
+2
View File
@@ -803,6 +803,8 @@ export const registerRoutes = async (
projectSshConfigDAL, projectSshConfigDAL,
sshCertificateAuthorityDAL, sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL, sshCertificateAuthoritySecretDAL,
sshCertificateDAL,
sshCertificateBodyDAL,
sshHostDAL, sshHostDAL,
sshHostLoginMappingDAL, sshHostLoginMappingDAL,
permissionService, permissionService,
@@ -18,6 +18,8 @@ export enum PostHogEventTypes {
SecretRequestDeleted = "Secret Request Deleted", SecretRequestDeleted = "Secret Request Deleted",
SignSshKey = "Sign SSH Key", SignSshKey = "Sign SSH Key",
IssueSshCreds = "Issue SSH Credentials", IssueSshCreds = "Issue SSH Credentials",
IssueSshHostUserCert = "Issue SSH Host User Certificate",
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
SignCert = "Sign PKI Certificate", SignCert = "Sign PKI Certificate",
IssueCert = "Issue PKI Certificate" IssueCert = "Issue PKI Certificate"
} }
@@ -161,6 +163,26 @@ export type TIssueSshCredsEvent = {
}; };
}; };
export type TIssueSshHostUserCertEvent = {
event: PostHogEventTypes.IssueSshHostUserCert;
properties: {
sshHostId: string;
hostname: string;
principals: string[];
userAgent?: string;
};
};
export type TIssueSshHostHostCertEvent = {
event: PostHogEventTypes.IssueSshHostHostCert;
properties: {
sshHostId: string;
hostname: string;
principals: string[];
userAgent?: string;
};
};
export type TSignCertificateEvent = { export type TSignCertificateEvent = {
event: PostHogEventTypes.SignCert; event: PostHogEventTypes.SignCert;
properties: { properties: {
@@ -195,6 +217,8 @@ export type TPostHogEvent = { distinctId: string } & (
| TSecretRequestDeletedEvent | TSecretRequestDeletedEvent
| TSignSshKeyEvent | TSignSshKeyEvent
| TIssueSshCredsEvent | TIssueSshCredsEvent
| TIssueSshHostUserCertEvent
| TIssueSshHostHostCertEvent
| TSignCertificateEvent | TSignCertificateEvent
| TIssueCertificateEvent | TIssueCertificateEvent
); );
+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.3 github.com/infisical/go-sdk v0.5.4
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.3 h1:6qQCkxR1NqeoYu/6gL9BvQARr/apupX7+Ei5adCRTCU= github.com/infisical/go-sdk v0.5.4 h1:/Jbl9DLYLmYA3A9W8YB7Kqhm8vymL1WeoITvjXBCq8w=
github.com/infisical/go-sdk v0.5.3/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs= github.com/infisical/go-sdk v0.5.4/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=
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -128,20 +128,36 @@ export const ProjectLayout = () => {
}} }}
> >
{({ isActive }) => ( {({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed"> <MenuItem isSelected={isActive} icon="server">
Overview Hosts
</MenuItem> </MenuItem>
)} )}
</Link> </Link>
<Link <Link
to={`/${ProjectType.SSH}/$projectId/hosts` as const} to={`/${ProjectType.SSH}/$projectId/certificates` as const}
params={{ params={{
projectId: currentWorkspace.id projectId: currentWorkspace.id
}} }}
> >
{({ isActive }) => ( {({ isActive }) => (
<MenuItem isSelected={isActive} icon="server" iconMode="reverse"> <MenuItem isSelected={isActive} icon="certificate" iconMode="reverse">
Hosts Certificates
</MenuItem>
)}
</Link>
<Link
to={`/${ProjectType.SSH}/$projectId/cas` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem
isSelected={isActive}
icon="certificate-authority"
iconMode="reverse"
>
Certificate Authorities
</MenuItem> </MenuItem>
)} )}
</Link> </Link>
@@ -65,7 +65,7 @@ const formatDescription = (type: ProjectType) => {
return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices."; return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices.";
if (type === ProjectType.KMS) if (type === ProjectType.KMS)
return "Centralize the management of keys for cryptographic operations, such as encryption and decryption."; return "Centralize the management of keys for cryptographic operations, such as encryption and decryption.";
return "Generate SSH credentials to provide secure and centralized SSH access control for your infrastructure."; return "Infisical SSH lets you issue SSH credentials to users for short-lived, secure SSH access to infrastructure.";
}; };
type Props = { type Props = {
@@ -1,77 +0,0 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { SshCaSection, SshCertificatesSection } from "./components";
enum TabSections {
SshCa = "ssh-certificate-authorities",
SshCertificates = "ssh-certificates"
}
export const OverviewPage = () => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "Certificates" })}</title>
</Helmet>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Overview"
description="Infisical SSH lets you issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure."
/>
<Tabs defaultValue={TabSections.SshCertificates}>
<TabList>
<Tab value={TabSections.SshCertificates}>SSH Certificates</Tab>
<Tab value={TabSections.SshCa}>Certificate Authorities</Tab>
</TabList>
<TabPanel value={TabSections.SshCertificates}>
<motion.div
key="panel-ssh-certificate-s"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificates}
renderGuardBanner
passThrough={false}
>
<SshCertificatesSection />
</ProjectPermissionCan>
</motion.div>
</TabPanel>
<TabPanel value={TabSections.SshCa}>
<motion.div
key="panel-ssh-certificate-authorities"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificateAuthorities}
renderGuardBanner
passThrough={false}
>
<SshCaSection />
</ProjectPermissionCan>
</motion.div>
</TabPanel>
</Tabs>
</div>
</div>
</div>
</>
);
};
@@ -20,7 +20,7 @@ import { useDeleteSshCa, useGetSshCaById } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { SshCaModal } from "../OverviewPage/components/SshCaModal"; import { SshCaModal } from "../SshCasPage/components/SshCaModal";
import { SshCaDetailsSection, SshCertificateTemplatesSection } from "./components"; import { SshCaDetailsSection, SshCertificateTemplatesSection } from "./components";
const Page = () => { const Page = () => {
@@ -0,0 +1,28 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { SshCaSection } from "./components";
export const SshCasPage = () => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "Certificates" })}</title>
</Helmet>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="SSH Certificate Authorities"
description="Manage the SSH certificate authorities used to sign user and host certificates, including custom and default CAs."
/>
<SshCaSection />
</div>
</div>
</div>
</>
);
};
@@ -0,0 +1 @@
export { SshCaSection } from "./SshCaSection";
@@ -1,9 +1,9 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { OverviewPage } from "./OverviewPage"; import { SshCasPage } from "./SshCasPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview" "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas"
)({ )({
component: OverviewPage component: SshCasPage
}); });
@@ -0,0 +1,28 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { SshCertificatesSection } from "./components";
export const SshCertsPage = () => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "Certificates" })}</title>
</Helmet>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="SSH Certificates"
description="View and audit all issued SSH certificates, including validity and associated access metadata."
/>
<SshCertificatesSection />
</div>
</div>
</div>
</>
);
};
@@ -1,21 +1,15 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { SshCertificateModal } from "../../SshCaByIDPage/components/SshCertificateModal"; import { SshCertificateModal } from "../../SshCaByIDPage/components/SshCertificateModal";
import { SshCertificatesTable } from "./SshCertificatesTable"; import { SshCertificatesTable } from "./SshCertificatesTable";
export const SshCertificatesSection = () => { export const SshCertificatesSection = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["sshCertificate"] as const); const { popUp, handlePopUpToggle } = usePopUp(["sshCertificate"] as const);
return ( return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"> <div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between"> <div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Certificates</p> <p className="text-xl font-semibold text-mineshaft-100">Certificates</p>
<ProjectPermissionCan {/* <ProjectPermissionCan
I={ProjectPermissionActions.Create} I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SshCertificates} a={ProjectPermissionSub.SshCertificates}
> >
@@ -30,7 +24,7 @@ export const SshCertificatesSection = () => {
Request Request
</Button> </Button>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan> */}
</div> </div>
<SshCertificatesTable /> <SshCertificatesTable />
<SshCertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <SshCertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
@@ -1,2 +1 @@
export { SshCaSection } from "./SshCaSection";
export { SshCertificatesSection } from "./SshCertificatesSection"; export { SshCertificatesSection } from "./SshCertificatesSection";
@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { SshCertsPage } from "./SshCertsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/certificates"
)({
component: SshCertsPage
});
@@ -17,7 +17,7 @@ export const SshHostsPage = () => {
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader <PageHeader
title="Hosts" title="Hosts"
description="Infisical SSH lets you issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure." description="Manage your SSH hosts, configure access policies, and define login behavior for secure connections."
/> />
<SshHostsSection /> <SshHostsSection />
</div> </div>
@@ -217,11 +217,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
</div> </div>
<div className="flex-grow"> <div className="flex-grow">
{i === 0 && ( {i === 0 && (
<FormLabel <FormLabel label="Allowed Principals" className="text-xs text-mineshaft-400" />
label="Allowed Principals"
className="text-xs text-mineshaft-400"
isOptional
/>
)} )}
<Controller <Controller
control={control} control={control}
@@ -21,9 +21,7 @@ export const SshHostsSection = () => {
const onRemoveSshHostSubmit = async (sshHostId: string) => { const onRemoveSshHostSubmit = async (sshHostId: string) => {
try { try {
console.log("pre onRemoveSshHostSubmit");
const host = await deleteSshHost({ sshHostId }); const host = await deleteSshHost({ sshHostId });
console.log("post onRemoveSshHostSubmit deleted host: ", host);
createNotification({ createNotification({
text: `Successfully deleted SSH host: ${host.hostname}`, text: `Successfully deleted SSH host: ${host.hostname}`,
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { SshHostsPage } from "./SshHostsPage"; import { SshHostsPage } from "./SshHostsPage";
export const Route = createFileRoute( export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/hosts" "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview"
)({ )({
component: SshHostsPage component: SshHostsPage
}); });
+53 -26
View File
@@ -78,8 +78,9 @@ import { Route as secretManagerIntegrationsRouteAzureKeyVaultOauthRedirectImport
import { Route as secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectImport } from './pages/secret-manager/integrations/route-azure-app-configurations-oauth-redirect' import { Route as secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectImport } from './pages/secret-manager/integrations/route-azure-app-configurations-oauth-redirect'
import { Route as projectAccessControlPageRouteCertManagerImport } from './pages/project/AccessControlPage/route-cert-manager' import { Route as projectAccessControlPageRouteCertManagerImport } from './pages/project/AccessControlPage/route-cert-manager'
import { Route as sshSettingsPageRouteImport } from './pages/ssh/SettingsPage/route' import { Route as sshSettingsPageRouteImport } from './pages/ssh/SettingsPage/route'
import { Route as sshOverviewPageRouteImport } from './pages/ssh/OverviewPage/route'
import { Route as sshSshHostsPageRouteImport } from './pages/ssh/SshHostsPage/route' import { Route as sshSshHostsPageRouteImport } from './pages/ssh/SshHostsPage/route'
import { Route as sshSshCertsPageRouteImport } from './pages/ssh/SshCertsPage/route'
import { Route as sshSshCasPageRouteImport } from './pages/ssh/SshCasPage/route'
import { Route as secretManagerSettingsPageRouteImport } from './pages/secret-manager/SettingsPage/route' import { Route as secretManagerSettingsPageRouteImport } from './pages/secret-manager/SettingsPage/route'
import { Route as secretManagerSecretRotationPageRouteImport } from './pages/secret-manager/SecretRotationPage/route' import { Route as secretManagerSecretRotationPageRouteImport } from './pages/secret-manager/SecretRotationPage/route'
import { Route as secretManagerOverviewPageRouteImport } from './pages/secret-manager/OverviewPage/route' import { Route as secretManagerOverviewPageRouteImport } from './pages/secret-manager/OverviewPage/route'
@@ -801,15 +802,21 @@ const sshSettingsPageRouteRoute = sshSettingsPageRouteImport.update({
getParentRoute: () => sshLayoutRoute, getParentRoute: () => sshLayoutRoute,
} as any) } as any)
const sshOverviewPageRouteRoute = sshOverviewPageRouteImport.update({ const sshSshHostsPageRouteRoute = sshSshHostsPageRouteImport.update({
id: '/overview', id: '/overview',
path: '/overview', path: '/overview',
getParentRoute: () => sshLayoutRoute, getParentRoute: () => sshLayoutRoute,
} as any) } as any)
const sshSshHostsPageRouteRoute = sshSshHostsPageRouteImport.update({ const sshSshCertsPageRouteRoute = sshSshCertsPageRouteImport.update({
id: '/hosts', id: '/certificates',
path: '/hosts', path: '/certificates',
getParentRoute: () => sshLayoutRoute,
} as any)
const sshSshCasPageRouteRoute = sshSshCasPageRouteImport.update({
id: '/cas',
path: '/cas',
getParentRoute: () => sshLayoutRoute, getParentRoute: () => sshLayoutRoute,
} as any) } as any)
@@ -2153,18 +2160,25 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof secretManagerSettingsPageRouteImport preLoaderRoute: typeof secretManagerSettingsPageRouteImport
parentRoute: typeof secretManagerLayoutImport parentRoute: typeof secretManagerLayoutImport
} }
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/hosts': { '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas': {
id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/hosts' id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas'
path: '/hosts' path: '/cas'
fullPath: '/ssh/$projectId/hosts' fullPath: '/ssh/$projectId/cas'
preLoaderRoute: typeof sshSshHostsPageRouteImport preLoaderRoute: typeof sshSshCasPageRouteImport
parentRoute: typeof sshLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/certificates': {
id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/certificates'
path: '/certificates'
fullPath: '/ssh/$projectId/certificates'
preLoaderRoute: typeof sshSshCertsPageRouteImport
parentRoute: typeof sshLayoutImport parentRoute: typeof sshLayoutImport
} }
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview': { '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview': {
id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview' id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview'
path: '/overview' path: '/overview'
fullPath: '/ssh/$projectId/overview' fullPath: '/ssh/$projectId/overview'
preLoaderRoute: typeof sshOverviewPageRouteImport preLoaderRoute: typeof sshSshHostsPageRouteImport
parentRoute: typeof sshLayoutImport parentRoute: typeof sshLayoutImport
} }
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings': { '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings': {
@@ -3477,8 +3491,9 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildr
) )
interface sshLayoutRouteChildren { interface sshLayoutRouteChildren {
sshSshCasPageRouteRoute: typeof sshSshCasPageRouteRoute
sshSshCertsPageRouteRoute: typeof sshSshCertsPageRouteRoute
sshSshHostsPageRouteRoute: typeof sshSshHostsPageRouteRoute sshSshHostsPageRouteRoute: typeof sshSshHostsPageRouteRoute
sshOverviewPageRouteRoute: typeof sshOverviewPageRouteRoute
sshSettingsPageRouteRoute: typeof sshSettingsPageRouteRoute sshSettingsPageRouteRoute: typeof sshSettingsPageRouteRoute
projectAccessControlPageRouteSshRoute: typeof projectAccessControlPageRouteSshRoute projectAccessControlPageRouteSshRoute: typeof projectAccessControlPageRouteSshRoute
sshSshCaByIDPageRouteRoute: typeof sshSshCaByIDPageRouteRoute sshSshCaByIDPageRouteRoute: typeof sshSshCaByIDPageRouteRoute
@@ -3488,8 +3503,9 @@ interface sshLayoutRouteChildren {
} }
const sshLayoutRouteChildren: sshLayoutRouteChildren = { const sshLayoutRouteChildren: sshLayoutRouteChildren = {
sshSshCasPageRouteRoute: sshSshCasPageRouteRoute,
sshSshCertsPageRouteRoute: sshSshCertsPageRouteRoute,
sshSshHostsPageRouteRoute: sshSshHostsPageRouteRoute, sshSshHostsPageRouteRoute: sshSshHostsPageRouteRoute,
sshOverviewPageRouteRoute: sshOverviewPageRouteRoute,
sshSettingsPageRouteRoute: sshSettingsPageRouteRoute, sshSettingsPageRouteRoute: sshSettingsPageRouteRoute,
projectAccessControlPageRouteSshRoute: projectAccessControlPageRouteSshRoute, projectAccessControlPageRouteSshRoute: projectAccessControlPageRouteSshRoute,
sshSshCaByIDPageRouteRoute: sshSshCaByIDPageRouteRoute, sshSshCaByIDPageRouteRoute: sshSshCaByIDPageRouteRoute,
@@ -3771,8 +3787,9 @@ export interface FileRoutesByFullPath {
'/secret-manager/$projectId/overview': typeof secretManagerOverviewPageRouteRoute '/secret-manager/$projectId/overview': typeof secretManagerOverviewPageRouteRoute
'/secret-manager/$projectId/secret-rotation': typeof secretManagerSecretRotationPageRouteRoute '/secret-manager/$projectId/secret-rotation': typeof secretManagerSecretRotationPageRouteRoute
'/secret-manager/$projectId/settings': typeof secretManagerSettingsPageRouteRoute '/secret-manager/$projectId/settings': typeof secretManagerSettingsPageRouteRoute
'/ssh/$projectId/hosts': typeof sshSshHostsPageRouteRoute '/ssh/$projectId/cas': typeof sshSshCasPageRouteRoute
'/ssh/$projectId/overview': typeof sshOverviewPageRouteRoute '/ssh/$projectId/certificates': typeof sshSshCertsPageRouteRoute
'/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute
'/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute '/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
'/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute '/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute '/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute
@@ -3944,8 +3961,9 @@ export interface FileRoutesByTo {
'/secret-manager/$projectId/overview': typeof secretManagerOverviewPageRouteRoute '/secret-manager/$projectId/overview': typeof secretManagerOverviewPageRouteRoute
'/secret-manager/$projectId/secret-rotation': typeof secretManagerSecretRotationPageRouteRoute '/secret-manager/$projectId/secret-rotation': typeof secretManagerSecretRotationPageRouteRoute
'/secret-manager/$projectId/settings': typeof secretManagerSettingsPageRouteRoute '/secret-manager/$projectId/settings': typeof secretManagerSettingsPageRouteRoute
'/ssh/$projectId/hosts': typeof sshSshHostsPageRouteRoute '/ssh/$projectId/cas': typeof sshSshCasPageRouteRoute
'/ssh/$projectId/overview': typeof sshOverviewPageRouteRoute '/ssh/$projectId/certificates': typeof sshSshCertsPageRouteRoute
'/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute
'/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute '/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
'/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute '/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute '/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute
@@ -4133,8 +4151,9 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview': typeof secretManagerOverviewPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview': typeof secretManagerOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secret-rotation': typeof secretManagerSecretRotationPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secret-rotation': typeof secretManagerSecretRotationPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/settings': typeof secretManagerSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/settings': typeof secretManagerSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/hosts': typeof sshSshHostsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas': typeof sshSshCasPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview': typeof sshOverviewPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/certificates': typeof sshSshCertsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview': typeof sshSshHostsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings': typeof sshSettingsPageRouteRoute '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings': typeof sshSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute '/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute
@@ -4315,7 +4334,8 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId/overview' | '/secret-manager/$projectId/overview'
| '/secret-manager/$projectId/secret-rotation' | '/secret-manager/$projectId/secret-rotation'
| '/secret-manager/$projectId/settings' | '/secret-manager/$projectId/settings'
| '/ssh/$projectId/hosts' | '/ssh/$projectId/cas'
| '/ssh/$projectId/certificates'
| '/ssh/$projectId/overview' | '/ssh/$projectId/overview'
| '/ssh/$projectId/settings' | '/ssh/$projectId/settings'
| '/cert-manager/$projectId/access-management' | '/cert-manager/$projectId/access-management'
@@ -4487,7 +4507,8 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId/overview' | '/secret-manager/$projectId/overview'
| '/secret-manager/$projectId/secret-rotation' | '/secret-manager/$projectId/secret-rotation'
| '/secret-manager/$projectId/settings' | '/secret-manager/$projectId/settings'
| '/ssh/$projectId/hosts' | '/ssh/$projectId/cas'
| '/ssh/$projectId/certificates'
| '/ssh/$projectId/overview' | '/ssh/$projectId/overview'
| '/ssh/$projectId/settings' | '/ssh/$projectId/settings'
| '/cert-manager/$projectId/access-management' | '/cert-manager/$projectId/access-management'
@@ -4674,7 +4695,8 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secret-rotation' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secret-rotation'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/settings' | '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/settings'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/hosts' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/certificates'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings' | '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management' | '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management'
@@ -5217,7 +5239,8 @@ export const routeTree = rootRoute
"filePath": "ssh/layout.tsx", "filePath": "ssh/layout.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId", "parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId",
"children": [ "children": [
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/hosts", "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas",
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/certificates",
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview", "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview",
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings", "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings",
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management", "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management",
@@ -5267,12 +5290,16 @@ export const routeTree = rootRoute
"filePath": "secret-manager/SettingsPage/route.tsx", "filePath": "secret-manager/SettingsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout" "parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout"
}, },
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/hosts": { "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas": {
"filePath": "ssh/SshHostsPage/route.tsx", "filePath": "ssh/SshCasPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout"
},
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/certificates": {
"filePath": "ssh/SshCertsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout" "parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout"
}, },
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview": { "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview": {
"filePath": "ssh/OverviewPage/route.tsx", "filePath": "ssh/SshHostsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout" "parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout"
}, },
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings": { "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings": {
+3 -2
View File
@@ -306,8 +306,9 @@ const kmsRoutes = route("/kms/$projectId", [
const sshRoutes = route("/ssh/$projectId", [ const sshRoutes = route("/ssh/$projectId", [
layout("ssh-layout", "ssh/layout.tsx", [ layout("ssh-layout", "ssh/layout.tsx", [
route("/overview", "ssh/OverviewPage/route.tsx"), route("/overview", "ssh/SshHostsPage/route.tsx"),
route("/hosts", "ssh/SshHostsPage/route.tsx"), route("/certificates", "ssh/SshCertsPage/route.tsx"),
route("/cas", "ssh/SshCasPage/route.tsx"),
route("/ca/$caId", "ssh/SshCaByIDPage/route.tsx"), route("/ca/$caId", "ssh/SshCaByIDPage/route.tsx"),
route("/settings", "ssh/SettingsPage/route.tsx"), route("/settings", "ssh/SettingsPage/route.tsx"),
route("/access-management", "project/AccessControlPage/route-ssh.tsx"), route("/access-management", "project/AccessControlPage/route-ssh.tsx"),