From ae62c5938239eb679b6e2b8c3decf064d808250d Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 27 Aug 2025 04:36:17 +0800 Subject: [PATCH] feat: add gateway registration and org-proxy initialization --- backend/src/@types/fastify.d.ts | 2 + backend/src/@types/knex.d.ts | 12 + ...1627_add-gateway-v2-pki-and-ssh-configs.ts | 43 ++ backend/src/db/schemas/index.ts | 2 + backend/src/db/schemas/models.ts | 4 +- .../src/db/schemas/org-gateway-config-v2.ts | 29 ++ backend/src/db/schemas/proxies.ts | 22 + backend/src/ee/routes/v1/proxy-router.ts | 51 +- backend/src/ee/routes/v2/gateway-router.ts | 29 ++ backend/src/ee/routes/v2/index.ts | 3 + .../services/gateway-v2/gateway-v2-service.ts | 274 +++++++++++ .../gateway-v2/org-gateway-config-v2-dal.ts | 11 + backend/src/ee/services/proxy/proxy-dal.ts | 11 + backend/src/ee/services/proxy/proxy-fns.ts | 3 + .../src/ee/services/proxy/proxy-service.ts | 442 +++++++++++++++++- backend/src/keystore/keystore.ts | 4 +- backend/src/lib/config/env.ts | 2 + .../server/plugins/auth/inject-identity.ts | 4 + backend/src/server/routes/index.ts | 16 +- 19 files changed, 944 insertions(+), 20 deletions(-) create mode 100644 backend/src/db/schemas/org-gateway-config-v2.ts create mode 100644 backend/src/db/schemas/proxies.ts create mode 100644 backend/src/ee/routes/v2/gateway-router.ts create mode 100644 backend/src/ee/services/gateway-v2/gateway-v2-service.ts create mode 100644 backend/src/ee/services/gateway-v2/org-gateway-config-v2-dal.ts create mode 100644 backend/src/ee/services/proxy/proxy-dal.ts create mode 100644 backend/src/ee/services/proxy/proxy-fns.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 977970f14..2b997eb46 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -16,6 +16,7 @@ import { TEventBusService } from "@app/ee/services/event/event-bus-service"; import { TServerSentEventsService } from "@app/ee/services/event/event-sse-service"; import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TIdentityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template"; @@ -305,6 +306,7 @@ declare module "fastify" { sse: TServerSentEventsService; identityAuthTemplate: TIdentityAuthTemplateServiceFactory; proxy: TProxyServiceFactory; + gatewayV2: TGatewayV2ServiceFactory; }; // 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 9fdc94aca..f2b768eb6 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -233,6 +233,9 @@ import { TOrgGatewayConfig, TOrgGatewayConfigInsert, TOrgGatewayConfigUpdate, + TOrgGatewayConfigV2, + TOrgGatewayConfigV2Insert, + TOrgGatewayConfigV2Update, TOrgMemberships, TOrgMembershipsInsert, TOrgMembershipsUpdate, @@ -293,6 +296,9 @@ import { TProjectUserMembershipRoles, TProjectUserMembershipRolesInsert, TProjectUserMembershipRolesUpdate, + TProxies, + TProxiesInsert, + TProxiesUpdate, TRateLimit, TRateLimitInsert, TRateLimitUpdate, @@ -1270,5 +1276,11 @@ declare module "knex/types/tables" { TOrgProxyConfigInsert, TOrgProxyConfigUpdate >; + [TableName.OrgGatewayConfigV2]: KnexOriginal.CompositeTableType< + TOrgGatewayConfigV2, + TOrgGatewayConfigV2Insert, + TOrgGatewayConfigV2Update + >; + [TableName.Proxy]: KnexOriginal.CompositeTableType; } } diff --git a/backend/src/db/migrations/20250825131627_add-gateway-v2-pki-and-ssh-configs.ts b/backend/src/db/migrations/20250825131627_add-gateway-v2-pki-and-ssh-configs.ts index c242d0f58..68aa15374 100644 --- a/backend/src/db/migrations/20250825131627_add-gateway-v2-pki-and-ssh-configs.ts +++ b/backend/src/db/migrations/20250825131627_add-gateway-v2-pki-and-ssh-configs.ts @@ -67,6 +67,43 @@ export async function up(knex: Knex): Promise { await createOnUpdateTrigger(knex, TableName.OrgProxyConfig); } + + if (!(await knex.schema.hasTable(TableName.OrgGatewayConfigV2))) { + await knex.schema.createTable(TableName.OrgGatewayConfigV2, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("orgId").notNullable().unique(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + t.timestamps(true, true, true); + t.binary("encryptedRootGatewayCaPrivateKey").notNullable(); + t.binary("encryptedRootGatewayCaCertificate").notNullable(); + t.binary("encryptedGatewayServerCaPrivateKey").notNullable(); + t.binary("encryptedGatewayServerCaCertificate").notNullable(); + t.binary("encryptedGatewayServerCaCertificateChain").notNullable(); + t.binary("encryptedGatewayClientCaPrivateKey").notNullable(); + t.binary("encryptedGatewayClientCaCertificate").notNullable(); + t.binary("encryptedGatewayClientCaCertificateChain").notNullable(); + }); + + await createOnUpdateTrigger(knex, TableName.OrgGatewayConfigV2); + } + + if (!(await knex.schema.hasTable(TableName.Proxy))) { + await knex.schema.createTable(TableName.Proxy, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + + t.uuid("orgId"); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + + t.uuid("identityId"); + t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + + t.string("name").notNullable().unique(); + t.string("ip").notNullable(); + }); + + await createOnUpdateTrigger(knex, TableName.Proxy); + } } export async function down(knex: Knex): Promise { @@ -75,4 +112,10 @@ export async function down(knex: Knex): Promise { await dropOnUpdateTrigger(knex, TableName.InstanceProxyConfig); await knex.schema.dropTableIfExists(TableName.InstanceProxyConfig); + + await dropOnUpdateTrigger(knex, TableName.OrgGatewayConfigV2); + await knex.schema.dropTableIfExists(TableName.OrgGatewayConfigV2); + + await dropOnUpdateTrigger(knex, TableName.Proxy); + await knex.schema.dropTableIfExists(TableName.Proxy); } diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 01c066035..03813ee49 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -76,6 +76,7 @@ export * from "./models"; export * from "./oidc-configs"; export * from "./org-bots"; export * from "./org-gateway-config"; +export * from "./org-gateway-config-v2"; export * from "./org-memberships"; export * from "./org-proxy-config"; export * from "./org-roles"; @@ -164,3 +165,4 @@ export * from "./user-group-membership"; export * from "./users"; export * from "./webhooks"; export * from "./workflow-integrations"; +export * from "./proxies"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index dd1526011..99681b986 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -182,7 +182,9 @@ export enum TableName { // gateway v2 InstanceProxyConfig = "instance_proxy_config", - OrgProxyConfig = "org_proxy_config" + OrgProxyConfig = "org_proxy_config", + OrgGatewayConfigV2 = "org_gateway_config_v2", + Proxy = "proxies" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId"; diff --git a/backend/src/db/schemas/org-gateway-config-v2.ts b/backend/src/db/schemas/org-gateway-config-v2.ts new file mode 100644 index 000000000..fab9a3182 --- /dev/null +++ b/backend/src/db/schemas/org-gateway-config-v2.ts @@ -0,0 +1,29 @@ +// 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 OrgGatewayConfigV2Schema = z.object({ + id: z.string().uuid(), + orgId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + encryptedRootGatewayCaPrivateKey: zodBuffer, + encryptedRootGatewayCaCertificate: zodBuffer, + encryptedGatewayServerCaPrivateKey: zodBuffer, + encryptedGatewayServerCaCertificate: zodBuffer, + encryptedGatewayServerCaCertificateChain: zodBuffer, + encryptedGatewayClientCaPrivateKey: zodBuffer, + encryptedGatewayClientCaCertificate: zodBuffer, + encryptedGatewayClientCaCertificateChain: zodBuffer +}); + +export type TOrgGatewayConfigV2 = z.infer; +export type TOrgGatewayConfigV2Insert = Omit, TImmutableDBKeys>; +export type TOrgGatewayConfigV2Update = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/proxies.ts b/backend/src/db/schemas/proxies.ts new file mode 100644 index 000000000..508c4d25e --- /dev/null +++ b/backend/src/db/schemas/proxies.ts @@ -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 ProxiesSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + orgId: z.string().uuid().nullable().optional(), + identityId: z.string().uuid().nullable().optional(), + name: z.string(), + ip: z.string() +}); + +export type TProxies = z.infer; +export type TProxiesInsert = Omit, TImmutableDBKeys>; +export type TProxiesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/ee/routes/v1/proxy-router.ts b/backend/src/ee/routes/v1/proxy-router.ts index d2c580708..561fe7780 100644 --- a/backend/src/ee/routes/v1/proxy-router.ts +++ b/backend/src/ee/routes/v1/proxy-router.ts @@ -1,24 +1,69 @@ import { z } from "zod"; +import { getConfig } from "@app/lib/config/env"; +import { UnauthorizedError } from "@app/lib/errors"; import { writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; export const registerProxyRouter = async (server: FastifyZodProvider) => { + const appCfg = getConfig(); + server.route({ method: "POST", - url: "/", + url: "/register-instance-proxy", config: { rateLimit: writeLimit }, schema: { body: z.object({ - ip: z.string() + ip: z.string(), + name: z.string() }), response: { 200: z.any() } }, + onRequest: (req, _, next) => { + const authHeader = req.headers.authorization; + + if (appCfg.PROXY_AUTH_SECRET && authHeader === `Bearer ${appCfg.PROXY_AUTH_SECRET}`) { + return next(); + } + + throw new UnauthorizedError({ + message: "Invalid proxy auth secret" + }); + }, handler: async (req) => { - return server.services.proxy.registerProxy(req.body); + return server.services.proxy.registerProxy({ + ...req.body + }); + } + }); + + server.route({ + method: "POST", + url: "/register-org-proxy", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + ip: z.string(), + name: z.string() + }), + response: { + 200: z.any() + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + return server.services.proxy.registerProxy({ + ...req.body, + identityId: req.permission.id, + orgId: req.permission.orgId + }); } }); }; diff --git a/backend/src/ee/routes/v2/gateway-router.ts b/backend/src/ee/routes/v2/gateway-router.ts new file mode 100644 index 000000000..31130b206 --- /dev/null +++ b/backend/src/ee/routes/v2/gateway-router.ts @@ -0,0 +1,29 @@ +import z from "zod"; + +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerGatewayV2Router = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + body: z.object({ + proxyName: z.string() + }), + response: { + 200: z.any() + } + }, + handler: async (req) => { + const gateway = await server.services.gatewayV2.registerGateway({ + orgId: req.permission.orgId, + proxyName: req.body.proxyName, + actorId: req.permission.id + }); + + return gateway; + } + }); +}; diff --git a/backend/src/ee/routes/v2/index.ts b/backend/src/ee/routes/v2/index.ts index e364f4949..b7ee038a1 100644 --- a/backend/src/ee/routes/v2/index.ts +++ b/backend/src/ee/routes/v2/index.ts @@ -9,6 +9,7 @@ import { import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; import { registerProjectRoleRouter } from "./project-role-router"; +import { registerGatewayV2Router } from "./gateway-router"; export const registerV2EERoutes = async (server: FastifyZodProvider) => { // org role starts with organization @@ -23,6 +24,8 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => { prefix: "/identity-project-additional-privilege" }); + await server.register(registerGatewayV2Router, { prefix: "/gateways" }); + await server.register( async (secretRotationV2Router) => { // register generic secret rotation endpoints diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts new file mode 100644 index 000000000..e55acbffa --- /dev/null +++ b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts @@ -0,0 +1,274 @@ +import * as x509 from "@peculiar/x509"; + +import { PgSqlLock } from "@app/keystore/keystore"; +import { crypto } from "@app/lib/crypto"; +import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns"; +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 { TProxyServiceFactory } from "../proxy/proxy-service"; +import { TOrgGatewayConfigV2DALFactory } from "./org-gateway-config-v2-dal"; + +type TGatewayV2ServiceFactoryDep = { + orgGatewayConfigV2DAL: Pick; + kmsService: TKmsServiceFactory; + proxyService: TProxyServiceFactory; +}; + +export type TGatewayV2ServiceFactory = ReturnType; + +export const gatewayV2ServiceFactory = ({ + orgGatewayConfigV2DAL, + kmsService, + proxyService +}: TGatewayV2ServiceFactoryDep) => { + const $getOrgCAs = async (orgId: string) => { + const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); + + const orgCAs = await orgGatewayConfigV2DAL.transaction(async (tx) => { + const orgGatewayConfigV2 = await orgGatewayConfigV2DAL.findOne({ orgId }); + if (orgGatewayConfigV2) return orgGatewayConfigV2; + + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayV2Init(orgId)]); + + // generate root CA + const rootCaKeyAlgorithm = CertKeyAlgorithm.RSA_2048; + const alg = keyAlgorithmToAlgCfg(rootCaKeyAlgorithm); + const rootCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + const rootCaSerialNumber = createSerialNumber(); + const rootCaSkObj = crypto.nativeCrypto.KeyObject.from(rootCaKeys.privateKey); + const rootCaIssuedAt = new Date(); + const rootCaExpiration = new Date(new Date().setFullYear(2045)); + + const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({ + name: `O=${orgId},CN=Infisical Gateway Root CA`, + serialNumber: rootCaSerialNumber, + notBefore: rootCaIssuedAt, + notAfter: rootCaExpiration, + signingAlgorithm: alg, + keys: rootCaKeys, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), + await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey) + ] + }); + + // generate server CA + const serverCaSerialNumber = createSerialNumber(); + const serverCaIssuedAt = new Date(); + const serverCaExpiration = new Date(new Date().setFullYear(2045)); + const serverCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const serverCaSkObj = crypto.nativeCrypto.KeyObject.from(serverCaKeys.privateKey); + const serverCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: serverCaSerialNumber, + subject: `O=${orgId},CN=Infisical Gateway Server CA`, + issuer: rootCaCert.subject, + notBefore: serverCaIssuedAt, + notAfter: serverCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: serverCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(serverCaKeys.publicKey) + ] + }); + + // generate client CA + const clientCaSerialNumber = createSerialNumber(); + const clientCaIssuedAt = new Date(); + const clientCaExpiration = new Date(new Date().setFullYear(2045)); + const clientCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const clientCaSkObj = crypto.nativeCrypto.KeyObject.from(clientCaKeys.privateKey); + const clientCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: clientCaSerialNumber, + subject: `O=${orgId},CN=Infisical Gateway Client CA`, + issuer: rootCaCert.subject, + notBefore: clientCaIssuedAt, + notAfter: clientCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: clientCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(clientCaKeys.publicKey) + ] + }); + + const encryptedRootGatewayCaPrivateKey = orgKmsEncryptor({ + plainText: Buffer.from( + rootCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + }).cipherTextBlob; + const encryptedRootGatewayCaCertificate = orgKmsEncryptor({ + plainText: Buffer.from(rootCaCert.rawData) + }).cipherTextBlob; + + const encryptedGatewayServerCaPrivateKey = orgKmsEncryptor({ + plainText: Buffer.from(serverCaSkObj.export({ type: "pkcs8", format: "der" })) + }).cipherTextBlob; + const encryptedGatewayServerCaCertificate = orgKmsEncryptor({ + plainText: Buffer.from(serverCaCert.rawData) + }).cipherTextBlob; + const encryptedGatewayServerCaCertificateChain = orgKmsEncryptor({ + plainText: Buffer.from(constructPemChainFromCerts([rootCaCert])) + }).cipherTextBlob; + + const encryptedGatewayClientCaPrivateKey = orgKmsEncryptor({ + plainText: Buffer.from(clientCaSkObj.export({ type: "pkcs8", format: "der" })) + }).cipherTextBlob; + const encryptedGatewayClientCaCertificate = orgKmsEncryptor({ + plainText: Buffer.from(clientCaCert.rawData) + }).cipherTextBlob; + const encryptedGatewayClientCaCertificateChain = orgKmsEncryptor({ + plainText: Buffer.from(constructPemChainFromCerts([rootCaCert])) + }).cipherTextBlob; + + return orgGatewayConfigV2DAL.create({ + orgId, + encryptedRootGatewayCaPrivateKey, + encryptedRootGatewayCaCertificate, + encryptedGatewayServerCaPrivateKey, + encryptedGatewayServerCaCertificate, + encryptedGatewayServerCaCertificateChain, + encryptedGatewayClientCaPrivateKey, + encryptedGatewayClientCaCertificate, + encryptedGatewayClientCaCertificateChain + }); + }); + + const rootGatewayCaPrivateKey = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedRootGatewayCaPrivateKey }); + const rootGatewayCaCertificate = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedRootGatewayCaCertificate }); + + const gatewayServerCaPrivateKey = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedGatewayServerCaPrivateKey }); + const gatewayServerCaCertificate = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedGatewayServerCaCertificate }); + const gatewayServerCaCertificateChain = orgKmsDecryptor({ + cipherTextBlob: orgCAs.encryptedGatewayServerCaCertificateChain + }); + + const gatewayClientCaPrivateKey = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedGatewayClientCaPrivateKey }); + const gatewayClientCaCertificate = orgKmsDecryptor({ + cipherTextBlob: orgCAs.encryptedGatewayClientCaCertificate + }); + const gatewayClientCaCertificateChain = orgKmsDecryptor({ + cipherTextBlob: orgCAs.encryptedGatewayClientCaCertificateChain + }); + + return { + rootGatewayCaPrivateKey, + rootGatewayCaCertificate, + gatewayServerCaPrivateKey, + gatewayServerCaCertificate, + gatewayServerCaCertificateChain, + gatewayClientCaPrivateKey, + gatewayClientCaCertificate, + gatewayClientCaCertificateChain + }; + }; + + const registerGateway = async ({ orgId, proxyName }: { orgId: string; actorId: string; proxyName: string }) => { + const orgCAs = await $getOrgCAs(orgId); + + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + const gatewayServerCaCert = new x509.X509Certificate(orgCAs.gatewayServerCaCertificate); + const rootGatewayCaCert = new x509.X509Certificate(orgCAs.rootGatewayCaCertificate); + + const gatewayServerCaSkObj = crypto.nativeCrypto.createPrivateKey({ + key: orgCAs.gatewayServerCaPrivateKey, + format: "der", + type: "pkcs8" + }); + const gatewayServerCaPrivateKey = await crypto.nativeCrypto.subtle.importKey( + "pkcs8", + gatewayServerCaSkObj.export({ format: "der", type: "pkcs8" }), + alg, + true, + ["sign"] + ); + + const gatewayServerKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const gatewayServerCertIssuedAt = new Date(); + const gatewayServerCertExpireAt = new Date(new Date().setMonth(new Date().getMonth() + 1)); + const gatewayServerCertPrivateKey = crypto.nativeCrypto.KeyObject.from(gatewayServerKeys.privateKey); + + const gatewayServerCertExtensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(gatewayServerCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(gatewayServerKeys.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) + ]; + + const gatewayServerSerialNumber = createSerialNumber(); + const gatewayServerCertificate = await x509.X509CertificateGenerator.create({ + serialNumber: gatewayServerSerialNumber, + subject: `O=${orgId},CN=Gateway`, + issuer: gatewayServerCaCert.subject, + notBefore: gatewayServerCertIssuedAt, + notAfter: gatewayServerCertExpireAt, + signingKey: gatewayServerCaPrivateKey, + publicKey: gatewayServerKeys.publicKey, + signingAlgorithm: alg, + extensions: gatewayServerCertExtensions + }); + + const proxyCredentials = await proxyService.generateSshCredentialsForGateway({ + proxyName, + orgId + }); + + return { + pki: { + serverCertificate: gatewayServerCertificate.toString("pem"), + serverCertificateChain: constructPemChainFromCerts([gatewayServerCaCert, rootGatewayCaCert]), + serverPrivateKey: gatewayServerCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(), + clientCA: rootGatewayCaCert.toString("pem") + }, + ssh: { + clientCertificate: proxyCredentials.clientSshCert, + clientPrivateKey: proxyCredentials.clientSshPrivateKey, + serverCAPublicKey: proxyCredentials.serverCAPublicKey + } + }; + }; + + return { + registerGateway + }; +}; diff --git a/backend/src/ee/services/gateway-v2/org-gateway-config-v2-dal.ts b/backend/src/ee/services/gateway-v2/org-gateway-config-v2-dal.ts new file mode 100644 index 000000000..8f16d798a --- /dev/null +++ b/backend/src/ee/services/gateway-v2/org-gateway-config-v2-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 TOrgGatewayConfigV2DALFactory = ReturnType; + +export const orgGatewayConfigV2DalFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.OrgGatewayConfigV2); + + return orm; +}; diff --git a/backend/src/ee/services/proxy/proxy-dal.ts b/backend/src/ee/services/proxy/proxy-dal.ts new file mode 100644 index 000000000..a1570f2a8 --- /dev/null +++ b/backend/src/ee/services/proxy/proxy-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 TProxyDALFactory = ReturnType; + +export const proxyDalFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.Proxy); + + return orm; +}; diff --git a/backend/src/ee/services/proxy/proxy-fns.ts b/backend/src/ee/services/proxy/proxy-fns.ts new file mode 100644 index 000000000..588a7b2ba --- /dev/null +++ b/backend/src/ee/services/proxy/proxy-fns.ts @@ -0,0 +1,3 @@ +export const isInstanceProxy = (proxyName: string) => { + return proxyName.startsWith("infisical-"); +}; diff --git a/backend/src/ee/services/proxy/proxy-service.ts b/backend/src/ee/services/proxy/proxy-service.ts index e3e5fb921..65ec9069e 100644 --- a/backend/src/ee/services/proxy/proxy-service.ts +++ b/backend/src/ee/services/proxy/proxy-service.ts @@ -1,7 +1,9 @@ import * as x509 from "@peculiar/x509"; +import { TProxies } from "@app/db/schemas"; import { PgSqlLock } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { @@ -9,12 +11,15 @@ import { 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 { createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns"; import { SshCertType } from "../ssh/ssh-certificate-authority-types"; import { SshCertKeyAlgorithm } from "../ssh-certificate/ssh-certificate-types"; import { TInstanceProxyConfigDALFactory } from "./instance-proxy-config-dal"; import { TOrgProxyConfigDALFactory } from "./org-proxy-config-dal"; +import { TProxyDALFactory } from "./proxy-dal"; +import { isInstanceProxy } from "./proxy-fns"; export type TProxyServiceFactory = ReturnType; @@ -23,10 +28,12 @@ const INSTANCE_PROXY_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; export const proxyServiceFactory = ({ instanceProxyConfigDAL, orgProxyConfigDAL, + proxyDAL, kmsService }: { instanceProxyConfigDAL: TInstanceProxyConfigDALFactory; orgProxyConfigDAL: TOrgProxyConfigDALFactory; + proxyDAL: TProxyDALFactory; kmsService: TKmsServiceFactory; }) => { const $getInstanceCAs = async () => { @@ -36,8 +43,7 @@ export const proxyServiceFactory = ({ await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.InstanceProxyConfigInit()]); - const rootCaKeyAlgorithm = CertKeyAlgorithm.RSA_2048; - const alg = keyAlgorithmToAlgCfg(rootCaKeyAlgorithm); + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); const rootCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); // generate root CA @@ -370,21 +376,303 @@ export const proxyServiceFactory = ({ }; }; - const registerProxy = async ({ ip }: { ip: string }) => { - // initialize instance CAs if not yet initialized + const $getOrgCAs = async (orgId: string) => { const instanceCAs = await $getInstanceCAs(); + const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId + }); - // TODO: check if identity used already has an existing proxy. If the same IP, return the existing proxy. If not, create a new proxy and overwrite + const orgProxyConfig = await orgProxyConfigDAL.transaction(async (tx) => { + const existingOrgProxyConfig = await orgProxyConfigDAL.findOne( + { + orgId + }, + tx + ); - // generate proxy server PKI certificate + if (existingOrgProxyConfig) { + return existingOrgProxyConfig; + } + + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgProxyConfigInit(orgId)]); + + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + const orgProxyCaCert = new x509.X509Certificate(instanceCAs.orgProxyPkiCaCertificate); + const rootProxyCaCert = new x509.X509Certificate(instanceCAs.rootProxyPkiCaCertificate); + const orgProxyCaSkObj = crypto.nativeCrypto.createPrivateKey({ + key: instanceCAs.orgProxyPkiCaPrivateKey, + format: "der", + type: "pkcs8" + }); + const orgProxyClientCaPrivateKey = await crypto.nativeCrypto.subtle.importKey( + "pkcs8", + orgProxyCaSkObj.export({ format: "der", type: "pkcs8" }), + alg, + true, + ["sign"] + ); + + // generate org proxy client CA + const orgProxyClientCaSerialNumber = createSerialNumber(); + const orgProxyClientCaIssuedAt = new Date(); + const orgProxyClientCaExpiration = new Date(new Date().setFullYear(2045)); + const orgProxyClientCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const orgProxyClientCaSkObj = crypto.nativeCrypto.KeyObject.from(orgProxyClientCaKeys.privateKey); + const orgProxyClientCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: orgProxyClientCaSerialNumber, + subject: `O=${orgId},CN=Infisical Org Proxy Client CA`, + issuer: orgProxyCaCert.subject, + notBefore: orgProxyClientCaIssuedAt, + notAfter: orgProxyClientCaExpiration, + signingKey: orgProxyClientCaPrivateKey, + publicKey: orgProxyClientCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(orgProxyCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(orgProxyClientCaKeys.publicKey) + ] + }); + const orgProxyClientCaChain = constructPemChainFromCerts([orgProxyCaCert, rootProxyCaCert]); + + // generate org SSH CA + const orgSshServerCaKeyPair = await createSshKeyPair(SshCertKeyAlgorithm.RSA_2048); + const orgSshClientCaKeyPair = await createSshKeyPair(SshCertKeyAlgorithm.RSA_2048); + + // generate org proxy server CA + const orgProxyServerCaSerialNumber = createSerialNumber(); + const orgProxyServerCaIssuedAt = new Date(); + const orgProxyServerCaExpiration = new Date(new Date().setFullYear(2045)); + const orgProxyServerCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const orgProxyServerCaSkObj = crypto.nativeCrypto.KeyObject.from(orgProxyServerCaKeys.privateKey); + const orgProxyServerCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: orgProxyServerCaSerialNumber, + subject: `O=${orgId},CN=Infisical Org Proxy Server CA`, + issuer: orgProxyCaCert.subject, + notBefore: orgProxyServerCaIssuedAt, + notAfter: orgProxyServerCaExpiration, + signingKey: orgProxyClientCaPrivateKey, + publicKey: orgProxyServerCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(orgProxyCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(orgProxyServerCaKeys.publicKey) + ] + }); + const orgProxyServerCaChain = constructPemChainFromCerts([orgProxyCaCert, rootProxyCaCert]); + + const encryptedProxyPkiClientCaPrivateKey = orgKmsEncryptor({ + plainText: Buffer.from( + orgProxyClientCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + }).cipherTextBlob; + const encryptedProxyPkiClientCaCertificate = orgKmsEncryptor({ + plainText: Buffer.from(orgProxyClientCaCert.rawData) + }).cipherTextBlob; + + const encryptedProxyPkiClientCaCertificateChain = orgKmsEncryptor({ + plainText: Buffer.from(orgProxyClientCaChain) + }).cipherTextBlob; + + const encryptedProxyPkiServerCaPrivateKey = orgKmsEncryptor({ + plainText: Buffer.from( + orgProxyServerCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + }).cipherTextBlob; + const encryptedProxyPkiServerCaCertificate = orgKmsEncryptor({ + plainText: Buffer.from(orgProxyServerCaCert.rawData) + }).cipherTextBlob; + const encryptedProxyPkiServerCaCertificateChain = orgKmsEncryptor({ + plainText: Buffer.from(orgProxyServerCaChain) + }).cipherTextBlob; + + const encryptedProxySshClientCaPublicKey = orgKmsEncryptor({ + plainText: Buffer.from(orgSshClientCaKeyPair.publicKey) + }).cipherTextBlob; + const encryptedProxySshClientCaPrivateKey = orgKmsEncryptor({ + plainText: Buffer.from(orgSshClientCaKeyPair.privateKey) + }).cipherTextBlob; + + const encryptedProxySshServerCaPublicKey = orgKmsEncryptor({ + plainText: Buffer.from(orgSshServerCaKeyPair.publicKey) + }).cipherTextBlob; + const encryptedProxySshServerCaPrivateKey = orgKmsEncryptor({ + plainText: Buffer.from(orgSshServerCaKeyPair.privateKey) + }).cipherTextBlob; + + return orgProxyConfigDAL.create({ + orgId, + encryptedProxyPkiClientCaPrivateKey, + encryptedProxyPkiClientCaCertificate, + encryptedProxyPkiClientCaCertificateChain, + encryptedProxyPkiServerCaPrivateKey, + encryptedProxyPkiServerCaCertificate, + encryptedProxyPkiServerCaCertificateChain, + encryptedProxySshClientCaPublicKey, + encryptedProxySshClientCaPrivateKey, + encryptedProxySshServerCaPublicKey, + encryptedProxySshServerCaPrivateKey + }); + }); + + const proxyPkiClientCaPrivateKey = orgKmsDecryptor({ + cipherTextBlob: orgProxyConfig.encryptedProxyPkiClientCaPrivateKey + }); + const proxyPkiClientCaCertificate = orgKmsDecryptor({ + cipherTextBlob: orgProxyConfig.encryptedProxyPkiClientCaCertificate + }); + const proxyPkiClientCaCertificateChain = orgKmsDecryptor({ + cipherTextBlob: orgProxyConfig.encryptedProxyPkiClientCaCertificateChain + }); + + const proxyPkiServerCaPrivateKey = orgKmsDecryptor({ + cipherTextBlob: orgProxyConfig.encryptedProxyPkiServerCaPrivateKey + }); + const proxyPkiServerCaCertificate = orgKmsDecryptor({ + cipherTextBlob: orgProxyConfig.encryptedProxyPkiServerCaCertificate + }); + const proxyPkiServerCaCertificateChain = orgKmsDecryptor({ + cipherTextBlob: orgProxyConfig.encryptedProxyPkiServerCaCertificateChain + }); + + const proxySshClientCaPublicKey = orgKmsDecryptor({ + cipherTextBlob: orgProxyConfig.encryptedProxySshClientCaPublicKey + }); + const proxySshClientCaPrivateKey = orgKmsDecryptor({ + cipherTextBlob: orgProxyConfig.encryptedProxySshClientCaPrivateKey + }); + + const proxySshServerCaPublicKey = orgKmsDecryptor({ + cipherTextBlob: orgProxyConfig.encryptedProxySshServerCaPublicKey + }); + const proxySshServerCaPrivateKey = orgKmsDecryptor({ + cipherTextBlob: orgProxyConfig.encryptedProxySshServerCaPrivateKey + }); + + return { + proxyPkiClientCaPrivateKey, + proxyPkiClientCaCertificate, + proxyPkiClientCaCertificateChain, + proxyPkiServerCaPrivateKey, + proxyPkiServerCaCertificate, + proxyPkiServerCaCertificateChain, + proxySshClientCaPublicKey, + proxySshClientCaPrivateKey, + proxySshServerCaPublicKey, + proxySshServerCaPrivateKey + }; + }; + + const generateSshCredentialsForGateway = async ({ proxyName, orgId }: { proxyName: string; orgId: string }) => { + let proxy: TProxies | null; + if (isInstanceProxy(proxyName)) { + proxy = await proxyDAL.findOne({ + name: proxyName + }); + } else { + proxy = await proxyDAL.findOne({ + orgId, + name: proxyName + }); + } + + if (!proxy) { + throw new NotFoundError({ + message: "Proxy not found" + }); + } + + const keyAlgorithm = SshCertKeyAlgorithm.RSA_2048; + const { publicKey: proxyClientSshPublicKey, privateKey: proxyClientSshPrivateKey } = + await createSshKeyPair(keyAlgorithm); + + if (isInstanceProxy(proxyName)) { + const instanceCAs = await $getInstanceCAs(); + const proxyClientSshCert = await createSshCert({ + caPrivateKey: instanceCAs.instanceProxySshServerCaPrivateKey.toString("utf8"), + clientPublicKey: proxyClientSshPublicKey, + keyId: `proxy-client-${proxy.id}`, + principals: [orgId], + certType: SshCertType.USER, + requestedTtl: "30d" + }); + + return { + clientSshCert: proxyClientSshCert.signedPublicKey, + clientSshPrivateKey: proxyClientSshPrivateKey, + serverCAPublicKey: instanceCAs.instanceProxySshServerCaPublicKey.toString("utf8") + }; + } + + const orgCAs = await $getOrgCAs(orgId); + const proxyClientSshCert = await createSshCert({ + caPrivateKey: orgCAs.proxySshServerCaPrivateKey.toString("utf8"), + clientPublicKey: proxyClientSshPublicKey, + keyId: `proxy-client-${proxy.id}`, + principals: [orgId], + certType: SshCertType.USER, + requestedTtl: "30d" + }); + + return { + clientSshCert: proxyClientSshCert.signedPublicKey, + clientSshPrivateKey: proxyClientSshPrivateKey, + serverCAPublicKey: orgCAs.proxySshServerCaPublicKey.toString("utf8") + }; + }; + + const $generateProxyCredentials = async ({ + ip, + orgId, + rootProxyPkiCaCertificate, + proxyPkiServerCaCertificate, + proxyPkiServerCaPrivateKey, + proxySshServerCaPrivateKey, + proxyPkiServerCaCertificateChain, + proxySshClientCaPublicKey + }: { + ip: string; + rootProxyPkiCaCertificate: Buffer; + proxyPkiServerCaCertificate: Buffer; + proxyPkiServerCaPrivateKey: Buffer; + proxySshServerCaPrivateKey: Buffer; + proxyPkiServerCaCertificateChain: Buffer; + proxySshClientCaPublicKey: Buffer; + orgId?: string; + }) => { const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); - const proxyServerCaCert = new x509.X509Certificate(instanceCAs.instanceProxyPkiServerCaCertificate); - const rootProxyCaCert = new x509.X509Certificate(instanceCAs.rootProxyPkiCaCertificate); + const proxyServerCaCert = new x509.X509Certificate(proxyPkiServerCaCertificate); + const rootProxyCaCert = new x509.X509Certificate(rootProxyPkiCaCertificate); const proxyServerCaSkObj = crypto.nativeCrypto.createPrivateKey({ - key: instanceCAs.instanceProxyPkiServerCaPrivateKey, + key: proxyPkiServerCaPrivateKey, format: "der", type: "pkcs8" }); + const proxyServerCaPrivateKey = await crypto.nativeCrypto.subtle.importKey( "pkcs8", proxyServerCaSkObj.export({ format: "der", type: "pkcs8" }), @@ -416,7 +704,7 @@ export const proxyServiceFactory = ({ const proxyServerSerialNumber = createSerialNumber(); const proxyServerCertificate = await x509.X509CertificateGenerator.create({ serialNumber: proxyServerSerialNumber, - subject: `CN=${ip},O=Infisical,OU=Proxy`, + subject: `CN=${ip},O=${orgId ?? "Infisical"},OU=Proxy`, issuer: proxyServerCaCert.subject, notBefore: proxyServerCertIssuedAt, notAfter: proxyServerCertExpireAt, @@ -432,7 +720,7 @@ export const proxyServiceFactory = ({ await createSshKeyPair(keyAlgorithm); const proxyServerSshCert = await createSshCert({ - caPrivateKey: instanceCAs.instanceProxySshServerCaPrivateKey.toString("utf8"), + caPrivateKey: proxySshServerCaPrivateKey.toString("utf8"), clientPublicKey: proxyServerSshPublicKey, keyId: "proxy-server", principals: [ip], @@ -445,7 +733,7 @@ export const proxyServiceFactory = ({ serverCertificate: proxyServerCertificate.toString("pem"), serverCertificateChain: prependCertToPemChain( proxyServerCaCert, - instanceCAs.instanceProxyPkiServerCaCertificateChain.toString("utf8") + proxyPkiServerCaCertificateChain.toString("utf8") ), serverPrivateKey: proxyServerCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(), clientCA: rootProxyCaCert.toString("pem") @@ -453,12 +741,138 @@ export const proxyServiceFactory = ({ ssh: { serverCertificate: proxyServerSshCert.signedPublicKey, serverPrivateKey: proxyServerSshPrivateKey, - clientCAPublicKey: instanceCAs.instanceProxySshClientCaPublicKey.toString("utf8") + clientCAPublicKey: proxySshClientCaPublicKey.toString("utf8") } }; }; + const registerProxy = async ({ + ip, + name, + identityId, + orgId + }: { + ip: string; + name: string; + identityId?: string; + orgId?: string; + }) => { + let proxy: TProxies; + const isOrgProxy = identityId && orgId; + + if (isOrgProxy) { + // organization proxy + if (isInstanceProxy(name)) { + throw new BadRequestError({ + message: "Org proxy name cannot start with 'infisical-'. This is reserved for internal use." + }); + } + + proxy = await proxyDAL.transaction(async (tx) => { + const existingProxy = await proxyDAL.findOne( + { + identityId, + orgId + }, + tx + ); + + if (existingProxy && (existingProxy.ip !== ip || existingProxy.name !== name)) { + throw new BadRequestError({ + message: "Org proxy with this machine identity already exists." + }); + } + + if (!existingProxy) { + return proxyDAL.create( + { + ip, + name, + identityId, + orgId + }, + tx + ); + } + + return existingProxy; + }); + } else { + // instance proxy + if (!name.startsWith("infisical-")) { + throw new BadRequestError({ + message: "Instance proxy name must start with 'infisical-'." + }); + } + + proxy = await proxyDAL.transaction(async (tx) => { + const existingProxy = await proxyDAL.findOne( + { + name + }, + tx + ); + + if (existingProxy && existingProxy.ip !== ip) { + throw new BadRequestError({ + message: "Instance proxy with this name already exists" + }); + } + + if (!existingProxy) { + return proxyDAL.create( + { + ip, + name + }, + tx + ); + } + + return existingProxy; + }); + } + + if (isInstanceProxy(name)) { + const instanceCAs = await $getInstanceCAs(); + return $generateProxyCredentials({ + ip, + rootProxyPkiCaCertificate: instanceCAs.rootProxyPkiCaCertificate, + + proxyPkiServerCaCertificate: instanceCAs.instanceProxyPkiServerCaCertificate, + proxyPkiServerCaPrivateKey: instanceCAs.instanceProxyPkiServerCaPrivateKey, + proxyPkiServerCaCertificateChain: instanceCAs.instanceProxyPkiServerCaCertificateChain, + + proxySshServerCaPrivateKey: instanceCAs.instanceProxySshServerCaPrivateKey, + proxySshClientCaPublicKey: instanceCAs.instanceProxySshClientCaPublicKey + }); + } + + if (proxy.orgId) { + const orgCAs = await $getOrgCAs(proxy.orgId); + const instanceCAs = await $getInstanceCAs(); + + return $generateProxyCredentials({ + ip, + orgId: proxy.orgId, + rootProxyPkiCaCertificate: instanceCAs.rootProxyPkiCaCertificate, + + proxyPkiServerCaCertificate: orgCAs.proxyPkiServerCaCertificate, + proxyPkiServerCaPrivateKey: orgCAs.proxyPkiServerCaPrivateKey, + proxyPkiServerCaCertificateChain: orgCAs.proxyPkiServerCaCertificateChain, + + proxySshServerCaPrivateKey: orgCAs.proxySshServerCaPrivateKey, + proxySshClientCaPublicKey: orgCAs.proxySshClientCaPublicKey + }); + } + + throw new BadRequestError({ + message: "Unhandled proxy type" + }); + }; + return { - registerProxy + registerProxy, + generateSshCredentialsForGateway }; }; diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index c4583b574..d7b097cbc 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -14,7 +14,9 @@ export const PgSqlLock = { CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`), CreateFolder: (envId: string, projectId: string) => pgAdvisoryLockHashText(`create-folder:${envId}-${projectId}`), SshInit: (projectId: string) => pgAdvisoryLockHashText(`ssh-bootstrap:${projectId}`), - InstanceProxyConfigInit: () => pgAdvisoryLockHashText("instance-proxy-config-init") + InstanceProxyConfigInit: () => pgAdvisoryLockHashText("instance-proxy-config-init"), + OrgGatewayV2Init: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-v2-init:${orgId}`), + OrgProxyConfigInit: (orgId: string) => pgAdvisoryLockHashText(`org-proxy-config-init:${orgId}`) } as const; // all the key prefixes used must be set here to avoid conflict diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 586a69655..6e926b21e 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -233,6 +233,8 @@ const envSchema = z GATEWAY_RELAY_REALM: zpStr(z.string().optional()), GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()), + PROXY_AUTH_SECRET: zpStr(z.string().optional()), + DYNAMIC_SECRET_ALLOW_INTERNAL_IP: zodStrBool.default("false"), DYNAMIC_SECRET_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()).default( process.env.INF_APP_CONNECTION_AWS_ACCESS_KEY_ID diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 97c62b545..0d0926d35 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -121,6 +121,10 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { return; } + if (req.url.includes("/api/v1/proxies/register-instance-proxy")) { + return; + } + const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET); if (!authMode) return; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index ce2e5dac6..2952b062d 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -147,6 +147,8 @@ import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { certificateDALFactory } from "@app/services/certificate/certificate-dal"; import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; +import { gatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; +import { orgGatewayConfigV2DalFactory } from "@app/ee/services/gateway-v2/org-gateway-config-v2-dal"; import { certificateServiceFactory } from "@app/services/certificate/certificate-service"; import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; @@ -317,6 +319,7 @@ import { registerV1Routes } from "./v1"; import { initializeOauthConfigSync } from "./v1/sso-router"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; +import { proxyDalFactory } from "@app/ee/services/proxy/proxy-dal"; const histogram = monitorEventLoopDelay({ resolution: 20 }); histogram.enable(); @@ -944,6 +947,9 @@ export const registerRoutes = async ( const instanceProxyConfigDAL = instanceProxyConfigDalFactory(db); const orgProxyConfigDAL = orgProxyConfigDalFactory(db); + const proxyDAL = proxyDalFactory(db); + + const orgGatewayConfigV2DAL = orgGatewayConfigV2DalFactory(db); const certificateService = certificateServiceFactory({ certificateDAL, @@ -1969,9 +1975,16 @@ export const registerRoutes = async ( const proxyService = proxyServiceFactory({ instanceProxyConfigDAL, orgProxyConfigDAL, + proxyDAL, kmsService }); + const gatewayV2Service = gatewayV2ServiceFactory({ + kmsService, + proxyService, + orgGatewayConfigV2DAL + }); + // setup the communication with license key server await licenseService.init(); @@ -2104,7 +2117,8 @@ export const registerRoutes = async ( reminder: reminderService, bus: eventBusService, sse: sseService, - proxy: proxyService + proxy: proxyService, + gatewayV2: gatewayV2Service }); const cronJobs: CronJob[] = [];