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
+180 -99
View File
@@ -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;
}
});
};