From 879361fd7a96256945393d75015bcb5ea1982a95 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 2 Sep 2025 03:46:57 +0800 Subject: [PATCH] feat: half-way done through integrating with platform --- ...0250901091637_add-gateway-v2-id-columns.ts | 33 +++ backend/src/db/schemas/dynamic-secrets.ts | 3 +- .../db/schemas/identity-kubernetes-auths.ts | 3 +- .../dynamic-secret/dynamic-secret-service.ts | 24 +- .../dynamic-secret/providers/index.ts | 2 +- .../dynamic-secret/providers/kubernetes.ts | 52 +++- .../dynamic-secret/providers/sql-database.ts | 28 +- .../gateway-v2/gateway-v2-constants.ts | 2 + .../services/gateway-v2/gateway-v2-service.ts | 71 ++++- .../src/ee/services/proxy/proxy-service.ts | 42 ++- .../secret-rotation-v2-service.ts | 14 +- .../secret-rotation-v2-types.ts | 4 +- .../sql-credentials-rotation-fns.ts | 3 +- backend/src/lib/gateway-v2/gateway-v2.ts | 278 ++++++++++++++++++ backend/src/server/routes/index.ts | 41 +-- .../app-connection/app-connection-fns.ts | 6 +- .../app-connection/app-connection-service.ts | 23 +- .../app-connection/app-connection-types.ts | 7 +- .../shared/sql/sql-connection-fns.ts | 51 +++- .../identity-kubernetes-auth-service.ts | 107 +++++-- 20 files changed, 683 insertions(+), 111 deletions(-) create mode 100644 backend/src/db/migrations/20250901091637_add-gateway-v2-id-columns.ts create mode 100644 backend/src/ee/services/gateway-v2/gateway-v2-constants.ts create mode 100644 backend/src/lib/gateway-v2/gateway-v2.ts diff --git a/backend/src/db/migrations/20250901091637_add-gateway-v2-id-columns.ts b/backend/src/db/migrations/20250901091637_add-gateway-v2-id-columns.ts new file mode 100644 index 000000000..cb46b1261 --- /dev/null +++ b/backend/src/db/migrations/20250901091637_add-gateway-v2-id-columns.ts @@ -0,0 +1,33 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayV2Id"))) { + await knex.schema.alterTable(TableName.DynamicSecret, (table) => { + table.uuid("gatewayV2Id"); + table.foreign("gatewayV2Id").references("id").inTable(TableName.GatewayV2).onDelete("SET NULL"); + }); + } + + if (!(await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayV2Id"))) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => { + table.uuid("gatewayV2Id"); + table.foreign("gatewayV2Id").references("id").inTable(TableName.GatewayV2).onDelete("SET NULL"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayV2Id")) { + await knex.schema.alterTable(TableName.DynamicSecret, (table) => { + table.dropColumn("gatewayV2Id"); + }); + } + + if (await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayV2Id")) { + await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => { + table.dropColumn("gatewayV2Id"); + }); + } +} diff --git a/backend/src/db/schemas/dynamic-secrets.ts b/backend/src/db/schemas/dynamic-secrets.ts index 637d0c632..526239f1c 100644 --- a/backend/src/db/schemas/dynamic-secrets.ts +++ b/backend/src/db/schemas/dynamic-secrets.ts @@ -29,7 +29,8 @@ export const DynamicSecretsSchema = z.object({ encryptedInput: zodBuffer, projectGatewayId: z.string().uuid().nullable().optional(), gatewayId: z.string().uuid().nullable().optional(), - usernameTemplate: z.string().nullable().optional() + usernameTemplate: z.string().nullable().optional(), + gatewayV2Id: z.string().uuid().nullable().optional() }); export type TDynamicSecrets = z.infer; diff --git a/backend/src/db/schemas/identity-kubernetes-auths.ts b/backend/src/db/schemas/identity-kubernetes-auths.ts index deb78bf8a..4789ef365 100644 --- a/backend/src/db/schemas/identity-kubernetes-auths.ts +++ b/backend/src/db/schemas/identity-kubernetes-auths.ts @@ -32,7 +32,8 @@ export const IdentityKubernetesAuthsSchema = z.object({ encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(), gatewayId: z.string().uuid().nullable().optional(), accessTokenPeriod: z.coerce.number().default(0), - tokenReviewMode: z.string().default("api") + tokenReviewMode: z.string().default("api"), + gatewayV2Id: z.string().uuid().nullable().optional() }); export type TIdentityKubernetesAuths = z.infer; diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index fe8c98d95..279134804 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -73,6 +73,7 @@ export const dynamicSecretServiceFactory = ({ metadata, usernameTemplate }) => { + let isGatewayV1 = true; const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` }); @@ -129,6 +130,10 @@ export const dynamicSecretServiceFactory = ({ }); } + if (!gateway) { + isGatewayV1 = false; + } + const { permission: orgPermission } = await permissionService.getOrgPermission( actor, actorId, @@ -163,7 +168,8 @@ export const dynamicSecretServiceFactory = ({ defaultTTL, folderId: folder.id, name, - gatewayId: selectedGatewayId, + gatewayId: isGatewayV1 ? selectedGatewayId : undefined, + gatewayV2Id: isGatewayV1 ? undefined : selectedGatewayId, usernameTemplate }, tx @@ -274,20 +280,27 @@ export const dynamicSecretServiceFactory = ({ const updatedInput = await selectedProvider.validateProviderInputs(newInput, { projectId }); let selectedGatewayId: string | null = null; + let isGatewayV1 = true; if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) { const gatewayId = updatedInput.gatewayId as string; const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId }); - if (!gateway) { + const [gatewayv2] = await gatewayV2DAL.find({ id: gatewayId, orgId: actorOrgId }); + + if (!gateway && !gatewayv2) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found` }); } + if (!gateway) { + isGatewayV1 = false; + } + const { permission: orgPermission } = await permissionService.getOrgPermission( actor, actorId, - gateway.orgId, + actorOrgId, actorAuthMethod, actorOrgId ); @@ -297,7 +310,7 @@ export const dynamicSecretServiceFactory = ({ OrgPermissionSubjects.Gateway ); - selectedGatewayId = gateway.id; + selectedGatewayId = gateway?.id ?? gatewayv2?.id; } const isConnected = await selectedProvider.validateConnection(newInput, { projectId }); @@ -313,7 +326,8 @@ export const dynamicSecretServiceFactory = ({ defaultTTL, name: newName ?? name, status: null, - gatewayId: selectedGatewayId, + gatewayId: isGatewayV1 ? selectedGatewayId : null, + gatewayV2Id: isGatewayV1 ? null : selectedGatewayId, usernameTemplate }, tx diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index 7907a10df..3ec0f795e 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -32,7 +32,7 @@ export const buildDynamicSecretProviders = ({ gatewayService, gatewayV2Service }: TBuildDynamicSecretProviderDTO): Record => ({ - [DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }), + [DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService, gatewayV2Service }), [DynamicSecretProviders.Cassandra]: CassandraProvider(), [DynamicSecretProviders.AwsIam]: AwsIamProvider(), [DynamicSecretProviders.Redis]: RedisDatabaseProvider(), diff --git a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts index 82add3738..e60b11576 100644 --- a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts +++ b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts @@ -64,7 +64,11 @@ export const KubernetesProvider = ({ }, gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise ): Promise => { - const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId(inputs.gatewayId); + const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ + gatewayId: inputs.gatewayId, + targetHost: inputs.targetHost, + targetPort: inputs.targetPort + }); if (gatewayV2ConnectionDetails) { const callbackResult = await withGatewayV2Proxy( async (port) => { @@ -77,7 +81,9 @@ export const KubernetesProvider = ({ { proxyIp: gatewayV2ConnectionDetails.proxyIp, gateway: gatewayV2ConnectionDetails.gateway, - proxy: gatewayV2ConnectionDetails.proxy + proxy: gatewayV2ConnectionDetails.proxy, + protocol: inputs.reviewTokenThroughGateway ? GatewayProxyProtocol.Http : GatewayProxyProtocol.Tcp, + httpsAgent: inputs.httpsAgent } ); @@ -379,8 +385,18 @@ export const KubernetesProvider = ({ return true; } catch (error) { let errorMessage = error instanceof Error ? error.message : "Unknown error"; - if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) { - errorMessage = (error.response?.data as { message: string }).message; + if (axios.isAxiosError(error)) { + if (error.response) { + let { message } = error?.response?.data as unknown as { message?: string }; + + if (!message && typeof error.response.data === "string") { + message = error.response.data; + } + + if (message) { + errorMessage = message; + } + } } const sanitizedErrorMessage = sanitizeString({ @@ -629,8 +645,18 @@ export const KubernetesProvider = ({ }; } catch (error) { let errorMessage = error instanceof Error ? error.message : "Unknown error"; - if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) { - errorMessage = (error.response?.data as { message: string }).message; + if (axios.isAxiosError(error)) { + if (error.response) { + let { message } = error?.response?.data as unknown as { message?: string }; + + if (!message && typeof error.response.data === "string") { + message = error.response.data; + } + + if (message) { + errorMessage = message; + } + } } const sanitizedErrorMessage = sanitizeString({ @@ -766,8 +792,18 @@ export const KubernetesProvider = ({ } } catch (error) { let errorMessage = error instanceof Error ? error.message : "Unknown error"; - if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) { - errorMessage = (error.response?.data as { message: string }).message; + if (axios.isAxiosError(error)) { + if (error.response) { + let { message } = error?.response?.data as unknown as { message?: string }; + + if (!message && typeof error.response.data === "string") { + message = error.response.data; + } + + if (message) { + errorMessage = message; + } + } } const sanitizedErrorMessage = sanitizeString({ diff --git a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts index c8d036ce3..331a0cb25 100644 --- a/backend/src/ee/services/dynamic-secret/providers/sql-database.ts +++ b/backend/src/ee/services/dynamic-secret/providers/sql-database.ts @@ -6,10 +6,12 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError } from "@app/lib/errors"; import { sanitizeString } from "@app/lib/fn"; import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars"; import { TGatewayServiceFactory } from "../../gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service"; import { verifyHostInputValidity } from "../dynamic-secret-fns"; import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models"; import { compileUsernameTemplate } from "./templateUtils"; @@ -128,9 +130,13 @@ const generateUsername = (provider: SqlProviders, usernameTemplate?: string | nu type TSqlDatabaseProviderDTO = { gatewayService: Pick; + gatewayV2Service: Pick; }; -export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => { +export const SqlDatabaseProvider = ({ + gatewayService, + gatewayV2Service +}: TSqlDatabaseProviderDTO): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs); @@ -183,6 +189,26 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO) providerInputs: z.infer, gatewayCallback: (host: string, port: number) => Promise ) => { + const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ + gatewayId: providerInputs.gatewayId as string, + targetHost: providerInputs.host, + targetPort: providerInputs.port + }); + + if (gatewayV2ConnectionDetails) { + return withGatewayV2Proxy( + async (port) => { + await gatewayCallback("localhost", port); + }, + { + proxyIp: gatewayV2ConnectionDetails.proxyIp, + gateway: gatewayV2ConnectionDetails.gateway, + proxy: gatewayV2ConnectionDetails.proxy, + protocol: GatewayProxyProtocol.Tcp + } + ); + } + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string); const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); await withGatewayProxy( diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-constants.ts b/backend/src/ee/services/gateway-v2/gateway-v2-constants.ts new file mode 100644 index 000000000..e67d4e890 --- /dev/null +++ b/backend/src/ee/services/gateway-v2/gateway-v2-constants.ts @@ -0,0 +1,2 @@ +export const GATEWAY_ROUTING_INFO_OID = "1.3.6.1.4.1.12345.100.1"; +export const GATEWAY_ACTOR_OID = "1.3.6.1.4.1.12345.100.2"; diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts index f2f78f480..0d62927d0 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts @@ -15,14 +15,17 @@ import { import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TLicenseServiceFactory } from "../license/license-service"; import { TProxyDALFactory } from "../proxy/proxy-dal"; import { isInstanceProxy } from "../proxy/proxy-fns"; import { TProxyServiceFactory } from "../proxy/proxy-service"; +import { GATEWAY_ACTOR_OID, GATEWAY_ROUTING_INFO_OID } from "./gateway-v2-constants"; import { TGatewayV2DALFactory } from "./gateway-v2-dal"; import { TOrgGatewayConfigV2DALFactory } from "./org-gateway-config-v2-dal"; type TGatewayV2ServiceFactoryDep = { orgGatewayConfigV2DAL: Pick; + licenseService: Pick; kmsService: TKmsServiceFactory; proxyService: TProxyServiceFactory; gatewayV2DAL: TGatewayV2DALFactory; @@ -33,6 +36,7 @@ export type TGatewayV2ServiceFactory = ReturnType { + const getPlatformConnectionDetailsByGatewayId = async ({ + gatewayId, + targetHost, + targetPort + }: { + gatewayId: string; + targetHost: string; + targetPort: number; + }) => { const gateway = await gatewayV2DAL.findById(gatewayId); if (!gateway) { return; @@ -248,12 +260,12 @@ export const gatewayV2ServiceFactory = ({ }); } - // const orgLicensePlan = await licenseService.getPlan(orgGatewayConfig.orgId); - // if (!orgLicensePlan.gateway) { - // throw new BadRequestError({ - // message: "Please upgrade your instance to Infisical's Enterprise plan to use gateways." - // }); - // } + const orgLicensePlan = await licenseService.getPlan(orgGatewayConfig.orgId); + if (!orgLicensePlan.gateway) { + throw new BadRequestError({ + message: "Please upgrade your instance to Infisical's Enterprise plan to use gateways." + }); + } const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -274,6 +286,12 @@ export const gatewayV2ServiceFactory = ({ }) ); + const gatewayServerCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedGatewayServerCaCertificate + }) + ); + const gatewayClientCaPrivateKey = orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedGatewayClientCaPrivateKey }); @@ -297,6 +315,23 @@ export const gatewayV2ServiceFactory = ({ const clientKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); const clientCertSerialNumber = createSerialNumber(); + const routingInfo = { + targetHost, + targetPort + }; + + const routingExtension = new x509.Extension( + GATEWAY_ROUTING_INFO_OID, + false, + Buffer.from(JSON.stringify(routingInfo)) + ); + + const actorExtension = new x509.Extension( + GATEWAY_ACTOR_OID, + false, + Buffer.from(JSON.stringify({ type: ActorType.PLATFORM })) + ); + const clientCert = await x509.X509CertificateGenerator.create({ serialNumber: clientCertSerialNumber, subject: `O=${orgGatewayConfig.orgId},OU=gateway-client,CN=${ActorType.PLATFORM}:${gatewayId}`, @@ -318,16 +353,18 @@ export const gatewayV2ServiceFactory = ({ x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT], true ), - new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true) + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true), + routingExtension, + actorExtension ] }); + const gatewayClientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey); const proxyCredentials = await proxyService.getCredentialsForClient({ proxyId: gateway.proxyId, orgId: gateway.orgId, - gatewayId, - actor: ActorType.PLATFORM + gatewayId }); return { @@ -335,8 +372,7 @@ export const gatewayV2ServiceFactory = ({ gateway: { clientCertificate: clientCert.toString("pem"), clientPrivateKey: gatewayClientCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(), - clientCertificateChain: constructPemChainFromCerts([gatewayClientCaCert, rootGatewayCaCert]), - serverCA: rootGatewayCaCert.toString("pem") + serverCertificateChain: constructPemChainFromCerts([gatewayServerCaCert, rootGatewayCaCert]) }, proxy: { clientCertificate: proxyCredentials.clientCertificate, @@ -385,6 +421,7 @@ export const gatewayV2ServiceFactory = ({ const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); const gatewayServerCaCert = new x509.X509Certificate(orgCAs.gatewayServerCaCertificate); const rootGatewayCaCert = new x509.X509Certificate(orgCAs.rootGatewayCaCertificate); + const gatewayClientCaCert = new x509.X509Certificate(orgCAs.gatewayClientCaCertificate); const gatewayServerCaSkObj = crypto.nativeCrypto.createPrivateKey({ key: orgCAs.gatewayServerCaPrivateKey, @@ -414,7 +451,12 @@ export const gatewayV2ServiceFactory = ({ x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT], true ), - new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true) + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true), + new x509.SubjectAlternativeNameExtension([ + { type: "dns", value: "localhost" }, + { type: "ip", value: "127.0.0.1" }, + { type: "ip", value: "::1" } + ]) ]; const gatewayServerSerialNumber = createSerialNumber(); @@ -441,9 +483,8 @@ export const gatewayV2ServiceFactory = ({ proxyIp: proxyCredentials.proxyIp, pki: { serverCertificate: gatewayServerCertificate.toString("pem"), - serverCertificateChain: constructPemChainFromCerts([gatewayServerCaCert, rootGatewayCaCert]), serverPrivateKey: gatewayServerCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(), - clientCA: rootGatewayCaCert.toString("pem") + clientCertificateChain: constructPemChainFromCerts([gatewayClientCaCert, rootGatewayCaCert]) }, ssh: { clientCertificate: proxyCredentials.clientSshCert, diff --git a/backend/src/ee/services/proxy/proxy-service.ts b/backend/src/ee/services/proxy/proxy-service.ts index 2576ff1fe..96ecf3574 100644 --- a/backend/src/ee/services/proxy/proxy-service.ts +++ b/backend/src/ee/services/proxy/proxy-service.ts @@ -4,7 +4,6 @@ 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 { ActorType } from "@app/services/auth/auth-type"; import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { @@ -689,7 +688,6 @@ export const proxyServiceFactory = ({ }; const $generateProxyClientCredentials = async ({ - actor, gatewayId, orgId, proxyPkiClientCaCertificate, @@ -697,7 +695,6 @@ export const proxyServiceFactory = ({ proxyPkiServerCaCertificate, proxyPkiServerCaCertificateChain }: { - actor: ActorType; gatewayId: string; orgId: string; proxyPkiClientCaCertificate: Buffer; @@ -728,29 +725,32 @@ export const proxyServiceFactory = ({ const clientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey); const clientCertSerialNumber = createSerialNumber(); + // Build standard extensions + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(proxyClientCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(clientKeys.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 clientCert = await x509.X509CertificateGenerator.create({ serialNumber: clientCertSerialNumber, - subject: `O=${orgId},OU=proxy-client,CN=${actor}:${gatewayId}`, + subject: `O=${orgId},OU=proxy-client,CN=${gatewayId}`, issuer: proxyClientCaCert.subject, notAfter: clientCertExpiration, notBefore: clientCertIssuedAt, signingKey: importedProxyClientCaPrivateKey, publicKey: clientKeys.publicKey, signingAlgorithm: alg, - extensions: [ - new x509.BasicConstraintsExtension(false), - await x509.AuthorityKeyIdentifierExtension.create(proxyClientCaCert, false), - await x509.SubjectKeyIdentifierExtension.create(clientKeys.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) - ] + extensions }); return { @@ -834,13 +834,11 @@ export const proxyServiceFactory = ({ const getCredentialsForClient = async ({ proxyId, orgId, - gatewayId, - actor + gatewayId }: { proxyId: string; orgId: string; gatewayId: string; - actor: ActorType; }) => { const proxy = await proxyDAL.findOne({ id: proxyId @@ -855,7 +853,6 @@ export const proxyServiceFactory = ({ if (isInstanceProxy(proxy.name)) { const instanceCAs = await $getInstanceCAs(); const proxyCertificateCredentials = await $generateProxyClientCredentials({ - actor, gatewayId, orgId, proxyPkiClientCaCertificate: instanceCAs.instanceProxyPkiClientCaCertificate, @@ -872,7 +869,6 @@ export const proxyServiceFactory = ({ const orgCAs = await $getOrgCAs(orgId); const proxyCertificateCredentials = await $generateProxyClientCredentials({ - actor, gatewayId, orgId, proxyPkiClientCaCertificate: orgCAs.proxyPkiClientCaCertificate, diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts index 65f60972f..97a0f5700 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-service.ts @@ -82,6 +82,7 @@ import { import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; +import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns"; import { oktaClientSecretRotationFactory } from "./okta-client-secret/okta-client-secret-rotation-fns"; import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal"; @@ -110,6 +111,7 @@ export type TSecretRotationV2ServiceFactoryDep = { appConnectionDAL: Pick; folderCommitService: Pick; gatewayService: Pick; + gatewayV2Service: Pick; }; export type TSecretRotationV2ServiceFactory = ReturnType; @@ -153,7 +155,8 @@ export const secretRotationV2ServiceFactory = ({ queueService, folderCommitService, appConnectionDAL, - gatewayService + gatewayService, + gatewayV2Service }: TSecretRotationV2ServiceFactoryDep) => { const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => { const appCfg = getConfig(); @@ -467,7 +470,8 @@ export const secretRotationV2ServiceFactory = ({ } as TSecretRotationV2WithConnection, appConnectionDAL, kmsService, - gatewayService + gatewayService, + gatewayV2Service ); // even though we have a db constraint we want to check before any rotation of credentials is attempted @@ -831,7 +835,8 @@ export const secretRotationV2ServiceFactory = ({ } as TSecretRotationV2WithConnection, appConnectionDAL, kmsService, - gatewayService + gatewayService, + gatewayV2Service ); const generatedCredentials = await decryptSecretRotationCredentials({ @@ -915,7 +920,8 @@ export const secretRotationV2ServiceFactory = ({ } as TSecretRotationV2WithConnection, appConnectionDAL, kmsService, - gatewayService + gatewayService, + gatewayV2Service ); const updatedRotation = await rotationFactory.rotateCredentials( diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts index ab348f172..2af2ddc7b 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-types.ts @@ -6,6 +6,7 @@ import { TAppConnectionDALFactory } from "@app/services/app-connection/app-conne import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; +import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; import { TAuth0ClientSecretRotation, TAuth0ClientSecretRotationGeneratedCredentials, @@ -253,7 +254,8 @@ export type TRotationFactory< secretRotation: T, appConnectionDAL: Pick, kmsService: Pick, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => { issueCredentials: TRotationFactoryIssueCredentials; revokeCredentials: TRotationFactoryRevokeCredentials; diff --git a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts index 1da1db376..6673baab1 100644 --- a/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-fns.ts @@ -41,7 +41,7 @@ const ORACLE_PASSWORD_REQUIREMENTS = { export const sqlCredentialsRotationFactory: TRotationFactory< TSqlCredentialsRotationWithConnection, TSqlCredentialsRotationGeneratedCredentials -> = (secretRotation, _appConnectionDAL, _kmsService, gatewayService) => { +> = (secretRotation, _appConnectionDAL, _kmsService, gatewayService, gatewayV2Service) => { const { connection, parameters: { username1, username2 }, @@ -67,6 +67,7 @@ export const sqlCredentialsRotationFactory: TRotationFactory< credentials: finalCredentials }, gatewayService, + gatewayV2Service, (client) => operation(client) ); }; diff --git a/backend/src/lib/gateway-v2/gateway-v2.ts b/backend/src/lib/gateway-v2/gateway-v2.ts new file mode 100644 index 000000000..a46fdd58c --- /dev/null +++ b/backend/src/lib/gateway-v2/gateway-v2.ts @@ -0,0 +1,278 @@ +import net from "node:net"; +import tls from "node:tls"; + +import https from "https"; + +import { splitPemChain } from "@app/services/certificate/certificate-fns"; + +import { BadRequestError } from "../errors"; +import { GatewayProxyProtocol } from "../gateway/types"; +import { logger } from "../logger"; + +/* +TODOs: +- Add heartbeat tracking to gateway connection +*/ + +interface IGatewayProxyServer { + server: net.Server; + port: number; + cleanup: () => Promise; + getProxyError: () => string; +} + +const createProxyConnection = async ({ + proxyIp, + clientCertificate, + clientPrivateKey, + serverCertificateChain +}: { + proxyIp: string; + clientCertificate: string; + clientPrivateKey: string; + serverCertificateChain: string; +}): Promise => { + const [host, portStr] = proxyIp.split(":"); + const port = parseInt(portStr, 10) || 443; + + const serverCAs = splitPemChain(serverCertificateChain); + const tlsOptions: tls.ConnectionOptions = { + host, + port, + cert: clientCertificate, + key: clientPrivateKey, + ca: serverCAs, + minVersion: "TLSv1.2", + rejectUnauthorized: true + }; + + return new Promise((resolve, reject) => { + try { + const socket = tls.connect(tlsOptions, () => { + logger.info("Proxy TLS connection established successfully"); + resolve(socket); + }); + + socket.on("error", (err: Error) => { + reject(new Error(`TLS connection error: ${err.message}`)); + }); + + socket.on("close", (hadError: boolean) => { + logger.error(`TLS connection closed${hadError ? " with error" : ""}`); + }); + + socket.on("timeout", () => { + logger.error(`TLS connection timeout after 30 seconds`); + socket.destroy(); + reject(new Error("TLS connection timeout")); + }); + + socket.setTimeout(30000); + } catch (error: unknown) { + reject(new Error(`Failed to create TLS connection: ${error instanceof Error ? error.message : String(error)}`)); + } + }); +}; + +const createGatewayConnection = async ( + proxyConn: net.Socket, + gateway: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string } +): Promise => { + const tlsOptions: tls.ConnectionOptions = { + socket: proxyConn, + cert: gateway.clientCertificate, + key: gateway.clientPrivateKey, + ca: splitPemChain(gateway.serverCertificateChain), + minVersion: "TLSv1.2", + maxVersion: "TLSv1.3", + rejectUnauthorized: true + }; + + return new Promise((resolve, reject) => { + try { + const gatewaySocket = tls.connect(tlsOptions, () => { + if (!gatewaySocket.authorized) { + const error = gatewaySocket.authorizationError; + gatewaySocket.destroy(); + reject(new Error(`Gateway TLS authorization failed: ${error?.message}`)); + return; + } + + logger.info("Gateway mTLS connection established successfully"); + resolve(gatewaySocket); + }); + + gatewaySocket.on("error", (err: Error) => { + reject(new Error(`Failed to establish gateway mTLS: ${err.message}`)); + }); + + gatewaySocket.setTimeout(30000); + gatewaySocket.on("timeout", () => { + gatewaySocket.destroy(); + reject(new Error("Gateway connection timeout")); + }); + } catch (error: unknown) { + reject( + new Error(`Failed to create gateway TLS connection: ${error instanceof Error ? error.message : String(error)}`) + ); + } + }); +}; + +const setupProxyServer = async ({ + protocol, + proxyIp, + gateway, + proxy, + httpsAgent +}: { + protocol: GatewayProxyProtocol; + proxyIp: string; + gateway: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string }; + proxy: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string }; + httpsAgent?: https.Agent; +}): Promise => { + const proxyErrorMsg: string[] = []; + + return new Promise((resolve, reject) => { + const server = net.createServer(); + + server.on("connection", (clientConn) => { + void (async () => { + try { + clientConn.setKeepAlive(true, 30000); + clientConn.setNoDelay(true); + + // Stage 1: Connect to proxy relay with TLS + const proxyConn = await createProxyConnection({ + proxyIp, + clientCertificate: proxy.clientCertificate, + clientPrivateKey: proxy.clientPrivateKey, + serverCertificateChain: proxy.serverCertificateChain + }); + + // Stage 2: Establish mTLS connection to gateway through the proxy + const gatewayConn = await createGatewayConnection(proxyConn, gateway); + + let command = ""; + + // Send protocol data to gateway + if (protocol === GatewayProxyProtocol.Http) { + command += "FORWARD-HTTP"; + // extract ca certificate from httpsAgent if present + if (httpsAgent) { + const agentOptions = httpsAgent.options; + if (agentOptions && agentOptions.ca) { + const caCert = Array.isArray(agentOptions.ca) ? agentOptions.ca.join("\n") : agentOptions.ca; + const caB64 = Buffer.from(caCert as string).toString("base64"); + command += ` ca=${caB64}`; + + const rejectUnauthorized = agentOptions.rejectUnauthorized !== false; + command += ` verify=${rejectUnauthorized}`; + } + } + + command += "\n"; + } else if (protocol === GatewayProxyProtocol.Tcp) { + command += `FORWARD-TCP\n`; + } else { + throw new BadRequestError({ + message: `Invalid protocol: ${protocol as string}` + }); + } + + gatewayConn.write(Buffer.from(command)); + + // Bidirectional data forwarding + clientConn.pipe(gatewayConn); + gatewayConn.pipe(clientConn); + + // Handle connection closure + clientConn.on("close", () => { + proxyConn.destroy(); + gatewayConn.destroy(); + }); + + proxyConn.on("close", () => { + clientConn.destroy(); + gatewayConn.destroy(); + }); + + gatewayConn.on("close", () => { + clientConn.destroy(); + proxyConn.destroy(); + }); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + proxyErrorMsg.push(errorMsg); + clientConn.destroy(); + } + })(); + }); + + server.on("error", (err) => { + reject(err); + }); + + server.listen(0, () => { + const address = server.address(); + if (!address || typeof address === "string") { + server.close(); + reject(new Error("Failed to get server port")); + return; + } + + console.log(`Gateway proxy started on port ${address.port}`); + resolve({ + server, + port: address.port, + cleanup: async () => { + try { + server.close(); + } catch (err) { + console.debug("Error closing server:", err); + } + }, + getProxyError: () => proxyErrorMsg.join(",") + }); + }); + }); +}; + +export const withGatewayV2Proxy = async ( + callback: (port: number) => Promise, + options: { + protocol: GatewayProxyProtocol; + proxyIp: string; + gateway: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string }; + proxy: { clientCertificate: string; clientPrivateKey: string; serverCertificateChain: string }; + httpsAgent?: https.Agent; + } +): Promise => { + const { protocol, proxyIp, gateway, proxy, httpsAgent } = options; + + const { port, cleanup, getProxyError } = await setupProxyServer({ + protocol, + proxyIp, + gateway, + proxy, + httpsAgent + }); + + try { + // Execute the callback with the allocated port + return await callback(port); + } catch (err) { + const proxyErrorMessage = getProxyError(); + if (proxyErrorMessage) { + logger.error("Proxy error:", proxyErrorMessage); + } + logger.error("Gateway error:", err instanceof Error ? err.message : String(err)); + + const errorMessage = proxyErrorMessage || (err instanceof Error ? err.message : String(err)); + throw new Error(errorMessage); + } finally { + // Ensure cleanup happens regardless of success or failure + await cleanup(); + } +}; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index bf8e1d364..2bded6dbc 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1463,6 +1463,22 @@ export const registerRoutes = async ( smtpService }); + const proxyService = proxyServiceFactory({ + instanceProxyConfigDAL, + orgProxyConfigDAL, + proxyDAL, + kmsService + }); + + const gatewayV2Service = gatewayV2ServiceFactory({ + kmsService, + licenseService, + proxyService, + orgGatewayConfigV2DAL, + gatewayV2DAL, + proxyDAL + }); + const identityService = identityServiceFactory({ permissionService, identityDAL, @@ -1517,6 +1533,7 @@ export const registerRoutes = async ( permissionService, licenseService }); + const identityUaService = identityUaServiceFactory({ identityOrgMembershipDAL, permissionService, @@ -1533,6 +1550,8 @@ export const registerRoutes = async ( permissionService, licenseService, gatewayService, + gatewayV2Service, + gatewayV2DAL, gatewayDAL, kmsService }); @@ -1628,21 +1647,6 @@ export const registerRoutes = async ( identityAuthTemplateDAL }); - const proxyService = proxyServiceFactory({ - instanceProxyConfigDAL, - orgProxyConfigDAL, - proxyDAL, - kmsService - }); - - const gatewayV2Service = gatewayV2ServiceFactory({ - kmsService, - proxyService, - orgGatewayConfigV2DAL, - gatewayV2DAL, - proxyDAL - }); - const dynamicSecretProviders = buildDynamicSecretProviders({ gatewayService, gatewayV2Service @@ -1791,7 +1795,9 @@ export const registerRoutes = async ( kmsService, licenseService, gatewayService, - gatewayDAL + gatewayV2Service, + gatewayDAL, + gatewayV2DAL }); const secretSyncService = secretSyncServiceFactory({ @@ -1890,7 +1896,8 @@ export const registerRoutes = async ( secretQueueService, queueService, appConnectionDAL, - gatewayService + gatewayService, + gatewayV2Service }); const certificateAuthorityService = certificateAuthorityServiceFactory({ diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index 9ffc358b6..647fc74e5 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -6,6 +6,7 @@ import { } from "@app/ee/services/app-connections/oci"; import { getOracleDBConnectionListItem, OracleDBConnectionMethod } from "@app/ee/services/app-connections/oracledb"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError } from "@app/lib/errors"; @@ -213,7 +214,8 @@ export const decryptAppConnectionCredentials = async ({ export const validateAppConnectionCredentials = async ( appConnection: TAppConnectionConfig, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ): Promise => { const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record = { [AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator, @@ -257,7 +259,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Netlify]: validateNetlifyConnectionCredentials as TAppConnectionCredentialsValidator }; - return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService); + return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service); }; export const getAppConnectionMethodName = (method: TAppConnection["method"]) => { diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index a9556b6cd..73563da79 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -5,6 +5,8 @@ import { ociConnectionService } from "@app/ee/services/app-connections/oci/oci-c import { ValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2DALFactory } from "@app/ee/services/gateway-v2/gateway-v2-dal"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionAppConnectionActions, @@ -109,7 +111,9 @@ export type TAppConnectionServiceFactoryDep = { kmsService: Pick; licenseService: Pick; gatewayService: Pick; + gatewayV2Service: Pick; gatewayDAL: Pick; + gatewayV2DAL: Pick; }; export type TAppConnectionServiceFactory = ReturnType; @@ -160,7 +164,9 @@ export const appConnectionServiceFactory = ({ kmsService, licenseService, gatewayService, - gatewayDAL + gatewayV2Service, + gatewayDAL, + gatewayV2DAL }: TAppConnectionServiceFactoryDep) => { const listAppConnectionsByOrg = async (actor: OrgServiceActor, app?: AppConnection) => { const { permission } = await permissionService.getOrgPermission( @@ -264,7 +270,8 @@ export const appConnectionServiceFactory = ({ ); const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actor.orgId }); - if (!gateway) { + const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: actor.orgId }); + if (!gateway && !gatewayV2) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found for org` }); @@ -286,7 +293,8 @@ export const appConnectionServiceFactory = ({ orgId: actor.orgId, gatewayId } as TAppConnectionConfig, - gatewayService + gatewayService, + gatewayV2Service ); try { @@ -319,7 +327,8 @@ export const appConnectionServiceFactory = ({ gatewayId } as TAppConnectionConfig, (platformCredentials) => createConnection(platformCredentials), - gatewayService + gatewayService, + gatewayV2Service ); } else { connection = await createConnection(validatedCredentials); @@ -415,7 +424,8 @@ export const appConnectionServiceFactory = ({ method, gatewayId } as TAppConnectionConfig, - gatewayService + gatewayService, + gatewayV2Service ); if (!updatedCredentials) @@ -456,7 +466,8 @@ export const appConnectionServiceFactory = ({ gatewayId } as TAppConnectionConfig, (platformCredentials) => updateConnection(platformCredentials), - gatewayService + gatewayService, + gatewayV2Service ); } else { updatedConnection = await updateConnection(updatedCredentials); diff --git a/backend/src/services/app-connection/app-connection-types.ts b/backend/src/services/app-connection/app-connection-types.ts index 55511138c..526996520 100644 --- a/backend/src/services/app-connection/app-connection-types.ts +++ b/backend/src/services/app-connection/app-connection-types.ts @@ -10,6 +10,7 @@ import { TValidateOracleDBConnectionCredentialsSchema } from "@app/ee/services/app-connections/oracledb"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types"; import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; @@ -401,13 +402,15 @@ export type TListAwsConnectionIamUsers = { export type TAppConnectionCredentialsValidator = ( appConnection: TAppConnectionConfig, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => Promise; export type TAppConnectionTransitionCredentialsToPlatform = ( appConnection: TAppConnectionConfig, callback: (credentials: TAppConnection["credentials"]) => Promise, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => Promise; export type TAppConnectionBaseConfig = { diff --git a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts index b9425d7be..636d59de5 100644 --- a/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts +++ b/backend/src/services/app-connection/shared/sql/sql-connection-fns.ts @@ -2,12 +2,14 @@ import knex, { Knex } from "knex"; import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TSqlCredentialsRotationGeneratedCredentials, TSqlCredentialsRotationWithConnection } from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; import { TAppConnectionRaw, TSqlConnection } from "@app/services/app-connection/app-connection-types"; @@ -104,12 +106,49 @@ export const getSqlConnectionClient = async (appConnection: Pick( config: TSqlConnectionConfig, gatewayService: Pick, + gatewayV2Service: Pick, operation: (client: Knex) => Promise ): Promise => { const { credentials, app, gatewayId } = config; - if (gatewayId && gatewayService) { + if (gatewayId && gatewayService && gatewayV2Service) { const [targetHost] = await verifyHostInputValidity(credentials.host, true); + const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ + gatewayId, + targetHost, + targetPort: credentials.port + }); + + if (platformConnectionDetails) { + return withGatewayV2Proxy( + async (proxyPort) => { + const client = knex({ + client: SQL_CONNECTION_CLIENT_MAP[app], + connection: { + database: credentials.database, + port: proxyPort, + host: "localhost", + user: credentials.username, + password: credentials.password, + connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT, + ...getConnectionConfig({ app, credentials }) + } + }); + try { + return await operation(client); + } finally { + await client.destroy(); + } + }, + { + protocol: GatewayProxyProtocol.Tcp, + proxyIp: platformConnectionDetails.proxyIp, + gateway: platformConnectionDetails.gateway, + proxy: platformConnectionDetails.proxy + } + ); + } + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId); const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); @@ -161,10 +200,11 @@ export const executeWithPotentialGateway = async ( export const validateSqlConnectionCredentials = async ( config: TSqlConnectionConfig, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => { try { - await executeWithPotentialGateway(config, gatewayService, async (client) => { + await executeWithPotentialGateway(config, gatewayService, gatewayV2Service, async (client) => { await client.raw(config.app === AppConnection.OracleDB ? `SELECT 1 FROM DUAL` : `Select 1`); }); return config.credentials; @@ -191,14 +231,15 @@ export const SQL_CONNECTION_ALTER_LOGIN_STATEMENT: Record< export const transferSqlConnectionCredentialsToPlatform = async ( config: TSqlConnectionConfig, callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise, - gatewayService: Pick + gatewayService: Pick, + gatewayV2Service: Pick ) => { const { credentials, app } = config; const newPassword = alphaNumericNanoId(32); try { - return await executeWithPotentialGateway(config, gatewayService, (client) => { + return await executeWithPotentialGateway(config, gatewayService, gatewayV2Service, (client) => { return client.transaction(async (tx) => { await tx.raw( ...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword }) diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 9584b122a..6b0955dc8 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -6,6 +6,8 @@ import RE2 from "re2"; import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2DALFactory } from "@app/ee/services/gateway-v2/gateway-v2-dal"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionGatewayActions, @@ -21,6 +23,7 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; @@ -54,11 +57,15 @@ type TIdentityKubernetesAuthServiceFactoryDep = { licenseService: Pick; kmsService: Pick; gatewayService: TGatewayServiceFactory; + gatewayV2Service: TGatewayV2ServiceFactory; gatewayDAL: Pick; + gatewayV2DAL: Pick; }; export type TIdentityKubernetesAuthServiceFactory = ReturnType; +const GATEWAY_AUTH_DEFAULT_HOST = "https://kubernetes.default.svc.cluster.local"; + export const identityKubernetesAuthServiceFactory = ({ identityKubernetesAuthDAL, identityOrgMembershipDAL, @@ -66,7 +73,9 @@ export const identityKubernetesAuthServiceFactory = ({ permissionService, licenseService, gatewayService, + gatewayV2Service, gatewayDAL, + gatewayV2DAL, kmsService }: TIdentityKubernetesAuthServiceFactoryDep) => { const $gatewayProxyWrapper = async ( @@ -79,6 +88,42 @@ export const identityKubernetesAuthServiceFactory = ({ }, gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise ): Promise => { + const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ + gatewayId: inputs.gatewayId, + targetHost: inputs.targetHost ?? GATEWAY_AUTH_DEFAULT_HOST, + targetPort: inputs.targetPort ?? 443 + }); + + if (gatewayV2ConnectionDetails) { + let httpsAgent: https.Agent | undefined; + if (!inputs.reviewTokenThroughGateway) { + httpsAgent = new https.Agent({ + ca: inputs.caCert, + rejectUnauthorized: Boolean(inputs.caCert) + }); + } + + const callbackResult = await withGatewayV2Proxy( + async (port) => { + const res = await gatewayCallback( + inputs.reviewTokenThroughGateway ? "http://localhost" : "https://localhost", + port, + httpsAgent + ); + return res; + }, + { + protocol: inputs.reviewTokenThroughGateway ? GatewayProxyProtocol.Http : GatewayProxyProtocol.Tcp, + proxyIp: gatewayV2ConnectionDetails.proxyIp, + gateway: gatewayV2ConnectionDetails.gateway, + proxy: gatewayV2ConnectionDetails.proxy, + httpsAgent + } + ); + + return callbackResult; + } + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId); const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); @@ -277,7 +322,7 @@ export const identityKubernetesAuthServiceFactory = ({ let data: TCreateTokenReviewResponse | undefined; if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) { - if (!identityKubernetesAuth.gatewayId) { + if (!identityKubernetesAuth.gatewayId && !identityKubernetesAuth.gatewayV2Id) { throw new BadRequestError({ message: "Gateway ID is required when token review mode is set to Gateway" }); @@ -285,7 +330,7 @@ export const identityKubernetesAuthServiceFactory = ({ data = await $gatewayProxyWrapper( { - gatewayId: identityKubernetesAuth.gatewayId, + gatewayId: (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId) as string, reviewTokenThroughGateway: true }, tokenReviewCallbackThroughGateway @@ -304,17 +349,18 @@ export const identityKubernetesAuthServiceFactory = ({ const [k8sHost, k8sPort] = kubernetesHost.split(":"); - data = identityKubernetesAuth.gatewayId - ? await $gatewayProxyWrapper( - { - gatewayId: identityKubernetesAuth.gatewayId, - targetHost: k8sHost, - targetPort: k8sPort ? Number(k8sPort) : 443, - reviewTokenThroughGateway: false - }, - tokenReviewCallbackRaw - ) - : await tokenReviewCallbackRaw(); + data = + identityKubernetesAuth.gatewayId || identityKubernetesAuth.gatewayV2Id + ? await $gatewayProxyWrapper( + { + gatewayId: (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId) as string, + targetHost: k8sHost, + targetPort: k8sPort ? Number(k8sPort) : 443, + reviewTokenThroughGateway: false + }, + tokenReviewCallbackRaw + ) + : await tokenReviewCallbackRaw(); } else { throw new BadRequestError({ message: `Invalid token review mode: ${identityKubernetesAuth.tokenReviewMode}` @@ -490,14 +536,20 @@ export const identityKubernetesAuthServiceFactory = ({ return extractIPDetails(accessTokenTrustedIp.ipAddress); }); + let isGatewayV1 = true; if (gatewayId) { const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); - if (!gateway) { + const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); + if (!gateway && !gatewayV2) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found` }); } + if (!gateway) { + isGatewayV1 = false; + } + const { permission: orgPermission } = await permissionService.getOrgPermission( actor, actorId, @@ -528,7 +580,8 @@ export const identityKubernetesAuthServiceFactory = ({ accessTokenMaxTTL, accessTokenTTL, accessTokenNumUsesLimit, - gatewayId, + gatewayId: isGatewayV1 ? gatewayId : null, + gatewayV2Id: isGatewayV1 ? null : gatewayId, accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps), encryptedKubernetesTokenReviewerJwt: tokenReviewerJwt ? encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob @@ -608,14 +661,21 @@ export const identityKubernetesAuthServiceFactory = ({ return extractIPDetails(accessTokenTrustedIp.ipAddress); }); + let isGatewayV1 = true; if (gatewayId) { const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); - if (!gateway) { + const [gatewayV2] = await gatewayV2DAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId }); + + if (!gateway && !gatewayV2) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found` }); } + if (!gateway) { + isGatewayV1 = false; + } + const { permission: orgPermission } = await permissionService.getOrgPermission( actor, actorId, @@ -629,13 +689,18 @@ export const identityKubernetesAuthServiceFactory = ({ ); } + const shouldUpdateGatewayId = Boolean(gatewayId); + const gatewayIdValue = isGatewayV1 ? gatewayId : null; + const gatewayV2IdValue = isGatewayV1 ? null : gatewayId; + const updateQuery: TIdentityKubernetesAuthsUpdate = { kubernetesHost, tokenReviewMode, allowedNamespaces, allowedNames, allowedAudience, - gatewayId, + gatewayId: shouldUpdateGatewayId ? gatewayIdValue : undefined, + gatewayV2Id: shouldUpdateGatewayId ? gatewayV2IdValue : undefined, accessTokenMaxTTL, accessTokenTTL, accessTokenNumUsesLimit, @@ -730,7 +795,13 @@ export const identityKubernetesAuthServiceFactory = ({ }).toString(); } - return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId }; + return { + ...identityKubernetesAuth, + caCert, + tokenReviewerJwt, + orgId: identityMembershipOrg.orgId, + gatewayId: identityKubernetesAuth.gatewayId ?? identityKubernetesAuth.gatewayV2Id + }; }; const revokeIdentityKubernetesAuth = async ({