From 1678fba06e4004d051884704b6d20d5ea38262f4 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 2 Oct 2025 22:52:57 +0800 Subject: [PATCH] misc: initial integration with local and pam proxy --- .../20251002113756_add-gateway-pam-key.ts | 19 ++ backend/src/db/schemas/gateways-v2.ts | 5 +- .../src/ee/routes/v1/pam-account-router.ts | 10 +- backend/src/ee/routes/v2/gateway-router.ts | 22 ++ .../gateway-v2/gateway-v2-constants.ts | 1 + .../services/gateway-v2/gateway-v2-service.ts | 230 +++++++++++++++++- .../pam-resource/pam-resource-service.ts | 32 ++- .../pam-resource/pam-resource-types.ts | 2 +- .../src/ee/services/relay/relay-service.ts | 16 +- backend/src/keystore/keystore.ts | 1 + backend/src/server/routes/index.ts | 3 +- 11 files changed, 314 insertions(+), 27 deletions(-) create mode 100644 backend/src/db/migrations/20251002113756_add-gateway-pam-key.ts diff --git a/backend/src/db/migrations/20251002113756_add-gateway-pam-key.ts b/backend/src/db/migrations/20251002113756_add-gateway-pam-key.ts new file mode 100644 index 000000000..b2bb10003 --- /dev/null +++ b/backend/src/db/migrations/20251002113756_add-gateway-pam-key.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasColumn(TableName.GatewayV2, "encryptedPamSessionKey"))) { + await knex.schema.alterTable(TableName.GatewayV2, (t) => { + t.binary("encryptedPamSessionKey"); + }); + } +} + +export async function down(knex: Knex): Promise { + if (await knex.schema.hasColumn(TableName.GatewayV2, "encryptedPamSessionKey")) { + await knex.schema.alterTable(TableName.GatewayV2, (t) => { + t.dropColumn("encryptedPamSessionKey"); + }); + } +} diff --git a/backend/src/db/schemas/gateways-v2.ts b/backend/src/db/schemas/gateways-v2.ts index 6aff8a168..1362793f6 100644 --- a/backend/src/db/schemas/gateways-v2.ts +++ b/backend/src/db/schemas/gateways-v2.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { zodBuffer } from "@app/lib/zod"; + import { TImmutableDBKeys } from "./models"; export const GatewaysV2Schema = z.object({ @@ -15,7 +17,8 @@ export const GatewaysV2Schema = z.object({ identityId: z.string().uuid(), relayId: z.string().uuid().nullable().optional(), name: z.string(), - heartbeat: z.date().nullable().optional() + heartbeat: z.date().nullable().optional(), + encryptedPamSessionKey: zodBuffer.nullable().optional() }); export type TGatewaysV2 = z.infer; diff --git a/backend/src/ee/routes/v1/pam-account-router.ts b/backend/src/ee/routes/v1/pam-account-router.ts index 6eb233f49..cc5cebf10 100644 --- a/backend/src/ee/routes/v1/pam-account-router.ts +++ b/backend/src/ee/routes/v1/pam-account-router.ts @@ -67,11 +67,9 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { body: z.object({ duration: z .string() - .nullable() .optional() .transform((val, ctx) => { if (val === undefined) return undefined; - if (!val || val === "permanent") return null; const parsedMs = ms(val); if (typeof parsedMs !== "number" || parsedMs <= 0) { @@ -88,8 +86,12 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { 200: z.object({ sessionId: z.string(), resourceType: z.nativeEnum(PamResource), - relayCertificate: z.string(), - gatewayCertificate: z.string(), + relayClientCertificate: z.string(), + relayClientPrivateKey: z.string(), + relayServerCertificateChain: z.string(), + gatewayClientCertificate: z.string(), + gatewayClientPrivateKey: z.string(), + gatewayServerCertificateChain: z.string(), relayHost: z.string() }) } diff --git a/backend/src/ee/routes/v2/gateway-router.ts b/backend/src/ee/routes/v2/gateway-router.ts index a7e656a64..56284729d 100644 --- a/backend/src/ee/routes/v2/gateway-router.ts +++ b/backend/src/ee/routes/v2/gateway-router.ts @@ -1,6 +1,7 @@ import z from "zod"; import { GatewaysV2Schema } from "@app/db/schemas"; +import { zodBuffer } from "@app/lib/zod"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -130,4 +131,25 @@ export const registerGatewayV2Router = async (server: FastifyZodProvider) => { return gateway; } }); + + server.route({ + method: "GET", + url: "/pam-session-key", + config: { + rateLimit: readLimit + }, + schema: { + response: { + 200: zodBuffer + } + }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const pamSessionKey = await server.services.gatewayV2.getPamSessionKey({ + orgPermission: req.permission + }); + + return pamSessionKey; + } + }); }; diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-constants.ts b/backend/src/ee/services/gateway-v2/gateway-v2-constants.ts index e67d4e890..7e41de91c 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-constants.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-constants.ts @@ -1,2 +1,3 @@ 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"; +export const PAM_INFO_OID = "1.3.6.1.4.1.12345.100.3"; 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 3dcb2418e..53e011b96 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts @@ -22,11 +22,12 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { TLicenseServiceFactory } from "../license/license-service"; +import { PamResource } from "../pam-resource/pam-resource-enums"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TRelayDALFactory } from "../relay/relay-dal"; import { TRelayServiceFactory } from "../relay/relay-service"; -import { GATEWAY_ACTOR_OID, GATEWAY_ROUTING_INFO_OID } from "./gateway-v2-constants"; +import { GATEWAY_ACTOR_OID, GATEWAY_ROUTING_INFO_OID, PAM_INFO_OID } from "./gateway-v2-constants"; import { TGatewayV2DALFactory } from "./gateway-v2-dal"; import { TOrgGatewayConfigV2DALFactory } from "./org-gateway-config-v2-dal"; @@ -268,8 +269,7 @@ export const gatewayV2ServiceFactory = ({ const getPlatformConnectionDetailsByGatewayId = async ({ gatewayId, targetHost, - targetPort, - actorMetadata + targetPort }: { gatewayId: string; targetHost: string; @@ -361,9 +361,7 @@ export const gatewayV2ServiceFactory = ({ const actorExtension = new x509.Extension( GATEWAY_ACTOR_OID, false, - Buffer.from( - JSON.stringify(actorMetadata ? { type: ActorType.PLATFORM, ...actorMetadata } : { type: ActorType.PLATFORM }) - ) + Buffer.from(JSON.stringify({ type: ActorType.PLATFORM })) ); const clientCert = await x509.X509CertificateGenerator.create({ @@ -418,6 +416,176 @@ export const gatewayV2ServiceFactory = ({ }; }; + const getPAMConnectionDetails = async ({ + gatewayId, + sessionId, + duration, + resourceType, + host, + port, + actorMetadata + }: { + gatewayId: string; + sessionId: string; + resourceType: PamResource; + duration?: number; + host: string; + port: number; + actorMetadata: { id: string; type: ActorType; name: string }; + }) => { + const gateway = await gatewayV2DAL.findById(gatewayId); + if (!gateway) { + return; + } + + const orgGatewayConfig = await orgGatewayConfigV2DAL.findOne({ orgId: gateway.orgId }); + if (!orgGatewayConfig) { + throw new NotFoundError({ message: `Gateway Config for org ${gateway.orgId} not found.` }); + } + + if (!gateway.relayId) { + throw new BadRequestError({ + message: "Gateway is not associated with a relay" + }); + } + + 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, + orgId: orgGatewayConfig.orgId + }); + + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + + const rootGatewayCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedRootGatewayCaCertificate + }) + ); + + const gatewayClientCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedGatewayClientCaCertificate + }) + ); + + const gatewayServerCaCert = new x509.X509Certificate( + orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedGatewayServerCaCertificate + }) + ); + + const gatewayClientCaPrivateKey = orgKmsDecryptor({ + cipherTextBlob: orgGatewayConfig.encryptedGatewayClientCaPrivateKey + }); + + const gatewayClientCaSkObj = crypto.nativeCrypto.createPrivateKey({ + key: gatewayClientCaPrivateKey, + format: "der", + type: "pkcs8" + }); + + const importedGatewayClientCaPrivateKey = await crypto.nativeCrypto.subtle.importKey( + "pkcs8", + gatewayClientCaSkObj.export({ format: "der", type: "pkcs8" }), + alg, + true, + ["sign"] + ); + + const clientCertIssuedAt = new Date(); + const clientCertExpiration = new Date(new Date().getTime() + (duration ?? 5 * 60 * 1000)); + const clientKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const clientCertSerialNumber = createSerialNumber(); + + const routingInfo = { + targetHost: host, + targetPort: port + }; + + const routingExtension = new x509.Extension( + GATEWAY_ROUTING_INFO_OID, + false, + Buffer.from(JSON.stringify(routingInfo)) + ); + + const pamInfoExtension = new x509.Extension( + PAM_INFO_OID, + false, + Buffer.from( + JSON.stringify({ + sessionId, + resourceType + }) + ) + ); + + const actorExtension = new x509.Extension( + GATEWAY_ACTOR_OID, + false, + Buffer.from(JSON.stringify({ type: actorMetadata.type, id: actorMetadata.id, name: actorMetadata.name })) + ); + + const clientCert = await x509.X509CertificateGenerator.create({ + serialNumber: clientCertSerialNumber, + subject: `O=${orgGatewayConfig.orgId},OU=gateway-client,CN=${actorMetadata.type}:${gatewayId}`, + issuer: gatewayClientCaCert.subject, + notAfter: clientCertExpiration, + notBefore: clientCertIssuedAt, + signingKey: importedGatewayClientCaPrivateKey, + publicKey: clientKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(gatewayClientCaCert, 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), + routingExtension, + actorExtension, + pamInfoExtension + ] + }); + + const gatewayClientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey); + + const relayCredentials = await relayService.getCredentialsForClient({ + relayId: gateway.relayId, + orgId: gateway.orgId, + orgName: gateway.orgName, + gatewayId, + gatewayName: gateway.name, + duration + }); + + return { + relayHost: relayCredentials.relayHost, + gateway: { + clientCertificate: clientCert.toString("pem"), + clientPrivateKey: gatewayClientCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(), + serverCertificateChain: constructPemChainFromCerts([gatewayServerCaCert, rootGatewayCaCert]) + }, + relay: { + clientCertificate: relayCredentials.clientCertificate, + clientPrivateKey: relayCredentials.clientPrivateKey, + serverCertificateChain: relayCredentials.serverCertificateChain + } + }; + }; + const registerGateway = async ({ orgId, actorId, @@ -652,11 +820,59 @@ export const gatewayV2ServiceFactory = ({ return gatewayV2DAL.deleteById(gateway.id); }; + const getPamSessionKey = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => { + const { permission } = await permissionService.getOrgPermission( + orgPermission.type, + orgPermission.id, + orgPermission.orgId, + orgPermission.authMethod, + orgPermission.orgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionGatewayActions.CreateGateways, + OrgPermissionSubjects.Gateway + ); + + return gatewayV2DAL.transaction(async (tx) => { + const gateway = await gatewayV2DAL.findOne( + { + identityId: orgPermission.id + }, + tx + ); + + if (!gateway) { + throw new NotFoundError({ message: "Gateway not found" }); + } + + const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: orgPermission.orgId + }); + + if (gateway.encryptedPamSessionKey) { + return decryptor({ cipherTextBlob: gateway.encryptedPamSessionKey }); + } + + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.GatewayPamSessionKey(gateway.id)]); + + const newPamSessionKey = crypto.randomBytes(32); + const { cipherTextBlob: encryptedPamSessionKey } = encryptor({ plainText: newPamSessionKey }); + + await gatewayV2DAL.updateById(gateway.id, { encryptedPamSessionKey }, tx); + + return newPamSessionKey; + }); + }; + return { listGateways, registerGateway, getPlatformConnectionDetailsByGatewayId, + getPAMConnectionDetails, deleteGatewayById, - heartbeat + heartbeat, + getPamSessionKey }; }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-service.ts b/backend/src/ee/services/pam-resource/pam-resource-service.ts index a981a9955..2cc6a1d6f 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-service.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-service.ts @@ -12,6 +12,7 @@ import { OrgServiceActor } from "@app/lib/types"; import { ActorType } from "@app/services/auth/auth-type"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TUserDALFactory } from "@app/services/user/user-dal"; import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; import { TLicenseServiceFactory } from "../license/license-service"; @@ -51,7 +52,11 @@ type TPamResourceServiceFactoryDep = { permissionService: Pick; licenseService: Pick; kmsService: Pick; - gatewayV2Service: Pick; + gatewayV2Service: Pick< + TGatewayV2ServiceFactory, + "getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId" + >; + userDAL: TUserDALFactory; }; export type TPamResourceServiceFactory = ReturnType; @@ -62,6 +67,7 @@ export const pamResourceServiceFactory = ({ pamAccountDAL, pamFolderDAL, projectDAL, + userDAL, permissionService, licenseService, kmsService, @@ -561,13 +567,19 @@ export const pamResourceServiceFactory = ({ const { connectionDetails, gatewayId, resourceType } = await decryptResource(resource, actor.orgId, kmsService); - const gatewayConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ + const user = await userDAL.findById(actor.id); + + const gatewayConnectionDetails = await gatewayV2Service.getPAMConnectionDetails({ gatewayId, - targetHost: connectionDetails.host, - targetPort: connectionDetails.port, + duration, + sessionId: session.id, + resourceType: resource.resourceType as PamResource, + host: connectionDetails.host, + port: connectionDetails.port, actorMetadata: { - sessionId: session.id, - resourceType: resource.resourceType + id: actor.id, + type: actor.type, + name: user.email ?? "" } }); @@ -578,8 +590,12 @@ export const pamResourceServiceFactory = ({ return { sessionId: session.id, resourceType, - relayCertificate: gatewayConnectionDetails.relay.clientCertificate, - gatewayCertificate: gatewayConnectionDetails.gateway.clientCertificate, + relayClientCertificate: gatewayConnectionDetails.relay.clientCertificate, + relayClientPrivateKey: gatewayConnectionDetails.relay.clientPrivateKey, + relayServerCertificateChain: gatewayConnectionDetails.relay.serverCertificateChain, + gatewayClientCertificate: gatewayConnectionDetails.gateway.clientCertificate, + gatewayClientPrivateKey: gatewayConnectionDetails.gateway.clientPrivateKey, + gatewayServerCertificateChain: gatewayConnectionDetails.gateway.serverCertificateChain, relayHost: gatewayConnectionDetails.relayHost, projectId: account.projectId }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-types.ts b/backend/src/ee/services/pam-resource/pam-resource-types.ts index 3a5f73dd6..31572fcd0 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-types.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-types.ts @@ -38,7 +38,7 @@ export type TAccessAccountDTO = { actorIp: string; actorName: string; actorUserAgent: string; - duration?: number | null; + duration?: number; }; // Resource factory diff --git a/backend/src/ee/services/relay/relay-service.ts b/backend/src/ee/services/relay/relay-service.ts index 2fef71259..696bd1f4f 100644 --- a/backend/src/ee/services/relay/relay-service.ts +++ b/backend/src/ee/services/relay/relay-service.ts @@ -708,7 +708,8 @@ export const relayServiceFactory = ({ relayPkiClientCaCertificate, relayPkiClientCaPrivateKey, relayPkiServerCaCertificate, - relayPkiServerCaCertificateChain + relayPkiServerCaCertificateChain, + duration }: { gatewayId: string; gatewayName: string; @@ -718,6 +719,7 @@ export const relayServiceFactory = ({ relayPkiClientCaPrivateKey: Buffer; relayPkiServerCaCertificate: Buffer; relayPkiServerCaCertificateChain: Buffer; + duration?: number; }) => { const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); const relayClientCaCert = new x509.X509Certificate(relayPkiClientCaCertificate); @@ -737,7 +739,7 @@ export const relayServiceFactory = ({ ); const clientCertIssuedAt = new Date(); - const clientCertExpiration = new Date(new Date().getTime() + 5 * 60 * 1000); + const clientCertExpiration = new Date(new Date().getTime() + (duration ?? 5 * 60 * 1000)); const clientKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); const clientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey); const clientCertSerialNumber = createSerialNumber(); @@ -866,13 +868,15 @@ export const relayServiceFactory = ({ orgId, orgName, gatewayId, - gatewayName + gatewayName, + duration }: { relayId: string; orgId: string; orgName: string; gatewayId: string; gatewayName: string; + duration?: number; }) => { const relay = await relayDAL.findOne({ id: relayId @@ -896,7 +900,8 @@ export const relayServiceFactory = ({ relayPkiClientCaCertificate: instanceCAs.instanceRelayPkiClientCaCertificate, relayPkiClientCaPrivateKey: instanceCAs.instanceRelayPkiClientCaPrivateKey, relayPkiServerCaCertificate: instanceCAs.instanceRelayPkiServerCaCertificate, - relayPkiServerCaCertificateChain: instanceCAs.instanceRelayPkiServerCaCertificateChain + relayPkiServerCaCertificateChain: instanceCAs.instanceRelayPkiServerCaCertificateChain, + duration }); return { @@ -914,7 +919,8 @@ export const relayServiceFactory = ({ relayPkiClientCaCertificate: orgCAs.relayPkiClientCaCertificate, relayPkiClientCaPrivateKey: orgCAs.relayPkiClientCaPrivateKey, relayPkiServerCaCertificate: orgCAs.relayPkiServerCaCertificate, - relayPkiServerCaCertificateChain: orgCAs.relayPkiServerCaCertificateChain + relayPkiServerCaCertificateChain: orgCAs.relayPkiServerCaCertificateChain, + duration }); return { diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 04ba8428b..328ae8f46 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -23,6 +23,7 @@ export const PgSqlLock = { InstanceRelayConfigInit: () => pgAdvisoryLockHashText("instance-relay-config-init"), OrgGatewayV2Init: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-v2-init:${orgId}`), OrgRelayConfigInit: (orgId: string) => pgAdvisoryLockHashText(`org-relay-config-init:${orgId}`), + GatewayPamSessionKey: (gatewayId: string) => pgAdvisoryLockHashText(`gateway-pam-session-key:${gatewayId}`), IdentityLogin: (identityId: string, nonce: string) => pgAdvisoryLockHashText(`identity-login:${identityId}:${nonce}`) } as const; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 8f968070d..c2b8eeba8 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2130,7 +2130,8 @@ export const registerRoutes = async ( permissionService, licenseService, kmsService, - gatewayV2Service + gatewayV2Service, + userDAL }); const pamSessionService = pamSessionServiceFactory({