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);
}
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");
});
}
}
+2 -1
View File
@@ -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>;
+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;
}
});
};
@@ -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:
+10
View File
@@ -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."
}
};
+7
View File
@@ -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
};
+2
View File
@@ -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
);