mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 22:28:15 +00:00
Add list accessible ssh hosts endpoint
This commit is contained in:
@@ -10,6 +10,51 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
|||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.array(
|
||||||
|
sanitizedSshHost.extend({
|
||||||
|
loginMappings: z.array(
|
||||||
|
z.object({
|
||||||
|
loginUser: z.string(),
|
||||||
|
allowedPrincipals: z.array(z.string())
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const hosts = await server.services.sshHost.listSshHosts({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
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
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
return hosts;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:sshHostId",
|
url: "/:sshHostId",
|
||||||
@@ -91,7 +136,9 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.default([])
|
.default([])
|
||||||
.describe(SSH_HOSTS.CREATE.loginMappings)
|
.describe(SSH_HOSTS.CREATE.loginMappings),
|
||||||
|
userSshCaId: z.string().describe(SSH_HOSTS.CREATE.userSshCaId).optional(),
|
||||||
|
hostSshCaId: z.string().describe(SSH_HOSTS.CREATE.hostSshCaId).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: sanitizedSshHost.extend({
|
200: sanitizedSshHost.extend({
|
||||||
@@ -265,7 +312,6 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
// TODO: consider just using the SSH issue creds endpoint
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:sshHostId/issue",
|
url: "/:sshHostId/issue",
|
||||||
config: {
|
config: {
|
||||||
@@ -287,15 +333,18 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: () => {
|
handler: async (req) => {
|
||||||
// const { serialNumber, signedPublicKey, privateKey, publicKey, certificateTemplate, ttl, keyId } =
|
const { serialNumber, signedPublicKey, privateKey, publicKey, keyAlgorithm } =
|
||||||
// await server.services.sshCertificateAuthority.issueSshCreds({
|
await server.services.sshHost.issueSshCredsFromHost({
|
||||||
// actor: req.permission.type,
|
sshHostId: req.params.sshHostId,
|
||||||
// actorId: req.permission.id,
|
actor: req.permission.type,
|
||||||
// actorAuthMethod: req.permission.authMethod,
|
actorId: req.permission.id,
|
||||||
// actorOrgId: req.permission.orgId,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
// ...req.body
|
actorOrgId: req.permission.orgId
|
||||||
// });
|
});
|
||||||
|
|
||||||
|
// TODO: add audit log
|
||||||
|
|
||||||
// await server.services.auditLog.createAuditLog({
|
// await server.services.auditLog.createAuditLog({
|
||||||
// ...req.auditLogInfo,
|
// ...req.auditLogInfo,
|
||||||
// orgId: req.permission.orgId,
|
// orgId: req.permission.orgId,
|
||||||
@@ -320,19 +369,13 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
|||||||
// ...req.auditLogInfo
|
// ...req.auditLogInfo
|
||||||
// }
|
// }
|
||||||
// });
|
// });
|
||||||
// return {
|
|
||||||
// serialNumber,
|
|
||||||
// signedKey: signedPublicKey,
|
|
||||||
// privateKey,
|
|
||||||
// publicKey,
|
|
||||||
// keyAlgorithm: req.body.keyAlgorithm
|
|
||||||
// };
|
|
||||||
return {
|
return {
|
||||||
serialNumber: "",
|
serialNumber,
|
||||||
signedKey: "",
|
signedKey: signedPublicKey,
|
||||||
privateKey: "",
|
privateKey,
|
||||||
publicKey: "",
|
publicKey,
|
||||||
keyAlgorithm: SshCertKeyAlgorithm.ED25519
|
keyAlgorithm
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,69 @@ export type TSshHostDALFactory = ReturnType<typeof sshHostDALFactory>;
|
|||||||
export const sshHostDALFactory = (db: TDbClient) => {
|
export const sshHostDALFactory = (db: TDbClient) => {
|
||||||
const sshHostOrm = ormify(db, TableName.SshHost);
|
const sshHostOrm = ormify(db, TableName.SshHost);
|
||||||
|
|
||||||
|
const findSshHostsWithPrincipalsAcrossProjects = async (projectIds: string[], principals: string[], tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const matchingSshHosts = await (tx || db.replicaNode())(TableName.SshHost)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SshHostLoginMapping,
|
||||||
|
`${TableName.SshHost}.id`,
|
||||||
|
`${TableName.SshHostLoginMapping}.sshHostId`
|
||||||
|
)
|
||||||
|
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
|
||||||
|
.whereRaw(`"${TableName.SshHostLoginMapping}"."allowedPrincipals" && ?::text[]`, [principals])
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.SshHost).as("sshHostId"),
|
||||||
|
db.ref("projectId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostname").withSchema(TableName.SshHost),
|
||||||
|
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||||
|
db.ref("loginUser").withSchema(TableName.SshHostLoginMapping),
|
||||||
|
db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping),
|
||||||
|
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||||
|
)
|
||||||
|
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
|
||||||
|
|
||||||
|
const grouped = groupBy(matchingSshHosts, (r) => r.sshHostId);
|
||||||
|
return Object.values(grouped).map((hostRows) => {
|
||||||
|
const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } = hostRows[0];
|
||||||
|
|
||||||
|
const loginMappingGrouped = groupBy(
|
||||||
|
hostRows.filter((r) => r.loginUser),
|
||||||
|
(r) => r.loginUser
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginMappings = Object.entries(loginMappingGrouped)
|
||||||
|
.map(([loginUser, entries]) => {
|
||||||
|
const filteredPrincipals = unique(entries.flatMap((entry) => entry.allowedPrincipals ?? [])).filter(
|
||||||
|
(principal) => principals.includes(principal)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filteredPrincipals.length === 0) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
loginUser,
|
||||||
|
allowedPrincipals: filteredPrincipals
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean) as { loginUser: string; allowedPrincipals: string[] }[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: sshHostId,
|
||||||
|
hostname,
|
||||||
|
projectId,
|
||||||
|
userCertTtl,
|
||||||
|
hostCertTtl,
|
||||||
|
loginMappings,
|
||||||
|
userSshCaId,
|
||||||
|
hostSshCaId
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostsWithPrincipalsAcrossProjects` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findSshHostsWithLoginMappings = async (projectId: string, tx?: Knex) => {
|
const findSshHostsWithLoginMappings = async (projectId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const rows = await (tx || db.replicaNode())(TableName.SshHost)
|
const rows = await (tx || db.replicaNode())(TableName.SshHost)
|
||||||
@@ -27,13 +90,15 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||||
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||||
db.ref("loginUser").withSchema(TableName.SshHostLoginMapping),
|
db.ref("loginUser").withSchema(TableName.SshHostLoginMapping),
|
||||||
db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping)
|
db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping),
|
||||||
|
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||||
)
|
)
|
||||||
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
|
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
|
||||||
|
|
||||||
const hostsGrouped = groupBy(rows, (r) => r.sshHostId);
|
const hostsGrouped = groupBy(rows, (r) => r.sshHostId);
|
||||||
return Object.values(hostsGrouped).map((hostRows) => {
|
return Object.values(hostsGrouped).map((hostRows) => {
|
||||||
const { sshHostId, hostname, userCertTtl, hostCertTtl } = hostRows[0];
|
const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0];
|
||||||
|
|
||||||
const loginMappingGrouped = groupBy(
|
const loginMappingGrouped = groupBy(
|
||||||
hostRows.filter((r) => r.loginUser),
|
hostRows.filter((r) => r.loginUser),
|
||||||
@@ -51,7 +116,9 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
hostname,
|
hostname,
|
||||||
userCertTtl,
|
userCertTtl,
|
||||||
hostCertTtl,
|
hostCertTtl,
|
||||||
loginMappings
|
loginMappings,
|
||||||
|
userSshCaId,
|
||||||
|
hostSshCaId
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -75,12 +142,14 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
db.ref("userCertTtl").withSchema(TableName.SshHost),
|
||||||
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
db.ref("hostCertTtl").withSchema(TableName.SshHost),
|
||||||
db.ref("loginUser").withSchema(TableName.SshHostLoginMapping),
|
db.ref("loginUser").withSchema(TableName.SshHostLoginMapping),
|
||||||
db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping)
|
db.ref("allowedPrincipals").withSchema(TableName.SshHostLoginMapping),
|
||||||
|
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rows.length === 0) return null;
|
if (rows.length === 0) return null;
|
||||||
|
|
||||||
const { sshHostId: id, projectId, hostname, userCertTtl, hostCertTtl } = rows[0];
|
const { sshHostId: id, projectId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0];
|
||||||
|
|
||||||
const loginMappingGrouped = groupBy(
|
const loginMappingGrouped = groupBy(
|
||||||
rows.filter((r) => r.loginUser),
|
rows.filter((r) => r.loginUser),
|
||||||
@@ -98,7 +167,9 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
hostname,
|
hostname,
|
||||||
userCertTtl,
|
userCertTtl,
|
||||||
hostCertTtl,
|
hostCertTtl,
|
||||||
loginMappings
|
loginMappings,
|
||||||
|
userSshCaId,
|
||||||
|
hostSshCaId
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostByIdWithLoginMappings` });
|
throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostByIdWithLoginMappings` });
|
||||||
@@ -107,6 +178,7 @@ export const sshHostDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...sshHostOrm,
|
...sshHostOrm,
|
||||||
|
findSshHostsWithPrincipalsAcrossProjects,
|
||||||
findSshHostsWithLoginMappings,
|
findSshHostsWithLoginMappings,
|
||||||
findSshHostByIdWithLoginMappings
|
findSshHostByIdWithLoginMappings
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,24 +1,54 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||||
|
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||||
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
import { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
||||||
import { TSshHostLoginMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-mapping-dal";
|
import { TSshHostLoginMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-mapping-dal";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } 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 { TCreateSshHostDTO, TDeleteSshHostDTO, TGetSshHostDTO, TUpdateSshHostDTO } from "./ssh-host-types";
|
import { convertActorToPrincipals, createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns";
|
||||||
|
import { SshCertType } from "../ssh/ssh-certificate-authority-types";
|
||||||
|
import {
|
||||||
|
TCreateSshHostDTO,
|
||||||
|
TDeleteSshHostDTO,
|
||||||
|
TGetSshHostDTO,
|
||||||
|
TIssueSshCredsFromHostDTO,
|
||||||
|
TListSshHostsDTO,
|
||||||
|
TUpdateSshHostDTO
|
||||||
|
} from "./ssh-host-types";
|
||||||
|
|
||||||
type TSshCertificateAuthorityServiceFactoryDep = {
|
type TSshCertificateAuthorityServiceFactoryDep = {
|
||||||
|
userDAL: Pick<TUserDALFactory, "findById">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "find">;
|
||||||
|
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">;
|
||||||
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findById">;
|
||||||
|
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">;
|
||||||
sshHostDAL: Pick<
|
sshHostDAL: Pick<
|
||||||
TSshHostDALFactory,
|
TSshHostDALFactory,
|
||||||
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" | "findSshHostByIdWithLoginMappings"
|
| "transaction"
|
||||||
|
| "create"
|
||||||
|
| "findById"
|
||||||
|
| "updateById"
|
||||||
|
| "deleteById"
|
||||||
|
| "findOne"
|
||||||
|
| "findSshHostByIdWithLoginMappings"
|
||||||
|
| "findSshHostsWithPrincipalsAcrossProjects"
|
||||||
>;
|
>;
|
||||||
sshHostLoginMappingDAL: Pick<
|
sshHostLoginMappingDAL: Pick<
|
||||||
TSshHostLoginMappingDALFactory,
|
TSshHostLoginMappingDALFactory,
|
||||||
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" | "insertMany" | "delete"
|
"transaction" | "create" | "findById" | "updateById" | "deleteById" | "findOne" | "insertMany" | "delete"
|
||||||
>;
|
>;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>;
|
export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>;
|
||||||
@@ -29,17 +59,72 @@ export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const sshHostServiceFactory = ({
|
export const sshHostServiceFactory = ({
|
||||||
projectDAL
|
userDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectSshConfigDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
sshHostDAL,
|
sshHostDAL,
|
||||||
sshHostLoginMappingDAL,
|
sshHostLoginMappingDAL,
|
||||||
permissionService
|
permissionService,
|
||||||
|
kmsService
|
||||||
}: TSshCertificateAuthorityServiceFactoryDep) => {
|
}: TSshCertificateAuthorityServiceFactoryDep) => {
|
||||||
|
/**
|
||||||
|
* Return list of all SSH hosts that a user has access to across all SSH projects in the organization
|
||||||
|
*/
|
||||||
|
const listSshHosts = async ({ actorId, actorAuthMethod, actor, actorOrgId }: TListSshHostsDTO) => {
|
||||||
|
const sshProjects = await projectDAL.find({
|
||||||
|
orgId: actorOrgId,
|
||||||
|
type: ProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectIdsWithAccess: string[] = [];
|
||||||
|
|
||||||
|
for await (const project of sshProjects) {
|
||||||
|
let hasAccess = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: consider glob-based permission items
|
||||||
|
hasAccess = permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SshHosts);
|
||||||
|
} catch {
|
||||||
|
hasAccess = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAccess) {
|
||||||
|
projectIdsWithAccess.push(project.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const principals = await convertActorToPrincipals({
|
||||||
|
// actor,
|
||||||
|
// actorId,
|
||||||
|
// userDAL
|
||||||
|
// });
|
||||||
|
|
||||||
|
const hosts = await sshHostDAL.findSshHostsWithPrincipalsAcrossProjects(projectIdsWithAccess, [
|
||||||
|
"[email protected]" // hardcode for now
|
||||||
|
]);
|
||||||
|
|
||||||
|
return hosts;
|
||||||
|
};
|
||||||
|
|
||||||
const createSshHost = async ({
|
const createSshHost = async ({
|
||||||
projectId,
|
projectId,
|
||||||
hostname,
|
hostname,
|
||||||
userCertTtl,
|
userCertTtl,
|
||||||
hostCertTtl,
|
hostCertTtl,
|
||||||
loginMappings,
|
loginMappings,
|
||||||
|
userSshCaId: requestedUserSshCaId,
|
||||||
|
hostSshCaId: requestedHostSshCaId,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actor,
|
actor,
|
||||||
@@ -56,6 +141,42 @@ export const sshHostServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.SshHosts);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.SshHosts);
|
||||||
|
|
||||||
|
const resolveSshCaId = async ({
|
||||||
|
requestedId,
|
||||||
|
fallbackId,
|
||||||
|
label
|
||||||
|
}: {
|
||||||
|
requestedId?: string;
|
||||||
|
fallbackId?: string | null;
|
||||||
|
label: "User" | "Host";
|
||||||
|
}) => {
|
||||||
|
const finalId = requestedId ?? fallbackId;
|
||||||
|
if (!finalId) {
|
||||||
|
throw new BadRequestError({ message: `Missing ${label.toLowerCase()} SSH CA` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ca = await sshCertificateAuthorityDAL.findById(finalId);
|
||||||
|
if (!ca) {
|
||||||
|
throw new BadRequestError({ message: `${label} SSH CA with ID '${finalId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return ca.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectSshConfig = await projectSshConfigDAL.findOne({ projectId });
|
||||||
|
|
||||||
|
const userSshCaId = await resolveSshCaId({
|
||||||
|
requestedId: requestedUserSshCaId,
|
||||||
|
fallbackId: projectSshConfig?.defaultUserSshCaId,
|
||||||
|
label: "User"
|
||||||
|
});
|
||||||
|
|
||||||
|
const hostSshCaId = await resolveSshCaId({
|
||||||
|
requestedId: requestedHostSshCaId,
|
||||||
|
fallbackId: projectSshConfig?.defaultHostSshCaId,
|
||||||
|
label: "Host"
|
||||||
|
});
|
||||||
|
|
||||||
const newSshHost = await sshHostDAL.transaction(async (tx) => {
|
const newSshHost = await sshHostDAL.transaction(async (tx) => {
|
||||||
const existingHost = await sshHostDAL.findOne(
|
const existingHost = await sshHostDAL.findOne(
|
||||||
{
|
{
|
||||||
@@ -71,16 +192,14 @@ export const sshHostServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// attach hosts?
|
|
||||||
|
|
||||||
// create host ssh cas in default bound to default user ca and host ca
|
|
||||||
|
|
||||||
const host = await sshHostDAL.create(
|
const host = await sshHostDAL.create(
|
||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
hostname,
|
hostname,
|
||||||
userCertTtl,
|
userCertTtl,
|
||||||
hostCertTtl
|
hostCertTtl,
|
||||||
|
userSshCaId,
|
||||||
|
hostSshCaId
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -211,10 +330,88 @@ export const sshHostServiceFactory = ({
|
|||||||
return host;
|
return host;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return SSH certificate and corresponding new SSH public-private key pair where
|
||||||
|
* SSH public key is signed using CA behind SSH certificate with name [templateName].
|
||||||
|
*
|
||||||
|
* Note: Used for issuing SSH credentials as part of request against a specific SSH Host.
|
||||||
|
*/
|
||||||
|
const issueSshCredsFromHost = async ({
|
||||||
|
sshHostId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TIssueSshCredsFromHostDTO) => {
|
||||||
|
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 keyId = `${actor}-${actorId}`;
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
// create user key pair
|
||||||
|
const keyAlgorithm = SshCertKeyAlgorithm.ED25519; // (dangtony98): will support more algorithms in the future
|
||||||
|
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
|
||||||
|
|
||||||
|
const principals = await convertActorToPrincipals({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
userDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const { serialNumber, signedPublicKey, ttl } = await createSshCert({
|
||||||
|
caPrivateKey: decryptedCaPrivateKey.toString("utf8"),
|
||||||
|
clientPublicKey: publicKey,
|
||||||
|
keyId,
|
||||||
|
principals,
|
||||||
|
requestedTtl: host.userCertTtl,
|
||||||
|
certType: SshCertType.USER
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
signedPublicKey,
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
ttl,
|
||||||
|
keyId,
|
||||||
|
keyAlgorithm
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
listSshHosts,
|
||||||
createSshHost,
|
createSshHost,
|
||||||
updateSshHost,
|
updateSshHost,
|
||||||
deleteSshHost,
|
deleteSshHost,
|
||||||
getSshHost
|
getSshHost,
|
||||||
|
issueSshCredsFromHost
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TCreateSshHostDTO = {
|
export type TCreateSshHostDTO = {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
userCertTtl: string;
|
userCertTtl: string;
|
||||||
@@ -8,6 +10,8 @@ export type TCreateSshHostDTO = {
|
|||||||
loginUser: string;
|
loginUser: string;
|
||||||
allowedPrincipals: string[];
|
allowedPrincipals: string[];
|
||||||
}[];
|
}[];
|
||||||
|
userSshCaId?: string;
|
||||||
|
hostSshCaId?: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TUpdateSshHostDTO = {
|
export type TUpdateSshHostDTO = {
|
||||||
@@ -28,3 +32,7 @@ export type TGetSshHostDTO = {
|
|||||||
export type TDeleteSshHostDTO = {
|
export type TDeleteSshHostDTO = {
|
||||||
sshHostId: string;
|
sshHostId: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIssueSshCredsFromHostDTO = {
|
||||||
|
sshHostId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certif
|
|||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
SshCaKeySource,
|
SshCaKeySource,
|
||||||
SshCaStatus,
|
SshCaStatus,
|
||||||
SshCertType,
|
SshCertType,
|
||||||
|
TConvertActorToPrincipalsDTO,
|
||||||
TCreateSshCaHelperDTO,
|
TCreateSshCaHelperDTO,
|
||||||
TCreateSshCertDTO
|
TCreateSshCertDTO
|
||||||
} from "./ssh-certificate-authority-types";
|
} from "./ssh-certificate-authority-types";
|
||||||
@@ -439,17 +441,32 @@ export const createSshCert = async ({
|
|||||||
clientPublicKey,
|
clientPublicKey,
|
||||||
keyId,
|
keyId,
|
||||||
principals,
|
principals,
|
||||||
requestedTtl,
|
requestedTtl, // in ms lib format
|
||||||
certType
|
certType
|
||||||
}: TCreateSshCertDTO) => {
|
}: TCreateSshCertDTO) => {
|
||||||
// validate if the requested [certType] is allowed under the template configuration
|
let ttl: number | undefined;
|
||||||
validateSshCertificateType(template, certType);
|
|
||||||
|
|
||||||
// validate if the requested [principals] are valid for the given [certType] under the template configuration
|
if (!template && requestedTtl) {
|
||||||
validateSshCertificatePrincipals(certType, template, principals);
|
const parsedTtl = Math.ceil(ms(requestedTtl) / 1000);
|
||||||
|
if (parsedTtl > 0) ttl = parsedTtl;
|
||||||
|
}
|
||||||
|
|
||||||
// validate if the requested TTL is valid under the template configuration
|
if (template) {
|
||||||
const ttl = validateSshCertificateTtl(template, requestedTtl);
|
// validate if the requested [certType] is allowed under the template configuration
|
||||||
|
validateSshCertificateType(template, certType);
|
||||||
|
|
||||||
|
// validate if the requested [principals] are valid for the given [certType] under the template configuration
|
||||||
|
validateSshCertificatePrincipals(certType, template, principals);
|
||||||
|
|
||||||
|
// validate if the requested TTL is valid under the template configuration
|
||||||
|
ttl = validateSshCertificateTtl(template, requestedTtl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ttl) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to create SSH certificate due to missing TTL"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
validateSshCertificateKeyId(keyId);
|
validateSshCertificateKeyId(keyId);
|
||||||
await validateSshPublicKey(clientPublicKey);
|
await validateSshPublicKey(clientPublicKey);
|
||||||
@@ -561,3 +578,23 @@ export const createSshCaHelper = async ({
|
|||||||
|
|
||||||
return sshCertificateAuthorityDAL.transaction(processCreation);
|
return sshCertificateAuthorityDAL.transaction(processCreation);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert an actor to a list of principals to be included in an SSH certificate.
|
||||||
|
*
|
||||||
|
* (dangtony98): This function is only supported for user actors at the moment and returns
|
||||||
|
* only the email of the associated user. In the future, we will consider other
|
||||||
|
* actor types and attributes such as group membership slugs and/or metadata to be
|
||||||
|
* included in the list of principals.
|
||||||
|
*/
|
||||||
|
export const convertActorToPrincipals = async ({ userDAL, actor, actorId }: TConvertActorToPrincipalsDTO) => {
|
||||||
|
if (actor !== ActorType.USER) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to convert actor to principals due to unsupported actor type"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await userDAL.findById(actorId);
|
||||||
|
|
||||||
|
return [user.username];
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,22 +7,14 @@ import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-cer
|
|||||||
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||||
import { TSshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal";
|
import { TSshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-body-dal";
|
||||||
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
||||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
|
||||||
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
import { SshCertTemplateStatus } from "../ssh-certificate-template/ssh-certificate-template-types";
|
import { SshCertTemplateStatus } from "../ssh-certificate-template/ssh-certificate-template-types";
|
||||||
|
import { createSshCaHelper, createSshCert, createSshKeyPair, getSshPublicKey } from "./ssh-certificate-authority-fns";
|
||||||
import {
|
import {
|
||||||
createSshCaHelper,
|
|
||||||
createSshCert,
|
|
||||||
createSshKeyPair,
|
|
||||||
getSshPublicKey,
|
|
||||||
validateExternalSshCaKeyPair
|
|
||||||
} from "./ssh-certificate-authority-fns";
|
|
||||||
import {
|
|
||||||
SshCaKeySource,
|
|
||||||
SshCaStatus,
|
SshCaStatus,
|
||||||
TCreateSshCaDTO,
|
TCreateSshCaDTO,
|
||||||
TDeleteSshCaDTO,
|
TDeleteSshCaDTO,
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-cer
|
|||||||
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||||
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
export enum SshCaStatus {
|
export enum SshCaStatus {
|
||||||
ACTIVE = "active",
|
ACTIVE = "active",
|
||||||
@@ -84,7 +86,7 @@ export type TGetSshCaCertificateTemplatesDTO = {
|
|||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TCreateSshCertDTO = {
|
export type TCreateSshCertDTO = {
|
||||||
template: TSshCertificateTemplates;
|
template?: TSshCertificateTemplates;
|
||||||
caPrivateKey: string;
|
caPrivateKey: string;
|
||||||
clientPublicKey: string;
|
clientPublicKey: string;
|
||||||
keyId: string;
|
keyId: string;
|
||||||
@@ -92,3 +94,9 @@ export type TCreateSshCertDTO = {
|
|||||||
requestedTtl?: string;
|
requestedTtl?: string;
|
||||||
certType: SshCertType;
|
certType: SshCertType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TConvertActorToPrincipalsDTO = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
userDAL: Pick<TUserDALFactory, "findById">;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1328,7 +1328,11 @@ export const SSH_HOSTS = {
|
|||||||
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
|
loginUser: "A login user on the remote machine (e.g. 'ec2-user', 'deploy', 'admin')",
|
||||||
allowedPrincipals: "A list of allowed principals that can log in as the login user.",
|
allowedPrincipals: "A list of allowed principals that can log in as the login user.",
|
||||||
loginMappings:
|
loginMappings:
|
||||||
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals."
|
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals.",
|
||||||
|
userSshCaId:
|
||||||
|
"The ID of the SSH CA to use for user certificates. If not specified, the default user SSH CA will be used if it exists.",
|
||||||
|
hostSshCaId:
|
||||||
|
"The ID of the SSH CA to use for host certificates. If not specified, the default host SSH CA will be used if it exists."
|
||||||
},
|
},
|
||||||
UPDATE: {
|
UPDATE: {
|
||||||
sshHostId: "The ID of the SSH host to update.",
|
sshHostId: "The ID of the SSH host to update.",
|
||||||
|
|||||||
@@ -798,9 +798,15 @@ export const registerRoutes = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const sshHostService = sshHostServiceFactory({
|
const sshHostService = sshHostServiceFactory({
|
||||||
|
userDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectSshConfigDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
sshHostDAL,
|
sshHostDAL,
|
||||||
sshHostLoginMappingDAL,
|
sshHostLoginMappingDAL,
|
||||||
permissionService
|
permissionService,
|
||||||
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||||
|
|||||||
@@ -1068,6 +1068,7 @@ export const projectServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
projectId
|
projectId
|
||||||
}: TListProjectSshHostsDTO) => {
|
}: TListProjectSshHostsDTO) => {
|
||||||
|
console.log("listProjectSshHosts: ", actor, actorId, actorAuthMethod, actorOrgId, projectId);
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const SshHostsPage = () => {
|
|||||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
<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">
|
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Overview"
|
title="Hosts"
|
||||||
description="Infisical SSH lets you issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure."
|
description="Infisical SSH lets you issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure."
|
||||||
/>
|
/>
|
||||||
<SshHostsSection />
|
<SshHostsSection />
|
||||||
|
|||||||
@@ -52,17 +52,8 @@ export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
data.map((host) => {
|
data.map((host) => {
|
||||||
return (
|
return (
|
||||||
<Tr
|
<Tr
|
||||||
className="h-10"
|
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
key={`ssh-host-${host.id}`}
|
key={`ssh-host-${host.id}`}
|
||||||
// onClick={() =>
|
|
||||||
// navigate({
|
|
||||||
// to: `/${ProjectType.SSH}/$projectId/ca/$caId` as const,
|
|
||||||
// params: {
|
|
||||||
// projectId: currentWorkspace.id,
|
|
||||||
// caId: ca.id
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
>
|
>
|
||||||
<Td>{host.hostname}</Td>
|
<Td>{host.hostname}</Td>
|
||||||
<Td className="flex justify-end">
|
<Td className="flex justify-end">
|
||||||
|
|||||||
Reference in New Issue
Block a user