From 84c534ef703ba70536c219b9cbdeb9cabe37983d Mon Sep 17 00:00:00 2001 From: = Date: Fri, 14 Feb 2025 13:11:50 +0530 Subject: [PATCH] feat: added api for admin and org gateway management --- backend/src/@types/fastify.d.ts | 2 + backend/src/@types/knex.d.ts | 20 ++ .../20250212191958_create-gateway.ts | 84 ++++++ .../src/db/schemas/gateway-instance-config.ts | 33 ++ backend/src/db/schemas/gateways.ts | 28 ++ backend/src/db/schemas/index.ts | 3 + backend/src/db/schemas/models.ts | 4 + backend/src/db/schemas/org-gateway-root-ca.ts | 27 ++ backend/src/ee/routes/v1/gateway-router.ts | 158 ++++++++++ backend/src/ee/routes/v1/index.ts | 3 + .../src/ee/services/gateway/gateway-dal.ts | 33 ++ .../gateway/gateway-instance-config-dal.ts | 11 + .../ee/services/gateway/gateway-service.ts | 284 ++++++++++++++++++ .../src/ee/services/gateway/gateway-types.ts | 21 ++ .../gateway/org-gateway-root-ca-dal.ts | 10 + .../src/ee/services/license/license-fns.ts | 3 +- .../src/ee/services/license/license-types.ts | 1 + .../ee/services/permission/org-permission.ts | 12 +- backend/src/keystore/keystore.ts | 12 +- backend/src/lib/config/env.ts | 7 + backend/src/lib/crypto/hashtext.ts | 29 ++ backend/src/lib/turn/credentials.ts | 16 + backend/src/server/routes/index.ts | 20 +- backend/src/server/routes/v1/admin-router.ts | 85 +++++- .../super-admin/super-admin-service.ts | 158 +++++++++- 25 files changed, 1053 insertions(+), 11 deletions(-) create mode 100644 backend/src/db/migrations/20250212191958_create-gateway.ts create mode 100644 backend/src/db/schemas/gateway-instance-config.ts create mode 100644 backend/src/db/schemas/gateways.ts create mode 100644 backend/src/db/schemas/org-gateway-root-ca.ts create mode 100644 backend/src/ee/routes/v1/gateway-router.ts create mode 100644 backend/src/ee/services/gateway/gateway-dal.ts create mode 100644 backend/src/ee/services/gateway/gateway-instance-config-dal.ts create mode 100644 backend/src/ee/services/gateway/gateway-service.ts create mode 100644 backend/src/ee/services/gateway/gateway-types.ts create mode 100644 backend/src/ee/services/gateway/org-gateway-root-ca-dal.ts create mode 100644 backend/src/lib/crypto/hashtext.ts create mode 100644 backend/src/lib/turn/credentials.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 1cb11545c..6cc850847 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -95,6 +95,7 @@ import { TUserServiceFactory } from "@app/services/user/user-service"; import { TUserEngagementServiceFactory } from "@app/services/user-engagement/user-engagement-service"; import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service"; import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; declare module "@fastify/request-context" { interface RequestContextData { @@ -228,6 +229,7 @@ declare module "fastify" { secretSync: TSecretSyncServiceFactory; kmip: TKmipServiceFactory; kmipOperation: TKmipOperationServiceFactory; + gateway: TGatewayServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 27f6b894c..b09b02046 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -68,6 +68,12 @@ import { TExternalKms, TExternalKmsInsert, TExternalKmsUpdate, + TGatewayInstanceConfig, + TGatewayInstanceConfigInsert, + TGatewayInstanceConfigUpdate, + TGateways, + TGatewaysInsert, + TGatewaysUpdate, TGitAppInstallSessions, TGitAppInstallSessionsInsert, TGitAppInstallSessionsUpdate, @@ -179,6 +185,9 @@ import { TOrgBots, TOrgBotsInsert, TOrgBotsUpdate, + TOrgGatewayRootCa, + TOrgGatewayRootCaInsert, + TOrgGatewayRootCaUpdate, TOrgMemberships, TOrgMembershipsInsert, TOrgMembershipsUpdate, @@ -930,5 +939,16 @@ declare module "knex/types/tables" { TKmipClientCertificatesInsert, TKmipClientCertificatesUpdate >; + [TableName.Gateway]: KnexOriginal.CompositeTableType; + [TableName.GatewayInstanceConfig]: KnexOriginal.CompositeTableType< + TGatewayInstanceConfig, + TGatewayInstanceConfigInsert, + TGatewayInstanceConfigUpdate + >; + [TableName.OrgGatewayRootCa]: KnexOriginal.CompositeTableType< + TOrgGatewayRootCa, + TOrgGatewayRootCaInsert, + TOrgGatewayRootCaUpdate + >; } } diff --git a/backend/src/db/migrations/20250212191958_create-gateway.ts b/backend/src/db/migrations/20250212191958_create-gateway.ts new file mode 100644 index 000000000..635cd7427 --- /dev/null +++ b/backend/src/db/migrations/20250212191958_create-gateway.ts @@ -0,0 +1,84 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.GatewayInstanceConfig))) { + await knex.schema.createTable(TableName.GatewayInstanceConfig, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("caKeyAlgorithm").notNullable(); + t.boolean("isDisabled").defaultTo(false); + + t.string("infisicalClientCaSerialNumber").notNullable(); + t.datetime("infisicalClientCaIssuedAt").notNullable(); + t.datetime("infisicalClientCaExpiration").notNullable(); + t.binary("encryptedInfisicalClientCaCertificate").notNullable(); + t.binary("encryptedInfisicalClientCaPrivateKey").notNullable(); + + t.string("infisicalClientCertSerialNumber").notNullable(); + t.string("infisicalClientCertKeyAlgorithm").notNullable(); + t.datetime("infisicalClientCertIssuedAt").notNullable(); + t.datetime("infisicalClientCertExpiration").notNullable(); + t.binary("encryptedInfisicalClientCertificate").notNullable(); + t.binary("encryptedInfisicalClientPrivateKey").notNullable(); + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.GatewayInstanceConfig); + } + + if (!(await knex.schema.hasTable(TableName.OrgGatewayRootCa))) { + await knex.schema.createTable(TableName.OrgGatewayRootCa, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("caKeyAlgorithm").notNullable(); + t.string("caSerialNumber").notNullable(); + t.datetime("caIssuedAt").notNullable(); + t.datetime("caExpiration").notNullable(); + t.binary("encryptedCaCertificate").notNullable(); + t.binary("encryptedCaPrivateKey").notNullable(); + + t.uuid("orgId").notNullable(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.OrgGatewayRootCa); + } + + if (!(await knex.schema.hasTable(TableName.Gateway))) { + await knex.schema.createTable(TableName.Gateway, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.string("name").notNullable(); + t.string("serialNumber").notNullable(); + t.string("keyAlgorithm").notNullable(); + t.datetime("issuedAt").notNullable(); + t.datetime("expiration").notNullable(); + + t.binary("relayAddress").notNullable(); + + t.uuid("orgGatewayRootCaId").notNullable(); + t.foreign("orgGatewayRootCaId").references("id").inTable(TableName.OrgGatewayRootCa).onDelete("CASCADE"); + + t.uuid("identityId").notNullable(); + t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + + t.timestamps(true, true, true); + }); + + await createOnUpdateTrigger(knex, TableName.Gateway); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.GatewayInstanceConfig); + await dropOnUpdateTrigger(knex, TableName.GatewayInstanceConfig); + + await knex.schema.dropTableIfExists(TableName.Gateway); + await dropOnUpdateTrigger(knex, TableName.Gateway); + + await knex.schema.dropTableIfExists(TableName.OrgGatewayRootCa); + await dropOnUpdateTrigger(knex, TableName.OrgGatewayRootCa); +} diff --git a/backend/src/db/schemas/gateway-instance-config.ts b/backend/src/db/schemas/gateway-instance-config.ts new file mode 100644 index 000000000..f59b4b781 --- /dev/null +++ b/backend/src/db/schemas/gateway-instance-config.ts @@ -0,0 +1,33 @@ +// 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 { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const GatewayInstanceConfigSchema = z.object({ + id: z.string().uuid(), + caKeyAlgorithm: z.string(), + isDisabled: z.boolean().default(false).nullable().optional(), + infisicalClientCaSerialNumber: z.string(), + infisicalClientCaIssuedAt: z.date(), + infisicalClientCaExpiration: z.date(), + encryptedInfisicalClientCaCertificate: zodBuffer, + encryptedInfisicalClientCaPrivateKey: zodBuffer, + infisicalClientCertSerialNumber: z.string(), + infisicalClientCertKeyAlgorithm: z.string(), + infisicalClientCertIssuedAt: z.date(), + infisicalClientCertExpiration: z.date(), + encryptedInfisicalClientCertificate: zodBuffer, + encryptedInfisicalClientPrivateKey: zodBuffer, + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TGatewayInstanceConfig = z.infer; +export type TGatewayInstanceConfigInsert = Omit, TImmutableDBKeys>; +export type TGatewayInstanceConfigUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/gateways.ts b/backend/src/db/schemas/gateways.ts new file mode 100644 index 000000000..7dc29fdb5 --- /dev/null +++ b/backend/src/db/schemas/gateways.ts @@ -0,0 +1,28 @@ +// 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 { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const GatewaysSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + serialNumber: z.string(), + keyAlgorithm: z.string(), + issuedAt: z.date(), + expiration: z.date(), + relayAddress: zodBuffer, + orgGatewayRootCaId: z.string().uuid(), + identityId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TGateways = z.infer; +export type TGatewaysInsert = Omit, TImmutableDBKeys>; +export type TGatewaysUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 737193836..9e03cb246 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -20,6 +20,8 @@ export * from "./certificates"; export * from "./dynamic-secret-leases"; export * from "./dynamic-secrets"; export * from "./external-kms"; +export * from "./gateway-instance-config"; +export * from "./gateways"; export * from "./git-app-install-sessions"; export * from "./git-app-org"; export * from "./group-project-membership-roles"; @@ -57,6 +59,7 @@ export * from "./ldap-group-maps"; export * from "./models"; export * from "./oidc-configs"; export * from "./org-bots"; +export * from "./org-gateway-root-ca"; export * from "./org-memberships"; export * from "./org-roles"; export * from "./organizations"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 3c5d1cad8..069a9ea51 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -113,6 +113,10 @@ export enum TableName { SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2", SnapshotSecretV2 = "secret_snapshot_secrets_v2", ProjectSplitBackfillIds = "project_split_backfill_ids", + // Gateway + GatewayInstanceConfig = "gateway_instance_config", + OrgGatewayRootCa = "org_gateway_root_ca", + Gateway = "gateways", // junction tables with tags SecretV2JnTag = "secret_v2_tag_junction", JnSecretTag = "secret_tag_junction", diff --git a/backend/src/db/schemas/org-gateway-root-ca.ts b/backend/src/db/schemas/org-gateway-root-ca.ts new file mode 100644 index 000000000..96c36fb37 --- /dev/null +++ b/backend/src/db/schemas/org-gateway-root-ca.ts @@ -0,0 +1,27 @@ +// 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 { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const OrgGatewayRootCaSchema = z.object({ + id: z.string().uuid(), + caKeyAlgorithm: z.string(), + caSerialNumber: z.string(), + caIssuedAt: z.date(), + caExpiration: z.date(), + encryptedCaCertificate: zodBuffer, + encryptedCaPrivateKey: zodBuffer, + orgId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TOrgGatewayRootCa = z.infer; +export type TOrgGatewayRootCaInsert = Omit, TImmutableDBKeys>; +export type TOrgGatewayRootCaUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/ee/routes/v1/gateway-router.ts b/backend/src/ee/routes/v1/gateway-router.ts new file mode 100644 index 000000000..27dbd850b --- /dev/null +++ b/backend/src/ee/routes/v1/gateway-router.ts @@ -0,0 +1,158 @@ +import { z } from "zod"; + +import { GatewaysSchema } from "@app/db/schemas"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +const SanitizedGatewaySchema = GatewaysSchema.pick({ + id: true, + identityId: true, + name: true, + createdAt: true, + updatedAt: true, + issuedAt: true, + serialNumber: true +}); + +export const registerGatewayRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/register-identity", + config: { + rateLimit: writeLimit + }, + schema: { + response: { + 200: z.object({ + turnServerUsername: z.string(), + turnServerPassword: z.string(), + turnServerAddress: z.string(), + infisicalStaticIp: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const relayDetails = await server.services.gateway.getGatewayRelayDetails( + req.permission.id, + req.permission.orgId + ); + return relayDetails; + } + }); + + server.route({ + method: "POST", + url: "/exchange-cert", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + relayAddress: z.string() + }), + response: { + 200: z.object({ + serialNumber: z.string(), + privateKey: z.string(), + certificate: z.string(), + certificateChain: z.string() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const gatewayCertificates = await server.services.gateway.exchangeAllocatedRelayAddress({ + identityOrg: req.permission.orgId, + identityId: req.permission.id, + relayAddress: req.body.relayAddress + }); + return gatewayCertificates; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + gateways: SanitizedGatewaySchema.extend({ + identity: z.object({ + name: z.string(), + id: z.string() + }) + }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), + handler: async (req) => { + const gateways = await server.services.gateway.listGateways({ + orgPermission: req.permission + }); + return { gateways }; + } + }); + + server.route({ + method: "GET", + url: "/:id", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + gateway: SanitizedGatewaySchema.extend({ + identity: z.object({ + name: z.string(), + id: z.string() + }) + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), + handler: async (req) => { + const gateway = await server.services.gateway.getGatewayById({ + orgPermission: req.permission, + id: req.params.id + }); + return { gateway }; + } + }); + + server.route({ + method: "DELETE", + url: "/:id", + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + gateway: SanitizedGatewaySchema + }) + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), + handler: async (req) => { + const gateway = await server.services.gateway.deleteGatewayById({ + orgPermission: req.permission, + id: req.params.id + }); + return { gateway }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 0567c76ef..e793c687d 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -7,6 +7,7 @@ import { registerCaCrlRouter } from "./certificate-authority-crl-router"; import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerExternalKmsRouter } from "./external-kms-router"; +import { registerGatewayRouter } from "./gateway-router"; import { registerGroupRouter } from "./group-router"; import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; import { registerKmipRouter } from "./kmip-router"; @@ -67,6 +68,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { { prefix: "/dynamic-secrets" } ); + await server.register(registerGatewayRouter, { prefix: "/gateways" }); + await server.register( async (pkiRouter) => { await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" }); diff --git a/backend/src/ee/services/gateway/gateway-dal.ts b/backend/src/ee/services/gateway/gateway-dal.ts new file mode 100644 index 000000000..86a40a3fe --- /dev/null +++ b/backend/src/ee/services/gateway/gateway-dal.ts @@ -0,0 +1,33 @@ +import { TDbClient } from "@app/db"; +import { TableName, TGateways } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex"; + +export type TGatewayDALFactory = ReturnType; + +export const gatewayDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Gateway); + + const find = async (filter: TFindFilter, { offset, limit, sort, tx }: TFindOpt = {}) => { + try { + const query = (tx || db)(TableName.Gateway) + // eslint-disable-next-line @typescript-eslint/no-misused-promises + .where(buildFindFilter(filter)) + .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) + .select(selectAllTableCols(TableName.Gateway)) + .select(db.ref("name").withSchema(TableName.Identity).as("identityName")); + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls }))); + } + + const docs = await query; + return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } })); + } catch (error) { + throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` }); + } + }; + + return { ...orm, find }; +}; diff --git a/backend/src/ee/services/gateway/gateway-instance-config-dal.ts b/backend/src/ee/services/gateway/gateway-instance-config-dal.ts new file mode 100644 index 000000000..14dcb7b9f --- /dev/null +++ b/backend/src/ee/services/gateway/gateway-instance-config-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TGatewayInstanceConfigDALFactory = ReturnType; + +export const gatewayInstanceConfigDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.GatewayInstanceConfig); + + return orm; +}; diff --git a/backend/src/ee/services/gateway/gateway-service.ts b/backend/src/ee/services/gateway/gateway-service.ts new file mode 100644 index 000000000..8f3ab3f9f --- /dev/null +++ b/backend/src/ee/services/gateway/gateway-service.ts @@ -0,0 +1,284 @@ +import crypto from "node:crypto"; + +import { ForbiddenError } from "@casl/ability"; +import * as x509 from "@peculiar/x509"; + +import { PgSqlLock } from "@app/keystore/keystore"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { getTurnCredentials } from "@app/lib/turn/credentials"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { + createSerialNumber, + keyAlgorithmToAlgCfg +} from "@app/services/certificate-authority/certificate-authority-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { KmsDataKey } from "@app/services/kms/kms-types"; + +import { TLicenseServiceFactory } from "../license/license-service"; +import { OrgGatewayPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { TGatewayDALFactory } from "./gateway-dal"; +import { TExchangeAllocatedRelayAddressDTO, TGetGatewayByIdDTO, TListGatewaysDTO } from "./gateway-types"; +import { TOrgGatewayRootCaDALFactory } from "./org-gateway-root-ca-dal"; + +type TGatewayServiceFactoryDep = { + gatewayDAL: TGatewayDALFactory; + orgGatewayRootCaDAL: Pick; + licenseService: Pick; + kmsService: Pick; + permissionService: Pick; +}; + +export type TGatewayServiceFactory = ReturnType; + +// TODO(gateway): missing permission check +export const gatewayServiceFactory = ({ + gatewayDAL, + licenseService, + orgGatewayRootCaDAL, + kmsService, + permissionService +}: TGatewayServiceFactoryDep) => { + const $validateOrgAccessToGateway = async (orgId: string) => { + if (!licenseService.onPremFeatures.gateway) { + throw new BadRequestError({ + message: + "Gateway handshake failed due to instance plan restrictions. Please upgrade your instance to Infisical's Enterprise plan." + }); + } + const orgLicensePlan = await licenseService.getPlan(orgId); + if (!orgLicensePlan.gateway) { + throw new BadRequestError({ + message: + "Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan." + }); + } + }; + + const getGatewayRelayDetails = async (actorId: string, actorOrgId: string) => { + const envCfg = getConfig(); + await $validateOrgAccessToGateway(actorOrgId); + + if ( + !envCfg.GATEWAY_RELAY_AUTH_SECRET || + !envCfg.GATEWAY_RELAY_ADDRESS || + !envCfg.GATEWAY_INFISICAL_STATIC_IP_ADDRESS + ) { + throw new BadRequestError({ + message: "Gateway handshake failed due to missing instance config." + }); + } + // TODO(gateway): keep it in 30mins redis after encryption to avoid multiple credentials spinning up + const { username: turnServerUsername, password: turnServerPassword } = getTurnCredentials( + actorId, + envCfg.GATEWAY_RELAY_AUTH_SECRET + ); + + return { + turnServerUsername, + turnServerPassword, + turnServerAddress: envCfg.GATEWAY_RELAY_ADDRESS, + infisicalStaticIp: envCfg.GATEWAY_INFISICAL_STATIC_IP_ADDRESS + }; + }; + + const exchangeAllocatedRelayAddress = async ({ + identityId, + identityOrg, + relayAddress + }: TExchangeAllocatedRelayAddressDTO) => { + const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identityOrg + }); + + const orgGatewayRootCa = await orgGatewayRootCaDAL.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayRootCaInit(identityOrg)]); + const existingGateway = await orgGatewayRootCaDAL.findOne({ orgId: identityOrg }); + if (existingGateway) return existingGateway; + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + // generate root CA + const orgGatewayRootCaSerialNumber = createSerialNumber(); + const orgGatewayRootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const orgGatewayCaSkObj = crypto.KeyObject.from(orgGatewayRootCaKeys.privateKey); + const orgGatewayCaIssuedAt = new Date(); + const orgGatewayCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 25)); + + const orgGatewayCaCert = await x509.X509CertificateGenerator.createSelfSigned({ + name: `CN=Org Gateway Root CA,O=${identityOrg}`, + serialNumber: orgGatewayRootCaSerialNumber, + notBefore: orgGatewayCaIssuedAt, + notAfter: orgGatewayCaExpiration, + signingAlgorithm: alg, + keys: orgGatewayRootCaKeys, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), + await x509.SubjectKeyIdentifierExtension.create(orgGatewayRootCaKeys.publicKey) + ] + }); + + return orgGatewayRootCaDAL.create({ + orgId: identityOrg, + caIssuedAt: orgGatewayCaIssuedAt, + caExpiration: orgGatewayCaExpiration, + caSerialNumber: orgGatewayRootCaSerialNumber, + encryptedCaCertificate: orgKmsEncryptor({ plainText: Buffer.from(orgGatewayCaCert.rawData) }).cipherTextBlob, + encryptedCaPrivateKey: orgKmsEncryptor({ + plainText: orgGatewayCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + }).cipherTextBlob, + caKeyAlgorithm: CertKeyAlgorithm.RSA_2048 + }); + }); + + const caCertObj = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayRootCa.encryptedCaCertificate + }) + ); + const caAlg = keyAlgorithmToAlgCfg(orgGatewayRootCa.caKeyAlgorithm as CertKeyAlgorithm); + const caSkObj = crypto.createPrivateKey({ + key: orgKmsDecryptor({ cipherTextBlob: orgGatewayRootCa.encryptedCaPrivateKey }), + format: "der", + type: "pkcs8" + }); + const caPrivateKey = await crypto.subtle.importKey( + "pkcs8", + caSkObj.export({ format: "der", type: "pkcs8" }), + caAlg, + true, + ["sign"] + ); + + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + const gatewayKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const certIssuedAt = new Date(); + // then need to periodically init + const certExpireAt = new Date(new Date().setMonth(new Date().getMonth() + 1)); + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), + await x509.SubjectKeyIdentifierExtension.create(gatewayKeys.publicKey), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true), + // san + new x509.SubjectAlternativeNameExtension([{ type: "ip", value: relayAddress }], false) + ]; + + const serialNumber = createSerialNumber(); + const privateKey = crypto.KeyObject.from(gatewayKeys.privateKey); + const gatewayCertificate = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: `CN=${identityId},O=${identityOrg}`, + issuer: caCertObj.subject, + notBefore: certIssuedAt, + notAfter: certExpireAt, + signingKey: caPrivateKey, + publicKey: gatewayKeys.publicKey, + signingAlgorithm: alg, + extensions + }); + + await gatewayDAL.transaction(async (tx) => { + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayCertExchange(identityOrg)]); + const existingGateway = await gatewayDAL.findOne({ identityId, orgGatewayRootCaId: orgGatewayRootCa.id }); + if (existingGateway) { + return gatewayDAL.updateById(existingGateway.id, { + keyAlgorithm: CertKeyAlgorithm.RSA_2048, + issuedAt: certIssuedAt, + expiration: certExpireAt, + serialNumber, + relayAddress: orgKmsEncryptor({ plainText: Buffer.from(relayAddress) }).cipherTextBlob + }); + } + + return gatewayDAL.create({ + keyAlgorithm: CertKeyAlgorithm.RSA_2048, + issuedAt: certIssuedAt, + expiration: certExpireAt, + serialNumber, + relayAddress: orgKmsEncryptor({ plainText: Buffer.from(relayAddress) }).cipherTextBlob, + identityId, + orgGatewayRootCaId: orgGatewayRootCa.id, + name: alphaNumericNanoId(8) + }); + }); + + return { + serialNumber, + privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) as string, + certificate: gatewayCertificate.toString("pem"), + certificateChain: caCertObj.toString("pem") + }; + }; + + const listGateways = async ({ orgPermission }: TListGatewaysDTO) => { + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + orgPermission.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgGatewayPermissionActions.Create, OrgPermissionSubjects.Gateway); + const rootCa = await orgGatewayRootCaDAL.findOne({ orgId: orgPermission.orgId }); + if (!rootCa) return []; + + const gateways = await gatewayDAL.find({ + orgGatewayRootCaId: rootCa.id + }); + return gateways; + }; + + const getGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => { + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + orgPermission.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgGatewayPermissionActions.Create, OrgPermissionSubjects.Gateway); + const rootCa = await orgGatewayRootCaDAL.findOne({ orgId: orgPermission.orgId }); + if (!rootCa) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); + + const [gateway] = await gatewayDAL.find({ id, orgGatewayRootCaId: rootCa.id }); + if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); + return gateway; + }; + + const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => { + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + orgPermission.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgGatewayPermissionActions.Create, OrgPermissionSubjects.Gateway); + const rootCa = await orgGatewayRootCaDAL.findOne({ orgId: orgPermission.orgId }); + if (!rootCa) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); + + const [gateway] = await gatewayDAL.delete({ id, orgGatewayRootCaId: rootCa.id }); + if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); + return gateway; + }; + + return { + getGatewayRelayDetails, + exchangeAllocatedRelayAddress, + listGateways, + getGatewayById, + deleteGatewayById + }; +}; diff --git a/backend/src/ee/services/gateway/gateway-types.ts b/backend/src/ee/services/gateway/gateway-types.ts new file mode 100644 index 000000000..40328d041 --- /dev/null +++ b/backend/src/ee/services/gateway/gateway-types.ts @@ -0,0 +1,21 @@ +import { OrgServiceActor } from "@app/lib/types"; + +export type TExchangeAllocatedRelayAddressDTO = { + identityId: string; + identityOrg: string; + relayAddress: string; +}; + +export type TListGatewaysDTO = { + orgPermission: OrgServiceActor; +}; + +export type TGetGatewayByIdDTO = { + id: string; + orgPermission: OrgServiceActor; +}; + +export type TDeleteGatewayByIdDTO = { + id: string; + orgPermission: OrgServiceActor; +}; diff --git a/backend/src/ee/services/gateway/org-gateway-root-ca-dal.ts b/backend/src/ee/services/gateway/org-gateway-root-ca-dal.ts new file mode 100644 index 000000000..4730f20f8 --- /dev/null +++ b/backend/src/ee/services/gateway/org-gateway-root-ca-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TOrgGatewayRootCaDALFactory = ReturnType; + +export const orgGatewayRootCaDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.OrgGatewayRootCa); + return orm; +}; diff --git a/backend/src/ee/services/license/license-fns.ts b/backend/src/ee/services/license/license-fns.ts index cab19a530..3f9e1b42c 100644 --- a/backend/src/ee/services/license/license-fns.ts +++ b/backend/src/ee/services/license/license-fns.ts @@ -51,7 +51,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ pkiEst: false, enforceMfa: false, projectTemplates: false, - kmip: false + kmip: false, + gateway: true }); export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 2b4dbe49e..626a0c999 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -69,6 +69,7 @@ export type TFeatureSet = { enforceMfa: boolean; projectTemplates: false; kmip: false; + gateway: true; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index 03d1747d4..c83a30f25 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -32,6 +32,14 @@ export enum OrgPermissionAdminConsoleAction { AccessAllProjects = "access-all-projects" } +export enum OrgGatewayPermissionActions { + // is there a better word for this. This mean can an identity be a gateway + Create = "create", + Read = "read", + Edit = "edit", + Delete = "delete" +} + export enum OrgPermissionSubjects { Workspace = "workspace", Role = "role", @@ -50,7 +58,8 @@ export enum OrgPermissionSubjects { AuditLogs = "audit-logs", ProjectTemplates = "project-templates", AppConnections = "app-connections", - Kmip = "kmip" + Kmip = "kmip", + Gateway = "gateway" } export type AppConnectionSubjectFields = { @@ -73,6 +82,7 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.Kms] | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] + | [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway] | [ OrgPermissionAppConnectionActions, ( diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index a5f3c24c0..982dc21f9 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -2,11 +2,13 @@ import { Redis } from "ioredis"; import { Redlock, Settings } from "@app/lib/red-lock"; -export enum PgSqlLock { - BootUpMigration = 2023, - SuperAdminInit = 2024, - KmsRootKeyInit = 2025 -} +export const PgSqlLock = { + BootUpMigration: 2023, + SuperAdminInit: 2024, + KmsRootKeyInit: 2025, + OrgGatewayRootCaInit: (orgId: string) => `org-gateway-root-ca:${orgId}`, + OrgGatewayCertExchange: (orgId: string) => `org-gateway-cert-exchange:${orgId}` +} as const; export type TKeyStoreFactory = ReturnType; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 801e937a2..a286b4203 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -184,6 +184,13 @@ const envSchema = z USE_PG_QUEUE: zodStrBool.default("false"), SHOULD_INIT_PG_QUEUE: zodStrBool.default("false"), + /* Gateway----------------------------------------------------------------------------- */ + GATEWAY_INFISICAL_STATIC_IP_ADDRESS: zpStr(z.string().optional()), + GATEWAY_RELAY_ADDRESS: zpStr(z.string().optional()), + GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()), + + /* ----------------------------------------------------------------------------- */ + /* App Connections ----------------------------------------------------------------------------- */ // aws diff --git a/backend/src/lib/crypto/hashtext.ts b/backend/src/lib/crypto/hashtext.ts new file mode 100644 index 000000000..221608382 --- /dev/null +++ b/backend/src/lib/crypto/hashtext.ts @@ -0,0 +1,29 @@ +// used for postgres lock +// this is something postgres does under the hood +// convert any string to a unique number +export const hashtext = (text: string) => { + // Convert text to UTF8 bytes array for consistent behavior with PostgreSQL + const encoder = new TextEncoder(); + const bytes = encoder.encode(text); + + // Implementation of hash_any + let result = 0; + + for (let i = 0; i < bytes.length; i += 1) { + // eslint-disable-next-line no-bitwise + result = ((result << 5) + result) ^ bytes[i]; + // Keep within 32-bit integer range + // eslint-disable-next-line no-bitwise + result >>>= 0; + } + + // Convert to signed 32-bit integer like PostgreSQL + // eslint-disable-next-line no-bitwise + return result | 0; +}; + +export const pgAdvisoryLockHashText = (text: string) => { + const hash = hashtext(text); + // Ensure positive value within PostgreSQL integer range + return Math.abs(hash) % 2 ** 31; +}; diff --git a/backend/src/lib/turn/credentials.ts b/backend/src/lib/turn/credentials.ts new file mode 100644 index 000000000..817148c30 --- /dev/null +++ b/backend/src/lib/turn/credentials.ts @@ -0,0 +1,16 @@ +import crypto from "node:crypto"; + +const TURN_TOKEN_TTL = 60 * 60 * 1000; // 24 hours in milliseconds +export const getTurnCredentials = (id: string, authSecret: string, ttl = TURN_TOKEN_TTL) => { + const timestamp = Math.floor((Date.now() + ttl) / 1000); + const username = `${timestamp}:${id}`; + + const hmac = crypto.createHmac("sha1", authSecret); + hmac.update(username); + const password = hmac.digest("base64"); + + return { + username, + password + }; +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 155b52cfb..fb8552744 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -27,6 +27,10 @@ import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic- import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; import { externalKmsDALFactory } from "@app/ee/services/external-kms/external-kms-dal"; import { externalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; +import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; +import { gatewayInstanceConfigDALFactory } from "@app/ee/services/gateway/gateway-instance-config-dal"; +import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { orgGatewayRootCaDALFactory } from "@app/ee/services/gateway/org-gateway-root-ca-dal"; import { groupDALFactory } from "@app/ee/services/group/group-dal"; import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; @@ -393,6 +397,10 @@ export const registerRoutes = async ( const kmipOrgConfigDAL = kmipOrgConfigDALFactory(db); const kmipOrgServerCertificateDAL = kmipOrgServerCertificateDALFactory(db); + const gatewayInstanceConfigDAL = gatewayInstanceConfigDALFactory(db); + const orgGatewayRootCaDAL = orgGatewayRootCaDALFactory(db); + const gatewayDAL = gatewayDALFactory(db); + const permissionService = permissionServiceFactory({ permissionDAL, orgRoleDAL, @@ -631,6 +639,7 @@ export const registerRoutes = async ( authService: loginService, serverCfgDAL: superAdminDAL, kmsRootConfigDAL, + gatewayInstanceConfigDAL, orgService, keyStore, licenseService, @@ -1457,6 +1466,14 @@ export const registerRoutes = async ( permissionService }); + const gatewayService = gatewayServiceFactory({ + orgGatewayRootCaDAL, + permissionService, + gatewayDAL, + kmsService, + licenseService + }); + await superAdminService.initServerCfg(); // setup the communication with license key server @@ -1557,7 +1574,8 @@ export const registerRoutes = async ( appConnection: appConnectionService, secretSync: secretSyncService, kmip: kmipService, - kmipOperation: kmipOperationService + kmipOperation: kmipOperationService, + gateway: gatewayService }); const cronJobs: CronJob[] = []; diff --git a/backend/src/server/routes/v1/admin-router.ts b/backend/src/server/routes/v1/admin-router.ts index 6ecebb274..4359ef3fe 100644 --- a/backend/src/server/routes/v1/admin-router.ts +++ b/backend/src/server/routes/v1/admin-router.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; +import { GatewayInstanceConfigSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "@app/lib/errors"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; @@ -12,6 +12,13 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { LoginMethod } from "@app/services/super-admin/super-admin-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; +const SanitizedInstanceGatewaySchema = GatewayInstanceConfigSchema.pick({ + isDisabled: true, + infisicalClientCaIssuedAt: true, + infisicalClientCaSerialNumber: true, + caKeyAlgorithm: true +}); + export const registerAdminRouter = async (server: FastifyZodProvider) => { server.route({ method: "GET", @@ -247,6 +254,82 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => { } }); + server.route({ + method: "POST", + url: "/gateway", + config: { + rateLimit: writeLimit + }, + schema: { + response: { + 200: z.object({ + message: z.string() + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async () => { + await server.services.superAdmin.setupInstanceGateway(); + return { message: "Gateway setup completed" }; + } + }); + + server.route({ + method: "PATCH", + url: "/gateway", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + isDisabled: z.boolean().optional() + }), + response: { + 200: z.object({ + message: z.string(), + gateway: SanitizedInstanceGatewaySchema + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async (req) => { + const gateway = await server.services.superAdmin.updateInstanceGateway(req.body); + return { message: "Gateway updated Successfully.", gateway }; + } + }); + + server.route({ + method: "GET", + url: "/gateway", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: z.object({ + gateway: SanitizedInstanceGatewaySchema + }) + } + }, + onRequest: (req, res, done) => { + verifyAuth([AuthMode.JWT])(req, res, () => { + verifySuperAdmin(req, res, done); + }); + }, + handler: async () => { + const gateway = await server.services.superAdmin.getInstanceGateway(); + return { gateway }; + } + }); + server.route({ method: "POST", url: "/signup", diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 9a6075423..cf9c35425 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,6 +1,9 @@ +import * as x509 from "@peculiar/x509"; import bcrypt from "bcrypt"; +import crypto, { KeyObject } from "crypto"; import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; +import { TGatewayInstanceConfigDALFactory } from "@app/ee/services/gateway/gateway-instance-config-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; @@ -10,6 +13,8 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TAuthLoginFactory } from "../auth/auth-login-service"; import { AuthMethod } from "../auth/auth-type"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificate/certificate-types"; +import { createSerialNumber, keyAlgorithmToAlgCfg } from "../certificate-authority/certificate-authority-fns"; import { KMS_ROOT_CONFIG_UUID } from "../kms/kms-fns"; import { TKmsRootConfigDALFactory } from "../kms/kms-root-config-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; @@ -31,6 +36,7 @@ type TSuperAdminServiceFactoryDep = { orgService: Pick; keyStore: Pick; licenseService: Pick; + gatewayInstanceConfigDAL: Pick; }; export type TSuperAdminServiceFactory = ReturnType; @@ -47,6 +53,7 @@ export let getServerCfg: () => Promise< const ADMIN_CONFIG_KEY = "infisical-admin-cfg"; const ADMIN_CONFIG_KEY_EXP = 60; // 60s const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000"; +const GATEWAY_INSTANCE_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; export const superAdminServiceFactory = ({ serverCfgDAL, @@ -57,7 +64,8 @@ export const superAdminServiceFactory = ({ keyStore, kmsRootConfigDAL, kmsService, - licenseService + licenseService, + gatewayInstanceConfigDAL }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself @@ -373,6 +381,149 @@ export const superAdminServiceFactory = ({ await kmsService.updateEncryptionStrategy(strategy); }; + const setupInstanceGateway = async () => { + if (!licenseService.onPremFeatures.gateway) { + throw new BadRequestError({ + message: "Failed to setup gateway ca due to plan restriction. Upgrade to Infisical's Enterprise plan." + }); + } + + const existingConfig = await gatewayInstanceConfigDAL.findById(GATEWAY_INSTANCE_CONFIG_UUID); + if (existingConfig) { + throw new BadRequestError({ + message: "Gateway has already been configured for the instance" + }); + } + + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + // generate root CA + const infisicalClientRootCaSerialNumber = createSerialNumber(); + const infisicalClientRootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const infisicalClientCaSkObj = KeyObject.from(infisicalClientRootCaKeys.privateKey); + const infisicalClientCaIssuedAt = new Date(); + const infisicalClientCaExpiration = new Date(new Date().setFullYear(new Date().getFullYear() + 25)); + + const infisicalClientCaCert = await x509.X509CertificateGenerator.createSelfSigned({ + name: "CN=Infisical Gateway Client Root CA", + serialNumber: infisicalClientRootCaSerialNumber, + notBefore: infisicalClientCaIssuedAt, + notAfter: infisicalClientCaExpiration, + signingAlgorithm: alg, + keys: infisicalClientRootCaKeys, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), + await x509.SubjectKeyIdentifierExtension.create(infisicalClientRootCaKeys.publicKey) + ] + }); + + const infisicalClientLeafCertserialNumber = createSerialNumber(); + const infisicalClientLeafKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(infisicalClientCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(infisicalClientLeafKeys.publicKey), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | + x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] | + x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true) + ]; + + const infisicalClientLeafCert = await x509.X509CertificateGenerator.create({ + serialNumber: infisicalClientLeafCertserialNumber, + subject: `OU=infisical,CN=infisical`, + issuer: infisicalClientCaCert.issuer, + notBefore: infisicalClientCaIssuedAt, + notAfter: infisicalClientCaExpiration, + signingKey: infisicalClientRootCaKeys.privateKey, + publicKey: infisicalClientLeafKeys.publicKey, + signingAlgorithm: alg, + extensions + }); + + const infisicalClientLeafCertSkObj = KeyObject.from(infisicalClientLeafKeys.privateKey); + const encryptWithRootKey = kmsService.encryptWithRootKey(); + await gatewayInstanceConfigDAL.create({ + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: GATEWAY_INSTANCE_CONFIG_UUID, + isDisabled: false, + caKeyAlgorithm: CertKeyAlgorithm.RSA_2048, + infisicalClientCaIssuedAt, + infisicalClientCaExpiration, + infisicalClientCaSerialNumber: infisicalClientRootCaSerialNumber, + encryptedInfisicalClientCaCertificate: encryptWithRootKey(Buffer.from(infisicalClientCaCert.rawData)), + encryptedInfisicalClientCaPrivateKey: encryptWithRootKey( + infisicalClientCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ), + infisicalClientCertIssuedAt: infisicalClientCaIssuedAt, + infisicalClientCertExpiration: infisicalClientCaExpiration, + infisicalClientCertSerialNumber: infisicalClientLeafCertserialNumber, + infisicalClientCertKeyAlgorithm: CertKeyAlgorithm.RSA_2048, + encryptedInfisicalClientCertificate: encryptWithRootKey(Buffer.from(infisicalClientLeafCert.rawData)), + encryptedInfisicalClientPrivateKey: encryptWithRootKey( + infisicalClientLeafCertSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + }); + }; + + const updateInstanceGateway = async ({ isDisabled }: { isDisabled?: boolean }) => { + if (!licenseService.onPremFeatures.gateway) { + throw new BadRequestError({ + message: "Failed to update gateway ca due to plan restriction. Upgrade to Infisical's Enterprise plan." + }); + } + + const existingConfig = await gatewayInstanceConfigDAL.findById(GATEWAY_INSTANCE_CONFIG_UUID); + if (!existingConfig) { + throw new NotFoundError({ + message: "Gateway instance config not found" + }); + } + + const updatedGatewayInstanceConfig = await gatewayInstanceConfigDAL.updateById(GATEWAY_INSTANCE_CONFIG_UUID, { + isDisabled + }); + return { + infisicalClientCaSerialNumber: updatedGatewayInstanceConfig.infisicalClientCaSerialNumber, + isDisabled: updatedGatewayInstanceConfig?.isDisabled, + caKeyAlgorithm: CertKeyAlgorithm.RSA_2048, + infisicalClientCaIssuedAt: updatedGatewayInstanceConfig.infisicalClientCaIssuedAt + }; + }; + + const getInstanceGateway = async () => { + if (!licenseService.onPremFeatures.gateway) { + throw new BadRequestError({ + message: "Failed to update gateway ca due to plan restriction. Upgrade to Infisical's Enterprise plan." + }); + } + + const existingConfig = await gatewayInstanceConfigDAL.findById(GATEWAY_INSTANCE_CONFIG_UUID); + if (!existingConfig) { + throw new NotFoundError({ + message: "Gateway instance config not found" + }); + } + + return { + infisicalClientCaSerialNumber: existingConfig.infisicalClientCaSerialNumber, + isDisabled: existingConfig?.isDisabled, + caKeyAlgorithm: CertKeyAlgorithm.RSA_2048, + infisicalClientCaIssuedAt: existingConfig.infisicalClientCaIssuedAt + }; + }; + return { initServerCfg, updateServerCfg, @@ -381,6 +532,9 @@ export const superAdminServiceFactory = ({ deleteUser, getAdminSlackConfig, updateRootEncryptionStrategy, - getConfiguredEncryptionStrategies + getConfiguredEncryptionStrategies, + setupInstanceGateway, + updateInstanceGateway, + getInstanceGateway }; };