mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 23:28:31 +00:00
Add ssh host endpoint for issuing ssh host cert
This commit is contained in:
@@ -47,6 +47,14 @@ export async function up(knex: Knex): Promise<void> {
|
||||
});
|
||||
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> {
|
||||
@@ -58,4 +66,11 @@ export async function down(knex: Knex): Promise<void> {
|
||||
|
||||
await knex.schema.dropTableIfExists(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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ export const SshCertificatesSchema = z.object({
|
||||
principals: z.string().array(),
|
||||
keyId: z.string(),
|
||||
notBefore: z.date(),
|
||||
notAfter: z.date()
|
||||
notAfter: z.date(),
|
||||
sshHostId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSshCertificates = z.infer<typeof SshCertificatesSchema>;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
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 { sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
|
||||
import { isValidHostname } from "@app/ee/services/ssh-host/ssh-host-validators";
|
||||
import { SSH_HOSTS } from "@app/lib/api-docs";
|
||||
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 { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -38,18 +41,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
// TODO: audit log
|
||||
// await server.services.auditLog.createAuditLog({
|
||||
// ...req.auditLogInfo,
|
||||
// projectId: certificateTemplate.projectId,
|
||||
// event: {
|
||||
// type: EventType.GET_SSH_CERTIFICATE_TEMPLATE,
|
||||
// metadata: {
|
||||
// certificateTemplateId: certificateTemplate.id
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// TODO: consider adding audit log
|
||||
|
||||
return hosts;
|
||||
}
|
||||
@@ -86,17 +78,17 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
// TODO: audit log
|
||||
// await server.services.auditLog.createAuditLog({
|
||||
// ...req.auditLogInfo,
|
||||
// projectId: certificateTemplate.projectId,
|
||||
// event: {
|
||||
// type: EventType.GET_SSH_CERTIFICATE_TEMPLATE,
|
||||
// metadata: {
|
||||
// certificateTemplateId: certificateTemplate.id
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: host.projectId,
|
||||
event: {
|
||||
type: EventType.GET_SSH_HOST,
|
||||
metadata: {
|
||||
sshHostId: host.id,
|
||||
hostname: host.hostname
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return host;
|
||||
}
|
||||
@@ -161,26 +153,22 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
// TODO: audit log
|
||||
// await server.services.auditLog.createAuditLog({
|
||||
// ...req.auditLogInfo,
|
||||
// projectId: ca.projectId,
|
||||
// event: {
|
||||
// type: EventType.CREATE_SSH_CERTIFICATE_TEMPLATE,
|
||||
// metadata: {
|
||||
// certificateTemplateId: certificateTemplate.id,
|
||||
// sshCaId: ca.id,
|
||||
// name: certificateTemplate.name,
|
||||
// ttl: certificateTemplate.ttl,
|
||||
// maxTTL: certificateTemplate.maxTTL,
|
||||
// allowedUsers: certificateTemplate.allowedUsers,
|
||||
// allowedHosts: certificateTemplate.allowedHosts,
|
||||
// allowUserCertificates: certificateTemplate.allowUserCertificates,
|
||||
// allowHostCertificates: certificateTemplate.allowHostCertificates,
|
||||
// allowCustomKeyIds: certificateTemplate.allowCustomKeyIds
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: host.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_SSH_HOST,
|
||||
metadata: {
|
||||
sshHostId: host.id,
|
||||
hostname: host.hostname,
|
||||
userCertTtl: host.userCertTtl,
|
||||
hostCertTtl: host.hostCertTtl,
|
||||
loginMappings: host.loginMappings,
|
||||
userSshCaId: host.userSshCaId,
|
||||
hostSshCaId: host.hostSshCaId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return host;
|
||||
}
|
||||
@@ -205,16 +193,17 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
.refine((v) => isValidHostname(v), {
|
||||
message: "Hostname must be a valid hostname"
|
||||
})
|
||||
.optional()
|
||||
.describe(SSH_HOSTS.UPDATE.hostname),
|
||||
userCertTtl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.default("1h")
|
||||
.optional()
|
||||
.describe(SSH_HOSTS.UPDATE.userCertTtl),
|
||||
hostCertTtl: z
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.default("1y")
|
||||
.optional()
|
||||
.describe(SSH_HOSTS.UPDATE.hostCertTtl),
|
||||
loginMappings: z
|
||||
.object({
|
||||
@@ -246,19 +235,22 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
...req.body
|
||||
});
|
||||
|
||||
// TODO: audit log
|
||||
// await server.services.auditLog.createAuditLog({
|
||||
// ...req.auditLogInfo,
|
||||
// projectId: ca.projectId,
|
||||
// event: {
|
||||
// type: EventType.UPDATE_SSH_CA,
|
||||
// metadata: {
|
||||
// sshCaId: ca.id,
|
||||
// friendlyName: ca.friendlyName,
|
||||
// status: ca.status as SshCaStatus
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: host.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_SSH_HOST,
|
||||
metadata: {
|
||||
sshHostId: host.id,
|
||||
hostname: host.hostname,
|
||||
userCertTtl: host.userCertTtl,
|
||||
hostCertTtl: host.hostCertTtl,
|
||||
loginMappings: host.loginMappings,
|
||||
userSshCaId: host.userSshCaId,
|
||||
hostSshCaId: host.hostSshCaId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return host;
|
||||
}
|
||||
@@ -295,17 +287,17 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
// TODO: audit log
|
||||
// await server.services.auditLog.createAuditLog({
|
||||
// ...req.auditLogInfo,
|
||||
// projectId: certificateTemplate.projectId,
|
||||
// event: {
|
||||
// type: EventType.DELETE_SSH_CERTIFICATE_TEMPLATE,
|
||||
// metadata: {
|
||||
// certificateTemplateId: certificateTemplate.id
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: host.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_SSH_HOST,
|
||||
metadata: {
|
||||
sshHostId: host.id,
|
||||
hostname: host.hostname
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return host;
|
||||
}
|
||||
@@ -313,7 +305,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/:sshHostId/issue",
|
||||
url: "/:sshHostId/issue-user-cert",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
@@ -321,7 +313,10 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
description: "Issue SSH credentials (certificate + key)",
|
||||
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: {
|
||||
200: z.object({
|
||||
@@ -334,41 +329,41 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { serialNumber, signedPublicKey, privateKey, publicKey, keyAlgorithm } =
|
||||
await server.services.sshHost.issueSshCredsFromHost({
|
||||
const { serialNumber, signedPublicKey, privateKey, publicKey, keyAlgorithm, host, principals } =
|
||||
await server.services.sshHost.issueSshHostUserCert({
|
||||
sshHostId: req.params.sshHostId,
|
||||
loginUser: req.body.loginUser,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
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({
|
||||
// ...req.auditLogInfo,
|
||||
// orgId: req.permission.orgId,
|
||||
// event: {
|
||||
// type: EventType.ISSUE_SSH_CREDS,
|
||||
// metadata: {
|
||||
// certificateTemplateId: certificateTemplate.id,
|
||||
// keyAlgorithm: req.body.keyAlgorithm,
|
||||
// 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
|
||||
// }
|
||||
// });
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueSshHostUserCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
sshHostId: req.params.sshHostId,
|
||||
hostname: host.hostname,
|
||||
principals,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
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",
|
||||
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-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",
|
||||
GET_CA = "get-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 {
|
||||
type: EventType.CREATE_CA;
|
||||
metadata: {
|
||||
@@ -2409,6 +2485,12 @@ export type Event =
|
||||
| UpdateSshCertificateTemplate
|
||||
| GetSshCertificateTemplate
|
||||
| DeleteSshCertificateTemplate
|
||||
| CreateSshHost
|
||||
| UpdateSshHost
|
||||
| DeleteSshHost
|
||||
| GetSshHost
|
||||
| IssueSshHostUserCert
|
||||
| IssueSshHostHostCert
|
||||
| CreateCa
|
||||
| GetCa
|
||||
| UpdateCa
|
||||
|
||||
@@ -5,5 +5,7 @@ export const sanitizedSshHost = SshHostsSchema.pick({
|
||||
projectId: true,
|
||||
hostname: 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 { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-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 { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-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 { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-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 {
|
||||
TCreateSshHostDTO,
|
||||
TDeleteSshHostDTO,
|
||||
TGetSshHostDTO,
|
||||
TIssueSshCredsFromHostDTO,
|
||||
TIssueSshHostHostCertDTO,
|
||||
TIssueSshHostUserCertDTO,
|
||||
TListSshHostsDTO,
|
||||
TUpdateSshHostDTO
|
||||
} from "./ssh-host-types";
|
||||
@@ -32,6 +40,8 @@ type TSshCertificateAuthorityServiceFactoryDep = {
|
||||
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">;
|
||||
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findById">;
|
||||
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
sshCertificateDAL: Pick<TSshCertificateDALFactory, "create" | "transaction">;
|
||||
sshCertificateBodyDAL: Pick<TSshCertificateBodyDALFactory, "create">;
|
||||
sshHostDAL: Pick<
|
||||
TSshHostDALFactory,
|
||||
| "transaction"
|
||||
@@ -64,6 +74,8 @@ export const sshHostServiceFactory = ({
|
||||
projectSshConfigDAL,
|
||||
sshCertificateAuthorityDAL,
|
||||
sshCertificateAuthoritySecretDAL,
|
||||
sshCertificateDAL,
|
||||
sshCertificateBodyDAL,
|
||||
sshHostDAL,
|
||||
sshHostLoginMappingDAL,
|
||||
permissionService,
|
||||
@@ -104,15 +116,13 @@ export const sshHostServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
// const principals = await convertActorToPrincipals({
|
||||
// actor,
|
||||
// actorId,
|
||||
// userDAL
|
||||
// });
|
||||
const principals = await convertActorToPrincipals({
|
||||
actor,
|
||||
actorId,
|
||||
userDAL
|
||||
});
|
||||
|
||||
const hosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects(projectIdsWithAccess, [
|
||||
"[email protected]" // hardcode for now
|
||||
]);
|
||||
const hosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects(projectIdsWithAccess, principals);
|
||||
|
||||
return hosts;
|
||||
};
|
||||
@@ -336,13 +346,14 @@ export const sshHostServiceFactory = ({
|
||||
*
|
||||
* Note: Used for issuing SSH credentials as part of request against a specific SSH Host.
|
||||
*/
|
||||
const issueSshCredsFromHost = async ({
|
||||
const issueSshHostUserCert = async ({
|
||||
sshHostId,
|
||||
loginUser,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TIssueSshCredsFromHostDTO) => {
|
||||
}: TIssueSshHostUserCertDTO) => {
|
||||
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
|
||||
if (!host) {
|
||||
throw new NotFoundError({
|
||||
@@ -376,16 +387,29 @@ export const sshHostServiceFactory = ({
|
||||
cipherTextBlob: sshCaSecret.encryptedPrivateKey
|
||||
});
|
||||
|
||||
// create user key pair
|
||||
const keyAlgorithm = SshCertKeyAlgorithm.ED25519; // (dangtony98): will support more algorithms in the future
|
||||
// (dangtony98): will support more algorithms in the future
|
||||
const keyAlgorithm = SshCertKeyAlgorithm.ED25519;
|
||||
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
|
||||
|
||||
const principals = await convertActorToPrincipals({
|
||||
const internalPrincipals = await convertActorToPrincipals({
|
||||
actor,
|
||||
actorId,
|
||||
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({
|
||||
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||
clientPublicKey: publicKey,
|
||||
@@ -395,23 +419,169 @@ export const sshHostServiceFactory = ({
|
||||
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 {
|
||||
host,
|
||||
principals,
|
||||
serialNumber,
|
||||
signedPublicKey,
|
||||
privateKey,
|
||||
publicKey,
|
||||
ttl,
|
||||
keyId,
|
||||
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 {
|
||||
listSshHosts,
|
||||
createSshHost,
|
||||
updateSshHost,
|
||||
deleteSshHost,
|
||||
getSshHost,
|
||||
issueSshCredsFromHost
|
||||
issueSshHostUserCert,
|
||||
issueSshHostHostCert,
|
||||
getSshHostUserCaPk
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,6 +33,12 @@ export type TDeleteSshHostDTO = {
|
||||
sshHostId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIssueSshCredsFromHostDTO = {
|
||||
export type TIssueSshHostUserCertDTO = {
|
||||
sshHostId: string;
|
||||
loginUser: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIssueSshHostHostCertDTO = {
|
||||
sshHostId: string;
|
||||
publicKey: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
@@ -300,7 +300,12 @@ export const validateSshCertificateTtl = (template: TSshCertificateTemplates, tt
|
||||
* that it only contains alphanumeric characters with no spaces.
|
||||
*/
|
||||
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)) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
|
||||
@@ -1348,11 +1348,21 @@ export const SSH_HOSTS = {
|
||||
sshHostId: "The ID of the SSH host to delete."
|
||||
},
|
||||
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.",
|
||||
serialNumber: "The serial number of the issued SSH certificate.",
|
||||
signedKey: "The SSH certificate or signed SSH public key.",
|
||||
privateKey: "The private key corresponding to 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."
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -93,3 +93,10 @@ export const userEngagementLimit: RateLimitOptions = {
|
||||
max: 5,
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
export const publicSshCaLimit: RateLimitOptions = {
|
||||
timeWindow: 60 * 1000,
|
||||
hook: "preValidation",
|
||||
max: 30, // conservative default
|
||||
keyGenerator: (req) => req.realIp
|
||||
};
|
||||
|
||||
@@ -803,6 +803,8 @@ export const registerRoutes = async (
|
||||
projectSshConfigDAL,
|
||||
sshCertificateAuthorityDAL,
|
||||
sshCertificateAuthoritySecretDAL,
|
||||
sshCertificateDAL,
|
||||
sshCertificateBodyDAL,
|
||||
sshHostDAL,
|
||||
sshHostLoginMappingDAL,
|
||||
permissionService,
|
||||
|
||||
@@ -18,6 +18,8 @@ export enum PostHogEventTypes {
|
||||
SecretRequestDeleted = "Secret Request Deleted",
|
||||
SignSshKey = "Sign SSH Key",
|
||||
IssueSshCreds = "Issue SSH Credentials",
|
||||
IssueSshHostUserCert = "Issue SSH Host User Certificate",
|
||||
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
|
||||
SignCert = "Sign 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 = {
|
||||
event: PostHogEventTypes.SignCert;
|
||||
properties: {
|
||||
@@ -195,6 +217,8 @@ export type TPostHogEvent = { distinctId: string } & (
|
||||
| TSecretRequestDeletedEvent
|
||||
| TSignSshKeyEvent
|
||||
| TIssueSshCredsEvent
|
||||
| TIssueSshHostUserCertEvent
|
||||
| TIssueSshHostHostCertEvent
|
||||
| TSignCertificateEvent
|
||||
| TIssueCertificateEvent
|
||||
);
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ require (
|
||||
github.com/fatih/semgroup v1.2.0
|
||||
github.com/gitleaks/go-gitdiff v0.8.0
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/infisical/go-sdk v0.5.3
|
||||
github.com/infisical/go-sdk v0.5.4
|
||||
github.com/infisical/infisical-kmip v0.3.5
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
|
||||
|
||||
+2
-2
@@ -277,8 +277,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/infisical/go-sdk v0.5.3 h1:6qQCkxR1NqeoYu/6gL9BvQARr/apupX7+Ei5adCRTCU=
|
||||
github.com/infisical/go-sdk v0.5.3/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
||||
github.com/infisical/go-sdk v0.5.4 h1:/Jbl9DLYLmYA3A9W8YB7Kqhm8vymL1WeoITvjXBCq8w=
|
||||
github.com/infisical/go-sdk v0.5.4/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
||||
github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
|
||||
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||
|
||||
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 }) => (
|
||||
<MenuItem isSelected={isActive} icon="lock-closed">
|
||||
Overview
|
||||
<MenuItem isSelected={isActive} icon="server">
|
||||
Hosts
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to={`/${ProjectType.SSH}/$projectId/hosts` as const}
|
||||
to={`/${ProjectType.SSH}/$projectId/certificates` as const}
|
||||
params={{
|
||||
projectId: currentWorkspace.id
|
||||
}}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="server" iconMode="reverse">
|
||||
Hosts
|
||||
<MenuItem isSelected={isActive} icon="certificate" iconMode="reverse">
|
||||
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>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ const formatDescription = (type: ProjectType) => {
|
||||
return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices.";
|
||||
if (type === ProjectType.KMS)
|
||||
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 = {
|
||||
|
||||
@@ -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 { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { SshCaModal } from "../OverviewPage/components/SshCaModal";
|
||||
import { SshCaModal } from "../SshCasPage/components/SshCaModal";
|
||||
import { SshCaDetailsSection, SshCertificateTemplatesSection } from "./components";
|
||||
|
||||
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";
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
import { OverviewPage } from "./OverviewPage";
|
||||
import { SshCasPage } from "./SshCasPage";
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+3
-9
@@ -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 { SshCertificateModal } from "../../SshCaByIDPage/components/SshCertificateModal";
|
||||
import { SshCertificatesTable } from "./SshCertificatesTable";
|
||||
|
||||
export const SshCertificatesSection = () => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["sshCertificate"] as const);
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["sshCertificate"] as const);
|
||||
return (
|
||||
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="mb-4 flex justify-between">
|
||||
<p className="text-xl font-semibold text-mineshaft-100">Certificates</p>
|
||||
<ProjectPermissionCan
|
||||
{/* <ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.SshCertificates}
|
||||
>
|
||||
@@ -30,7 +24,7 @@ export const SshCertificatesSection = () => {
|
||||
Request
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</ProjectPermissionCan> */}
|
||||
</div>
|
||||
<SshCertificatesTable />
|
||||
<SshCertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
-1
@@ -1,2 +1 @@
|
||||
export { SshCaSection } from "./SshCaSection";
|
||||
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">
|
||||
<PageHeader
|
||||
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 />
|
||||
</div>
|
||||
|
||||
@@ -217,11 +217,7 @@ export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
{i === 0 && (
|
||||
<FormLabel
|
||||
label="Allowed Principals"
|
||||
className="text-xs text-mineshaft-400"
|
||||
isOptional
|
||||
/>
|
||||
<FormLabel label="Allowed Principals" className="text-xs text-mineshaft-400" />
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
|
||||
@@ -21,9 +21,7 @@ export const SshHostsSection = () => {
|
||||
|
||||
const onRemoveSshHostSubmit = async (sshHostId: string) => {
|
||||
try {
|
||||
console.log("pre onRemoveSshHostSubmit");
|
||||
const host = await deleteSshHost({ sshHostId });
|
||||
console.log("post onRemoveSshHostSubmit deleted host: ", host);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully deleted SSH host: ${host.hostname}`,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { SshHostsPage } from "./SshHostsPage";
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
@@ -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 projectAccessControlPageRouteCertManagerImport } from './pages/project/AccessControlPage/route-cert-manager'
|
||||
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 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 secretManagerSecretRotationPageRouteImport } from './pages/secret-manager/SecretRotationPage/route'
|
||||
import { Route as secretManagerOverviewPageRouteImport } from './pages/secret-manager/OverviewPage/route'
|
||||
@@ -801,15 +802,21 @@ const sshSettingsPageRouteRoute = sshSettingsPageRouteImport.update({
|
||||
getParentRoute: () => sshLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const sshOverviewPageRouteRoute = sshOverviewPageRouteImport.update({
|
||||
const sshSshHostsPageRouteRoute = sshSshHostsPageRouteImport.update({
|
||||
id: '/overview',
|
||||
path: '/overview',
|
||||
getParentRoute: () => sshLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const sshSshHostsPageRouteRoute = sshSshHostsPageRouteImport.update({
|
||||
id: '/hosts',
|
||||
path: '/hosts',
|
||||
const sshSshCertsPageRouteRoute = sshSshCertsPageRouteImport.update({
|
||||
id: '/certificates',
|
||||
path: '/certificates',
|
||||
getParentRoute: () => sshLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const sshSshCasPageRouteRoute = sshSshCasPageRouteImport.update({
|
||||
id: '/cas',
|
||||
path: '/cas',
|
||||
getParentRoute: () => sshLayoutRoute,
|
||||
} as any)
|
||||
|
||||
@@ -2153,18 +2160,25 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof secretManagerSettingsPageRouteImport
|
||||
parentRoute: typeof secretManagerLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/hosts': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/hosts'
|
||||
path: '/hosts'
|
||||
fullPath: '/ssh/$projectId/hosts'
|
||||
preLoaderRoute: typeof sshSshHostsPageRouteImport
|
||||
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas'
|
||||
path: '/cas'
|
||||
fullPath: '/ssh/$projectId/cas'
|
||||
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
|
||||
}
|
||||
'/_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'
|
||||
fullPath: '/ssh/$projectId/overview'
|
||||
preLoaderRoute: typeof sshOverviewPageRouteImport
|
||||
preLoaderRoute: typeof sshSshHostsPageRouteImport
|
||||
parentRoute: typeof sshLayoutImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings': {
|
||||
@@ -3477,8 +3491,9 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildr
|
||||
)
|
||||
|
||||
interface sshLayoutRouteChildren {
|
||||
sshSshCasPageRouteRoute: typeof sshSshCasPageRouteRoute
|
||||
sshSshCertsPageRouteRoute: typeof sshSshCertsPageRouteRoute
|
||||
sshSshHostsPageRouteRoute: typeof sshSshHostsPageRouteRoute
|
||||
sshOverviewPageRouteRoute: typeof sshOverviewPageRouteRoute
|
||||
sshSettingsPageRouteRoute: typeof sshSettingsPageRouteRoute
|
||||
projectAccessControlPageRouteSshRoute: typeof projectAccessControlPageRouteSshRoute
|
||||
sshSshCaByIDPageRouteRoute: typeof sshSshCaByIDPageRouteRoute
|
||||
@@ -3488,8 +3503,9 @@ interface sshLayoutRouteChildren {
|
||||
}
|
||||
|
||||
const sshLayoutRouteChildren: sshLayoutRouteChildren = {
|
||||
sshSshCasPageRouteRoute: sshSshCasPageRouteRoute,
|
||||
sshSshCertsPageRouteRoute: sshSshCertsPageRouteRoute,
|
||||
sshSshHostsPageRouteRoute: sshSshHostsPageRouteRoute,
|
||||
sshOverviewPageRouteRoute: sshOverviewPageRouteRoute,
|
||||
sshSettingsPageRouteRoute: sshSettingsPageRouteRoute,
|
||||
projectAccessControlPageRouteSshRoute: projectAccessControlPageRouteSshRoute,
|
||||
sshSshCaByIDPageRouteRoute: sshSshCaByIDPageRouteRoute,
|
||||
@@ -3771,8 +3787,9 @@ export interface FileRoutesByFullPath {
|
||||
'/secret-manager/$projectId/overview': typeof secretManagerOverviewPageRouteRoute
|
||||
'/secret-manager/$projectId/secret-rotation': typeof secretManagerSecretRotationPageRouteRoute
|
||||
'/secret-manager/$projectId/settings': typeof secretManagerSettingsPageRouteRoute
|
||||
'/ssh/$projectId/hosts': typeof sshSshHostsPageRouteRoute
|
||||
'/ssh/$projectId/overview': typeof sshOverviewPageRouteRoute
|
||||
'/ssh/$projectId/cas': typeof sshSshCasPageRouteRoute
|
||||
'/ssh/$projectId/certificates': typeof sshSshCertsPageRouteRoute
|
||||
'/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute
|
||||
'/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
|
||||
'/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
|
||||
'/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute
|
||||
@@ -3944,8 +3961,9 @@ export interface FileRoutesByTo {
|
||||
'/secret-manager/$projectId/overview': typeof secretManagerOverviewPageRouteRoute
|
||||
'/secret-manager/$projectId/secret-rotation': typeof secretManagerSecretRotationPageRouteRoute
|
||||
'/secret-manager/$projectId/settings': typeof secretManagerSettingsPageRouteRoute
|
||||
'/ssh/$projectId/hosts': typeof sshSshHostsPageRouteRoute
|
||||
'/ssh/$projectId/overview': typeof sshOverviewPageRouteRoute
|
||||
'/ssh/$projectId/cas': typeof sshSshCasPageRouteRoute
|
||||
'/ssh/$projectId/certificates': typeof sshSshCertsPageRouteRoute
|
||||
'/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute
|
||||
'/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
|
||||
'/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
|
||||
'/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/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/ssh/$projectId/_ssh-layout/hosts': typeof sshSshHostsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview': typeof sshOverviewPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas': typeof sshSshCasPageRouteRoute
|
||||
'/_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/cert-manager/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute
|
||||
'/_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/secret-rotation'
|
||||
| '/secret-manager/$projectId/settings'
|
||||
| '/ssh/$projectId/hosts'
|
||||
| '/ssh/$projectId/cas'
|
||||
| '/ssh/$projectId/certificates'
|
||||
| '/ssh/$projectId/overview'
|
||||
| '/ssh/$projectId/settings'
|
||||
| '/cert-manager/$projectId/access-management'
|
||||
@@ -4487,7 +4507,8 @@ export interface FileRouteTypes {
|
||||
| '/secret-manager/$projectId/overview'
|
||||
| '/secret-manager/$projectId/secret-rotation'
|
||||
| '/secret-manager/$projectId/settings'
|
||||
| '/ssh/$projectId/hosts'
|
||||
| '/ssh/$projectId/cas'
|
||||
| '/ssh/$projectId/certificates'
|
||||
| '/ssh/$projectId/overview'
|
||||
| '/ssh/$projectId/settings'
|
||||
| '/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/secret-rotation'
|
||||
| '/_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/settings'
|
||||
| '/_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",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId",
|
||||
"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/settings",
|
||||
"/_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",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/hosts": {
|
||||
"filePath": "ssh/SshHostsPage/route.tsx",
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas": {
|
||||
"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"
|
||||
},
|
||||
"/_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"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings": {
|
||||
|
||||
@@ -306,8 +306,9 @@ const kmsRoutes = route("/kms/$projectId", [
|
||||
|
||||
const sshRoutes = route("/ssh/$projectId", [
|
||||
layout("ssh-layout", "ssh/layout.tsx", [
|
||||
route("/overview", "ssh/OverviewPage/route.tsx"),
|
||||
route("/hosts", "ssh/SshHostsPage/route.tsx"),
|
||||
route("/overview", "ssh/SshHostsPage/route.tsx"),
|
||||
route("/certificates", "ssh/SshCertsPage/route.tsx"),
|
||||
route("/cas", "ssh/SshCasPage/route.tsx"),
|
||||
route("/ca/$caId", "ssh/SshCaByIDPage/route.tsx"),
|
||||
route("/settings", "ssh/SettingsPage/route.tsx"),
|
||||
route("/access-management", "project/AccessControlPage/route-ssh.tsx"),
|
||||
|
||||
Reference in New Issue
Block a user