Merge branch 'main' into feat/terraformCloudIntegration
@@ -14,3 +14,11 @@ docs/self-hosting/guides/automated-bootstrapping.mdx:jwt:74
|
|||||||
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx:generic-api-key:72
|
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx:generic-api-key:72
|
||||||
k8-operator/config/samples/crd/pushsecret/source-secret-with-templating.yaml:private-key:11
|
k8-operator/config/samples/crd/pushsecret/source-secret-with-templating.yaml:private-key:11
|
||||||
k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml:private-key:52
|
k8-operator/config/samples/crd/pushsecret/push-secret-with-template.yaml:private-key:52
|
||||||
|
backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts:generic-api-key:125
|
||||||
|
frontend/src/components/permissions/AccessTree/nodes/RoleNode.tsx:generic-api-key:67
|
||||||
|
frontend/src/components/secret-rotations-v2/RotateSecretRotationV2Modal.tsx:generic-api-key:14
|
||||||
|
frontend/src/components/secret-rotations-v2/SecretRotationV2StatusBadge.tsx:generic-api-key:11
|
||||||
|
frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx:generic-api-key:23
|
||||||
|
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:28
|
||||||
|
frontend/src/hooks/api/secretRotationsV2/types/index.ts:generic-api-key:65
|
||||||
|
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationItem.tsx:generic-api-key:26
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/
|
|||||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||||
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
import { TSshCertificateAuthorityServiceFactory } from "@app/ee/services/ssh/ssh-certificate-authority-service";
|
||||||
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
import { TSshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
||||||
|
import { TSshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service";
|
||||||
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
||||||
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
@@ -206,6 +207,7 @@ declare module "fastify" {
|
|||||||
certificateTemplate: TCertificateTemplateServiceFactory;
|
certificateTemplate: TCertificateTemplateServiceFactory;
|
||||||
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
|
sshCertificateAuthority: TSshCertificateAuthorityServiceFactory;
|
||||||
sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
|
sshCertificateTemplate: TSshCertificateTemplateServiceFactory;
|
||||||
|
sshHost: TSshHostServiceFactory;
|
||||||
certificateAuthority: TCertificateAuthorityServiceFactory;
|
certificateAuthority: TCertificateAuthorityServiceFactory;
|
||||||
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
certificateAuthorityCrl: TCertificateAuthorityCrlServiceFactory;
|
||||||
certificateEst: TCertificateEstServiceFactory;
|
certificateEst: TCertificateEstServiceFactory;
|
||||||
|
|||||||
@@ -232,6 +232,9 @@ import {
|
|||||||
TProjectSplitBackfillIds,
|
TProjectSplitBackfillIds,
|
||||||
TProjectSplitBackfillIdsInsert,
|
TProjectSplitBackfillIdsInsert,
|
||||||
TProjectSplitBackfillIdsUpdate,
|
TProjectSplitBackfillIdsUpdate,
|
||||||
|
TProjectSshConfigs,
|
||||||
|
TProjectSshConfigsInsert,
|
||||||
|
TProjectSshConfigsUpdate,
|
||||||
TProjectsUpdate,
|
TProjectsUpdate,
|
||||||
TProjectTemplates,
|
TProjectTemplates,
|
||||||
TProjectTemplatesInsert,
|
TProjectTemplatesInsert,
|
||||||
@@ -380,6 +383,15 @@ import {
|
|||||||
TSshCertificateTemplates,
|
TSshCertificateTemplates,
|
||||||
TSshCertificateTemplatesInsert,
|
TSshCertificateTemplatesInsert,
|
||||||
TSshCertificateTemplatesUpdate,
|
TSshCertificateTemplatesUpdate,
|
||||||
|
TSshHostLoginUserMappings,
|
||||||
|
TSshHostLoginUserMappingsInsert,
|
||||||
|
TSshHostLoginUserMappingsUpdate,
|
||||||
|
TSshHostLoginUsers,
|
||||||
|
TSshHostLoginUsersInsert,
|
||||||
|
TSshHostLoginUsersUpdate,
|
||||||
|
TSshHosts,
|
||||||
|
TSshHostsInsert,
|
||||||
|
TSshHostsUpdate,
|
||||||
TSuperAdmin,
|
TSuperAdmin,
|
||||||
TSuperAdminInsert,
|
TSuperAdminInsert,
|
||||||
TSuperAdminUpdate,
|
TSuperAdminUpdate,
|
||||||
@@ -425,6 +437,7 @@ declare module "knex/types/tables" {
|
|||||||
interface Tables {
|
interface Tables {
|
||||||
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
[TableName.Users]: KnexOriginal.CompositeTableType<TUsers, TUsersInsert, TUsersUpdate>;
|
||||||
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
[TableName.Groups]: KnexOriginal.CompositeTableType<TGroups, TGroupsInsert, TGroupsUpdate>;
|
||||||
|
[TableName.SshHost]: KnexOriginal.CompositeTableType<TSshHosts, TSshHostsInsert, TSshHostsUpdate>;
|
||||||
[TableName.SshCertificateAuthority]: KnexOriginal.CompositeTableType<
|
[TableName.SshCertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||||
TSshCertificateAuthorities,
|
TSshCertificateAuthorities,
|
||||||
TSshCertificateAuthoritiesInsert,
|
TSshCertificateAuthoritiesInsert,
|
||||||
@@ -450,6 +463,16 @@ declare module "knex/types/tables" {
|
|||||||
TSshCertificateBodiesInsert,
|
TSshCertificateBodiesInsert,
|
||||||
TSshCertificateBodiesUpdate
|
TSshCertificateBodiesUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.SshHostLoginUser]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshHostLoginUsers,
|
||||||
|
TSshHostLoginUsersInsert,
|
||||||
|
TSshHostLoginUsersUpdate
|
||||||
|
>;
|
||||||
|
[TableName.SshHostLoginUserMapping]: KnexOriginal.CompositeTableType<
|
||||||
|
TSshHostLoginUserMappings,
|
||||||
|
TSshHostLoginUserMappingsInsert,
|
||||||
|
TSshHostLoginUserMappingsUpdate
|
||||||
|
>;
|
||||||
[TableName.CertificateAuthority]: KnexOriginal.CompositeTableType<
|
[TableName.CertificateAuthority]: KnexOriginal.CompositeTableType<
|
||||||
TCertificateAuthorities,
|
TCertificateAuthorities,
|
||||||
TCertificateAuthoritiesInsert,
|
TCertificateAuthoritiesInsert,
|
||||||
@@ -554,6 +577,11 @@ declare module "knex/types/tables" {
|
|||||||
[TableName.SuperAdmin]: KnexOriginal.CompositeTableType<TSuperAdmin, TSuperAdminInsert, TSuperAdminUpdate>;
|
[TableName.SuperAdmin]: KnexOriginal.CompositeTableType<TSuperAdmin, TSuperAdminInsert, TSuperAdminUpdate>;
|
||||||
[TableName.ApiKey]: KnexOriginal.CompositeTableType<TApiKeys, TApiKeysInsert, TApiKeysUpdate>;
|
[TableName.ApiKey]: KnexOriginal.CompositeTableType<TApiKeys, TApiKeysInsert, TApiKeysUpdate>;
|
||||||
[TableName.Project]: KnexOriginal.CompositeTableType<TProjects, TProjectsInsert, TProjectsUpdate>;
|
[TableName.Project]: KnexOriginal.CompositeTableType<TProjects, TProjectsInsert, TProjectsUpdate>;
|
||||||
|
[TableName.ProjectSshConfig]: KnexOriginal.CompositeTableType<
|
||||||
|
TProjectSshConfigs,
|
||||||
|
TProjectSshConfigsInsert,
|
||||||
|
TProjectSshConfigsUpdate
|
||||||
|
>;
|
||||||
[TableName.ProjectMembership]: KnexOriginal.CompositeTableType<
|
[TableName.ProjectMembership]: KnexOriginal.CompositeTableType<
|
||||||
TProjectMemberships,
|
TProjectMemberships,
|
||||||
TProjectMembershipsInsert,
|
TProjectMembershipsInsert,
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.SshCertificateAuthority, "keySource"))) {
|
||||||
|
await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => {
|
||||||
|
t.string("keySource");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Backfilling the keySource to internal
|
||||||
|
await knex(TableName.SshCertificateAuthority).update({ keySource: "internal" });
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => {
|
||||||
|
t.string("keySource").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasColumn(TableName.SshCertificate, "sshCaId")) {
|
||||||
|
await knex.schema.alterTable(TableName.SshCertificate, (t) => {
|
||||||
|
t.uuid("sshCaId").nullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SshCertificateAuthority, "keySource")) {
|
||||||
|
await knex.schema.alterTable(TableName.SshCertificateAuthority, (t) => {
|
||||||
|
t.dropColumn("keySource");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshHost))) {
|
||||||
|
await knex.schema.createTable(TableName.SshHost, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.string("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.string("hostname").notNullable();
|
||||||
|
t.string("userCertTtl").notNullable();
|
||||||
|
t.string("hostCertTtl").notNullable();
|
||||||
|
t.uuid("userSshCaId").notNullable();
|
||||||
|
t.foreign("userSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.uuid("hostSshCaId").notNullable();
|
||||||
|
t.foreign("hostSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.unique(["projectId", "hostname"]);
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshHostLoginUser))) {
|
||||||
|
await knex.schema.createTable(TableName.SshHostLoginUser, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshHostId").notNullable();
|
||||||
|
t.foreign("sshHostId").references("id").inTable(TableName.SshHost).onDelete("CASCADE");
|
||||||
|
t.string("loginUser").notNullable(); // e.g. ubuntu, root, ec2-user, ...
|
||||||
|
t.unique(["sshHostId", "loginUser"]);
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshHostLoginUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SshHostLoginUserMapping))) {
|
||||||
|
await knex.schema.createTable(TableName.SshHostLoginUserMapping, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.uuid("sshHostLoginUserId").notNullable();
|
||||||
|
t.foreign("sshHostLoginUserId").references("id").inTable(TableName.SshHostLoginUser).onDelete("CASCADE");
|
||||||
|
t.uuid("userId").nullable();
|
||||||
|
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
t.unique(["sshHostLoginUserId", "userId"]);
|
||||||
|
});
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SshHostLoginUserMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await knex.schema.hasTable(TableName.ProjectSshConfig))) {
|
||||||
|
// new table to store configuration for projects of type SSH (i.e. Infisical SSH)
|
||||||
|
await knex.schema.createTable(TableName.ProjectSshConfig, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
t.string("projectId").notNullable();
|
||||||
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
t.uuid("defaultUserSshCaId");
|
||||||
|
t.foreign("defaultUserSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
t.uuid("defaultHostSshCaId");
|
||||||
|
t.foreign("defaultHostSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("CASCADE");
|
||||||
|
});
|
||||||
|
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> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.ProjectSshConfig);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.ProjectSshConfig);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshHostLoginUserMapping);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshHostLoginUserMapping);
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshHostLoginUser);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshHostLoginUser);
|
||||||
|
|
||||||
|
const hasColumn = await knex.schema.hasColumn(TableName.SshCertificate, "sshHostId");
|
||||||
|
if (hasColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.SshCertificate, (t) => {
|
||||||
|
t.dropColumn("sshHostId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SshHost);
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.SshHost);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "note");
|
||||||
|
if (!hasCol) {
|
||||||
|
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
|
||||||
|
t.string("note").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasCol = await knex.schema.hasColumn(TableName.AccessApprovalRequest, "note");
|
||||||
|
if (hasCol) {
|
||||||
|
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
|
||||||
|
t.dropColumn("note");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,8 @@ export const AccessApprovalRequestsSchema = z.object({
|
|||||||
permissions: z.unknown(),
|
permissions: z.unknown(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
requestedByUserId: z.string().uuid()
|
requestedByUserId: z.string().uuid(),
|
||||||
|
note: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;
|
export type TAccessApprovalRequests = z.infer<typeof AccessApprovalRequestsSchema>;
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export * from "./project-memberships";
|
|||||||
export * from "./project-roles";
|
export * from "./project-roles";
|
||||||
export * from "./project-slack-configs";
|
export * from "./project-slack-configs";
|
||||||
export * from "./project-split-backfill-ids";
|
export * from "./project-split-backfill-ids";
|
||||||
|
export * from "./project-ssh-configs";
|
||||||
export * from "./project-templates";
|
export * from "./project-templates";
|
||||||
export * from "./project-user-additional-privilege";
|
export * from "./project-user-additional-privilege";
|
||||||
export * from "./project-user-membership-roles";
|
export * from "./project-user-membership-roles";
|
||||||
@@ -125,6 +126,9 @@ export * from "./ssh-certificate-authority-secrets";
|
|||||||
export * from "./ssh-certificate-bodies";
|
export * from "./ssh-certificate-bodies";
|
||||||
export * from "./ssh-certificate-templates";
|
export * from "./ssh-certificate-templates";
|
||||||
export * from "./ssh-certificates";
|
export * from "./ssh-certificates";
|
||||||
|
export * from "./ssh-host-login-user-mappings";
|
||||||
|
export * from "./ssh-host-login-users";
|
||||||
|
export * from "./ssh-hosts";
|
||||||
export * from "./super-admin";
|
export * from "./super-admin";
|
||||||
export * from "./totp-configs";
|
export * from "./totp-configs";
|
||||||
export * from "./trusted-ips";
|
export * from "./trusted-ips";
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export enum TableName {
|
export enum TableName {
|
||||||
Users = "users",
|
Users = "users",
|
||||||
|
SshHost = "ssh_hosts",
|
||||||
|
SshHostLoginUser = "ssh_host_login_users",
|
||||||
|
SshHostLoginUserMapping = "ssh_host_login_user_mappings",
|
||||||
SshCertificateAuthority = "ssh_certificate_authorities",
|
SshCertificateAuthority = "ssh_certificate_authorities",
|
||||||
SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets",
|
SshCertificateAuthoritySecret = "ssh_certificate_authority_secrets",
|
||||||
SshCertificateTemplate = "ssh_certificate_templates",
|
SshCertificateTemplate = "ssh_certificate_templates",
|
||||||
@@ -38,6 +41,7 @@ export enum TableName {
|
|||||||
SuperAdmin = "super_admin",
|
SuperAdmin = "super_admin",
|
||||||
RateLimit = "rate_limit",
|
RateLimit = "rate_limit",
|
||||||
ApiKey = "api_keys",
|
ApiKey = "api_keys",
|
||||||
|
ProjectSshConfig = "project_ssh_configs",
|
||||||
Project = "projects",
|
Project = "projects",
|
||||||
ProjectBot = "project_bots",
|
ProjectBot = "project_bots",
|
||||||
Environment = "project_environments",
|
Environment = "project_environments",
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const ProjectSshConfigsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
projectId: z.string(),
|
||||||
|
defaultUserSshCaId: z.string().uuid().nullable().optional(),
|
||||||
|
defaultHostSshCaId: z.string().uuid().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TProjectSshConfigs = z.infer<typeof ProjectSshConfigsSchema>;
|
||||||
|
export type TProjectSshConfigsInsert = Omit<z.input<typeof ProjectSshConfigsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TProjectSshConfigsUpdate = Partial<Omit<z.input<typeof ProjectSshConfigsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -14,7 +14,8 @@ export const SshCertificateAuthoritiesSchema = z.object({
|
|||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
friendlyName: z.string(),
|
friendlyName: z.string(),
|
||||||
keyAlgorithm: z.string()
|
keyAlgorithm: z.string(),
|
||||||
|
keySource: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSshCertificateAuthorities = z.infer<typeof SshCertificateAuthoritiesSchema>;
|
export type TSshCertificateAuthorities = z.infer<typeof SshCertificateAuthoritiesSchema>;
|
||||||
|
|||||||
@@ -11,14 +11,15 @@ export const SshCertificatesSchema = z.object({
|
|||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
sshCaId: z.string().uuid(),
|
sshCaId: z.string().uuid().nullable().optional(),
|
||||||
sshCertificateTemplateId: z.string().uuid().nullable().optional(),
|
sshCertificateTemplateId: z.string().uuid().nullable().optional(),
|
||||||
serialNumber: z.string(),
|
serialNumber: z.string(),
|
||||||
certType: z.string(),
|
certType: z.string(),
|
||||||
principals: z.string().array(),
|
principals: z.string().array(),
|
||||||
keyId: z.string(),
|
keyId: z.string(),
|
||||||
notBefore: z.date(),
|
notBefore: z.date(),
|
||||||
notAfter: z.date()
|
notAfter: z.date(),
|
||||||
|
sshHostId: z.string().uuid().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSshCertificates = z.infer<typeof SshCertificatesSchema>;
|
export type TSshCertificates = z.infer<typeof SshCertificatesSchema>;
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SshHostLoginUserMappingsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
sshHostLoginUserId: z.string().uuid(),
|
||||||
|
userId: z.string().uuid().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshHostLoginUserMappings = z.infer<typeof SshHostLoginUserMappingsSchema>;
|
||||||
|
export type TSshHostLoginUserMappingsInsert = Omit<z.input<typeof SshHostLoginUserMappingsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshHostLoginUserMappingsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof SshHostLoginUserMappingsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SshHostLoginUsersSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
sshHostId: z.string().uuid(),
|
||||||
|
loginUser: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshHostLoginUsers = z.infer<typeof SshHostLoginUsersSchema>;
|
||||||
|
export type TSshHostLoginUsersInsert = Omit<z.input<typeof SshHostLoginUsersSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshHostLoginUsersUpdate = Partial<Omit<z.input<typeof SshHostLoginUsersSchema>, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SshHostsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
projectId: z.string(),
|
||||||
|
hostname: z.string(),
|
||||||
|
userCertTtl: z.string(),
|
||||||
|
hostCertTtl: z.string(),
|
||||||
|
userSshCaId: z.string().uuid(),
|
||||||
|
hostSshCaId: z.string().uuid()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSshHosts = z.infer<typeof SshHostsSchema>;
|
||||||
|
export type TSshHostsInsert = Omit<z.input<typeof SshHostsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSshHostsUpdate = Partial<Omit<z.input<typeof SshHostsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -22,7 +22,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
body: z.object({
|
body: z.object({
|
||||||
permissions: z.any().array(),
|
permissions: z.any().array(),
|
||||||
isTemporary: z.boolean(),
|
isTemporary: z.boolean(),
|
||||||
temporaryRange: z.string().optional()
|
temporaryRange: z.string().optional(),
|
||||||
|
note: z.string().max(255).optional()
|
||||||
}),
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
projectSlug: z.string().trim()
|
projectSlug: z.string().trim()
|
||||||
@@ -43,7 +44,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectSlug: req.query.projectSlug,
|
projectSlug: req.query.projectSlug,
|
||||||
temporaryRange: req.body.temporaryRange,
|
temporaryRange: req.body.temporaryRange,
|
||||||
isTemporary: req.body.isTemporary
|
isTemporary: req.body.isTemporary,
|
||||||
|
note: req.body.note
|
||||||
});
|
});
|
||||||
return { approval: request };
|
return { approval: request };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { registerSnapshotRouter } from "./snapshot-router";
|
|||||||
import { registerSshCaRouter } from "./ssh-certificate-authority-router";
|
import { registerSshCaRouter } from "./ssh-certificate-authority-router";
|
||||||
import { registerSshCertRouter } from "./ssh-certificate-router";
|
import { registerSshCertRouter } from "./ssh-certificate-router";
|
||||||
import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router";
|
import { registerSshCertificateTemplateRouter } from "./ssh-certificate-template-router";
|
||||||
|
import { registerSshHostRouter } from "./ssh-host-router";
|
||||||
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
import { registerTrustedIpRouter } from "./trusted-ip-router";
|
||||||
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router";
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
await sshRouter.register(registerSshCaRouter, { prefix: "/ca" });
|
await sshRouter.register(registerSshCaRouter, { prefix: "/ca" });
|
||||||
await sshRouter.register(registerSshCertRouter, { prefix: "/certificates" });
|
await sshRouter.register(registerSshCertRouter, { prefix: "/certificates" });
|
||||||
await sshRouter.register(registerSshCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
await sshRouter.register(registerSshCertificateTemplateRouter, { prefix: "/certificate-templates" });
|
||||||
|
await sshRouter.register(registerSshHostRouter, { prefix: "/hosts" });
|
||||||
},
|
},
|
||||||
{ prefix: "/ssh" }
|
{ prefix: "/ssh" }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { normalizeSshPrivateKey } from "@app/ee/services/ssh/ssh-certificate-authority-fns";
|
||||||
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
||||||
import { SshCaStatus } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
import { SshCaKeySource, SshCaStatus } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
||||||
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
|
||||||
|
|
||||||
export const registerSshCaRouter = async (server: FastifyZodProvider) => {
|
export const registerSshCaRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -20,13 +21,33 @@ export const registerSshCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
schema: {
|
schema: {
|
||||||
description: "Create SSH CA",
|
description: "Create SSH CA",
|
||||||
body: z.object({
|
body: z
|
||||||
|
.object({
|
||||||
projectId: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.projectId),
|
projectId: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.projectId),
|
||||||
friendlyName: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
|
friendlyName: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.friendlyName),
|
||||||
keyAlgorithm: z
|
keyAlgorithm: z
|
||||||
.nativeEnum(CertKeyAlgorithm)
|
.nativeEnum(SshCertKeyAlgorithm)
|
||||||
.default(CertKeyAlgorithm.RSA_2048)
|
.default(SshCertKeyAlgorithm.ED25519)
|
||||||
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm)
|
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keyAlgorithm),
|
||||||
|
publicKey: z.string().trim().optional().describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.publicKey),
|
||||||
|
privateKey: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.optional()
|
||||||
|
.transform((val) => (val ? normalizeSshPrivateKey(val) : undefined))
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.privateKey),
|
||||||
|
keySource: z
|
||||||
|
.nativeEnum(SshCaKeySource)
|
||||||
|
.default(SshCaKeySource.INTERNAL)
|
||||||
|
.describe(SSH_CERTIFICATE_AUTHORITIES.CREATE.keySource)
|
||||||
|
})
|
||||||
|
.refine((data) => data.keySource === SshCaKeySource.INTERNAL || (!!data.publicKey && !!data.privateKey), {
|
||||||
|
message: "publicKey and privateKey are required when keySource is external",
|
||||||
|
path: ["publicKey"]
|
||||||
|
})
|
||||||
|
.refine((data) => data.keySource === SshCaKeySource.EXTERNAL || !!data.keyAlgorithm, {
|
||||||
|
message: "keyAlgorithm is required when keySource is internal",
|
||||||
|
path: ["keyAlgorithm"]
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
import { SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
import { SSH_CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
||||||
@@ -108,8 +108,8 @@ export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
.min(1)
|
.min(1)
|
||||||
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.certificateTemplateId),
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.certificateTemplateId),
|
||||||
keyAlgorithm: z
|
keyAlgorithm: z
|
||||||
.nativeEnum(CertKeyAlgorithm)
|
.nativeEnum(SshCertKeyAlgorithm)
|
||||||
.default(CertKeyAlgorithm.RSA_2048)
|
.default(SshCertKeyAlgorithm.ED25519)
|
||||||
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm),
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm),
|
||||||
certType: z
|
certType: z
|
||||||
.nativeEnum(SshCertType)
|
.nativeEnum(SshCertType)
|
||||||
@@ -133,7 +133,7 @@ export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
privateKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.privateKey),
|
privateKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.privateKey),
|
||||||
publicKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.publicKey),
|
publicKey: z.string().describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.publicKey),
|
||||||
keyAlgorithm: z
|
keyAlgorithm: z
|
||||||
.nativeEnum(CertKeyAlgorithm)
|
.nativeEnum(SshCertKeyAlgorithm)
|
||||||
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm)
|
.describe(SSH_CERTIFICATE_AUTHORITIES.ISSUE_SSH_CREDENTIALS.keyAlgorithm)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ export const registerSshCertificateTemplateRouter = async (server: FastifyZodPro
|
|||||||
allowHostCertificates: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowHostCertificates),
|
allowHostCertificates: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowHostCertificates),
|
||||||
allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds)
|
allowCustomKeyIds: z.boolean().describe(SSH_CERTIFICATE_TEMPLATES.CREATE.allowCustomKeyIds)
|
||||||
})
|
})
|
||||||
.refine((data) => ms(data.maxTTL) > ms(data.ttl), {
|
.refine((data) => ms(data.maxTTL) >= ms(data.ttl), {
|
||||||
message: "Max TLL must be greater than TTL",
|
message: "Max TLL must be greater than or equal to TTL",
|
||||||
path: ["maxTTL"]
|
path: ["maxTTL"]
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
|
|||||||
@@ -0,0 +1,444 @@
|
|||||||
|
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 { loginMappingSchema, 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 { 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({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.array(
|
||||||
|
sanitizedSshHost.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
return hosts;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshHostId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
sshHostId: z.string().describe(SSH_HOSTS.GET.sshHostId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshHost.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const host = await server.services.sshHost.getSshHost({
|
||||||
|
sshHostId: req.params.sshHostId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Add an SSH Host",
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string().describe(SSH_HOSTS.CREATE.projectId),
|
||||||
|
hostname: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.refine((v) => isValidHostname(v), {
|
||||||
|
message: "Hostname must be a valid hostname"
|
||||||
|
})
|
||||||
|
.describe(SSH_HOSTS.CREATE.hostname),
|
||||||
|
userCertTtl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.default("8h")
|
||||||
|
.describe(SSH_HOSTS.CREATE.userCertTtl),
|
||||||
|
hostCertTtl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.default("1y")
|
||||||
|
.describe(SSH_HOSTS.CREATE.hostCertTtl),
|
||||||
|
loginMappings: z.array(loginMappingSchema).default([]).describe(SSH_HOSTS.CREATE.loginMappings),
|
||||||
|
userSshCaId: z.string().describe(SSH_HOSTS.CREATE.userSshCaId).optional(),
|
||||||
|
hostSshCaId: z.string().describe(SSH_HOSTS.CREATE.hostSshCaId).optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshHost.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const host = await server.services.sshHost.createSshHost({
|
||||||
|
...req.body,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:sshHostId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
description: "Update SSH Host",
|
||||||
|
params: z.object({
|
||||||
|
sshHostId: z.string().trim().describe(SSH_HOSTS.UPDATE.sshHostId)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
hostname: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.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")
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_HOSTS.UPDATE.userCertTtl),
|
||||||
|
hostCertTtl: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
|
.optional()
|
||||||
|
.describe(SSH_HOSTS.UPDATE.hostCertTtl),
|
||||||
|
loginMappings: z.array(loginMappingSchema).optional().describe(SSH_HOSTS.UPDATE.loginMappings)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshHost.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const host = await server.services.sshHost.updateSshHost({
|
||||||
|
sshHostId: req.params.sshHostId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:sshHostId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
sshHostId: z.string().describe(SSH_HOSTS.DELETE.sshHostId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: sanitizedSshHost.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const host = await server.services.sshHost.deleteSshHost({
|
||||||
|
sshHostId: req.params.sshHostId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:sshHostId/issue-user-cert",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
description: "Issue SSH certificate for user",
|
||||||
|
params: z.object({
|
||||||
|
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({
|
||||||
|
serialNumber: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.serialNumber),
|
||||||
|
signedKey: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.signedKey),
|
||||||
|
privateKey: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.privateKey),
|
||||||
|
publicKey: z.string().describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.publicKey),
|
||||||
|
keyAlgorithm: z.nativeEnum(SshCertKeyAlgorithm).describe(SSH_HOSTS.ISSUE_SSH_CREDENTIALS.keyAlgorithm)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
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.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.IssueSshHostUserCert,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
sshHostId: req.params.sshHostId,
|
||||||
|
hostname: host.hostname,
|
||||||
|
principals,
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
serialNumber,
|
||||||
|
signedKey: signedPublicKey,
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
keyAlgorithm
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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.ISSUE_HOST_CERT.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().describe(SSH_HOSTS.GET_USER_CA_PUBLIC_KEY.publicKey)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const publicKey = await server.services.sshHost.getSshHostUserCaPk(req.params.sshHostId);
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:sshHostId/host-ca-public-key",
|
||||||
|
config: {
|
||||||
|
rateLimit: publicSshCaLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Get public key of the host SSH CA linked to the host",
|
||||||
|
params: z.object({
|
||||||
|
sshHostId: z.string().trim().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.sshHostId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.string().describe(SSH_HOSTS.GET_HOST_CA_PUBLIC_KEY.publicKey)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const publicKey = await server.services.sshHost.getSshHostHostCaPk(req.params.sshHostId);
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -94,7 +94,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
projectSlug
|
projectSlug,
|
||||||
|
note
|
||||||
}: TCreateAccessApprovalRequestDTO) => {
|
}: TCreateAccessApprovalRequestDTO) => {
|
||||||
const cfg = getConfig();
|
const cfg = getConfig();
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
@@ -209,7 +210,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
requestedByUserId: actorId,
|
requestedByUserId: actorId,
|
||||||
temporaryRange: temporaryRange || null,
|
temporaryRange: temporaryRange || null,
|
||||||
permissions: JSON.stringify(requestedPermissions),
|
permissions: JSON.stringify(requestedPermissions),
|
||||||
isTemporary
|
isTemporary,
|
||||||
|
note: note || null
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -232,7 +234,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
environment: envSlug,
|
environment: envSlug,
|
||||||
permissions: accessTypes,
|
permissions: accessTypes,
|
||||||
approvalUrl
|
approvalUrl,
|
||||||
|
note
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -252,7 +255,8 @@ export const accessApprovalRequestServiceFactory = ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
environment: envSlug,
|
environment: envSlug,
|
||||||
permissions: accessTypes,
|
permissions: accessTypes,
|
||||||
approvalUrl
|
approvalUrl,
|
||||||
|
note
|
||||||
},
|
},
|
||||||
template: SmtpTemplates.AccessApprovalRequest
|
template: SmtpTemplates.AccessApprovalRequest
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export type TCreateAccessApprovalRequestDTO = {
|
|||||||
permissions: unknown;
|
permissions: unknown;
|
||||||
isTemporary: boolean;
|
isTemporary: boolean;
|
||||||
temporaryRange?: string;
|
temporaryRange?: string;
|
||||||
|
note?: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TListApprovalRequestsDTO = {
|
export type TListApprovalRequestsDTO = {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
TUpdateSecretRotationV2DTO
|
TUpdateSecretRotationV2DTO
|
||||||
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types";
|
||||||
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
import { SshCaStatus, SshCertType } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
import { SshCertTemplateStatus } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-types";
|
||||||
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
import { SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
@@ -189,6 +190,12 @@ export enum EventType {
|
|||||||
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
|
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
|
||||||
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
|
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
|
||||||
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
|
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
|
||||||
|
CREATE_SSH_HOST = "create-ssh-host",
|
||||||
|
UPDATE_SSH_HOST = "update-ssh-host",
|
||||||
|
DELETE_SSH_HOST = "delete-ssh-host",
|
||||||
|
GET_SSH_HOST = "get-ssh-host",
|
||||||
|
ISSUE_SSH_HOST_USER_CERT = "issue-ssh-host-user-cert",
|
||||||
|
ISSUE_SSH_HOST_HOST_CERT = "issue-ssh-host-host-cert",
|
||||||
CREATE_CA = "create-certificate-authority",
|
CREATE_CA = "create-certificate-authority",
|
||||||
GET_CA = "get-certificate-authority",
|
GET_CA = "get-certificate-authority",
|
||||||
UPDATE_CA = "update-certificate-authority",
|
UPDATE_CA = "update-certificate-authority",
|
||||||
@@ -1377,7 +1384,7 @@ interface IssueSshCreds {
|
|||||||
type: EventType.ISSUE_SSH_CREDS;
|
type: EventType.ISSUE_SSH_CREDS;
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
keyAlgorithm: CertKeyAlgorithm;
|
keyAlgorithm: SshCertKeyAlgorithm;
|
||||||
certType: SshCertType;
|
certType: SshCertType;
|
||||||
principals: string[];
|
principals: string[];
|
||||||
ttl: string;
|
ttl: string;
|
||||||
@@ -1473,6 +1480,80 @@ interface DeleteSshCertificateTemplate {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateSshHost {
|
||||||
|
type: EventType.CREATE_SSH_HOST;
|
||||||
|
metadata: {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname: string;
|
||||||
|
userCertTtl: string;
|
||||||
|
hostCertTtl: string;
|
||||||
|
loginMappings: {
|
||||||
|
loginUser: string;
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: 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: {
|
||||||
|
usernames: string[];
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
userSshCaId?: string;
|
||||||
|
hostSshCaId?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeleteSshHost {
|
||||||
|
type: EventType.DELETE_SSH_HOST;
|
||||||
|
metadata: {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetSshHost {
|
||||||
|
type: EventType.GET_SSH_HOST;
|
||||||
|
metadata: {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IssueSshHostUserCert {
|
||||||
|
type: EventType.ISSUE_SSH_HOST_USER_CERT;
|
||||||
|
metadata: {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname: string;
|
||||||
|
loginUser: string;
|
||||||
|
principals: string[];
|
||||||
|
ttl: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IssueSshHostHostCert {
|
||||||
|
type: EventType.ISSUE_SSH_HOST_HOST_CERT;
|
||||||
|
metadata: {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname: string;
|
||||||
|
serialNumber: string;
|
||||||
|
principals: string[];
|
||||||
|
ttl: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateCa {
|
interface CreateCa {
|
||||||
type: EventType.CREATE_CA;
|
type: EventType.CREATE_CA;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2493,6 +2574,12 @@ export type Event =
|
|||||||
| UpdateSshCertificateTemplate
|
| UpdateSshCertificateTemplate
|
||||||
| GetSshCertificateTemplate
|
| GetSshCertificateTemplate
|
||||||
| DeleteSshCertificateTemplate
|
| DeleteSshCertificateTemplate
|
||||||
|
| CreateSshHost
|
||||||
|
| UpdateSshHost
|
||||||
|
| DeleteSshHost
|
||||||
|
| GetSshHost
|
||||||
|
| IssueSshHostUserCert
|
||||||
|
| IssueSshHostHostCert
|
||||||
| CreateCa
|
| CreateCa
|
||||||
| GetCa
|
| GetCa
|
||||||
| UpdateCa
|
| UpdateCa
|
||||||
|
|||||||
@@ -67,6 +67,14 @@ export enum ProjectPermissionGroupActions {
|
|||||||
GrantPrivileges = "grant-privileges"
|
GrantPrivileges = "grant-privileges"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionSshHostActions {
|
||||||
|
Read = "read",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete",
|
||||||
|
IssueHostCert = "issue-host-cert"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSecretSyncActions {
|
export enum ProjectPermissionSecretSyncActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
Create = "create",
|
Create = "create",
|
||||||
@@ -121,6 +129,7 @@ export enum ProjectPermissionSub {
|
|||||||
SshCertificateAuthorities = "ssh-certificate-authorities",
|
SshCertificateAuthorities = "ssh-certificate-authorities",
|
||||||
SshCertificates = "ssh-certificates",
|
SshCertificates = "ssh-certificates",
|
||||||
SshCertificateTemplates = "ssh-certificate-templates",
|
SshCertificateTemplates = "ssh-certificate-templates",
|
||||||
|
SshHosts = "ssh-hosts",
|
||||||
PkiAlerts = "pki-alerts",
|
PkiAlerts = "pki-alerts",
|
||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
@@ -160,6 +169,10 @@ export type IdentityManagementSubjectFields = {
|
|||||||
identityId: string;
|
identityId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SshHostSubjectFields = {
|
||||||
|
hostname: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ProjectPermissionSet =
|
export type ProjectPermissionSet =
|
||||||
| [
|
| [
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
@@ -215,6 +228,10 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||||
|
| [
|
||||||
|
ProjectPermissionSshHostActions,
|
||||||
|
ProjectPermissionSub.SshHosts | (ForcedSubject<ProjectPermissionSub.SshHosts> & SshHostSubjectFields)
|
||||||
|
]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
||||||
@@ -313,6 +330,21 @@ const IdentityManagementConditionSchema = z
|
|||||||
})
|
})
|
||||||
.partial();
|
.partial();
|
||||||
|
|
||||||
|
const SshHostConditionSchema = z
|
||||||
|
.object({
|
||||||
|
hostname: z.union([
|
||||||
|
z.string(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
|
||||||
|
[PermissionConditionOperators.$GLOB]: PermissionConditionSchema[PermissionConditionOperators.$GLOB],
|
||||||
|
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
|
||||||
const GeneralPermissionSchema = [
|
const GeneralPermissionSchema = [
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
|
||||||
@@ -561,6 +593,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
|||||||
"When specified, only matching conditions will be allowed to access given resource."
|
"When specified, only matching conditions will be allowed to access given resource."
|
||||||
).optional()
|
).optional()
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
subject: z.literal(ProjectPermissionSub.SshHosts).describe("The entity this permission pertains to."),
|
||||||
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSshHostActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
),
|
||||||
|
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||||
|
conditions: SshHostConditionSchema.describe(
|
||||||
|
"When specified, only matching conditions will be allowed to access given resource."
|
||||||
|
).optional()
|
||||||
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.SecretRotation).describe("The entity this permission pertains to."),
|
||||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||||
@@ -613,6 +655,17 @@ const buildAdminPermissionRules = () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
can(
|
||||||
|
[
|
||||||
|
ProjectPermissionSshHostActions.Edit,
|
||||||
|
ProjectPermissionSshHostActions.Read,
|
||||||
|
ProjectPermissionSshHostActions.Create,
|
||||||
|
ProjectPermissionSshHostActions.Delete,
|
||||||
|
ProjectPermissionSshHostActions.IssueHostCert
|
||||||
|
],
|
||||||
|
ProjectPermissionSub.SshHosts
|
||||||
|
);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionMemberActions.Create,
|
ProjectPermissionMemberActions.Create,
|
||||||
@@ -873,6 +926,8 @@ const buildMemberPermissionRules = () => {
|
|||||||
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||||
|
|
||||||
|
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionCmekActions.Create,
|
ProjectPermissionCmekActions.Create,
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export enum SshCertKeyAlgorithm {
|
||||||
|
RSA_2048 = "RSA_2048",
|
||||||
|
RSA_4096 = "RSA_4096",
|
||||||
|
ECDSA_P256 = "EC_prime256v1",
|
||||||
|
ECDSA_P384 = "EC_secp384r1",
|
||||||
|
ED25519 = "ED25519"
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { groupBy, unique } from "@app/lib/fn";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshHostDALFactory = ReturnType<typeof sshHostDALFactory>;
|
||||||
|
|
||||||
|
export const sshHostDALFactory = (db: TDbClient) => {
|
||||||
|
const sshHostOrm = ormify(db, TableName.SshHost);
|
||||||
|
|
||||||
|
const findUserAccessibleSshHosts = async (projectIds: string[], userId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const user = await (tx || db.replicaNode())(TableName.Users).where({ id: userId }).select("username").first();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new DatabaseError({ name: `${TableName.Users}: UserNotFound`, error: new Error("User not found") });
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await (tx || db.replicaNode())(TableName.SshHost)
|
||||||
|
.leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SshHostLoginUserMapping,
|
||||||
|
`${TableName.SshHostLoginUser}.id`,
|
||||||
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SshHostLoginUserMapping}.userId`)
|
||||||
|
.whereIn(`${TableName.SshHost}.projectId`, projectIds)
|
||||||
|
.andWhere(`${TableName.SshHostLoginUserMapping}.userId`, userId)
|
||||||
|
.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.SshHostLoginUser),
|
||||||
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||||
|
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||||
|
)
|
||||||
|
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
|
||||||
|
|
||||||
|
const grouped = groupBy(rows, (r) => r.sshHostId);
|
||||||
|
return Object.values(grouped).map((hostRows) => {
|
||||||
|
const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId, projectId } = hostRows[0];
|
||||||
|
|
||||||
|
const loginMappingGrouped = groupBy(hostRows, (r) => r.loginUser);
|
||||||
|
|
||||||
|
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser]) => ({
|
||||||
|
loginUser,
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: [user.username]
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
try {
|
||||||
|
const rows = await (tx || db.replicaNode())(TableName.SshHost)
|
||||||
|
.leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SshHostLoginUserMapping,
|
||||||
|
`${TableName.SshHostLoginUser}.id`,
|
||||||
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||||
|
.where(`${TableName.SshHost}.projectId`, projectId)
|
||||||
|
.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.SshHostLoginUser),
|
||||||
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||||
|
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||||
|
)
|
||||||
|
.orderBy(`${TableName.SshHost}.updatedAt`, "desc");
|
||||||
|
|
||||||
|
const hostsGrouped = groupBy(rows, (r) => r.sshHostId);
|
||||||
|
return Object.values(hostsGrouped).map((hostRows) => {
|
||||||
|
const { sshHostId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = hostRows[0];
|
||||||
|
|
||||||
|
const loginMappingGrouped = groupBy(
|
||||||
|
hostRows.filter((r) => r.loginUser),
|
||||||
|
(r) => r.loginUser
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||||
|
loginUser,
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: sshHostId,
|
||||||
|
hostname,
|
||||||
|
projectId,
|
||||||
|
userCertTtl,
|
||||||
|
hostCertTtl,
|
||||||
|
loginMappings,
|
||||||
|
userSshCaId,
|
||||||
|
hostSshCaId
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostsWithLoginMappings` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findSshHostByIdWithLoginMappings = async (sshHostId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const rows = await (tx || db.replicaNode())(TableName.SshHost)
|
||||||
|
.leftJoin(TableName.SshHostLoginUser, `${TableName.SshHost}.id`, `${TableName.SshHostLoginUser}.sshHostId`)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SshHostLoginUserMapping,
|
||||||
|
`${TableName.SshHostLoginUser}.id`,
|
||||||
|
`${TableName.SshHostLoginUserMapping}.sshHostLoginUserId`
|
||||||
|
)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.SshHostLoginUserMapping}.userId`, `${TableName.Users}.id`)
|
||||||
|
.where(`${TableName.SshHost}.id`, sshHostId)
|
||||||
|
.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.SshHostLoginUser),
|
||||||
|
db.ref("username").withSchema(TableName.Users),
|
||||||
|
db.ref("userId").withSchema(TableName.SshHostLoginUserMapping),
|
||||||
|
db.ref("userSshCaId").withSchema(TableName.SshHost),
|
||||||
|
db.ref("hostSshCaId").withSchema(TableName.SshHost)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rows.length === 0) return null;
|
||||||
|
|
||||||
|
const { sshHostId: id, projectId, hostname, userCertTtl, hostCertTtl, userSshCaId, hostSshCaId } = rows[0];
|
||||||
|
|
||||||
|
const loginMappingGrouped = groupBy(
|
||||||
|
rows.filter((r) => r.loginUser),
|
||||||
|
(r) => r.loginUser
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginMappings = Object.entries(loginMappingGrouped).map(([loginUser, entries]) => ({
|
||||||
|
loginUser,
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: unique(entries.map((e) => e.username)).filter(Boolean)
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
projectId,
|
||||||
|
hostname,
|
||||||
|
userCertTtl,
|
||||||
|
hostCertTtl,
|
||||||
|
loginMappings,
|
||||||
|
userSshCaId,
|
||||||
|
hostSshCaId
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: `${TableName.SshHost}: FindSshHostByIdWithLoginMappings` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...sshHostOrm,
|
||||||
|
findSshHostsWithLoginMappings,
|
||||||
|
findUserAccessibleSshHosts,
|
||||||
|
findSshHostByIdWithLoginMappings
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshHostLoginUserMappingDALFactory = ReturnType<typeof sshHostLoginUserMappingDALFactory>;
|
||||||
|
|
||||||
|
export const sshHostLoginUserMappingDALFactory = (db: TDbClient) => {
|
||||||
|
const sshHostLoginUserMappingOrm = ormify(db, TableName.SshHostLoginUserMapping);
|
||||||
|
return sshHostLoginUserMappingOrm;
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SshHostsSchema } from "@app/db/schemas";
|
||||||
|
|
||||||
|
export const sanitizedSshHost = SshHostsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
projectId: true,
|
||||||
|
hostname: true,
|
||||||
|
userCertTtl: true,
|
||||||
|
hostCertTtl: true,
|
||||||
|
userSshCaId: true,
|
||||||
|
hostSshCaId: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export const loginMappingSchema = z.object({
|
||||||
|
loginUser: z.string().trim(),
|
||||||
|
allowedPrincipals: z.object({
|
||||||
|
usernames: z.array(z.string().trim()).transform((usernames) => Array.from(new Set(usernames)))
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -0,0 +1,694 @@
|
|||||||
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
|
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionSshHostActions, 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 { TSshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
|
||||||
|
import { TSshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||||
|
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
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,
|
||||||
|
getSshPublicKey
|
||||||
|
} from "../ssh/ssh-certificate-authority-fns";
|
||||||
|
import { SshCertType } from "../ssh/ssh-certificate-authority-types";
|
||||||
|
import {
|
||||||
|
TCreateSshHostDTO,
|
||||||
|
TDeleteSshHostDTO,
|
||||||
|
TGetSshHostDTO,
|
||||||
|
TIssueSshHostHostCertDTO,
|
||||||
|
TIssueSshHostUserCertDTO,
|
||||||
|
TListSshHostsDTO,
|
||||||
|
TUpdateSshHostDTO
|
||||||
|
} from "./ssh-host-types";
|
||||||
|
|
||||||
|
type TSshHostServiceFactoryDep = {
|
||||||
|
userDAL: Pick<TUserDALFactory, "findById" | "find">;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "find">;
|
||||||
|
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "findOne">;
|
||||||
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "findOne">;
|
||||||
|
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "findOne">;
|
||||||
|
sshCertificateDAL: Pick<TSshCertificateDALFactory, "create" | "transaction">;
|
||||||
|
sshCertificateBodyDAL: Pick<TSshCertificateBodyDALFactory, "create">;
|
||||||
|
sshHostDAL: Pick<
|
||||||
|
TSshHostDALFactory,
|
||||||
|
| "transaction"
|
||||||
|
| "create"
|
||||||
|
| "findById"
|
||||||
|
| "updateById"
|
||||||
|
| "deleteById"
|
||||||
|
| "findOne"
|
||||||
|
| "findSshHostByIdWithLoginMappings"
|
||||||
|
| "findUserAccessibleSshHosts"
|
||||||
|
>;
|
||||||
|
sshHostLoginUserDAL: TSshHostLoginUserDALFactory;
|
||||||
|
sshHostLoginUserMappingDAL: TSshHostLoginUserMappingDALFactory;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSshHostServiceFactory = ReturnType<typeof sshHostServiceFactory>;
|
||||||
|
|
||||||
|
export const sshHostServiceFactory = ({
|
||||||
|
userDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectSshConfigDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
sshCertificateDAL,
|
||||||
|
sshCertificateBodyDAL,
|
||||||
|
sshHostDAL,
|
||||||
|
sshHostLoginUserMappingDAL,
|
||||||
|
sshHostLoginUserDAL,
|
||||||
|
permissionService,
|
||||||
|
kmsService
|
||||||
|
}: TSshHostServiceFactoryDep) => {
|
||||||
|
/**
|
||||||
|
* Return list of all SSH hosts that a user can issue user SSH certificates for
|
||||||
|
* (i.e. is able to access / connect to) across all SSH projects in the organization
|
||||||
|
*/
|
||||||
|
const listSshHosts = async ({ actorId, actorAuthMethod, actor, actorOrgId }: TListSshHostsDTO) => {
|
||||||
|
if (actor !== ActorType.USER) {
|
||||||
|
// (dangtony98): only support user for now
|
||||||
|
throw new BadRequestError({ message: `Actor type ${actor} not supported` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const sshProjects = await projectDAL.find({
|
||||||
|
orgId: actorOrgId,
|
||||||
|
type: ProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
const allowedHosts = [];
|
||||||
|
|
||||||
|
for await (const project of sshProjects) {
|
||||||
|
try {
|
||||||
|
await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: project.id,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
const projectHosts = await sshHostDAL.findUserAccessibleSshHosts([project.id], actorId);
|
||||||
|
|
||||||
|
allowedHosts.push(...projectHosts);
|
||||||
|
} catch {
|
||||||
|
// intentionally ignore projects where user lacks access
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowedHosts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSshHost = async ({
|
||||||
|
projectId,
|
||||||
|
hostname,
|
||||||
|
userCertTtl,
|
||||||
|
hostCertTtl,
|
||||||
|
loginMappings,
|
||||||
|
userSshCaId: requestedUserSshCaId,
|
||||||
|
hostSshCaId: requestedHostSshCaId,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TCreateSshHostDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSshHostActions.Create,
|
||||||
|
subject(ProjectPermissionSub.SshHosts, {
|
||||||
|
hostname
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
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.findOne({
|
||||||
|
id: finalId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ca) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `${label} SSH CA with ID '${finalId}' not found in project '${projectId}'`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 host = await sshHostDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
hostname,
|
||||||
|
userCertTtl,
|
||||||
|
hostCertTtl,
|
||||||
|
userSshCaId,
|
||||||
|
hostSshCaId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
// (dangtony98): room to optimize
|
||||||
|
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
||||||
|
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
||||||
|
{
|
||||||
|
sshHostId: host.id,
|
||||||
|
loginUser
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allowedPrincipals.usernames.length > 0) {
|
||||||
|
const users = await userDAL.find(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
username: allowedPrincipals.usernames
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
|
||||||
|
const foundUsernames = new Set(users.map((u) => u.username));
|
||||||
|
|
||||||
|
for (const uname of allowedPrincipals.usernames) {
|
||||||
|
if (!foundUsernames.has(uname)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid username: ${uname}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const user of users) {
|
||||||
|
// check that each user has access to the SSH project
|
||||||
|
await permissionService.getUserProjectPermission({
|
||||||
|
userId: user.id,
|
||||||
|
projectId,
|
||||||
|
authMethod: actorAuthMethod,
|
||||||
|
userOrgId: actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await sshHostLoginUserMappingDAL.insertMany(
|
||||||
|
users.map((user) => ({
|
||||||
|
sshHostLoginUserId: sshHostLoginUser.id,
|
||||||
|
userId: user.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSshHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(host.id, tx);
|
||||||
|
if (!newSshHostWithLoginMappings) {
|
||||||
|
throw new NotFoundError({ message: `SSH host with ID '${host.id}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSshHostWithLoginMappings;
|
||||||
|
});
|
||||||
|
|
||||||
|
return newSshHost;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSshHost = async ({
|
||||||
|
sshHostId,
|
||||||
|
hostname,
|
||||||
|
userCertTtl,
|
||||||
|
hostCertTtl,
|
||||||
|
loginMappings,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
actorOrgId
|
||||||
|
}: TUpdateSshHostDTO) => {
|
||||||
|
const host = await sshHostDAL.findById(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(
|
||||||
|
ProjectPermissionSshHostActions.Edit,
|
||||||
|
subject(ProjectPermissionSub.SshHosts, {
|
||||||
|
hostname: host.hostname
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedHost = await sshHostDAL.transaction(async (tx) => {
|
||||||
|
await sshHostDAL.updateById(
|
||||||
|
sshHostId,
|
||||||
|
{
|
||||||
|
hostname,
|
||||||
|
userCertTtl,
|
||||||
|
hostCertTtl
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loginMappings) {
|
||||||
|
await sshHostLoginUserDAL.delete({ sshHostId: host.id }, tx);
|
||||||
|
if (loginMappings.length) {
|
||||||
|
for await (const { loginUser, allowedPrincipals } of loginMappings) {
|
||||||
|
const sshHostLoginUser = await sshHostLoginUserDAL.create(
|
||||||
|
{
|
||||||
|
sshHostId: host.id,
|
||||||
|
loginUser
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allowedPrincipals.usernames.length > 0) {
|
||||||
|
const users = await userDAL.find(
|
||||||
|
{
|
||||||
|
$in: {
|
||||||
|
username: allowedPrincipals.usernames
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ tx }
|
||||||
|
);
|
||||||
|
|
||||||
|
const foundUsernames = new Set(users.map((u) => u.username));
|
||||||
|
|
||||||
|
for (const uname of allowedPrincipals.usernames) {
|
||||||
|
if (!foundUsernames.has(uname)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Invalid username: ${uname}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const user of users) {
|
||||||
|
await permissionService.getUserProjectPermission({
|
||||||
|
userId: user.id,
|
||||||
|
projectId: host.projectId,
|
||||||
|
authMethod: actorAuthMethod,
|
||||||
|
userOrgId: actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await sshHostLoginUserMappingDAL.insertMany(
|
||||||
|
users.map((user) => ({
|
||||||
|
sshHostLoginUserId: sshHostLoginUser.id,
|
||||||
|
userId: user.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedHostWithLoginMappings = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId, tx);
|
||||||
|
if (!updatedHostWithLoginMappings) {
|
||||||
|
throw new NotFoundError({ message: `SSH host with ID '${sshHostId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedHostWithLoginMappings;
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedHost;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSshHost = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteSshHostDTO) => {
|
||||||
|
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(
|
||||||
|
ProjectPermissionSshHostActions.Delete,
|
||||||
|
subject(ProjectPermissionSub.SshHosts, {
|
||||||
|
hostname: host.hostname
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await sshHostDAL.deleteById(sshHostId);
|
||||||
|
|
||||||
|
return host;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSshHost = async ({ sshHostId, actorId, actorAuthMethod, actor, actorOrgId }: TGetSshHostDTO) => {
|
||||||
|
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(
|
||||||
|
ProjectPermissionSshHostActions.Read,
|
||||||
|
subject(ProjectPermissionSub.SshHosts, {
|
||||||
|
hostname: host.hostname
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
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 issueSshHostUserCert = async ({
|
||||||
|
sshHostId,
|
||||||
|
loginUser,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TIssueSshHostUserCertDTO) => {
|
||||||
|
const host = await sshHostDAL.findSshHostByIdWithLoginMappings(sshHostId);
|
||||||
|
if (!host) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `SSH host with ID ${sshHostId} not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId: host.projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
const internalPrincipals = await convertActorToPrincipals({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
userDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapping = host.loginMappings.find(
|
||||||
|
(m) =>
|
||||||
|
m.loginUser === loginUser &&
|
||||||
|
m.allowedPrincipals.usernames.some((allowed) => internalPrincipals.includes(allowed))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mapping) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: `You are not allowed to login as ${loginUser} on this host`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
// (dangtony98): will support more algorithms in the future
|
||||||
|
const keyAlgorithm = SshCertKeyAlgorithm.ED25519;
|
||||||
|
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
|
||||||
|
|
||||||
|
// (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,
|
||||||
|
keyId,
|
||||||
|
principals,
|
||||||
|
requestedTtl: host.userCertTtl,
|
||||||
|
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.userSshCaId,
|
||||||
|
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,
|
||||||
|
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(
|
||||||
|
ProjectPermissionSshHostActions.IssueHostCert,
|
||||||
|
subject(ProjectPermissionSub.SshHosts, {
|
||||||
|
hostname: host.hostname
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
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.id}`;
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSshHostHostCaPk = 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.hostSshCaId });
|
||||||
|
|
||||||
|
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,
|
||||||
|
issueSshHostUserCert,
|
||||||
|
issueSshHostHostCert,
|
||||||
|
getSshHostUserCaPk,
|
||||||
|
getSshHostHostCaPk
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TListSshHostsDTO = Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TCreateSshHostDTO = {
|
||||||
|
hostname: string;
|
||||||
|
userCertTtl: string;
|
||||||
|
hostCertTtl: string;
|
||||||
|
loginMappings: {
|
||||||
|
loginUser: string;
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: string[];
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
userSshCaId?: string;
|
||||||
|
hostSshCaId?: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateSshHostDTO = {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname?: string;
|
||||||
|
userCertTtl?: string;
|
||||||
|
hostCertTtl?: string;
|
||||||
|
loginMappings?: {
|
||||||
|
loginUser: string;
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: string[];
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetSshHostDTO = {
|
||||||
|
sshHostId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteSshHostDTO = {
|
||||||
|
sshHostId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIssueSshHostUserCertDTO = {
|
||||||
|
sshHostId: string;
|
||||||
|
loginUser: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TIssueSshHostHostCertDTO = {
|
||||||
|
sshHostId: string;
|
||||||
|
publicKey: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||||
|
|
||||||
|
export const isValidHostname = (value: string): boolean => {
|
||||||
|
if (typeof value !== "string") return false;
|
||||||
|
if (value.length > 255) return false;
|
||||||
|
|
||||||
|
// Only allow strict FQDNs, no wildcards or IPs
|
||||||
|
return isFQDN(value, {
|
||||||
|
require_tld: true,
|
||||||
|
allow_underscores: false,
|
||||||
|
allow_trailing_dot: false,
|
||||||
|
allow_numeric_tld: true,
|
||||||
|
allow_wildcard: false
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSshHostLoginUserDALFactory = ReturnType<typeof sshHostLoginUserDALFactory>;
|
||||||
|
|
||||||
|
export const sshHostLoginUserDALFactory = (db: TDbClient) => {
|
||||||
|
const sshHostLoginUserOrm = ormify(db, TableName.SshHostLoginUser);
|
||||||
|
return sshHostLoginUserOrm;
|
||||||
|
};
|
||||||
@@ -1,21 +1,31 @@
|
|||||||
import { execFile } from "child_process";
|
import { execFile } from "child_process";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { promises as fs } from "fs";
|
import { promises as fs } from "fs";
|
||||||
|
import { Knex } from "knex";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
import { TSshCertificateTemplates } from "@app/db/schemas";
|
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||||
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
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 { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isValidHostPattern,
|
isValidHostPattern,
|
||||||
isValidUserPattern
|
isValidUserPattern
|
||||||
} from "../ssh-certificate-template/ssh-certificate-template-validators";
|
} from "../ssh-certificate-template/ssh-certificate-template-validators";
|
||||||
import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-types";
|
import {
|
||||||
|
SshCaKeySource,
|
||||||
|
SshCaStatus,
|
||||||
|
SshCertType,
|
||||||
|
TConvertActorToPrincipalsDTO,
|
||||||
|
TCreateSshCaHelperDTO,
|
||||||
|
TCreateSshCertDTO
|
||||||
|
} from "./ssh-certificate-authority-types";
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
@@ -31,31 +41,35 @@ export const createSshCertSerialNumber = () => {
|
|||||||
* Return a pair of SSH CA keys based on the specified key algorithm [keyAlgorithm].
|
* Return a pair of SSH CA keys based on the specified key algorithm [keyAlgorithm].
|
||||||
* We use this function because the key format generated by `ssh-keygen` is unique.
|
* We use this function because the key format generated by `ssh-keygen` is unique.
|
||||||
*/
|
*/
|
||||||
export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm) => {
|
export const createSshKeyPair = async (keyAlgorithm: SshCertKeyAlgorithm) => {
|
||||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-"));
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-key-"));
|
||||||
const privateKeyFile = path.join(tempDir, "id_key");
|
const privateKeyFile = path.join(tempDir, "id_key");
|
||||||
const publicKeyFile = `${privateKeyFile}.pub`;
|
const publicKeyFile = `${privateKeyFile}.pub`;
|
||||||
|
|
||||||
let keyType: string;
|
let keyType: string;
|
||||||
let keyBits: string;
|
let keyBits: string | null;
|
||||||
|
|
||||||
switch (keyAlgorithm) {
|
switch (keyAlgorithm) {
|
||||||
case CertKeyAlgorithm.RSA_2048:
|
case SshCertKeyAlgorithm.RSA_2048:
|
||||||
keyType = "rsa";
|
keyType = "rsa";
|
||||||
keyBits = "2048";
|
keyBits = "2048";
|
||||||
break;
|
break;
|
||||||
case CertKeyAlgorithm.RSA_4096:
|
case SshCertKeyAlgorithm.RSA_4096:
|
||||||
keyType = "rsa";
|
keyType = "rsa";
|
||||||
keyBits = "4096";
|
keyBits = "4096";
|
||||||
break;
|
break;
|
||||||
case CertKeyAlgorithm.ECDSA_P256:
|
case SshCertKeyAlgorithm.ECDSA_P256:
|
||||||
keyType = "ecdsa";
|
keyType = "ecdsa";
|
||||||
keyBits = "256";
|
keyBits = "256";
|
||||||
break;
|
break;
|
||||||
case CertKeyAlgorithm.ECDSA_P384:
|
case SshCertKeyAlgorithm.ECDSA_P384:
|
||||||
keyType = "ecdsa";
|
keyType = "ecdsa";
|
||||||
keyBits = "384";
|
keyBits = "384";
|
||||||
break;
|
break;
|
||||||
|
case SshCertKeyAlgorithm.ED25519:
|
||||||
|
keyType = "ed25519";
|
||||||
|
keyBits = null;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Failed to produce SSH CA key pair generation command due to unrecognized key algorithm"
|
message: "Failed to produce SSH CA key pair generation command due to unrecognized key algorithm"
|
||||||
@@ -63,10 +77,16 @@ export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const args = ["-t", keyType];
|
||||||
|
if (keyBits !== null) {
|
||||||
|
args.push("-b", keyBits);
|
||||||
|
}
|
||||||
|
args.push("-f", privateKeyFile, "-N", "");
|
||||||
|
|
||||||
// Generate the SSH key pair
|
// Generate the SSH key pair
|
||||||
// The "-N ''" sets an empty passphrase
|
// The "-N ''" sets an empty passphrase
|
||||||
// The keys are created in the temporary directory
|
// The keys are created in the temporary directory
|
||||||
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""], {
|
await execFileAsync("ssh-keygen", args, {
|
||||||
timeout: EXEC_TIMEOUT_MS
|
timeout: EXEC_TIMEOUT_MS
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -280,7 +300,12 @@ export const validateSshCertificateTtl = (template: TSshCertificateTemplates, tt
|
|||||||
* that it only contains alphanumeric characters with no spaces.
|
* that it only contains alphanumeric characters with no spaces.
|
||||||
*/
|
*/
|
||||||
export const validateSshCertificateKeyId = (keyId: string) => {
|
export const validateSshCertificateKeyId = (keyId: string) => {
|
||||||
const regex = characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen]);
|
const regex = characterValidator([
|
||||||
|
CharacterType.AlphaNumeric,
|
||||||
|
CharacterType.Hyphen,
|
||||||
|
CharacterType.Colon,
|
||||||
|
CharacterType.Period
|
||||||
|
]);
|
||||||
if (!regex(keyId)) {
|
if (!regex(keyId)) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message:
|
message:
|
||||||
@@ -322,6 +347,96 @@ const validateSshPublicKey = async (publicKey: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getKeyAlgorithmFromFingerprintOutput = (output: string): SshCertKeyAlgorithm | undefined => {
|
||||||
|
const parts = output.trim().split(" ");
|
||||||
|
const bitsInt = parseInt(parts[0], 10);
|
||||||
|
const keyTypeRaw = parts.at(-1)?.replace(/[()]/g, ""); // remove surrounding parentheses
|
||||||
|
|
||||||
|
if (keyTypeRaw === "RSA") {
|
||||||
|
return bitsInt === 2048 ? SshCertKeyAlgorithm.RSA_2048 : SshCertKeyAlgorithm.RSA_4096;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyTypeRaw === "ECDSA") {
|
||||||
|
return bitsInt === 256 ? SshCertKeyAlgorithm.ECDSA_P256 : SshCertKeyAlgorithm.ECDSA_P384;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyTypeRaw === "ED25519") {
|
||||||
|
return SshCertKeyAlgorithm.ED25519;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeSshPrivateKey = (raw: string): string => {
|
||||||
|
return `${raw
|
||||||
|
.replace(/\r\n/g, "\n") // Windows CRLF → LF
|
||||||
|
.replace(/\r/g, "\n") // Old Mac CR → LF
|
||||||
|
.replace(/\\n/g, "\n") // Double-escaped \n
|
||||||
|
.trim()}\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the format of the SSH private key
|
||||||
|
*
|
||||||
|
* Returns the SSH public key corresponding to the private key
|
||||||
|
* and the key algorithm categorization.
|
||||||
|
*/
|
||||||
|
export const validateSshPrivateKey = async (privateKey: string) => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "ssh-privkey-"));
|
||||||
|
const privateKeyFile = path.join(tempDir, "id_key");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(privateKeyFile, privateKey, {
|
||||||
|
encoding: "utf8",
|
||||||
|
mode: 0o600
|
||||||
|
});
|
||||||
|
|
||||||
|
// This will fail if the private key is malformed or unreadable
|
||||||
|
const { stdout: publicKey } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], {
|
||||||
|
timeout: EXEC_TIMEOUT_MS
|
||||||
|
});
|
||||||
|
|
||||||
|
const { stdout: fingerprint } = await execFileAsync("ssh-keygen", ["-lf", privateKeyFile]);
|
||||||
|
const keyAlgorithm = getKeyAlgorithmFromFingerprintOutput(fingerprint);
|
||||||
|
|
||||||
|
if (!keyAlgorithm) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate SSH private key format: The key algorithm is not supported."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
publicKey,
|
||||||
|
keyAlgorithm
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to validate SSH private key format: could not be parsed."
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that the provided public and private keys are valid and constitute
|
||||||
|
* a matching SSH key pair.
|
||||||
|
*/
|
||||||
|
export const validateExternalSshCaKeyPair = async (publicKey: string, privateKey: string) => {
|
||||||
|
await validateSshPublicKey(publicKey);
|
||||||
|
|
||||||
|
const { publicKey: derivedPublicKey, keyAlgorithm } = await validateSshPrivateKey(privateKey);
|
||||||
|
|
||||||
|
if (publicKey.trim() !== derivedPublicKey.trim()) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to validate matching SSH key pair: The provided public key does not match the public key derived from the private key."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyAlgorithm;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an SSH certificate for a user or host.
|
* Create an SSH certificate for a user or host.
|
||||||
*/
|
*/
|
||||||
@@ -331,9 +446,17 @@ export const createSshCert = async ({
|
|||||||
clientPublicKey,
|
clientPublicKey,
|
||||||
keyId,
|
keyId,
|
||||||
principals,
|
principals,
|
||||||
requestedTtl,
|
requestedTtl, // in ms lib format
|
||||||
certType
|
certType
|
||||||
}: TCreateSshCertDTO) => {
|
}: TCreateSshCertDTO) => {
|
||||||
|
let ttl: number | undefined;
|
||||||
|
|
||||||
|
if (!template && requestedTtl) {
|
||||||
|
const parsedTtl = Math.ceil(ms(requestedTtl) / 1000);
|
||||||
|
if (parsedTtl > 0) ttl = parsedTtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template) {
|
||||||
// validate if the requested [certType] is allowed under the template configuration
|
// validate if the requested [certType] is allowed under the template configuration
|
||||||
validateSshCertificateType(template, certType);
|
validateSshCertificateType(template, certType);
|
||||||
|
|
||||||
@@ -341,7 +464,14 @@ export const createSshCert = async ({
|
|||||||
validateSshCertificatePrincipals(certType, template, principals);
|
validateSshCertificatePrincipals(certType, template, principals);
|
||||||
|
|
||||||
// validate if the requested TTL is valid under the template configuration
|
// validate if the requested TTL is valid under the template configuration
|
||||||
const ttl = validateSshCertificateTtl(template, requestedTtl);
|
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);
|
||||||
@@ -388,3 +518,88 @@ export const createSshCert = async ({
|
|||||||
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createSshCaHelper = async ({
|
||||||
|
projectId,
|
||||||
|
friendlyName,
|
||||||
|
keyAlgorithm: requestedKeyAlgorithm,
|
||||||
|
keySource,
|
||||||
|
externalPk,
|
||||||
|
externalSk,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
kmsService,
|
||||||
|
tx: outerTx
|
||||||
|
}: TCreateSshCaHelperDTO) => {
|
||||||
|
// Function to handle the actual creation logic
|
||||||
|
const processCreation = async (tx: Knex) => {
|
||||||
|
let publicKey: string;
|
||||||
|
let privateKey: string;
|
||||||
|
let keyAlgorithm: SshCertKeyAlgorithm = requestedKeyAlgorithm;
|
||||||
|
if (keySource === SshCaKeySource.INTERNAL) {
|
||||||
|
// generate SSH CA key pair internally
|
||||||
|
({ publicKey, privateKey } = await createSshKeyPair(requestedKeyAlgorithm));
|
||||||
|
} else {
|
||||||
|
// use external SSH CA key pair
|
||||||
|
if (!externalPk || !externalSk) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Public and private keys are required when key source is external"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
publicKey = externalPk;
|
||||||
|
privateKey = externalSk;
|
||||||
|
keyAlgorithm = await validateExternalSshCaKeyPair(publicKey, privateKey);
|
||||||
|
}
|
||||||
|
const ca = await sshCertificateAuthorityDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
friendlyName,
|
||||||
|
status: SshCaStatus.ACTIVE,
|
||||||
|
keyAlgorithm,
|
||||||
|
keySource
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey(
|
||||||
|
{
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await sshCertificateAuthoritySecretDAL.create(
|
||||||
|
{
|
||||||
|
sshCaId: ca.id,
|
||||||
|
encryptedPrivateKey: secretManagerEncryptor({ plainText: Buffer.from(privateKey, "utf8") }).cipherTextBlob
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return { ...ca, publicKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (outerTx) {
|
||||||
|
return processCreation(outerTx);
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ export const sanitizedSshCa = SshCertificateAuthoritiesSchema.pick({
|
|||||||
projectId: true,
|
projectId: true,
|
||||||
friendlyName: true,
|
friendlyName: true,
|
||||||
status: true,
|
status: true,
|
||||||
keyAlgorithm: true
|
keyAlgorithm: true,
|
||||||
|
keySource: true
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ 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 { createSshCert, createSshKeyPair, getSshPublicKey } from "./ssh-certificate-authority-fns";
|
import { createSshCaHelper, createSshCert, createSshKeyPair, getSshPublicKey } from "./ssh-certificate-authority-fns";
|
||||||
import {
|
import {
|
||||||
SshCaStatus,
|
SshCaStatus,
|
||||||
TCreateSshCaDTO,
|
TCreateSshCaDTO,
|
||||||
@@ -59,7 +59,10 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
const createSshCa = async ({
|
const createSshCa = async ({
|
||||||
projectId,
|
projectId,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
keyAlgorithm,
|
keyAlgorithm: requestedKeyAlgorithm,
|
||||||
|
publicKey: externalPk,
|
||||||
|
privateKey: externalSk,
|
||||||
|
keySource,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actor,
|
actor,
|
||||||
@@ -79,33 +82,16 @@ export const sshCertificateAuthorityServiceFactory = ({
|
|||||||
ProjectPermissionSub.SshCertificateAuthorities
|
ProjectPermissionSub.SshCertificateAuthorities
|
||||||
);
|
);
|
||||||
|
|
||||||
const newCa = await sshCertificateAuthorityDAL.transaction(async (tx) => {
|
const newCa = await createSshCaHelper({
|
||||||
const ca = await sshCertificateAuthorityDAL.create(
|
|
||||||
{
|
|
||||||
projectId,
|
projectId,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
status: SshCaStatus.ACTIVE,
|
keyAlgorithm: requestedKeyAlgorithm,
|
||||||
keyAlgorithm
|
keySource,
|
||||||
},
|
externalPk,
|
||||||
tx
|
externalSk,
|
||||||
);
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
const { publicKey, privateKey } = await createSshKeyPair(keyAlgorithm);
|
kmsService
|
||||||
|
|
||||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
|
||||||
type: KmsDataKey.SecretManager,
|
|
||||||
projectId
|
|
||||||
});
|
|
||||||
|
|
||||||
await sshCertificateAuthoritySecretDAL.create(
|
|
||||||
{
|
|
||||||
sshCaId: ca.id,
|
|
||||||
encryptedPrivateKey: secretManagerEncryptor({ plainText: Buffer.from(privateKey, "utf8") }).cipherTextBlob
|
|
||||||
},
|
|
||||||
tx
|
|
||||||
);
|
|
||||||
|
|
||||||
return { ...ca, publicKey };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return newCa;
|
return newCa;
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TSshCertificateTemplates } from "@app/db/schemas";
|
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||||
|
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 { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
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",
|
||||||
DISABLED = "disabled"
|
DISABLED = "disabled"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SshCaKeySource {
|
||||||
|
INTERNAL = "internal",
|
||||||
|
EXTERNAL = "external"
|
||||||
|
}
|
||||||
|
|
||||||
export enum SshCertType {
|
export enum SshCertType {
|
||||||
USER = "user",
|
USER = "user",
|
||||||
HOST = "host"
|
HOST = "host"
|
||||||
@@ -14,9 +26,25 @@ export enum SshCertType {
|
|||||||
|
|
||||||
export type TCreateSshCaDTO = {
|
export type TCreateSshCaDTO = {
|
||||||
friendlyName: string;
|
friendlyName: string;
|
||||||
keyAlgorithm: CertKeyAlgorithm;
|
keyAlgorithm: SshCertKeyAlgorithm;
|
||||||
|
publicKey?: string;
|
||||||
|
privateKey?: string;
|
||||||
|
keySource: SshCaKeySource;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TCreateSshCaHelperDTO = {
|
||||||
|
projectId: string;
|
||||||
|
friendlyName: string;
|
||||||
|
keyAlgorithm: SshCertKeyAlgorithm;
|
||||||
|
keySource: SshCaKeySource;
|
||||||
|
externalPk?: string;
|
||||||
|
externalSk?: string;
|
||||||
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "transaction" | "create">;
|
||||||
|
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
tx?: Knex;
|
||||||
|
};
|
||||||
|
|
||||||
export type TGetSshCaDTO = {
|
export type TGetSshCaDTO = {
|
||||||
caId: string;
|
caId: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
@@ -37,7 +65,7 @@ export type TDeleteSshCaDTO = {
|
|||||||
|
|
||||||
export type TIssueSshCredsDTO = {
|
export type TIssueSshCredsDTO = {
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
keyAlgorithm: CertKeyAlgorithm;
|
keyAlgorithm: SshCertKeyAlgorithm;
|
||||||
certType: SshCertType;
|
certType: SshCertType;
|
||||||
principals: string[];
|
principals: string[];
|
||||||
ttl?: string;
|
ttl?: string;
|
||||||
@@ -58,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;
|
||||||
@@ -66,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">;
|
||||||
|
};
|
||||||
|
|||||||
@@ -519,6 +519,9 @@ export const PROJECTS = {
|
|||||||
LIST_SSH_CAS: {
|
LIST_SSH_CAS: {
|
||||||
projectId: "The ID of the project to list SSH CAs for."
|
projectId: "The ID of the project to list SSH CAs for."
|
||||||
},
|
},
|
||||||
|
LIST_SSH_HOSTS: {
|
||||||
|
projectId: "The ID of the project to list SSH hosts for."
|
||||||
|
},
|
||||||
LIST_SSH_CERTIFICATES: {
|
LIST_SSH_CERTIFICATES: {
|
||||||
projectId: "The ID of the project to list SSH certificates for.",
|
projectId: "The ID of the project to list SSH certificates for.",
|
||||||
offset: "The offset to start from. If you enter 10, it will start from the 10th SSH certificate.",
|
offset: "The offset to start from. If you enter 10, it will start from the 10th SSH certificate.",
|
||||||
@@ -1253,7 +1256,11 @@ export const SSH_CERTIFICATE_AUTHORITIES = {
|
|||||||
CREATE: {
|
CREATE: {
|
||||||
projectId: "The ID of the project to create the SSH CA in.",
|
projectId: "The ID of the project to create the SSH CA in.",
|
||||||
friendlyName: "A friendly name for the SSH CA.",
|
friendlyName: "A friendly name for the SSH CA.",
|
||||||
keyAlgorithm: "The type of public key algorithm and size, in bits, of the key pair for the SSH CA."
|
keyAlgorithm:
|
||||||
|
"The type of public key algorithm and size, in bits, of the key pair for the SSH CA; required if keySource is internal.",
|
||||||
|
publicKey: "The public key for the SSH CA key pair; required if keySource is external.",
|
||||||
|
privateKey: "The private key for the SSH CA key pair; required if keySource is external.",
|
||||||
|
keySource: "The source of the SSH CA key pair. This can be one of internal or external."
|
||||||
},
|
},
|
||||||
GET: {
|
GET: {
|
||||||
sshCaId: "The ID of the SSH CA to get."
|
sshCaId: "The ID of the SSH CA to get."
|
||||||
@@ -1327,6 +1334,62 @@ export const SSH_CERTIFICATE_TEMPLATES = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SSH_HOSTS = {
|
||||||
|
GET: {
|
||||||
|
sshHostId: "The ID of the SSH host to get."
|
||||||
|
},
|
||||||
|
CREATE: {
|
||||||
|
projectId: "The ID of the project to create the SSH host in.",
|
||||||
|
hostname: "The hostname of the SSH host.",
|
||||||
|
userCertTtl: "The time to live for user certificates issued under this host.",
|
||||||
|
hostCertTtl: "The time to live for host certificates issued under this host.",
|
||||||
|
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.",
|
||||||
|
loginMappings:
|
||||||
|
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project.",
|
||||||
|
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: {
|
||||||
|
sshHostId: "The ID of the SSH host to update.",
|
||||||
|
hostname: "The hostname of the SSH host to update to.",
|
||||||
|
userCertTtl: "The time to live for user certificates issued under this host to update to.",
|
||||||
|
hostCertTtl: "The time to live for host certificates issued under this host to update to.",
|
||||||
|
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.",
|
||||||
|
loginMappings:
|
||||||
|
"A list of login mappings for the SSH host. Each login mapping contains a login user and a list of corresponding allowed principals being usernames of users in the Infisical SSH project."
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
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: {
|
||||||
|
sshHostId: "The ID of the SSH host to issue the SSH certificate for.",
|
||||||
|
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.",
|
||||||
|
publicKey: "The public key of the user SSH CA linked to the SSH host."
|
||||||
|
},
|
||||||
|
GET_HOST_CA_PUBLIC_KEY: {
|
||||||
|
sshHostId: "The ID of the SSH host to get the host SSH CA public key for.",
|
||||||
|
publicKey: "The public key of the host SSH CA linked to the SSH host."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const CERTIFICATE_AUTHORITIES = {
|
export const CERTIFICATE_AUTHORITIES = {
|
||||||
CREATE: {
|
CREATE: {
|
||||||
projectSlug: "Slug of the project to create the CA in.",
|
projectSlug: "Slug of the project to create the CA in.",
|
||||||
|
|||||||
@@ -93,3 +93,10 @@ export const userEngagementLimit: RateLimitOptions = {
|
|||||||
max: 5,
|
max: 5,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const publicSshCaLimit: RateLimitOptions = {
|
||||||
|
timeWindow: 60 * 1000,
|
||||||
|
hook: "preValidation",
|
||||||
|
max: 30, // conservative default
|
||||||
|
keyGenerator: (req) => req.realIp
|
||||||
|
};
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ import { sshCertificateBodyDALFactory } from "@app/ee/services/ssh-certificate/s
|
|||||||
import { sshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
import { sshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
||||||
import { sshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
import { sshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||||
import { sshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
import { sshCertificateTemplateServiceFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-service";
|
||||||
|
import { sshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
||||||
|
import { sshHostLoginUserMappingDALFactory } from "@app/ee/services/ssh-host/ssh-host-login-user-mapping-dal";
|
||||||
|
import { sshHostServiceFactory } from "@app/ee/services/ssh-host/ssh-host-service";
|
||||||
|
import { sshHostLoginUserDALFactory } from "@app/ee/services/ssh-host/ssh-login-user-dal";
|
||||||
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
||||||
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
@@ -184,6 +188,7 @@ import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-co
|
|||||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { projectQueueFactory } from "@app/services/project/project-queue";
|
import { projectQueueFactory } from "@app/services/project/project-queue";
|
||||||
import { projectServiceFactory } from "@app/services/project/project-service";
|
import { projectServiceFactory } from "@app/services/project/project-service";
|
||||||
|
import { projectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
||||||
import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
import { projectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
import { projectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
@@ -292,6 +297,7 @@ export const registerRoutes = async (
|
|||||||
const apiKeyDAL = apiKeyDALFactory(db);
|
const apiKeyDAL = apiKeyDALFactory(db);
|
||||||
|
|
||||||
const projectDAL = projectDALFactory(db);
|
const projectDAL = projectDALFactory(db);
|
||||||
|
const projectSshConfigDAL = projectSshConfigDALFactory(db);
|
||||||
const projectMembershipDAL = projectMembershipDALFactory(db);
|
const projectMembershipDAL = projectMembershipDALFactory(db);
|
||||||
const projectUserAdditionalPrivilegeDAL = projectUserAdditionalPrivilegeDALFactory(db);
|
const projectUserAdditionalPrivilegeDAL = projectUserAdditionalPrivilegeDALFactory(db);
|
||||||
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db);
|
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db);
|
||||||
@@ -385,6 +391,9 @@ export const registerRoutes = async (
|
|||||||
const sshCertificateAuthorityDAL = sshCertificateAuthorityDALFactory(db);
|
const sshCertificateAuthorityDAL = sshCertificateAuthorityDALFactory(db);
|
||||||
const sshCertificateAuthoritySecretDAL = sshCertificateAuthoritySecretDALFactory(db);
|
const sshCertificateAuthoritySecretDAL = sshCertificateAuthoritySecretDALFactory(db);
|
||||||
const sshCertificateTemplateDAL = sshCertificateTemplateDALFactory(db);
|
const sshCertificateTemplateDAL = sshCertificateTemplateDALFactory(db);
|
||||||
|
const sshHostDAL = sshHostDALFactory(db);
|
||||||
|
const sshHostLoginUserDAL = sshHostLoginUserDALFactory(db);
|
||||||
|
const sshHostLoginUserMappingDAL = sshHostLoginUserMappingDALFactory(db);
|
||||||
|
|
||||||
const kmsDAL = kmskeyDALFactory(db);
|
const kmsDAL = kmskeyDALFactory(db);
|
||||||
const internalKmsDAL = internalKmsDALFactory(db);
|
const internalKmsDAL = internalKmsDALFactory(db);
|
||||||
@@ -796,6 +805,21 @@ export const registerRoutes = async (
|
|||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sshHostService = sshHostServiceFactory({
|
||||||
|
userDAL,
|
||||||
|
projectDAL,
|
||||||
|
projectSshConfigDAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
sshCertificateDAL,
|
||||||
|
sshCertificateBodyDAL,
|
||||||
|
sshHostDAL,
|
||||||
|
sshHostLoginUserDAL,
|
||||||
|
sshHostLoginUserMappingDAL,
|
||||||
|
permissionService,
|
||||||
|
kmsService
|
||||||
|
});
|
||||||
|
|
||||||
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
const certificateAuthorityService = certificateAuthorityServiceFactory({
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthorityCertDAL,
|
certificateAuthorityCertDAL,
|
||||||
@@ -938,6 +962,7 @@ export const registerRoutes = async (
|
|||||||
const projectService = projectServiceFactory({
|
const projectService = projectServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
projectSshConfigDAL,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
queueService,
|
queueService,
|
||||||
@@ -959,8 +984,10 @@ export const registerRoutes = async (
|
|||||||
pkiAlertDAL,
|
pkiAlertDAL,
|
||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
sshCertificateAuthorityDAL,
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
sshCertificateDAL,
|
sshCertificateDAL,
|
||||||
sshCertificateTemplateDAL,
|
sshCertificateTemplateDAL,
|
||||||
|
sshHostDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
@@ -1603,6 +1630,7 @@ export const registerRoutes = async (
|
|||||||
certificate: certificateService,
|
certificate: certificateService,
|
||||||
sshCertificateAuthority: sshCertificateAuthorityService,
|
sshCertificateAuthority: sshCertificateAuthorityService,
|
||||||
sshCertificateTemplate: sshCertificateTemplateService,
|
sshCertificateTemplate: sshCertificateTemplateService,
|
||||||
|
sshHost: sshHostService,
|
||||||
certificateAuthority: certificateAuthorityService,
|
certificateAuthority: certificateAuthorityService,
|
||||||
certificateTemplate: certificateTemplateService,
|
certificateTemplate: certificateTemplateService,
|
||||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { InfisicalProjectTemplate } from "@app/ee/services/project-template/proj
|
|||||||
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
import { sanitizedSshCa } from "@app/ee/services/ssh/ssh-certificate-authority-schema";
|
||||||
import { sanitizedSshCertificate } from "@app/ee/services/ssh-certificate/ssh-certificate-schema";
|
import { sanitizedSshCertificate } from "@app/ee/services/ssh-certificate/ssh-certificate-schema";
|
||||||
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
import { sanitizedSshCertificateTemplate } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-schema";
|
||||||
|
import { loginMappingSchema, sanitizedSshHost } from "@app/ee/services/ssh-host/ssh-host-schema";
|
||||||
import { PROJECTS } from "@app/lib/api-docs";
|
import { PROJECTS } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { slugSchema } from "@app/server/lib/schemas";
|
||||||
@@ -600,4 +601,38 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
return { cas };
|
return { cas };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectId/ssh-hosts",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
projectId: z.string().trim().describe(PROJECTS.LIST_SSH_HOSTS.projectId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
hosts: z.array(
|
||||||
|
sanitizedSshHost.extend({
|
||||||
|
loginMappings: z.array(loginMappingSchema)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const hosts = await server.services.project.listProjectSshHosts({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { hosts };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -138,8 +138,8 @@ export type TValidateAppConnectionCredentialsSchema =
|
|||||||
| TValidateHumanitecConnectionCredentialsSchema
|
| TValidateHumanitecConnectionCredentialsSchema
|
||||||
| TValidatePostgresConnectionCredentialsSchema
|
| TValidatePostgresConnectionCredentialsSchema
|
||||||
| TValidateMsSqlConnectionCredentialsSchema
|
| TValidateMsSqlConnectionCredentialsSchema
|
||||||
| TValidateTerraformCloudConnectionCredentialsSchema
|
|
||||||
| TValidateCamundaConnectionCredentialsSchema
|
| TValidateCamundaConnectionCredentialsSchema
|
||||||
|
| TValidateTerraformCloudConnectionCredentialsSchema
|
||||||
| TValidateVercelConnectionCredentialsSchema;
|
| TValidateVercelConnectionCredentialsSchema;
|
||||||
|
|
||||||
export type TListAwsConnectionKmsKeys = {
|
export type TListAwsConnectionKmsKeys = {
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export const projectRoleServiceFactory = ({
|
|||||||
validateHandlebarTemplate("Project Role Update", JSON.stringify(data.permissions || []), {
|
validateHandlebarTemplate("Project Role Update", JSON.stringify(data.permissions || []), {
|
||||||
allowedExpressions: (val) => val.includes("identity.")
|
allowedExpressions: (val) => val.includes("identity.")
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatedRole = await projectRoleDAL.updateById(projectRole.id, {
|
const updatedRole = await projectRoleDAL.updateById(projectRole.id, {
|
||||||
...data,
|
...data,
|
||||||
permissions: data.permissions ? data.permissions : undefined
|
permissions: data.permissions ? data.permissions : undefined
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
import { ProjectVersion, TProjects } from "@app/db/schemas";
|
import { ProjectVersion, TProjects } from "@app/db/schemas";
|
||||||
|
import { createSshCaHelper } from "@app/ee/services/ssh/ssh-certificate-authority-fns";
|
||||||
|
import { SshCaKeySource } from "@app/ee/services/ssh/ssh-certificate-authority-types";
|
||||||
|
import { SshCertKeyAlgorithm } from "@app/ee/services/ssh-certificate/ssh-certificate-types";
|
||||||
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||||
import { NotFoundError } from "@app/lib/errors";
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
|
||||||
import { AddUserToWsDTO } from "./project-types";
|
import { AddUserToWsDTO, TBootstrapSshProjectDTO } from "./project-types";
|
||||||
|
|
||||||
export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => {
|
export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => {
|
||||||
const plaintextProjectKey = decryptAsymmetric({
|
const plaintextProjectKey = decryptAsymmetric({
|
||||||
@@ -102,3 +105,48 @@ export const getProjectKmsCertificateKeyId = async ({
|
|||||||
|
|
||||||
return keyId;
|
return keyId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstraps an SSH project.
|
||||||
|
* - Creates a user and host SSH CA
|
||||||
|
* - Creates a project SSH config with the user and host SSH CA as defaults
|
||||||
|
*/
|
||||||
|
export const bootstrapSshProject = async ({
|
||||||
|
projectId,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectSshConfigDAL,
|
||||||
|
tx
|
||||||
|
}: TBootstrapSshProjectDTO) => {
|
||||||
|
const userSshCa = await createSshCaHelper({
|
||||||
|
projectId,
|
||||||
|
friendlyName: "User CA",
|
||||||
|
keyAlgorithm: SshCertKeyAlgorithm.ED25519,
|
||||||
|
keySource: SshCaKeySource.INTERNAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
kmsService,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
|
||||||
|
const hostSshCa = await createSshCaHelper({
|
||||||
|
projectId,
|
||||||
|
friendlyName: "Host CA",
|
||||||
|
keyAlgorithm: SshCertKeyAlgorithm.ED25519,
|
||||||
|
keySource: SshCaKeySource.INTERNAL,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
kmsService,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
|
||||||
|
await projectSshConfigDAL.create(
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
defaultHostSshCaId: hostSshCa.id,
|
||||||
|
defaultUserSshCaId: userSshCa.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -15,13 +15,16 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
|||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
|
ProjectPermissionSshHostActions,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
||||||
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
|
||||||
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||||
|
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||||
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
||||||
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 { TSshHostDALFactory } from "@app/ee/services/ssh-host/ssh-host-dal";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
@@ -61,8 +64,9 @@ import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
|||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TProjectDALFactory } from "./project-dal";
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
|
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns";
|
||||||
import { TProjectQueueFactory } from "./project-queue";
|
import { TProjectQueueFactory } from "./project-queue";
|
||||||
|
import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
|
||||||
import {
|
import {
|
||||||
TCreateProjectDTO,
|
TCreateProjectDTO,
|
||||||
TDeleteProjectDTO,
|
TDeleteProjectDTO,
|
||||||
@@ -77,6 +81,7 @@ import {
|
|||||||
TListProjectSshCasDTO,
|
TListProjectSshCasDTO,
|
||||||
TListProjectSshCertificatesDTO,
|
TListProjectSshCertificatesDTO,
|
||||||
TListProjectSshCertificateTemplatesDTO,
|
TListProjectSshCertificateTemplatesDTO,
|
||||||
|
TListProjectSshHostsDTO,
|
||||||
TLoadProjectKmsBackupDTO,
|
TLoadProjectKmsBackupDTO,
|
||||||
TProjectAccessRequestDTO,
|
TProjectAccessRequestDTO,
|
||||||
TSearchProjectsDTO,
|
TSearchProjectsDTO,
|
||||||
@@ -97,8 +102,8 @@ export const DEFAULT_PROJECT_ENVS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
type TProjectServiceFactoryDep = {
|
type TProjectServiceFactoryDep = {
|
||||||
// TODO: Pick
|
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
|
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "create">;
|
||||||
projectQueue: TProjectQueueFactory;
|
projectQueue: TProjectQueueFactory;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
@@ -123,9 +128,11 @@ type TProjectServiceFactoryDep = {
|
|||||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||||
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
||||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
||||||
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "find">;
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "find" | "create" | "transaction">;
|
||||||
|
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create">;
|
||||||
sshCertificateDAL: Pick<TSshCertificateDALFactory, "find" | "countSshCertificatesInProject">;
|
sshCertificateDAL: Pick<TSshCertificateDALFactory, "find" | "countSshCertificatesInProject">;
|
||||||
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find">;
|
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find">;
|
||||||
|
sshHostDAL: Pick<TSshHostDALFactory, "find" | "findSshHostsWithLoginMappings">;
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
@@ -144,6 +151,7 @@ type TProjectServiceFactoryDep = {
|
|||||||
| "getKmsById"
|
| "getKmsById"
|
||||||
| "getProjectSecretManagerKmsKeyId"
|
| "getProjectSecretManagerKmsKeyId"
|
||||||
| "deleteInternalKms"
|
| "deleteInternalKms"
|
||||||
|
| "createCipherPairWithDataKey"
|
||||||
>;
|
>;
|
||||||
projectTemplateService: TProjectTemplateServiceFactory;
|
projectTemplateService: TProjectTemplateServiceFactory;
|
||||||
};
|
};
|
||||||
@@ -152,6 +160,7 @@ export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
|||||||
|
|
||||||
export const projectServiceFactory = ({
|
export const projectServiceFactory = ({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
|
projectSshConfigDAL,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
projectQueue,
|
projectQueue,
|
||||||
@@ -177,8 +186,10 @@ export const projectServiceFactory = ({
|
|||||||
pkiCollectionDAL,
|
pkiCollectionDAL,
|
||||||
pkiAlertDAL,
|
pkiAlertDAL,
|
||||||
sshCertificateAuthorityDAL,
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
sshCertificateDAL,
|
sshCertificateDAL,
|
||||||
sshCertificateTemplateDAL,
|
sshCertificateTemplateDAL,
|
||||||
|
sshHostDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
@@ -266,6 +277,17 @@ export const projectServiceFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (type === ProjectType.SSH) {
|
||||||
|
await bootstrapSshProject({
|
||||||
|
projectId: project.id,
|
||||||
|
sshCertificateAuthorityDAL,
|
||||||
|
sshCertificateAuthoritySecretDAL,
|
||||||
|
kmsService,
|
||||||
|
projectSshConfigDAL,
|
||||||
|
tx
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// set ghost user as admin of project
|
// set ghost user as admin of project
|
||||||
const projectMembership = await projectMembershipDAL.create(
|
const projectMembership = await projectMembershipDAL.create(
|
||||||
{
|
{
|
||||||
@@ -1046,6 +1068,48 @@ export const projectServiceFactory = ({
|
|||||||
return cas;
|
return cas;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of SSH hosts for project
|
||||||
|
*/
|
||||||
|
const listProjectSshHosts = async ({
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actor,
|
||||||
|
projectId
|
||||||
|
}: TListProjectSshHostsDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SSH
|
||||||
|
});
|
||||||
|
|
||||||
|
const allowedHosts = [];
|
||||||
|
|
||||||
|
// (dangtony98): room to optimize
|
||||||
|
const hosts = await sshHostDAL.findSshHostsWithLoginMappings(projectId);
|
||||||
|
|
||||||
|
for (const host of hosts) {
|
||||||
|
try {
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionSshHostActions.Read,
|
||||||
|
subject(ProjectPermissionSub.SshHosts, {
|
||||||
|
hostname: host.hostname
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
allowedHosts.push(host);
|
||||||
|
} catch {
|
||||||
|
// intentionally ignore projects where user lacks access
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowedHosts;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of SSH certificates for project
|
* Return list of SSH certificates for project
|
||||||
*/
|
*/
|
||||||
@@ -1443,6 +1507,7 @@ export const projectServiceFactory = ({
|
|||||||
listProjectPkiCollections,
|
listProjectPkiCollections,
|
||||||
listProjectCertificateTemplates,
|
listProjectCertificateTemplates,
|
||||||
listProjectSshCas,
|
listProjectSshCas,
|
||||||
|
listProjectSshHosts,
|
||||||
listProjectSshCertificates,
|
listProjectSshCertificates,
|
||||||
listProjectSshCertificateTemplates,
|
listProjectSshCertificateTemplates,
|
||||||
updateVersionLimit,
|
updateVersionLimit,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TProjectSshConfigDALFactory = ReturnType<typeof projectSshConfigDALFactory>;
|
||||||
|
|
||||||
|
export const projectSshConfigDALFactory = (db: TDbClient) => {
|
||||||
|
const projectSshConfigOrm = ormify(db, TableName.ProjectSshConfig);
|
||||||
|
|
||||||
|
return projectSshConfigOrm;
|
||||||
|
};
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { ProjectType, SortDirection, TProjectKeys } from "@app/db/schemas";
|
import { ProjectType, TProjectKeys, SortDirection } from "@app/db/schemas";
|
||||||
|
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
|
||||||
|
import { TSshCertificateAuthoritySecretDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-secret-dal";
|
||||||
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
|
import { TProjectSshConfigDALFactory } from "@app/services/project/project-ssh-config-dal";
|
||||||
import { OrgServiceActor, TProjectPermission } from "@app/lib/types";
|
import { OrgServiceActor, TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
@@ -143,6 +147,7 @@ export type TGetProjectKmsKey = TProjectPermission;
|
|||||||
export type TListProjectCertificateTemplatesDTO = TProjectPermission;
|
export type TListProjectCertificateTemplatesDTO = TProjectPermission;
|
||||||
|
|
||||||
export type TListProjectSshCasDTO = TProjectPermission;
|
export type TListProjectSshCasDTO = TProjectPermission;
|
||||||
|
export type TListProjectSshHostsDTO = TProjectPermission;
|
||||||
export type TListProjectSshCertificateTemplatesDTO = TProjectPermission;
|
export type TListProjectSshCertificateTemplatesDTO = TProjectPermission;
|
||||||
export type TListProjectSshCertificatesDTO = {
|
export type TListProjectSshCertificatesDTO = {
|
||||||
offset: number;
|
offset: number;
|
||||||
@@ -159,6 +164,15 @@ export type TUpdateProjectSlackConfig = {
|
|||||||
secretRequestChannels: string;
|
secretRequestChannels: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TBootstrapSshProjectDTO = {
|
||||||
|
projectId: string;
|
||||||
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "transaction" | "create">;
|
||||||
|
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create">;
|
||||||
|
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "create">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
tx?: Knex;
|
||||||
|
};
|
||||||
|
|
||||||
export enum SearchProjectSortBy {
|
export enum SearchProjectSortBy {
|
||||||
NAME = "name"
|
NAME = "name"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,12 @@ View the complete details <${appCfg.SITE_URL}/secret-manager/${payload.projectId
|
|||||||
|
|
||||||
The following permissions are requested: ${payload.permissions.join(", ")}
|
The following permissions are requested: ${payload.permissions.join(", ")}
|
||||||
|
|
||||||
View the request and approve or deny it <${payload.approvalUrl}|here>.`;
|
View the request and approve or deny it <${payload.approvalUrl}|here>.${
|
||||||
|
payload.note
|
||||||
|
? `
|
||||||
|
User Note: ${payload.note}`
|
||||||
|
: ""
|
||||||
|
}`;
|
||||||
|
|
||||||
const payloadBlocks = [
|
const payloadBlocks = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -76,5 +76,6 @@ export type TSlackNotification =
|
|||||||
projectName: string;
|
projectName: string;
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
approvalUrl: string;
|
approvalUrl: string;
|
||||||
|
note?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,6 +40,9 @@
|
|||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
{{#if note}}
|
||||||
|
<p>User Note: "{{note}}"</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
View the request and approve or deny it
|
View the request and approve or deny it
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export enum PostHogEventTypes {
|
|||||||
SecretRequestDeleted = "Secret Request Deleted",
|
SecretRequestDeleted = "Secret Request Deleted",
|
||||||
SignSshKey = "Sign SSH Key",
|
SignSshKey = "Sign SSH Key",
|
||||||
IssueSshCreds = "Issue SSH Credentials",
|
IssueSshCreds = "Issue SSH Credentials",
|
||||||
|
IssueSshHostUserCert = "Issue SSH Host User Certificate",
|
||||||
|
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
|
||||||
SignCert = "Sign PKI Certificate",
|
SignCert = "Sign PKI Certificate",
|
||||||
IssueCert = "Issue PKI Certificate"
|
IssueCert = "Issue PKI Certificate"
|
||||||
}
|
}
|
||||||
@@ -161,6 +163,26 @@ export type TIssueSshCredsEvent = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TIssueSshHostUserCertEvent = {
|
||||||
|
event: PostHogEventTypes.IssueSshHostUserCert;
|
||||||
|
properties: {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname: string;
|
||||||
|
principals: string[];
|
||||||
|
userAgent?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIssueSshHostHostCertEvent = {
|
||||||
|
event: PostHogEventTypes.IssueSshHostHostCert;
|
||||||
|
properties: {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname: string;
|
||||||
|
principals: string[];
|
||||||
|
userAgent?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type TSignCertificateEvent = {
|
export type TSignCertificateEvent = {
|
||||||
event: PostHogEventTypes.SignCert;
|
event: PostHogEventTypes.SignCert;
|
||||||
properties: {
|
properties: {
|
||||||
@@ -195,6 +217,8 @@ export type TPostHogEvent = { distinctId: string } & (
|
|||||||
| TSecretRequestDeletedEvent
|
| TSecretRequestDeletedEvent
|
||||||
| TSignSshKeyEvent
|
| TSignSshKeyEvent
|
||||||
| TIssueSshCredsEvent
|
| TIssueSshCredsEvent
|
||||||
|
| TIssueSshHostUserCertEvent
|
||||||
|
| TIssueSshHostHostCertEvent
|
||||||
| TSignCertificateEvent
|
| TSignCertificateEvent
|
||||||
| TIssueCertificateEvent
|
| TIssueCertificateEvent
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ require (
|
|||||||
github.com/fatih/semgroup v1.2.0
|
github.com/fatih/semgroup v1.2.0
|
||||||
github.com/gitleaks/go-gitdiff v0.8.0
|
github.com/gitleaks/go-gitdiff v0.8.0
|
||||||
github.com/h2non/filetype v1.1.3
|
github.com/h2non/filetype v1.1.3
|
||||||
github.com/infisical/go-sdk v0.5.1
|
github.com/infisical/go-sdk v0.5.8
|
||||||
github.com/infisical/infisical-kmip v0.3.5
|
github.com/infisical/infisical-kmip v0.3.5
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
|
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
|
||||||
|
|||||||
@@ -277,8 +277,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
|||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/infisical/go-sdk v0.5.1 h1:bl0D4A6CmvfL8RwEQTcZh39nsxC6q3HSs76/4J8grWY=
|
github.com/infisical/go-sdk v0.5.8 h1:bCetYLp7HWt8DnU9KPh1n8n3z5pjmunkGDB4bA3lEFs=
|
||||||
github.com/infisical/go-sdk v0.5.1/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
github.com/infisical/go-sdk v0.5.8/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
||||||
github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
|
github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
|
||||||
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
|
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
|
||||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
"github.com/Infisical/infisical-merge/packages/util"
|
"github.com/Infisical/infisical-merge/packages/util"
|
||||||
infisicalSdk "github.com/infisical/go-sdk"
|
infisicalSdk "github.com/infisical/go-sdk"
|
||||||
infisicalSdkUtil "github.com/infisical/go-sdk/packages/util"
|
infisicalSdkUtil "github.com/infisical/go-sdk/packages/util"
|
||||||
|
"github.com/manifoldco/promptui"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/crypto/ssh/agent"
|
"golang.org/x/crypto/ssh/agent"
|
||||||
@@ -48,6 +50,18 @@ var sshSignKeyCmd = &cobra.Command{
|
|||||||
Run: signKey,
|
Run: signKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sshConnectCmd = &cobra.Command{
|
||||||
|
Use: "connect",
|
||||||
|
Short: "Connect to an SSH host using issued credentials",
|
||||||
|
Run: sshConnect,
|
||||||
|
}
|
||||||
|
|
||||||
|
var sshAddHostCmd = &cobra.Command{
|
||||||
|
Use: "add-host",
|
||||||
|
Short: "Register a new SSH host with Infisical",
|
||||||
|
Run: sshAddHost,
|
||||||
|
}
|
||||||
|
|
||||||
var algoToFileName = map[infisicalSdkUtil.CertKeyAlgorithm]string{
|
var algoToFileName = map[infisicalSdkUtil.CertKeyAlgorithm]string{
|
||||||
infisicalSdkUtil.RSA2048: "id_rsa_2048",
|
infisicalSdkUtil.RSA2048: "id_rsa_2048",
|
||||||
infisicalSdkUtil.RSA4096: "id_rsa_4096",
|
infisicalSdkUtil.RSA4096: "id_rsa_4096",
|
||||||
@@ -240,7 +254,7 @@ func issueCredentials(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse addToAgent flag")
|
util.HandleError(err, "Unable to parse addToAgent flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
if outFilePath == "" && addToAgent == false {
|
if outFilePath == "" && !addToAgent {
|
||||||
util.PrintErrorMessageAndExit("You must provide either --outFilePath or --addToAgent flag to use this command")
|
util.PrintErrorMessageAndExit("You must provide either --outFilePath or --addToAgent flag to use this command")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,6 +609,380 @@ func signKey(cmd *cobra.Command, args []string) {
|
|||||||
fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath)
|
fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sshConnect(cmd *cobra.Command, args []string) {
|
||||||
|
util.RequireLogin()
|
||||||
|
util.RequireLocalWorkspaceFile()
|
||||||
|
|
||||||
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
if loggedInUserDetails.LoginExpired {
|
||||||
|
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||||
|
}
|
||||||
|
|
||||||
|
infisicalToken := loggedInUserDetails.UserCredentials.JTWToken
|
||||||
|
|
||||||
|
writeHostCaToFile, err := cmd.Flags().GetBool("writeHostCaToFile")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse --writeHostCaToFile flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to get custom headers")
|
||||||
|
}
|
||||||
|
|
||||||
|
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||||
|
SiteUrl: config.INFISICAL_URL,
|
||||||
|
UserAgent: api.USER_AGENT,
|
||||||
|
AutoTokenRefresh: false,
|
||||||
|
CustomHeaders: customHeaders,
|
||||||
|
})
|
||||||
|
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||||
|
|
||||||
|
// Fetch SSH Hosts
|
||||||
|
hosts, err := infisicalClient.Ssh().GetSshHosts(infisicalSdk.GetSshHostsOptions{})
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to fetch SSH hosts")
|
||||||
|
}
|
||||||
|
if len(hosts) == 0 {
|
||||||
|
util.PrintErrorMessageAndExit("You do not have access to any SSH hosts")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompt to select host
|
||||||
|
hostNames := make([]string, len(hosts))
|
||||||
|
for i, h := range hosts {
|
||||||
|
hostNames[i] = h.Hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
hostPrompt := promptui.Select{
|
||||||
|
Label: "Select an SSH Host",
|
||||||
|
Items: hostNames,
|
||||||
|
Size: 10,
|
||||||
|
}
|
||||||
|
hostIdx, _, err := hostPrompt.Run()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Prompt failed")
|
||||||
|
}
|
||||||
|
selectedHost := hosts[hostIdx]
|
||||||
|
|
||||||
|
// Prompt to select login user
|
||||||
|
if len(selectedHost.LoginMappings) == 0 {
|
||||||
|
util.PrintErrorMessageAndExit("No login users available for selected host")
|
||||||
|
}
|
||||||
|
|
||||||
|
loginUsers := make([]string, len(selectedHost.LoginMappings))
|
||||||
|
for i, m := range selectedHost.LoginMappings {
|
||||||
|
loginUsers[i] = m.LoginUser
|
||||||
|
}
|
||||||
|
|
||||||
|
loginPrompt := promptui.Select{
|
||||||
|
Label: "Select Login User",
|
||||||
|
Items: loginUsers,
|
||||||
|
Size: 5,
|
||||||
|
}
|
||||||
|
loginIdx, _, err := loginPrompt.Run()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Prompt failed")
|
||||||
|
}
|
||||||
|
selectedLoginUser := selectedHost.LoginMappings[loginIdx].LoginUser
|
||||||
|
|
||||||
|
// Issue SSH creds for host
|
||||||
|
creds, err := infisicalClient.Ssh().IssueSshHostUserCert(selectedHost.ID, infisicalSdk.IssueSshHostUserCertOptions{
|
||||||
|
LoginUser: selectedLoginUser,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to issue SSH credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write Host CA public key to known_hosts if enabled
|
||||||
|
if writeHostCaToFile {
|
||||||
|
hostCaPublicKey, err := infisicalClient.Ssh().GetSshHostHostCaPublicKey(selectedHost.ID)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to fetch Host CA public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build @cert-authority line
|
||||||
|
caLine := fmt.Sprintf("@cert-authority %s %s\n", selectedHost.Hostname, strings.TrimSpace(hostCaPublicKey))
|
||||||
|
|
||||||
|
// Determine known_hosts path
|
||||||
|
sshDir := filepath.Join(os.Getenv("HOME"), ".ssh")
|
||||||
|
knownHostsPath := filepath.Join(sshDir, "known_hosts")
|
||||||
|
|
||||||
|
// Ensure ~/.ssh exists
|
||||||
|
if _, err := os.Stat(sshDir); os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(sshDir, 0700); err != nil {
|
||||||
|
util.HandleError(err, "Failed to create ~/.ssh directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if CA line already exists
|
||||||
|
knownHostsBytes, _ := os.ReadFile(knownHostsPath)
|
||||||
|
if !strings.Contains(string(knownHostsBytes), caLine) {
|
||||||
|
f, err := os.OpenFile(knownHostsPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to open known_hosts file")
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if _, err := f.WriteString(caLine); err != nil {
|
||||||
|
util.HandleError(err, "Failed to write Host CA to known_hosts")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("📁 Wrote Host CA entry to %s\n", knownHostsPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load credentials into SSH agent
|
||||||
|
err = addCredentialsToAgent(creds.PrivateKey, creds.SignedKey)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to add credentials to SSH agent")
|
||||||
|
}
|
||||||
|
fmt.Println("✔ SSH credentials successfully added to agent")
|
||||||
|
|
||||||
|
// Connect to host using system ssh and agent
|
||||||
|
target := fmt.Sprintf("%s@%s", selectedLoginUser, selectedHost.Hostname)
|
||||||
|
fmt.Printf("Connecting to %s...\n", target)
|
||||||
|
|
||||||
|
sshCmd := exec.Command("ssh", target)
|
||||||
|
sshCmd.Stdin = os.Stdin
|
||||||
|
sshCmd.Stdout = os.Stdout
|
||||||
|
sshCmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
err = sshCmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "SSH connection failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sshAddHost(cmd *cobra.Command, args []string) {
|
||||||
|
|
||||||
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse token")
|
||||||
|
}
|
||||||
|
|
||||||
|
var infisicalToken string
|
||||||
|
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||||
|
infisicalToken = token.Token
|
||||||
|
} else {
|
||||||
|
util.RequireLogin()
|
||||||
|
util.RequireLocalWorkspaceFile()
|
||||||
|
|
||||||
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to authenticate")
|
||||||
|
}
|
||||||
|
if loggedInUserDetails.LoginExpired {
|
||||||
|
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login]")
|
||||||
|
}
|
||||||
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
|
}
|
||||||
|
|
||||||
|
projectId, err := cmd.Flags().GetString("projectId")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse --projectId flag")
|
||||||
|
}
|
||||||
|
if projectId == "" {
|
||||||
|
util.PrintErrorMessageAndExit("You must provide --projectId")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname, err := cmd.Flags().GetString("hostname")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse --hostname flag")
|
||||||
|
}
|
||||||
|
if hostname == "" {
|
||||||
|
util.PrintErrorMessageAndExit("You must provide --hostname")
|
||||||
|
}
|
||||||
|
|
||||||
|
writeUserCaToFile, err := cmd.Flags().GetBool("writeUserCaToFile")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse --writeUserCaToFile flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
userCaOutFilePath, err := cmd.Flags().GetString("userCaOutFilePath")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse --userCaOutFilePath flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
writeHostCertToFile, err := cmd.Flags().GetBool("writeHostCertToFile")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse --writeHostCertToFile flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
configureSshd, err := cmd.Flags().GetBool("configureSshd")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse --configureSshd flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
forceOverwrite, err := cmd.Flags().GetBool("force")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse --force flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if configureSshd && (!writeUserCaToFile || !writeHostCertToFile) {
|
||||||
|
util.PrintErrorMessageAndExit("--configureSshd requires both --writeUserCaToFile and --writeHostCertToFile to also be set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-check for file overwrites before proceeding
|
||||||
|
if writeUserCaToFile {
|
||||||
|
if strings.HasPrefix(userCaOutFilePath, "~") {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to resolve ~ in userCaOutFilePath")
|
||||||
|
}
|
||||||
|
userCaOutFilePath = strings.Replace(userCaOutFilePath, "~", homeDir, 1)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(userCaOutFilePath); err == nil && !forceOverwrite {
|
||||||
|
util.PrintErrorMessageAndExit("File already exists at " + userCaOutFilePath + ". Use --force to overwrite.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keyTypes := []string{"ed25519", "ecdsa", "rsa"}
|
||||||
|
var hostKeyPath, certOutPath, hostPrivateKeyPath string
|
||||||
|
if writeHostCertToFile {
|
||||||
|
for _, keyType := range keyTypes {
|
||||||
|
pub := fmt.Sprintf("/etc/ssh/ssh_host_%s_key.pub", keyType)
|
||||||
|
cert := fmt.Sprintf("/etc/ssh/ssh_host_%s_key-cert.pub", keyType)
|
||||||
|
priv := fmt.Sprintf("/etc/ssh/ssh_host_%s_key", keyType)
|
||||||
|
|
||||||
|
if _, err := os.Stat(pub); err == nil {
|
||||||
|
hostKeyPath = pub
|
||||||
|
certOutPath = cert
|
||||||
|
hostPrivateKeyPath = priv
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostKeyPath == "" {
|
||||||
|
util.PrintErrorMessageAndExit("No supported SSH host public key found at /etc/ssh")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(certOutPath); err == nil && !forceOverwrite {
|
||||||
|
util.PrintErrorMessageAndExit("File already exists at " + certOutPath + ". Use --force to overwrite.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if configureSshd {
|
||||||
|
sshdConfig := "/etc/ssh/sshd_config"
|
||||||
|
existing, err := os.ReadFile(sshdConfig)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to read sshd_config")
|
||||||
|
}
|
||||||
|
configLines := []string{
|
||||||
|
"TrustedUserCAKeys " + userCaOutFilePath,
|
||||||
|
"HostKey " + hostPrivateKeyPath,
|
||||||
|
"HostCertificate " + certOutPath,
|
||||||
|
}
|
||||||
|
for _, line := range configLines {
|
||||||
|
for _, existingLine := range strings.Split(string(existing), "\n") {
|
||||||
|
trimmed := strings.TrimSpace(existingLine)
|
||||||
|
if trimmed == line && !strings.HasPrefix(trimmed, "#") && !forceOverwrite {
|
||||||
|
util.PrintErrorMessageAndExit("sshd_config already contains: " + line + ". Use --force to overwrite.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to get custom headers")
|
||||||
|
}
|
||||||
|
|
||||||
|
client := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||||
|
SiteUrl: config.INFISICAL_URL,
|
||||||
|
UserAgent: api.USER_AGENT,
|
||||||
|
AutoTokenRefresh: false,
|
||||||
|
CustomHeaders: customHeaders,
|
||||||
|
})
|
||||||
|
client.Auth().SetAccessToken(infisicalToken)
|
||||||
|
|
||||||
|
host, err := client.Ssh().AddSshHost(infisicalSdk.AddSshHostOptions{
|
||||||
|
ProjectID: projectId,
|
||||||
|
Hostname: hostname,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to register SSH host")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("✅ Successfully registered host:", host.Hostname)
|
||||||
|
|
||||||
|
if writeUserCaToFile {
|
||||||
|
publicKey, err := client.Ssh().GetSshHostUserCaPublicKey(host.ID)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to fetch associated User CA public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeToFile(userCaOutFilePath, publicKey, 0644); err != nil {
|
||||||
|
util.HandleError(err, "Failed to write User CA public key to file")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("📁 Wrote User CA public key to:", userCaOutFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if writeHostCertToFile {
|
||||||
|
pubKeyBytes, err := os.ReadFile(hostKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to read SSH host public key")
|
||||||
|
}
|
||||||
|
res, err := client.Ssh().IssueSshHostHostCert(host.ID, infisicalSdk.IssueSshHostHostCertOptions{
|
||||||
|
PublicKey: string(pubKeyBytes),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to issue SSH host certificate")
|
||||||
|
}
|
||||||
|
if err := writeToFile(certOutPath, res.SignedKey, 0644); err != nil {
|
||||||
|
util.HandleError(err, "Failed to write SSH host certificate to file")
|
||||||
|
}
|
||||||
|
fmt.Println("📁 Wrote host certificate to:", certOutPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if configureSshd {
|
||||||
|
sshdConfig := "/etc/ssh/sshd_config"
|
||||||
|
contentBytes, err := os.ReadFile(sshdConfig)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to read sshd_config")
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(contentBytes), "\n")
|
||||||
|
|
||||||
|
configMap := map[string]string{
|
||||||
|
"TrustedUserCAKeys": userCaOutFilePath,
|
||||||
|
"HostKey": hostPrivateKeyPath,
|
||||||
|
"HostCertificate": certOutPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
seenKeys := map[string]bool{}
|
||||||
|
for i, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
for key, value := range configMap {
|
||||||
|
if strings.HasPrefix(trimmed, key+" ") {
|
||||||
|
seenKeys[key] = true
|
||||||
|
if strings.HasPrefix(trimmed, "#") || forceOverwrite {
|
||||||
|
lines[i] = fmt.Sprintf("%s %s", key, value)
|
||||||
|
} else {
|
||||||
|
util.PrintErrorMessageAndExit("sshd_config already contains: " + trimmed + ". Use --force to overwrite.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append missing lines
|
||||||
|
for key, value := range configMap {
|
||||||
|
if !seenKeys[key] {
|
||||||
|
lines = append(lines, fmt.Sprintf("%s %s", key, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write back to file
|
||||||
|
if err := os.WriteFile(sshdConfig, []byte(strings.Join(lines, "\n")), 0644); err != nil {
|
||||||
|
util.HandleError(err, "Failed to update sshd_config")
|
||||||
|
}
|
||||||
|
fmt.Println("📄 Updated sshd_config entries")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
sshSignKeyCmd.Flags().String("token", "", "Issue SSH certificate using machine identity access token")
|
sshSignKeyCmd.Flags().String("token", "", "Issue SSH certificate using machine identity access token")
|
||||||
sshSignKeyCmd.Flags().String("certificateTemplateId", "", "The ID of the SSH certificate template to issue the SSH certificate for")
|
sshSignKeyCmd.Flags().String("certificateTemplateId", "", "The ID of the SSH certificate template to issue the SSH certificate for")
|
||||||
@@ -617,5 +1005,20 @@ func init() {
|
|||||||
sshIssueCredentialsCmd.Flags().String("outFilePath", "", "The path to write the SSH credentials to such as ~/.ssh, ./some_folder, ./some_folder/id_rsa-cert.pub. If not provided, the credentials will be saved to the current working directory")
|
sshIssueCredentialsCmd.Flags().String("outFilePath", "", "The path to write the SSH credentials to such as ~/.ssh, ./some_folder, ./some_folder/id_rsa-cert.pub. If not provided, the credentials will be saved to the current working directory")
|
||||||
sshIssueCredentialsCmd.Flags().Bool("addToAgent", false, "Whether to add issued SSH credentials to the SSH agent")
|
sshIssueCredentialsCmd.Flags().Bool("addToAgent", false, "Whether to add issued SSH credentials to the SSH agent")
|
||||||
sshCmd.AddCommand(sshIssueCredentialsCmd)
|
sshCmd.AddCommand(sshIssueCredentialsCmd)
|
||||||
|
|
||||||
|
sshConnectCmd.Flags().Bool("writeHostCaToFile", true, "Write Host CA public key to ~/.ssh/known_hosts as a separate entry if doesn't already exist")
|
||||||
|
sshCmd.AddCommand(sshConnectCmd)
|
||||||
|
|
||||||
|
sshAddHostCmd.Flags().String("token", "", "Use a machine identity access token")
|
||||||
|
sshAddHostCmd.Flags().String("projectId", "", "Project ID the host belongs to (required)")
|
||||||
|
sshAddHostCmd.Flags().String("hostname", "", "Hostname of the SSH host (required)")
|
||||||
|
sshAddHostCmd.Flags().Bool("writeUserCaToFile", false, "Write User CA public key to /etc/ssh/infisical_user_ca.pub")
|
||||||
|
sshAddHostCmd.Flags().String("userCaOutFilePath", "/etc/ssh/infisical_user_ca.pub", "Custom file path to write the User CA public key")
|
||||||
|
sshAddHostCmd.Flags().Bool("writeHostCertToFile", false, "Write SSH host certificate to /etc/ssh/ssh_host_<type>_key-cert.pub")
|
||||||
|
sshAddHostCmd.Flags().Bool("configureSshd", false, "Update TrustedUserCAKeys, HostKey, and HostCertificate in the sshd_config file")
|
||||||
|
sshAddHostCmd.Flags().Bool("force", false, "Force overwrite of existing certificate files as part of writeUserCaToFile and writeHostCertToFile")
|
||||||
|
|
||||||
|
sshCmd.AddCommand(sshAddHostCmd)
|
||||||
|
|
||||||
rootCmd.AddCommand(sshCmd)
|
rootCmd.AddCommand(sshCmd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,363 @@
|
|||||||
|
---
|
||||||
|
title: "Infisical SSH"
|
||||||
|
sidebarTitle: "Infisical SSH"
|
||||||
|
description: "Learn how to generate SSH credentials to provide secure and centralized SSH access control for your infrastructure."
|
||||||
|
---
|
||||||
|
|
||||||
|
## Concept
|
||||||
|
|
||||||
|
Infisical can be used to issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure;
|
||||||
|
this improves on many limitations of traditional SSH key-based authentication via mitigation of private key compromise, static key management,
|
||||||
|
unauthorized access, and SSH key sprawl.
|
||||||
|
|
||||||
|
The following concepts are useful to know when working with Infisical SSH:
|
||||||
|
|
||||||
|
- SSH Certificate Authority (CA): A trusted authority that issues SSH certificates.
|
||||||
|
- Certificate Template: A set of policies bound to an SSH CA for certificates issued under that template; a CA can possess multiple templates, each with different policies for a different purpose (e.g. for admin versus developer access).
|
||||||
|
- SSH Certificate: A short-lived, credential issued by the SSH CA granting time-bound access to infrastructure.
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[SSH CA]
|
||||||
|
A --> B[Certificate Template A]
|
||||||
|
A --> C[Certificate Template N]
|
||||||
|
B --> D[SSH Certificate A]
|
||||||
|
C --> E[SSH Certificate N]
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
When using Infisical SSH to provision client access to a remote host, an operator must create an SSH CA in Infisical; a certificate template under it,
|
||||||
|
specifying policies such as allowed users that can be requested under that template by a client; and configure the host to trust certificates issued by the Infisical SSH CA.
|
||||||
|
|
||||||
|
When a client needs access to a host, they authenticate with Infisical and request an SSH certificate (and optionally key pair)
|
||||||
|
to be used to access the host for a time-bound session as part of the SSH operation.
|
||||||
|
|
||||||
|
## Client Workflow
|
||||||
|
|
||||||
|
The following sequence diagram illustrates the client workflow for accessing a remote host using an SSH certificate (and optionally key pair)
|
||||||
|
supplied by Infisical.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Client as Client
|
||||||
|
participant Infisical as Infisical (SSH CA)
|
||||||
|
participant Host as Remote Host
|
||||||
|
|
||||||
|
Note over Client,Client: Step 1: Client Authentication with Infisical
|
||||||
|
Client->>Infisical: Send credential(s) to authenticate with Infisical
|
||||||
|
|
||||||
|
Infisical-->>Client: Return access token
|
||||||
|
|
||||||
|
Note over Client,Infisical: Step 2: SSH Certificate Request
|
||||||
|
Client->>Infisical: Make authenticated request for SSH certificate via either /api/v1/ssh/issue or /api/v1/ssh/sign
|
||||||
|
|
||||||
|
Infisical-->>Client: Return signed SSH certificate (and optionally key pair)
|
||||||
|
|
||||||
|
Note over Client,Client: Step 3: SSH Operation
|
||||||
|
Client->>Host: SSH into Host using the SSH certificate
|
||||||
|
|
||||||
|
Host-->>Client: Grant access to the host
|
||||||
|
```
|
||||||
|
|
||||||
|
At a high-level, Infisical issues a signed SSH certificate to a client that can be used to access a remote host.
|
||||||
|
|
||||||
|
To be more specific:
|
||||||
|
|
||||||
|
1. The client authenticates with Infisical; this can be done using a user or machine identity [authentication method](/documentation/platform/identities/machine-identities) or a user [authentication method](/documentation/platform/identities/user-identities).
|
||||||
|
2. The client makes an authenticated request for an SSH certificate via either the `/api/v1/ssh/issue` or `/api/v1/ssh/sign` endpoints. Note that if the client wishes to use an existing SSH key pair, it can use the `/api/v1/ssh/sign` endpoint; otherwise, it can use the `/api/v1/ssh/issue` endpoint to have Infisical issue a new SSH key pair along with the certificate.
|
||||||
|
3. The client uses the issued SSH certificate (and potentially SSH key pair) to temporarily access the host.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Note that the workflow above requires an operator to perform additional
|
||||||
|
configuration on the remote host to trust SSH certificates issued by
|
||||||
|
Infisical.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## Guide to Configuring Infisical SSH
|
||||||
|
|
||||||
|
In the following steps, we explore how to configure Infisical SSH to start issuing SSH certificates to clients as well as a remote host to trust these certificates
|
||||||
|
as part of the SSH operation.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Configuring an SSH CA for client key signing">
|
||||||
|
1.1. Start by creating an SSH project in the SSH tab of your organization.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1.2. Next, create an SSH CA in the **Certificate Authorities** tab of the
|
||||||
|
project; this CA will be used for client key signing.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here's some guidance on each field:
|
||||||
|
|
||||||
|
- Friendly Name: A friendly name for the CA; this is only for display.
|
||||||
|
- Key Source: Whether the CA's key pair should be generated internally or supplied from an external source. Select **Internal**.
|
||||||
|
- Key Algorithm: The type of public key algorithm and size, in bits, of the key pair for the CA. Supported key algorithms are `RSA 2048`, `RSA 4096`, `ECDSA P-256`, and `ECDSA P-384` with the default being `RSA 2048`.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Configuring a certificate template on the CA">
|
||||||
|
|
||||||
|
2.1. Next, create a certificate template in the **Certificate Templates** section of the newly-created CA.
|
||||||
|
|
||||||
|
A certificate template is a set of policies for certificates issued under that template; each template is bound to a specific CA.
|
||||||
|
|
||||||
|
With certificate templates, you can specify, for example, that certificates issued under a template are only allowed for users with a specific username like `ec2-user` or perhaps that the max TTL requested cannot exceed 1 hour.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here's some guidance on each field:
|
||||||
|
|
||||||
|
- SSH Template Name: A name for the certificate template; this must be a valid slug.
|
||||||
|
- Allowed Users: A comma-separated list of valid usernames (e.g. `ec2-user`) on the remote host for which a client can request a certificate for. If you wish to allow a client to request a certificate for any username, set this to `*`; alternatively, if left blank, the template will not allow issuance of certificates under any username.
|
||||||
|
- Allowed Hosts: A comma-separated list of valid hostnames/domains on the remote host for which a client can request a certificate for. Each item in the list can be either a wildcard hostname (e.g. `*.acme.com`), a specific hostname (e.g. `example.com`), an IPv4 address (e.g. `192.168.1.1`), or an IPv6 address. If left empty, the template will not allow any hostnames; if set to `*`, the template will allow any hostname.
|
||||||
|
- Default TTL: The default Time-to-Live (TTL) for certificates issued under this template when a client does not explicitly specify a TTL in the certificate request. We recommend setting a shorter **Default TTL** for client certificates such as `30m`.
|
||||||
|
- Max TTL: The maximum TTL for certificates issued under this template.
|
||||||
|
- Allow User Certificates: Whether or not to allow issuance of user certificates; this should be set to `true`.
|
||||||
|
- Allow Host Certificates: Whether or not to allow issuance of host certificates; this is not relevant for this step.
|
||||||
|
- Allow Custom Key IDs: Whether or not to allow clients to specify a custom key ID to be included on the certificate as part of the certificate request.
|
||||||
|
|
||||||
|
2.2. Finally, add the user(s) you wish to be able to request an SSH certificate to the SSH project through the **Access Control** tab.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Configuring the remote host to trust the client">
|
||||||
|
|
||||||
|
3.1. Begin by downloading the client CA's public key from the CA's details section.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
The CA's public key can also be retrieved programmatically via API by making a `GET` request to the endpoint [here](/api-reference/endpoints/ssh/ca/public-key).
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
3.2. Next, create a file containing this public key in the SSH folder of the remote host; we'll call the file `ca.pub`.
|
||||||
|
|
||||||
|
This would result in the file at the path `/etc/ssh/ca.pub`.
|
||||||
|
|
||||||
|
3.3. Next, add the following lines to the `/etc/ssh/sshd_config` file on the remote host.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TrustedUserCAKeys /etc/ssh/ca.pub
|
||||||
|
|
||||||
|
PubkeyAcceptedKeyTypes=+ssh-rsa,[email protected]
|
||||||
|
```
|
||||||
|
|
||||||
|
3.4. Finally, reload the SSH daemon on the remote host to apply the changes.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl reload sshd
|
||||||
|
```
|
||||||
|
|
||||||
|
At this point, the remote host is configured to trust SSH certificates issued by the Infisical SSH CA.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## Guide to Using Infisical SSH to Access a Host
|
||||||
|
|
||||||
|
In the following steps, we show how to obtain an SSH certificate and use it for a client to access a host via CLI:
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
The subsequent guide assumes the following prerequisites:
|
||||||
|
|
||||||
|
- SSH Agent is running: The `ssh-agent` must be actively running on the host machine.
|
||||||
|
- OpenSSH is installed: The system should have OpenSSH installed; this includes
|
||||||
|
both the `ssh` client and `ssh-agent`.
|
||||||
|
- `SSH_AUTH_SOCK` environment variable
|
||||||
|
is set; the `SSH_AUTH_SOCK` variable should point to the UNIX socket that
|
||||||
|
`ssh-agent` uses for communication.
|
||||||
|
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Authenticate with Infisical">
|
||||||
|
|
||||||
|
```bash
|
||||||
|
infisical login
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Obtain an SSH certificate for the client and load it into the SSH agent">
|
||||||
|
Run the `infisical ssh issue-credentials` command, specifying the `--addToAgent` flag to automatically load the SSH certificate into the SSH agent.
|
||||||
|
```bash
|
||||||
|
infisical ssh issue-credentials --certificateTemplateId=<certificate-template-id> --principals=<username> --addToAgent
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's some guidance on each flag:
|
||||||
|
|
||||||
|
- `certificateTemplateId`: The ID of the certificate template to use for issuing the SSH certificate.
|
||||||
|
- `principals`: The comma-delimited username(s) or hostname(s) to include in the SSH certificate.
|
||||||
|
|
||||||
|
For fuller documentation on commands and flags supported by the Infisical CLI for SSH, refer to the docs [here](/cli/commands/ssh).
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="SSH into the host">
|
||||||
|
Finally, SSH into the desired host; the SSH operation will be performed using the SSH certificate loaded into the SSH agent.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh username@hostname
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Note that the above workflow can be executed via API or other client methods
|
||||||
|
such as SDK.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
## Guide to Configuring Host Key Signing
|
||||||
|
|
||||||
|
In the following steps, we show how to configure host key signing for clients to verify the identity of a remote host before attempting the SSH operation; this is recommended to reduce the probability of a client accessing a malicious machine.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
This guide expects that the remote host already has an existing SSH key pair (typically found in the `/etc/ssh/` folder at `/etc/ssh/ssh_host_<algo>_key` and `.pub`).
|
||||||
|
|
||||||
|
If the remote host does not have an existing SSH key pair, you can generate a new key pair using the `ssh-keygen` command: `ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N ''`. This will generate:
|
||||||
|
|
||||||
|
- A private key: `/etc/ssh/ssh_host_rsa_key`.
|
||||||
|
- A public key: `/etc/ssh/ssh_host_rsa_key.pub`.
|
||||||
|
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
<Step title="Configuring an SSH CA for host key signing">
|
||||||
|
1.1. In the same SSH project, create another SSH CA in the **Certificate Authorities** tab; this CA will be used for host key signing.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here's some guidance on each field:
|
||||||
|
|
||||||
|
- Friendly Name: A friendly name for the CA; this is only for display.
|
||||||
|
- Key Source: Whether the CA's key pair should be generated internally or supplied from an external source. Select **External**.
|
||||||
|
- Public Key: The public key for the CA (i.e. the host's SSH public key).
|
||||||
|
- Private Key: The private key for the CA (i.e. the host's SSH private key).
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Configuring a certificate template on the CA">
|
||||||
|
|
||||||
|
2.1. Next, create a certificate template in the **Certificate Templates** section of the newly-created CA.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Here's some guidance on each field:
|
||||||
|
|
||||||
|
- SSH Template Name: A name for the certificate template; this must be a valid slug.
|
||||||
|
- Allowed Users: A comma-separated list of valid usernames (e.g. `ec2-user`) on the remote host for which a client can request a certificate for. If you wish to allow a client to request a certificate for any username, set this to `*`; alternatively, if left blank, the template will not allow issuance of certificates under any username.
|
||||||
|
- Allowed Hosts: A comma-separated list of valid hostnames/domains on the remote host for which a client can request a certificate for. Each item in the list can be either a wildcard hostname (e.g. `*.acme.com`), a specific hostname (e.g. `example.com`), an IPv4 address (e.g. `192.168.1.1`), or an IPv6 address. If left empty, the template will not allow any hostnames; if set to `*`, the template will allow any hostname.
|
||||||
|
- Default TTL: The default Time-to-Live (TTL) for certificates issued under this template when a client does not explicitly specify a TTL in the certificate request. We recommend setting a longer **Default TTL** for host certificates such as `2y`.
|
||||||
|
- Max TTL: The maximum TTL for certificates issued under this template.
|
||||||
|
- Allow User Certificates: Whether or not to allow issuance of user certificates; this is not relevant for this step.
|
||||||
|
- Allow Host Certificates: Whether or not to allow issuance of host certificates; this should be set to `true`.
|
||||||
|
- Allow Custom Key IDs: Whether or not to allow clients to specify a custom key ID to be included on the certificate as part of the certificate request.
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Configuring the remote host with an SSH certificate">
|
||||||
|
|
||||||
|
3.1. Obtain an SSH certificate for the host by requesting one from the **Certificates** tab.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
You should select **Sign SSH Key** under the **Operation** field.
|
||||||
|
|
||||||
|
Then input your host's SSH public key under the **SSH Public Key** field and hostname under the **Principal(s)** field; the host's public key should be in the `/etc/ssh` folder of the host as used in step 1.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3.2. Create a file containing the certificate in the SSH folder of the remote host; we'll call it `ssh_host_key-cert.pub`.
|
||||||
|
|
||||||
|
3.3. Set permissions on the certificate to be `0640`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chmod 0640 /etc/ssh/ssh_host_key-cert.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
3.4. Next, add the following lines to the `/etc/ssh/sshd_config` file on the remote host.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HostKey /etc/ssh/ssh_host_rsa_key
|
||||||
|
HostCertificate /etc/ssh/ssh_host_key-cert.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
You should adjust the `HostKey` directive to match the path to the host's SSH private key as used in step 1.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
3.5. Finally, reload the SSH daemon on the remote host to apply the changes.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl reload sshd
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Configuring the client to trust the remote host">
|
||||||
|
4.1. Begin by downloading the host CA's public key from the CA's details section.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
The CA's public key can also be retrieved programmatically via API by making a `GET` request to the endpoint [here](/api-reference/endpoints/ssh/ca/public-key).
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
4.2. Next, add the resulting public key to the `known_hosts` file on the client machine (e.g. at the path `~/.ssh/known_hosts`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
@cert-authority *.example.com ssh-rsa ...
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="SSH into the host">
|
||||||
|
Finally, SSH into the desired host as usual; the SSH operation will now also include client-side host verification.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh username@hostname
|
||||||
|
```
|
||||||
|
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
<AccordionGroup>
|
||||||
|
<Accordion title="How can I confirm if the Infisical SSH workflow is working?">
|
||||||
|
After configuring Infisical SSH, you can add the `-vvv` flag as part of the
|
||||||
|
SSH operation to see verbose output from the SSH client.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -vvv username@hostname
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see output from the SSH client that includes the following if both client key signing and host key signing are working:
|
||||||
|
|
||||||
|
Host certificate was verified and trusted:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
debug1: Host 'example.com' is known and matches the ECDSA-CERT host certificate.
|
||||||
|
debug1: Found CA key in /Users/user/.ssh/known_hosts:1
|
||||||
|
```
|
||||||
|
|
||||||
|
You authenticated with your user certificate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
debug1: Offering public key: Added via Infisical CLI RSA-CERT SHA256:...
|
||||||
|
debug1: Server accepts key: Added via Infisical CLI RSA-CERT SHA256:...
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
</AccordionGroup>
|
||||||
@@ -1,210 +1,179 @@
|
|||||||
---
|
---
|
||||||
title: "Infisical SSH"
|
title: "Infisical SSH"
|
||||||
sidebarTitle: "Infisical SSH"
|
sidebarTitle: "Infisical SSH"
|
||||||
description: "Learn how to generate SSH credentials to provide secure and centralized SSH access control for your infrastructure."
|
description: "Learn how to securely provision user SSH access to your infrastructure using SSH certificates."
|
||||||
---
|
---
|
||||||
|
|
||||||
## Concept
|
## Concept
|
||||||
|
|
||||||
Infisical can be used to issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure;
|
Infisical SSH can be configured to provide users on your team short-lived, secure SSH access to infrastructure. Under the hood, it uses SSH certificates
|
||||||
this improves on many limitations of traditional SSH key-based authentication via mitigation of private key compromise, static key management,
|
and improves upon traditional SSH key-based authentication by mitigating private key compromise, static key management,
|
||||||
unauthorized access, and SSH key sprawl.
|
unauthorized access, and SSH key sprawl.
|
||||||
|
|
||||||
The following concepts are useful to know when working with Infisical SSH:
|
The following entities and concepts are important to understand when using Infisical SSH:
|
||||||
|
|
||||||
- SSH Certificate Authority (CA): A trusted authority that issues SSH certificates.
|
- Administrator: An individual on your team who is responsible for configuring Infisical SSH.
|
||||||
- Certificate Template: A set of policies bound to a SSH CA for certificates issued under that template; a CA can possess multiple templates, each with different policies for a different purpose (e.g. for admin versus developer access).
|
- Users: Other individuals on your team that need access to the remote host.
|
||||||
- SSH Certificate: A short-lived, credential issued by the SSH CA granting time-bound access to infrastructure.
|
- Host: A remote machine (e.g. EC2 instance, GCP VM, Azure VM, on-prem Linux server, Raspberry Pi, VMware VM, etc.) that users need SSH access to that is registered with Infisical SSH.
|
||||||
|
|
||||||
<div align="center">
|
## Workflow
|
||||||
|
|
||||||
```mermaid
|
The typical workflow for using Infisical SSH consists of the following steps:
|
||||||
graph TD
|
|
||||||
A[SSH CA]
|
|
||||||
A --> B[Certificate Template A]
|
|
||||||
A --> C[Certificate Template N]
|
|
||||||
B --> D[SSH Certificate A]
|
|
||||||
C --> E[SSH Certificate N]
|
|
||||||
|
|
||||||
```
|
1. The administrator registers a remote host with Infisical using the Infisical CLI via the `infisical ssh add-host` command.
|
||||||
|
2. The administrator configures Infisical SSH to grant users access to the remote host.
|
||||||
|
3. User(s) access the remote host using the Infisical CLI via the `infisical ssh connect` command.
|
||||||
|
|
||||||
</div>
|
## Admin Guide for Configuring Infisical SSH
|
||||||
|
|
||||||
When using Infisical SSH to provision client access to a remote host, an operator must create a SSH CA in Infisical; a certificate template under it,
|
In the following steps, we explore how to configure Infisical SSH to control and streamline your team's SSH access to infrastructure. As part of this guide,
|
||||||
specifying policies such as allowed users that can be requested under that template by a client; and configure the host to trust certificates issued by the Infisical SSH CA.
|
we will register a remote host with Infisical through a [machine identity](/documentation/platform/identities/machine-identities) and configure Infisical to grant user(s) access to the remote host.
|
||||||
|
|
||||||
When a client needs access to a host, they authenticate with Infisical and request a SSH certificate (and optionally key pair)
|
|
||||||
to be used to access the host for a time-bound session as part of the SSH operation.
|
|
||||||
|
|
||||||
## Client Workflow
|
|
||||||
|
|
||||||
The following sequence diagram illustrates the client workflow for accessing a remote host using an SSH certificate (and optionally key pair)
|
|
||||||
supplied by Infisical.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Client as Client
|
|
||||||
participant Infisical as Infisical (SSH CA)
|
|
||||||
participant Host as Remote Host
|
|
||||||
|
|
||||||
Note over Client,Client: Step 1: Client Authentication with Infisical
|
|
||||||
Client->>Infisical: Send credential(s) to authenticate with Infisical
|
|
||||||
|
|
||||||
Infisical-->>Client: Return access token
|
|
||||||
|
|
||||||
Note over Client,Infisical: Step 2: SSH Certificate Request
|
|
||||||
Client->>Infisical: Make authenticated request for SSH certificate via either /api/v1/ssh/issue or /api/v1/ssh/sign
|
|
||||||
|
|
||||||
Infisical-->>Client: Return signed SSH certificate (and optionally key pair)
|
|
||||||
|
|
||||||
Note over Client,Client: Step 3: SSH Operation
|
|
||||||
Client->>Host: SSH into Host using the SSH certificate
|
|
||||||
|
|
||||||
Host-->>Client: Grant access to the host
|
|
||||||
```
|
|
||||||
|
|
||||||
At a high-level, Infisical issues a signed SSH certificate to a client that can be used to access a remote host.
|
|
||||||
|
|
||||||
To be more specific:
|
|
||||||
|
|
||||||
1. The client authenticates with Infisical; this can be done using a machine identity [authentication method](/documentation/platform/identities/machine-identities) or a user [authentication method](/documentation/platform/identities/user-identities).
|
|
||||||
2. The client makes an authenticated request for an SSH certificate via either the `/api/v1/ssh/issue` or `/api/v1/ssh/sign` endpoints. Note that if the client wishes to use an existing SSH key pair, it can use the `/api/v1/ssh/sign` endpoint; otherwise, it can use the `/api/v1/ssh/issue` endpoint to have Infisical issue a new SSH key pair in conjunction with the certificate.
|
|
||||||
3. The client uses the issued SSH certificate (and potentially SSH key pair) to temporarily access the host.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
Note that the workflow above requires an operator to perform additional
|
|
||||||
configuration on the remote host to trust SSH certificates issued by
|
|
||||||
Infisical.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## Guide to Configuring Infisical SSH
|
|
||||||
|
|
||||||
In the following steps, we explore how to configure Infisical SSH to start issuing SSH certificates to clients as well as a remote host to trust these certificates
|
|
||||||
as part of the SSH operation.
|
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Configuring Infisical SSH">
|
<Step title="Create an Infisical SSH project">
|
||||||
1.1. Start by creating a SSH project in the SSH tab of your organization.
|
1.1. Start by creating a new Infisical SSH project in Infisical.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
1.2. Next, create a CA in the **Certificate Authorities** tab of the
|
1.2. Create a custom role in the project under Access Control > Project Roles to grant the machine identity that we will create in step 2 the ability to **Create** and **Issue Host Certificates** on the **SSH Host** resource; this will enable the linked machine identity to bootstrap a remote host with Infisical
|
||||||
project.
|
and establish the necessary configuration on it.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Here's some guidance on each field:
|
|
||||||
|
|
||||||
- Friendly Name: A friendly name for the CA; this is only for display.
|
|
||||||
- Key Algorithm: The type of public key algorithm and size, in bits, of the key pair for the CA. Supported key algorithms are `RSA 2048`, `RSA 4096`, `ECDSA P-256`, and `ECDSA P-384` with the default being `RSA 2048`.
|
|
||||||
|
|
||||||
1.3. Next, create a certificate template in the **Certificate Templates** section of the newly-created CA.
|
|
||||||
|
|
||||||
A certificate template is a set of policies for certificates issued under that template; each template is bound to a specific CA.
|
|
||||||
|
|
||||||
With certificate templates, you can specify, for example, that certificates issued under a template are only allowed for users with a specific username like `ec2-user` or perhaps that the max TTL requested cannot exceed 1 year.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Here's some guidance on each field:
|
|
||||||
|
|
||||||
- SSH Template Name: A name for the certificate template; this must be a valid slug.
|
|
||||||
- Allowed Users: A comma-separated list of valid usernames (e.g. `ec2-user`) on the remote host for which a client can request a certificate for. If you wish to allow a client to request a certificate for any username, set this to `*`; alternatively, if left blank, the template will not allow issuance of certificates under any username.
|
|
||||||
- Allowed Hosts: A comma-separated list of valid hostnames/domains on the remote host for which a client can request a certificate for. Each item in the list can be either a wildcard hostname (e.g. `*.acme.com`), a specific hostname (e.g. `example.com`), an IPv4 address (e.g. `192.168.1.1`), or an IPv6 address. If left empty, the template will not allow any hostnames; if set to `*`, the template will allow any hostname.
|
|
||||||
- Default TTL: The default Time-to-Live (TTL) for certificates issued under this template when a client does not explicitly specify a TTL in the certificate request.
|
|
||||||
- Max TTL: The maximum TTL for certificates issued under this template.
|
|
||||||
- Allow User Certificates: Whether or not to allow issuance of user certificates.
|
|
||||||
- Allow Host Certificates: Whether or not to allow issuance of host certificates.
|
|
||||||
- Allow Custom Key IDs: Whether or not to allow clients to specify a custom key ID to be included on the certificate as part of the certificate request.
|
|
||||||
|
|
||||||
1.4. Finally, add the user(s) you wish to be able to request a SSH certificate to the SSH project through the **Access Control** tab.
|
|
||||||
|
|
||||||
|

|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Configuring the remote host">
|
<Step title="Create a machine identity for bootstrapping Infisical SSH">
|
||||||
|
2.1. Follow the instructions [here](/documentation/platform/identities/universal-auth) to configure a [machine identity](/documentation/platform/identities/machine-identities) in Infisical with Universal Auth.
|
||||||
|
|
||||||
2.1. Begin by downloading the CA's public key from the CA's details section.
|
By the end of this step, you should have a **Client ID** and **Client Secret** on hand as part of the Universal Auth configuration for the identity to authenticate with Infisical
|
||||||
|
as part of registering a remote host in step 3.
|
||||||

|
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
The CA's public key can also be retrieved programmatically via API by making a `GET` request to the `/ssh/ca/<ca-id>/public-key` endpoint.
|
You may use other authentication methods as suitable (e.g. [AWS Auth](/documentation/platform/identities/aws-auth), [Azure Auth](/documentation/platform/identities/azure-auth), [GCP Auth](/documentation/platform/identities/gcp-auth), etc.) as part of the machine identity configuration but, to keep this example simple, we will be using Universal Auth.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
2.2. Next, create a file containing this public key in the SSH folder of the remote host; we'll call the file `ca.pub`.
|
2.2. Add the machine identity to the Infisical SSH project you created in the previous step and assign it the custom role you created in step 1.2.
|
||||||
|
|
||||||
This would result in the file at the path `/etc/ssh/ca.pub`.
|

|
||||||
|
|
||||||
2.3. Next, add the following lines to the `/etc/ssh/sshd_config` file on the remote host.
|
</Step>
|
||||||
|
<Step title="Configure the remote host">
|
||||||
|
3.1. Follow the instructions [here](/cli/overview) to install the Infisical CLI onto the remote host.
|
||||||
|
|
||||||
|
3.2. Run the commands below to register the remote host with Infisical.
|
||||||
|
|
||||||
|
Use the **Client ID** and **Client Secret** from the machine identity you created in step 2.1 as part of the `infisical login` command
|
||||||
|
to obtain an access token and save it as an environment variable.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
TrustedUserCAKeys /etc/ssh/ca.pub
|
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=<identity-client-id> --client-secret=<identity-client-secret> --silent --plain)
|
||||||
|
|
||||||
PubkeyAcceptedKeyTypes=+ssh-rsa,[email protected]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2.4. Finally, reload the SSH daemon on the remote host to apply the changes.
|
Next, use the `infisical ssh add-host` command to register the remote host with Infisical. As part of this command, input the ID of the Infisical SSH project you created in step 1 for the `--projectId` flag and the hostname of the remote host for the `--hostname` flag.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo infisical ssh add-host --projectId=<project-id> --hostname=<hostname> --token="$INFISICAL_TOKEN" --writeUserCaToFile --writeHostCertToFile --configureSshd
|
||||||
|
```
|
||||||
|
|
||||||
|
<Tip>
|
||||||
|
Note that if you're self-hosting Infisical, you can use the `--domain` flag on the `infisical login` command to specify the domain of your Infisical instance.
|
||||||
|
|
||||||
|
For more information on the `infisical ssh add-host` command, please refer to the Infisical CLI [documentation](/cli/overview).
|
||||||
|
</Tip>
|
||||||
|
|
||||||
|
If successful, you should see output similar to the following:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
✅ Successfully registered host: <hostname>
|
||||||
|
📁 Wrote User CA public key to: /etc/ssh/infisical_user_ca.pub
|
||||||
|
📁 Wrote host certificate to: /etc/ssh/ssh_host_ed25519_key-cert.pub
|
||||||
|
📄 Updated sshd_config entries
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, use the following command to reload the SSH daemon on the remote host to apply the changes:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl reload sshd
|
sudo systemctl reload sshd
|
||||||
```
|
```
|
||||||
|
|
||||||
At this point, the remote host is configured to trust SSH certificates issued by the Infisical SSH CA.
|
<Note>
|
||||||
|
The command may differ depending on the host. For older versions of Ubuntu/Debian/CentOS, you may need to use `sudo service ssh reload` instead;
|
||||||
|
for Alpine or minimal systems, `/etc/init.d/sshd reload`.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
Back in Infisical, you should now see the remote host you just registered in the Infisical SSH project you created in step 1 under the **Hosts** tab.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</Step>
|
||||||
|
<Step title="Grant users access to the remote host">
|
||||||
|
4.1. Add the user(s) you wish to grant access to the remote host to the Infisical SSH project under Access Control > Users.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4.2. On the registered host in the **Hosts** tab, click **Edit SSH Host** and add a login mapping for the user(s) you added in step 4.1.
|
||||||
|
|
||||||
|
The login mapping dictates what user(s) will be allowed access to the remote host and under a specific login user; in the allowed principals,
|
||||||
|
you should select user(s) part of the Infisical SSH project that will be allowed to login to the remote host as the login user.
|
||||||
|
|
||||||
|
For instance, if you add a mapping with the login user `ec2-user` to some users John and Alice in Infisical, then they will be allowed to login to the remote host as `ec2-user` which is a system user that
|
||||||
|
exists on the remote host.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Note that you should configure authorized principals files for each login user you add to the remote host.
|
||||||
|
</Note>
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
## Guide to Using Infisical SSH to Access a Host
|
## User Guide for SSHing to a Host
|
||||||
|
|
||||||
We show how to obtain a SSH certificate and use it for a client to access a host via CLI:
|
Once Infisical SSH is configured by an administrator, users can SSH to the remote host using the Infisical CLI.
|
||||||
|
|
||||||
<Note>
|
|
||||||
The subsequent guide assumes the following prerequisites:
|
|
||||||
|
|
||||||
- SSH Agent is running: The `ssh-agent` must be actively running on the host machine.
|
|
||||||
- OpenSSH is installed: The system should have OpenSSH installed; this includes
|
|
||||||
both the `ssh` client and `ssh-agent`.
|
|
||||||
- `SSH_AUTH_SOCK` environment variable
|
|
||||||
is set; the `SSH_AUTH_SOCK` variable should point to the UNIX socket that
|
|
||||||
`ssh-agent` uses for communication.
|
|
||||||
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Authenticate with Infisical">
|
<Step title="Install the Infisical CLI">
|
||||||
|
Follow the instructions [here](/cli/overview) to install the Infisical CLI onto your local machine.
|
||||||
|
</Step>
|
||||||
|
<Step title="Log in with the CLI">
|
||||||
|
Run the `infisical login` command to authenticate with Infisical.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
infisical login
|
infisical login
|
||||||
```
|
```
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Obtain a SSH certificate and load it into the SSH agent">
|
<Step title="Connect to the remote host">
|
||||||
Run the `infisical ssh issue-credentials` command, specifying the `--addToAgent` flag to automatically load the SSH certificate into the SSH agent.
|
Run the `infisical ssh connect` command to connect to a remote host.
|
||||||
```bash
|
|
||||||
infisical ssh issue-credentials --certificateTemplateId=<certificate-template-id> --principals=<username> --addToAgent
|
|
||||||
```
|
|
||||||
|
|
||||||
Here's some guidance on each flag:
|
|
||||||
|
|
||||||
- `certificateTemplateId`: The ID of the certificate template to use for issuing the SSH certificate.
|
|
||||||
- `principals`: The comma-delimited username(s) or hostname(s) to include in the SSH certificate.
|
|
||||||
|
|
||||||
For fuller documentation on commands and flags supported by the Infisical CLI for SSH, refer to the docs [here](/cli/commands/ssh).
|
|
||||||
|
|
||||||
</Step>
|
|
||||||
<Step title="SSH into the host">
|
|
||||||
Finally, SSH into the desired host; the SSH operation will be performed using the SSH certificate loaded into the SSH agent.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh username@hostname
|
infisical ssh connect
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll be prompted to select an SSH Host from a list of accessible hosts; this is based on project membership and login mappings configured on hosts by
|
||||||
|
the administrator.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Use the arrow keys to navigate: ↓ ↑ → ←
|
||||||
|
? Select an SSH Host:
|
||||||
|
▸ ec2-12-345-678-910.ap-northeast-1.compute.amazonaws.com
|
||||||
|
```
|
||||||
|
|
||||||
|
After selecting a host, you'll be prompted to select a login user from a list of allowed login users:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
? Select Login User:
|
||||||
|
▸ ec2-user
|
||||||
|
```
|
||||||
|
|
||||||
|
If successful, you should be able to SSH to the remote host.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
✔ ec2-54-199-104-116.ap-northeast-1.compute.amazonaws.com
|
||||||
|
✔ ec2-user
|
||||||
|
✔ SSH credentials successfully added to agent
|
||||||
|
Connecting to [email protected]...
|
||||||
```
|
```
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|
||||||
<Note>
|
|
||||||
Note that the above workflow can be executed via API or other client methods
|
|
||||||
such as SDK.
|
|
||||||
</Note>
|
|
||||||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 491 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 588 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 510 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 587 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 539 KiB |
|
After Width: | Height: | Size: 838 KiB |
|
Before Width: | Height: | Size: 698 KiB After Width: | Height: | Size: 476 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 852 KiB |
|
After Width: | Height: | Size: 564 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 720 KiB |
|
After Width: | Height: | Size: 480 KiB |
|
After Width: | Height: | Size: 643 KiB |
@@ -75,6 +75,14 @@ export enum ProjectPermissionGroupActions {
|
|||||||
GrantPrivileges = "grant-privileges"
|
GrantPrivileges = "grant-privileges"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionSshHostActions {
|
||||||
|
Read = "read",
|
||||||
|
Create = "create",
|
||||||
|
Edit = "edit",
|
||||||
|
Delete = "delete",
|
||||||
|
IssueHostCert = "issue-host-cert"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSecretRotationActions {
|
export enum ProjectPermissionSecretRotationActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
ReadGeneratedCredentials = "read-generated-credentials",
|
ReadGeneratedCredentials = "read-generated-credentials",
|
||||||
@@ -148,6 +156,7 @@ export enum ProjectPermissionSub {
|
|||||||
SshCertificateAuthorities = "ssh-certificate-authorities",
|
SshCertificateAuthorities = "ssh-certificate-authorities",
|
||||||
SshCertificateTemplates = "ssh-certificate-templates",
|
SshCertificateTemplates = "ssh-certificate-templates",
|
||||||
SshCertificates = "ssh-certificates",
|
SshCertificates = "ssh-certificates",
|
||||||
|
SshHosts = "ssh-hosts",
|
||||||
PkiAlerts = "pki-alerts",
|
PkiAlerts = "pki-alerts",
|
||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
Kms = "kms",
|
Kms = "kms",
|
||||||
@@ -244,6 +253,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
|
||||||
|
| [ProjectPermissionSshHostActions, ProjectPermissionSub.SshHosts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ export type TAccessApprovalRequest = {
|
|||||||
member: string;
|
member: string;
|
||||||
status: string;
|
status: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
note?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAccessApproval = {
|
export type TAccessApproval = {
|
||||||
@@ -119,6 +121,7 @@ export type TProjectUserPrivilege = {
|
|||||||
|
|
||||||
export type TCreateAccessRequestDTO = {
|
export type TCreateAccessRequestDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
|
note?: string;
|
||||||
} & Omit<TProjectUserPrivilege, "id" | "createdAt" | "updatedAt" | "slug" | "projectMembershipId">;
|
} & Omit<TProjectUserPrivilege, "id" | "createdAt" | "updatedAt" | "slug" | "projectMembershipId">;
|
||||||
|
|
||||||
export type TGetAccessApprovalRequestsDTO = {
|
export type TGetAccessApprovalRequestsDTO = {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export * from "./serverDetails";
|
|||||||
export * from "./serviceTokens";
|
export * from "./serviceTokens";
|
||||||
export * from "./sshCa";
|
export * from "./sshCa";
|
||||||
export * from "./sshCertificateTemplates";
|
export * from "./sshCertificateTemplates";
|
||||||
|
export * from "./sshHost";
|
||||||
export * from "./ssoConfig";
|
export * from "./ssoConfig";
|
||||||
export * from "./subscriptions";
|
export * from "./subscriptions";
|
||||||
export * from "./tags";
|
export * from "./tags";
|
||||||
|
|||||||
@@ -12,3 +12,47 @@ export const sshCertTypeToNameMap: { [K in SshCertType]: string } = {
|
|||||||
[SshCertType.USER]: "User",
|
[SshCertType.USER]: "User",
|
||||||
[SshCertType.HOST]: "Host"
|
[SshCertType.HOST]: "Host"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum SshCaKeySource {
|
||||||
|
INTERNAL = "internal",
|
||||||
|
EXTERNAL = "external"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SshCertKeyAlgorithm {
|
||||||
|
RSA_2048 = "RSA_2048",
|
||||||
|
RSA_4096 = "RSA_4096",
|
||||||
|
ECDSA_P256 = "EC_prime256v1",
|
||||||
|
ECDSA_P384 = "EC_secp384r1",
|
||||||
|
ED25519 = "ED25519"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sshCertKeyAlgorithmToNameMap: { [K in SshCertKeyAlgorithm]: string } = {
|
||||||
|
[SshCertKeyAlgorithm.RSA_2048]: "RSA 2048",
|
||||||
|
[SshCertKeyAlgorithm.RSA_4096]: "RSA 4096",
|
||||||
|
[SshCertKeyAlgorithm.ECDSA_P256]: "ECDSA P256",
|
||||||
|
[SshCertKeyAlgorithm.ECDSA_P384]: "ECDSA P384",
|
||||||
|
[SshCertKeyAlgorithm.ED25519]: "ED25519"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sshCertKeyAlgorithms = [
|
||||||
|
{
|
||||||
|
label: sshCertKeyAlgorithmToNameMap[SshCertKeyAlgorithm.RSA_2048],
|
||||||
|
value: SshCertKeyAlgorithm.RSA_2048
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: sshCertKeyAlgorithmToNameMap[SshCertKeyAlgorithm.RSA_4096],
|
||||||
|
value: SshCertKeyAlgorithm.RSA_4096
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: sshCertKeyAlgorithmToNameMap[SshCertKeyAlgorithm.ECDSA_P256],
|
||||||
|
value: SshCertKeyAlgorithm.ECDSA_P256
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: sshCertKeyAlgorithmToNameMap[SshCertKeyAlgorithm.ECDSA_P384],
|
||||||
|
value: SshCertKeyAlgorithm.ECDSA_P384
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: sshCertKeyAlgorithmToNameMap[SshCertKeyAlgorithm.ED25519],
|
||||||
|
value: SshCertKeyAlgorithm.ED25519
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { CertKeyAlgorithm } from "../certificates/enums";
|
import { SshCaKeySource, SshCaStatus, SshCertKeyAlgorithm, SshCertType } from "./constants";
|
||||||
import { SshCaStatus, SshCertType } from "./constants";
|
|
||||||
|
|
||||||
export type TSshCertificate = {
|
export type TSshCertificate = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -18,16 +17,27 @@ export type TSshCertificateAuthority = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
status: SshCaStatus;
|
status: SshCaStatus;
|
||||||
friendlyName: string;
|
friendlyName: string;
|
||||||
keyAlgorithm: CertKeyAlgorithm;
|
keyAlgorithm: SshCertKeyAlgorithm;
|
||||||
|
keySource: SshCaKeySource;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateSshCaDTO = {
|
export type TCreateSshCaDTO =
|
||||||
|
| {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
friendlyName?: string;
|
friendlyName?: string;
|
||||||
keyAlgorithm: CertKeyAlgorithm;
|
keySource: SshCaKeySource.INTERNAL;
|
||||||
|
keyAlgorithm: SshCertKeyAlgorithm;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
projectId: string;
|
||||||
|
friendlyName?: string;
|
||||||
|
keySource: SshCaKeySource.EXTERNAL;
|
||||||
|
keyAlgorithm: SshCertKeyAlgorithm;
|
||||||
|
publicKey: string;
|
||||||
|
privateKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateSshCaDTO = {
|
export type TUpdateSshCaDTO = {
|
||||||
@@ -58,7 +68,7 @@ export type TSignSshKeyResponse = {
|
|||||||
export type TIssueSshCredsDTO = {
|
export type TIssueSshCredsDTO = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
keyAlgorithm: CertKeyAlgorithm;
|
keyAlgorithm: SshCertKeyAlgorithm;
|
||||||
certType: SshCertType;
|
certType: SshCertType;
|
||||||
principals: string[];
|
principals: string[];
|
||||||
ttl?: string;
|
ttl?: string;
|
||||||
@@ -70,5 +80,5 @@ export type TIssueSshCredsResponse = {
|
|||||||
signedKey: string;
|
signedKey: string;
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
keyAlgorithm: CertKeyAlgorithm;
|
keyAlgorithm: SshCertKeyAlgorithm;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { useCreateSshHost, useDeleteSshHost, useUpdateSshHost } from "./mutations";
|
||||||
|
export { fetchSshHostUserCaPublicKey, useGetSshHostById } from "./queries";
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { workspaceKeys } from "../workspace/query-keys";
|
||||||
|
import { TCreateSshHostDTO, TDeleteSshHostDTO, TSshHost, TUpdateSshHostDTO } from "./types";
|
||||||
|
|
||||||
|
export const useCreateSshHost = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TSshHost, object, TCreateSshHostDTO>({
|
||||||
|
mutationFn: async (body) => {
|
||||||
|
const { data: host } = await apiRequest.post("/api/v1/ssh/hosts", body);
|
||||||
|
return host;
|
||||||
|
},
|
||||||
|
onSuccess: ({ projectId }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshHosts(projectId) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateSshHost = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TSshHost, object, TUpdateSshHostDTO>({
|
||||||
|
mutationFn: async ({ sshHostId, ...body }) => {
|
||||||
|
const { data: host } = await apiRequest.patch(`/api/v1/ssh/hosts/${sshHostId}`, body);
|
||||||
|
return host;
|
||||||
|
},
|
||||||
|
onSuccess: ({ projectId }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshHosts(projectId) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteSshHost = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TSshHost, object, TDeleteSshHostDTO>({
|
||||||
|
mutationFn: async ({ sshHostId }) => {
|
||||||
|
const { data: host } = await apiRequest.delete(`/api/v1/ssh/hosts/${sshHostId}`);
|
||||||
|
return host;
|
||||||
|
},
|
||||||
|
onSuccess: ({ projectId }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshHosts(projectId) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { TSshHost } from "./types";
|
||||||
|
|
||||||
|
export const sshHostKeys = {
|
||||||
|
getSshHostById: (sshHostId: string) => [{ sshHostId }, "ssh-host"],
|
||||||
|
getSshHostUserCaPublicKey: (sshHostId: string) => [{ sshHostId }, "ssh-host-user-ca-public-key"]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetSshHostById = (sshHostId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: sshHostKeys.getSshHostById(sshHostId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data: sshHost } = await apiRequest.get<TSshHost>(`/api/v1/ssh/hosts/${sshHostId}`);
|
||||||
|
return sshHost;
|
||||||
|
},
|
||||||
|
enabled: Boolean(sshHostId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchSshHostUserCaPublicKey = async (sshHostId: string): Promise<string> => {
|
||||||
|
const { data } = await apiRequest.get<string>(
|
||||||
|
`/api/v1/ssh/hosts/${sshHostId}/user-ca-public-key`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
export type TSshHost = {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
hostname: string;
|
||||||
|
userCertTtl: string;
|
||||||
|
hostCertTtl: string;
|
||||||
|
loginMappings: {
|
||||||
|
loginUser: string;
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: string[];
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateSshHostDTO = {
|
||||||
|
projectId: string;
|
||||||
|
hostname: string;
|
||||||
|
userCertTtl?: string;
|
||||||
|
hostCertTtl?: string;
|
||||||
|
loginMappings: {
|
||||||
|
loginUser: string;
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: string[];
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateSshHostDTO = {
|
||||||
|
sshHostId: string;
|
||||||
|
hostname?: string;
|
||||||
|
userCertTtl?: string;
|
||||||
|
hostCertTtl?: string;
|
||||||
|
loginMappings?: {
|
||||||
|
loginUser: string;
|
||||||
|
allowedPrincipals: {
|
||||||
|
usernames: string[];
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteSshHostDTO = {
|
||||||
|
sshHostId: string;
|
||||||
|
};
|
||||||
@@ -36,6 +36,7 @@ export {
|
|||||||
useListWorkspaceSshCas,
|
useListWorkspaceSshCas,
|
||||||
useListWorkspaceSshCertificates,
|
useListWorkspaceSshCertificates,
|
||||||
useListWorkspaceSshCertificateTemplates,
|
useListWorkspaceSshCertificateTemplates,
|
||||||
|
useListWorkspaceSshHosts,
|
||||||
useNameWorkspaceSecrets,
|
useNameWorkspaceSecrets,
|
||||||
useSearchProjects,
|
useSearchProjects,
|
||||||
useToggleAutoCapitalization,
|
useToggleAutoCapitalization,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { TPkiCollection } from "../pkiCollections/types";
|
|||||||
import { EncryptedSecret } from "../secrets/types";
|
import { EncryptedSecret } from "../secrets/types";
|
||||||
import { TSshCertificate, TSshCertificateAuthority } from "../sshCa/types";
|
import { TSshCertificate, TSshCertificateAuthority } from "../sshCa/types";
|
||||||
import { TSshCertificateTemplate } from "../sshCertificateTemplates/types";
|
import { TSshCertificateTemplate } from "../sshCertificateTemplates/types";
|
||||||
|
import { TSshHost } from "../sshHost/types";
|
||||||
import { userKeys } from "../users/query-keys";
|
import { userKeys } from "../users/query-keys";
|
||||||
import { TWorkspaceUser } from "../users/types";
|
import { TWorkspaceUser } from "../users/types";
|
||||||
import { ProjectSlackConfig } from "../workflowIntegrations/types";
|
import { ProjectSlackConfig } from "../workflowIntegrations/types";
|
||||||
@@ -827,6 +828,19 @@ export const useListWorkspaceSshCas = (projectId: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useListWorkspaceSshHosts = (projectId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: workspaceKeys.getWorkspaceSshHosts(projectId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const {
|
||||||
|
data: { hosts }
|
||||||
|
} = await apiRequest.get<{ hosts: TSshHost[] }>(`/api/v2/workspace/${projectId}/ssh-hosts`);
|
||||||
|
return hosts;
|
||||||
|
},
|
||||||
|
enabled: Boolean(projectId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useListWorkspaceSshCertificateTemplates = (projectId: string) => {
|
export const useListWorkspaceSshCertificateTemplates = (projectId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: workspaceKeys.getWorkspaceSshCertificateTemplates(projectId),
|
queryKey: workspaceKeys.getWorkspaceSshCertificateTemplates(projectId),
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export const workspaceKeys = {
|
|||||||
getWorkspaceSshCas: (projectId: string) => [{ projectId }, "workspace-ssh-cas"] as const,
|
getWorkspaceSshCas: (projectId: string) => [{ projectId }, "workspace-ssh-cas"] as const,
|
||||||
allWorkspaceSshCertificates: (projectId: string) =>
|
allWorkspaceSshCertificates: (projectId: string) =>
|
||||||
[{ projectId }, "workspace-ssh-certificates"] as const,
|
[{ projectId }, "workspace-ssh-certificates"] as const,
|
||||||
|
getWorkspaceSshHosts: (projectId: string) => [{ projectId }, "workspace-ssh-hosts"] as const,
|
||||||
specificWorkspaceSshCertificates: ({
|
specificWorkspaceSshCertificates: ({
|
||||||
offset,
|
offset,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ export const ProjectLayout = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{isSSH && (
|
{isSSH && (
|
||||||
|
<>
|
||||||
<Link
|
<Link
|
||||||
to={`/${ProjectType.SSH}/$projectId/overview` as const}
|
to={`/${ProjectType.SSH}/$projectId/overview` as const}
|
||||||
params={{
|
params={{
|
||||||
@@ -141,11 +142,40 @@ export const ProjectLayout = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<MenuItem isSelected={isActive} icon="lock-closed">
|
<MenuItem isSelected={isActive} icon="server">
|
||||||
Overview
|
Hosts
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
|
{/* <Link
|
||||||
|
to={`/${ProjectType.SSH}/$projectId/certificates` as const}
|
||||||
|
params={{
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<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> */}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{isSecretManager && (
|
{isSecretManager && (
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const formatDescription = (type: ProjectType) => {
|
|||||||
return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices.";
|
return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices.";
|
||||||
if (type === ProjectType.KMS)
|
if (type === ProjectType.KMS)
|
||||||
return "Centralize the management of keys for cryptographic operations, such as encryption and decryption.";
|
return "Centralize the management of keys for cryptographic operations, such as encryption and decryption.";
|
||||||
return "Generate SSH credentials to provide secure and centralized SSH access control for your infrastructure.";
|
return "Infisical SSH lets you issue SSH credentials to users for short-lived, secure SSH access to infrastructure.";
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ const secretPermissionSchema = z.object({
|
|||||||
z.object({
|
z.object({
|
||||||
isTemporary: z.literal(false)
|
isTemporary: z.literal(false)
|
||||||
})
|
})
|
||||||
])
|
]),
|
||||||
|
note: z.string().optional()
|
||||||
});
|
});
|
||||||
type TSecretPermissionForm = z.infer<typeof secretPermissionSchema>;
|
type TSecretPermissionForm = z.infer<typeof secretPermissionSchema>;
|
||||||
export const SpecificPrivilegeSecretForm = ({
|
export const SpecificPrivilegeSecretForm = ({
|
||||||
@@ -231,7 +232,8 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
action,
|
action,
|
||||||
subject: [ProjectPermissionSub.Secrets],
|
subject: [ProjectPermissionSub.Secrets],
|
||||||
conditions
|
conditions
|
||||||
}))
|
})),
|
||||||
|
note: data.note
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -541,6 +543,18 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mb-4 flex w-full">
|
||||||
|
<Controller
|
||||||
|
control={privilegeForm.control}
|
||||||
|
name="note"
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="w-full">
|
||||||
|
<FormLabel label="Note" className="mb-2" />
|
||||||
|
<Input {...field} isDisabled={isMemberEditDisabled} maxLength={255} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{!!policies && (
|
{!!policies && (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSecretRotationActions,
|
ProjectPermissionSecretRotationActions,
|
||||||
ProjectPermissionSecretSyncActions,
|
ProjectPermissionSecretSyncActions,
|
||||||
|
ProjectPermissionSshHostActions,
|
||||||
TPermissionCondition,
|
TPermissionCondition,
|
||||||
TPermissionConditionOperators
|
TPermissionConditionOperators
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
@@ -108,6 +109,14 @@ const GroupPolicyActionSchema = z.object({
|
|||||||
[ProjectPermissionGroupActions.GrantPrivileges]: z.boolean().optional()
|
[ProjectPermissionGroupActions.GrantPrivileges]: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const SshHostPolicyActionSchema = z.object({
|
||||||
|
[ProjectPermissionSshHostActions.Read]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionSshHostActions.Create]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionSshHostActions.Edit]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionSshHostActions.Delete]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionSshHostActions.IssueHostCert]: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
const SecretRollbackPolicyActionSchema = z.object({
|
const SecretRollbackPolicyActionSchema = z.object({
|
||||||
read: z.boolean().optional(),
|
read: z.boolean().optional(),
|
||||||
create: z.boolean().optional()
|
create: z.boolean().optional()
|
||||||
@@ -215,6 +224,12 @@ export const projectRoleFormSchema = z.object({
|
|||||||
),
|
),
|
||||||
[ProjectPermissionSub.SshCertificates]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.SshCertificates]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.SshCertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.SshCertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
|
||||||
|
[ProjectPermissionSub.SshHosts]: SshHostPolicyActionSchema.extend({
|
||||||
|
inverted: z.boolean().optional(),
|
||||||
|
conditions: ConditionSchema
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.default([]),
|
||||||
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]),
|
||||||
@@ -241,6 +256,7 @@ type TConditionalFields =
|
|||||||
| ProjectPermissionSub.SecretFolders
|
| ProjectPermissionSub.SecretFolders
|
||||||
| ProjectPermissionSub.SecretImports
|
| ProjectPermissionSub.SecretImports
|
||||||
| ProjectPermissionSub.DynamicSecrets
|
| ProjectPermissionSub.DynamicSecrets
|
||||||
|
| ProjectPermissionSub.SshHosts
|
||||||
| ProjectPermissionSub.SecretRotation
|
| ProjectPermissionSub.SecretRotation
|
||||||
| ProjectPermissionSub.Identity;
|
| ProjectPermissionSub.Identity;
|
||||||
|
|
||||||
@@ -251,8 +267,9 @@ export const isConditionalSubjects = (
|
|||||||
subject === ProjectPermissionSub.DynamicSecrets ||
|
subject === ProjectPermissionSub.DynamicSecrets ||
|
||||||
subject === ProjectPermissionSub.SecretImports ||
|
subject === ProjectPermissionSub.SecretImports ||
|
||||||
subject === ProjectPermissionSub.SecretFolders ||
|
subject === ProjectPermissionSub.SecretFolders ||
|
||||||
subject === ProjectPermissionSub.SecretRotation ||
|
subject === ProjectPermissionSub.Identity ||
|
||||||
subject === ProjectPermissionSub.Identity;
|
subject === ProjectPermissionSub.SshHosts ||
|
||||||
|
subject === ProjectPermissionSub.SecretRotation;
|
||||||
|
|
||||||
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
|
||||||
const formConditions: z.infer<typeof ConditionSchema> = [];
|
const formConditions: z.infer<typeof ConditionSchema> = [];
|
||||||
@@ -308,7 +325,10 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
ProjectPermissionSub.SecretApproval,
|
ProjectPermissionSub.SecretApproval,
|
||||||
ProjectPermissionSub.Tags,
|
ProjectPermissionSub.Tags,
|
||||||
ProjectPermissionSub.SecretRotation,
|
ProjectPermissionSub.SecretRotation,
|
||||||
ProjectPermissionSub.Kms
|
ProjectPermissionSub.Kms,
|
||||||
|
ProjectPermissionSub.SshCertificateTemplates,
|
||||||
|
ProjectPermissionSub.SshCertificateAuthorities,
|
||||||
|
ProjectPermissionSub.SshCertificates
|
||||||
].includes(subject)
|
].includes(subject)
|
||||||
) {
|
) {
|
||||||
// from above statement we are sure it won't be undefined
|
// from above statement we are sure it won't be undefined
|
||||||
@@ -577,7 +597,32 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
if (canRemoveSecrets)
|
if (canRemoveSecrets)
|
||||||
formVal[subject]![0][ProjectPermissionSecretSyncActions.RemoveSecrets] = true;
|
formVal[subject]![0][ProjectPermissionSecretSyncActions.RemoveSecrets] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subject === ProjectPermissionSub.SshHosts) {
|
||||||
|
if (!formVal[subject]) formVal[subject] = [];
|
||||||
|
|
||||||
|
formVal[subject]!.push({
|
||||||
|
[ProjectPermissionSshHostActions.Edit]: action.includes(
|
||||||
|
ProjectPermissionSshHostActions.Edit
|
||||||
|
),
|
||||||
|
[ProjectPermissionSshHostActions.Delete]: action.includes(
|
||||||
|
ProjectPermissionSshHostActions.Delete
|
||||||
|
),
|
||||||
|
[ProjectPermissionSshHostActions.Create]: action.includes(
|
||||||
|
ProjectPermissionSshHostActions.Create
|
||||||
|
),
|
||||||
|
[ProjectPermissionSshHostActions.Read]: action.includes(
|
||||||
|
ProjectPermissionSshHostActions.Read
|
||||||
|
),
|
||||||
|
[ProjectPermissionSshHostActions.IssueHostCert]: action.includes(
|
||||||
|
ProjectPermissionSshHostActions.IssueHostCert
|
||||||
|
),
|
||||||
|
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
|
||||||
|
inverted
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return formVal;
|
return formVal;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -901,6 +946,16 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
|||||||
{ label: "Remove", value: "delete" }
|
{ label: "Remove", value: "delete" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
[ProjectPermissionSub.SshHosts]: {
|
||||||
|
title: "SSH Hosts",
|
||||||
|
actions: [
|
||||||
|
{ label: "Read", value: ProjectPermissionSshHostActions.Read },
|
||||||
|
{ label: "Create", value: ProjectPermissionSshHostActions.Create },
|
||||||
|
{ label: "Modify", value: ProjectPermissionSshHostActions.Edit },
|
||||||
|
{ label: "Remove", value: ProjectPermissionSshHostActions.Delete },
|
||||||
|
{ label: "Issue Host Certificate", value: ProjectPermissionSshHostActions.IssueHostCert }
|
||||||
|
]
|
||||||
|
},
|
||||||
[ProjectPermissionSub.PkiCollections]: {
|
[ProjectPermissionSub.PkiCollections]: {
|
||||||
title: "PKI Collections",
|
title: "PKI Collections",
|
||||||
actions: [
|
actions: [
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
|||||||
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
|
||||||
import { evaluatePermissionsAbility } from "@app/helpers/permissions";
|
import { evaluatePermissionsAbility } from "@app/helpers/permissions";
|
||||||
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
|
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
|
||||||
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
|
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
|
||||||
@@ -33,6 +34,7 @@ import {
|
|||||||
TFormSchema
|
TFormSchema
|
||||||
} from "./ProjectRoleModifySection.utils";
|
} from "./ProjectRoleModifySection.utils";
|
||||||
import { SecretPermissionConditions } from "./SecretPermissionConditions";
|
import { SecretPermissionConditions } from "./SecretPermissionConditions";
|
||||||
|
import { SshHostPermissionConditions } from "./SshHostPermissionConditions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
roleSlug: string;
|
roleSlug: string;
|
||||||
@@ -51,6 +53,10 @@ export const renderConditionalComponents = (
|
|||||||
return <IdentityManagementPermissionConditions isDisabled={isDisabled} />;
|
return <IdentityManagementPermissionConditions isDisabled={isDisabled} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subject === ProjectPermissionSub.SshHosts) {
|
||||||
|
return <SshHostPermissionConditions isDisabled={isDisabled} />;
|
||||||
|
}
|
||||||
|
|
||||||
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
|
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +140,9 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
{currentWorkspace.type === ProjectType.SecretManager && (
|
||||||
<AccessTree permissions={formattedPermissions} />
|
<AccessTree permissions={formattedPermissions} />
|
||||||
|
)}
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||||
|
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
PermissionConditionOperators,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
|
||||||
|
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
|
||||||
|
import { TFormSchema } from "./ProjectRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
position?: number;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SshHostPermissionConditions = ({ position = 0, isDisabled }: Props) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { errors }
|
||||||
|
} = useFormContext<TFormSchema>();
|
||||||
|
|
||||||
|
const permissionSubject = ProjectPermissionSub.SshHosts;
|
||||||
|
const items = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: `permissions.${permissionSubject}.${position}.conditions`
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
|
||||||
|
<p className="mt-2 text-gray-300">Conditions</p>
|
||||||
|
<p className="text-sm text-mineshaft-400">
|
||||||
|
Conditions determine when a policy will be applied (always if no conditions are present).
|
||||||
|
</p>
|
||||||
|
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
|
||||||
|
All conditions must evaluate to true for the policy to take effect.
|
||||||
|
</p>
|
||||||
|
<div className="mt-2 flex flex-col space-y-2">
|
||||||
|
{items.fields.map((el, index) => {
|
||||||
|
const condition =
|
||||||
|
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
|
||||||
|
lhs: string;
|
||||||
|
rhs: string;
|
||||||
|
operator: string;
|
||||||
|
}) || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={el.id}
|
||||||
|
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
|
||||||
|
>
|
||||||
|
<div className="w-1/4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value="hostname">Hostname</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-36 items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => field.onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$EQ}>Equals</SelectItem>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob</SelectItem>
|
||||||
|
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
asChild
|
||||||
|
content={getConditionOperatorHelperInfo(
|
||||||
|
condition?.operator as PermissionConditionOperators
|
||||||
|
)}
|
||||||
|
className="max-w-xs"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="plus"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="p-2.5"
|
||||||
|
onClick={() => items.remove(index)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
|
||||||
|
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
|
||||||
|
<FontAwesomeIcon icon={faWarning} className="text-red" />
|
||||||
|
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
variant="star"
|
||||||
|
size="xs"
|
||||||
|
className="mt-3"
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onClick={() =>
|
||||||
|
items.append({
|
||||||
|
lhs: "hostname",
|
||||||
|
operator: PermissionConditionOperators.$EQ,
|
||||||
|
rhs: ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add Condition
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -64,7 +64,7 @@ export const SecretApprovalsPage = () => {
|
|||||||
<Tabs defaultValue={defaultTab}>
|
<Tabs defaultValue={defaultTab}>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab value={TabSection.SecretApprovalRequests}>
|
<Tab value={TabSection.SecretApprovalRequests}>
|
||||||
Secret Requests
|
Change Requests
|
||||||
{Boolean(secretApprovalReqCount?.open) && (
|
{Boolean(secretApprovalReqCount?.open) && (
|
||||||
<Badge className="ml-2">{secretApprovalReqCount?.open}</Badge>
|
<Badge className="ml-2">{secretApprovalReqCount?.open}</Badge>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -136,6 +136,13 @@ export const ReviewAccessRequestModal = ({
|
|||||||
<span className="font-bold">Access Type: </span>
|
<span className="font-bold">Access Type: </span>
|
||||||
<span>{getAccessLabel()}</span>
|
<span>{getAccessLabel()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{request.note && (
|
||||||
|
<div className="mt-1">
|
||||||
|
<span className="font-bold">User Note: </span>
|
||||||
|
<span>{request.note}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
|
|||||||