misc: initial integration with local and pam proxy

This commit is contained in:
Sheen Capadngan
2025-10-02 22:52:57 +08:00
parent fc8baca7ce
commit 1678fba06e
11 changed files with 314 additions and 27 deletions

View File

@@ -0,0 +1,19 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
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<void> {
if (await knex.schema.hasColumn(TableName.GatewayV2, "encryptedPamSessionKey")) {
await knex.schema.alterTable(TableName.GatewayV2, (t) => {
t.dropColumn("encryptedPamSessionKey");
});
}
}

View File

@@ -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<typeof GatewaysV2Schema>;

View File

@@ -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()
})
}

View File

@@ -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;
}
});
};

View File

@@ -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";

View File

@@ -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
};
};

View File

@@ -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<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
gatewayV2Service: Pick<
TGatewayV2ServiceFactory,
"getPAMConnectionDetails" | "getPlatformConnectionDetailsByGatewayId"
>;
userDAL: TUserDALFactory;
};
export type TPamResourceServiceFactory = ReturnType<typeof pamResourceServiceFactory>;
@@ -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
};

View File

@@ -38,7 +38,7 @@ export type TAccessAccountDTO = {
actorIp: string;
actorName: string;
actorUserAgent: string;
duration?: number | null;
duration?: number;
};
// Resource factory

View File

@@ -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 {

View File

@@ -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;

View File

@@ -2130,7 +2130,8 @@ export const registerRoutes = async (
permissionService,
licenseService,
kmsService,
gatewayV2Service
gatewayV2Service,
userDAL
});
const pamSessionService = pamSessionServiceFactory({