diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index f2b768eb6..da75c8d94 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -101,6 +101,9 @@ import { TGateways, TGatewaysInsert, TGatewaysUpdate, + TGatewaysV2, + TGatewaysV2Insert, + TGatewaysV2Update, TGitAppInstallSessions, TGitAppInstallSessionsInsert, TGitAppInstallSessionsUpdate, @@ -1282,5 +1285,6 @@ declare module "knex/types/tables" { TOrgGatewayConfigV2Update >; [TableName.Proxy]: KnexOriginal.CompositeTableType; + [TableName.GatewayV2]: KnexOriginal.CompositeTableType; } } diff --git a/backend/src/db/migrations/20250825131627_add-gateway-v2-pki-and-ssh-configs.ts b/backend/src/db/migrations/20250825131627_add-gateway-v2-pki-and-ssh-configs.ts index e35948ef1..c21d739b8 100644 --- a/backend/src/db/migrations/20250825131627_add-gateway-v2-pki-and-ssh-configs.ts +++ b/backend/src/db/migrations/20250825131627_add-gateway-v2-pki-and-ssh-configs.ts @@ -110,14 +110,14 @@ export async function up(knex: Knex): Promise { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.timestamps(true, true, true); - t.uuid("orgId"); + t.uuid("orgId").notNullable(); t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); - t.uuid("identityId").unique(); + t.uuid("identityId").notNullable().unique(); t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); t.uuid("proxyId"); - t.foreign("proxyId").references("id").inTable(TableName.Proxy).onDelete("CASCADE"); + t.foreign("proxyId").references("id").inTable(TableName.Proxy).onDelete("SET NULL"); t.string("name").notNullable().unique(); }); @@ -136,9 +136,9 @@ export async function down(knex: Knex): Promise { await dropOnUpdateTrigger(knex, TableName.OrgGatewayConfigV2); await knex.schema.dropTableIfExists(TableName.OrgGatewayConfigV2); - await dropOnUpdateTrigger(knex, TableName.Proxy); - await knex.schema.dropTableIfExists(TableName.Proxy); - await dropOnUpdateTrigger(knex, TableName.GatewayV2); await knex.schema.dropTableIfExists(TableName.GatewayV2); + + await dropOnUpdateTrigger(knex, TableName.Proxy); + await knex.schema.dropTableIfExists(TableName.Proxy); } diff --git a/backend/src/db/schemas/gateways-v2.ts b/backend/src/db/schemas/gateways-v2.ts new file mode 100644 index 000000000..722b39361 --- /dev/null +++ b/backend/src/db/schemas/gateways-v2.ts @@ -0,0 +1,22 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const GatewaysV2Schema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + orgId: z.string().uuid(), + identityId: z.string().uuid(), + proxyId: z.string().uuid().nullable().optional(), + name: z.string() +}); + +export type TGatewaysV2 = z.infer; +export type TGatewaysV2Insert = Omit, TImmutableDBKeys>; +export type TGatewaysV2Update = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 03813ee49..5311265b5 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -31,6 +31,7 @@ export * from "./folder-commits"; export * from "./folder-tree-checkpoint-resources"; export * from "./folder-tree-checkpoints"; export * from "./gateways"; +export * from "./gateways-v2"; export * from "./git-app-install-sessions"; export * from "./git-app-org"; export * from "./github-org-sync-configs"; @@ -98,6 +99,7 @@ export * from "./project-templates"; export * from "./project-user-additional-privilege"; export * from "./project-user-membership-roles"; export * from "./projects"; +export * from "./proxies"; export * from "./rate-limit"; export * from "./resource-metadata"; export * from "./saml-configs"; @@ -165,4 +167,3 @@ export * from "./user-group-membership"; export * from "./users"; export * from "./webhooks"; export * from "./workflow-integrations"; -export * from "./proxies"; diff --git a/backend/src/ee/routes/v2/gateway-router.ts b/backend/src/ee/routes/v2/gateway-router.ts index 31130b206..64b794fad 100644 --- a/backend/src/ee/routes/v2/gateway-router.ts +++ b/backend/src/ee/routes/v2/gateway-router.ts @@ -7,23 +7,43 @@ export const registerGatewayV2Router = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/", - onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { body: z.object({ - proxyName: z.string() + proxyName: z.string(), + name: z.string() }), response: { 200: z.any() } }, + onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const gateway = await server.services.gatewayV2.registerGateway({ orgId: req.permission.orgId, proxyName: req.body.proxyName, - actorId: req.permission.id + actorId: req.permission.id, + name: req.body.name }); return gateway; } }); + + server.route({ + method: "GET", + url: "/", + schema: { + response: { + 200: z.any() + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const gateways = await server.services.gatewayV2.listGateways({ + orgPermission: req.permission + }); + + return gateways; + } + }); }; 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 73dcbe6e3..fe8c98d95 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -19,6 +19,7 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal"; import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue"; import { TGatewayDALFactory } from "../gateway/gateway-dal"; +import { TGatewayV2DALFactory } from "../gateway-v2/gateway-v2-dal"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TDynamicSecretDALFactory } from "./dynamic-secret-dal"; import { DynamicSecretStatus, TDynamicSecretServiceFactory } from "./dynamic-secret-types"; @@ -39,6 +40,7 @@ type TDynamicSecretServiceFactoryDep = { permissionService: Pick; kmsService: Pick; gatewayDAL: Pick; + gatewayV2DAL: Pick; resourceMetadataDAL: Pick; }; @@ -53,6 +55,7 @@ export const dynamicSecretServiceFactory = ({ projectDAL, kmsService, gatewayDAL, + gatewayV2DAL, resourceMetadataDAL }: TDynamicSecretServiceFactoryDep): TDynamicSecretServiceFactory => { const create: TDynamicSecretServiceFactory["create"] = async ({ @@ -118,8 +121,9 @@ export const dynamicSecretServiceFactory = ({ const gatewayId = inputs.gatewayId as string; const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId }); + const [gatewayv2] = await gatewayV2DAL.find({ id: gatewayId, orgId: actorOrgId }); - if (!gateway) { + if (!gateway && !gatewayv2) { throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found` }); @@ -128,7 +132,7 @@ export const dynamicSecretServiceFactory = ({ const { permission: orgPermission } = await permissionService.getOrgPermission( actor, actorId, - gateway.orgId, + gateway?.orgId ?? gatewayv2?.orgId, actorAuthMethod, actorOrgId ); @@ -138,7 +142,7 @@ export const dynamicSecretServiceFactory = ({ OrgPermissionSubjects.Gateway ); - selectedGatewayId = gateway.id; + selectedGatewayId = gateway?.id ?? gatewayv2?.id; } const isConnected = await selectedProvider.validateConnection(provider.inputs, { projectId }); diff --git a/backend/src/ee/services/dynamic-secret/providers/index.ts b/backend/src/ee/services/dynamic-secret/providers/index.ts index 184b9fc89..7907a10df 100644 --- a/backend/src/ee/services/dynamic-secret/providers/index.ts +++ b/backend/src/ee/services/dynamic-secret/providers/index.ts @@ -1,6 +1,7 @@ import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake"; import { TGatewayServiceFactory } from "../../gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service"; import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache"; import { AwsIamProvider } from "./aws-iam"; import { AzureEntraIDProvider } from "./azure-entra-id"; @@ -24,10 +25,12 @@ import { VerticaProvider } from "./vertica"; type TBuildDynamicSecretProviderDTO = { gatewayService: Pick; + gatewayV2Service: Pick; }; export const buildDynamicSecretProviders = ({ - gatewayService + gatewayService, + gatewayV2Service }: TBuildDynamicSecretProviderDTO): Record => ({ [DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }), [DynamicSecretProviders.Cassandra]: CassandraProvider(), @@ -44,7 +47,7 @@ export const buildDynamicSecretProviders = ({ [DynamicSecretProviders.Snowflake]: SnowflakeProvider(), [DynamicSecretProviders.Totp]: TotpProvider(), [DynamicSecretProviders.SapAse]: SapAseProvider(), - [DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }), + [DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService, gatewayV2Service }), [DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }), [DynamicSecretProviders.GcpIam]: GcpIamProvider(), [DynamicSecretProviders.Github]: GithubProvider(), diff --git a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts index 3d69c3282..82add3738 100644 --- a/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts +++ b/backend/src/ee/services/dynamic-secret/providers/kubernetes.ts @@ -5,12 +5,14 @@ import https from "https"; import { BadRequestError } from "@app/lib/errors"; import { sanitizeString } from "@app/lib/fn"; import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types"; import { TDynamicSecretKubernetesLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types"; import { TGatewayServiceFactory } from "../../gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service"; import { DynamicSecretKubernetesSchema, KubernetesAuthMethod, @@ -26,6 +28,7 @@ const GATEWAY_AUTH_DEFAULT_URL = "https://kubernetes.default.svc.cluster.local"; type TKubernetesProviderDTO = { gatewayService: Pick; + gatewayV2Service: Pick; }; const generateUsername = (usernameTemplate?: string | null) => { @@ -38,7 +41,10 @@ const generateUsername = (usernameTemplate?: string | null) => { }); }; -export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): TDynamicProviderFns => { +export const KubernetesProvider = ({ + gatewayService, + gatewayV2Service +}: TKubernetesProviderDTO): TDynamicProviderFns => { const validateProviderInputs = async (inputs: unknown) => { const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs); if (!providerInputs.gatewayId && providerInputs.url) { @@ -58,6 +64,26 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): }, gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise ): Promise => { + const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId(inputs.gatewayId); + if (gatewayV2ConnectionDetails) { + const callbackResult = await withGatewayV2Proxy( + async (port) => { + return gatewayCallback( + inputs.reviewTokenThroughGateway ? "http://localhost" : "https://localhost", + port, + inputs.httpsAgent + ); + }, + { + proxyIp: gatewayV2ConnectionDetails.proxyIp, + gateway: gatewayV2ConnectionDetails.gateway, + proxy: gatewayV2ConnectionDetails.proxy + } + ); + + return callbackResult; + } + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId); const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); diff --git a/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts b/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts new file mode 100644 index 000000000..763858de0 --- /dev/null +++ b/backend/src/ee/services/gateway-v2/gateway-v2-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TGatewayV2DALFactory = ReturnType; + +export const gatewayV2DalFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.GatewayV2); + + return orm; +}; 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 ec7617c93..f2f78f480 100644 --- a/backend/src/ee/services/gateway-v2/gateway-v2-service.ts +++ b/backend/src/ee/services/gateway-v2/gateway-v2-service.ts @@ -1,7 +1,11 @@ import * as x509 from "@peculiar/x509"; +import { TProxies } from "@app/db/schemas"; import { PgSqlLock } from "@app/keystore/keystore"; import { crypto } from "@app/lib/crypto"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { OrgServiceActor } from "@app/lib/types"; +import { ActorType } from "@app/services/auth/auth-type"; import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns"; import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { @@ -11,13 +15,18 @@ import { import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TProxyDALFactory } from "../proxy/proxy-dal"; +import { isInstanceProxy } from "../proxy/proxy-fns"; import { TProxyServiceFactory } from "../proxy/proxy-service"; +import { TGatewayV2DALFactory } from "./gateway-v2-dal"; import { TOrgGatewayConfigV2DALFactory } from "./org-gateway-config-v2-dal"; type TGatewayV2ServiceFactoryDep = { orgGatewayConfigV2DAL: Pick; kmsService: TKmsServiceFactory; proxyService: TProxyServiceFactory; + gatewayV2DAL: TGatewayV2DALFactory; + proxyDAL: TProxyDALFactory; }; export type TGatewayV2ServiceFactory = ReturnType; @@ -25,7 +34,9 @@ export type TGatewayV2ServiceFactory = ReturnType { const $getOrgCAs = async (orgId: string) => { const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({ @@ -197,11 +208,179 @@ export const gatewayV2ServiceFactory = ({ }; }; - const registerGateway = async ({ orgId, proxyName }: { orgId: string; actorId: string; proxyName: string }) => { + const listGateways = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => { + // const { permission } = await permissionService.getOrgPermission( + // orgPermission.type, + // orgPermission.id, + // orgPermission.orgId, + // orgPermission.authMethod, + // orgPermission.orgId + // ); + // ForbiddenError.from(permission).throwUnlessCan( + // OrgPermissionGatewayActions.ListGateways, + // OrgPermissionSubjects.Gateway + // ); + + const orgGatewayConfig = await orgGatewayConfigV2DAL.findOne({ orgId: orgPermission.orgId }); + if (!orgGatewayConfig) return []; + + const gateways = await gatewayV2DAL.find({ + orgId: orgPermission.orgId + }); + + return gateways; + }; + + const getPlatformConnectionDetailsByGatewayId = async (gatewayId: 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.proxyId) { + throw new BadRequestError({ + message: "Gateway is not associated with a proxy" + }); + } + + // 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 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() + 5 * 60 * 1000); + const clientKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const clientCertSerialNumber = createSerialNumber(); + + const clientCert = await x509.X509CertificateGenerator.create({ + serialNumber: clientCertSerialNumber, + subject: `O=${orgGatewayConfig.orgId},OU=gateway-client,CN=${ActorType.PLATFORM}:${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) + ] + }); + const gatewayClientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey); + + const proxyCredentials = await proxyService.getCredentialsForClient({ + proxyId: gateway.proxyId, + orgId: gateway.orgId, + gatewayId, + actor: ActorType.PLATFORM + }); + + return { + proxyIp: proxyCredentials.proxyIp, + gateway: { + clientCertificate: clientCert.toString("pem"), + clientPrivateKey: gatewayClientCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(), + clientCertificateChain: constructPemChainFromCerts([gatewayClientCaCert, rootGatewayCaCert]), + serverCA: rootGatewayCaCert.toString("pem") + }, + proxy: { + clientCertificate: proxyCredentials.clientCertificate, + clientPrivateKey: proxyCredentials.clientPrivateKey, + serverCertificateChain: proxyCredentials.serverCertificateChain + } + }; + }; + + const registerGateway = async ({ + orgId, + actorId, + proxyName, + name + }: { + orgId: string; + actorId: string; + proxyName: string; + name: string; + }) => { const orgCAs = await $getOrgCAs(orgId); - // TODO: Save gateway to DB and set Gateway ID as principal in SSH certificate - // only throw error if proxy is different from existing DB record + let proxy: TProxies; + if (isInstanceProxy(proxyName)) { + proxy = await proxyDAL.findOne({ name: proxyName }); + } else { + proxy = await proxyDAL.findOne({ orgId, name: proxyName }); + } + + if (!proxy) { + throw new Error("Proxy not found"); + } + + const [gateway] = await gatewayV2DAL.upsert( + [ + { + orgId, + name, + identityId: actorId, + proxyId: proxy.id + } + ], + ["identityId"] + ); const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); const gatewayServerCaCert = new x509.X509Certificate(orgCAs.gatewayServerCaCertificate); @@ -253,11 +432,12 @@ export const gatewayV2ServiceFactory = ({ const proxyCredentials = await proxyService.getCredentialsForGateway({ proxyName, - orgId + orgId, + gatewayId: gateway.id }); return { - // TODO: return gateway ID + gatewayId: gateway.id, proxyIp: proxyCredentials.proxyIp, pki: { serverCertificate: gatewayServerCertificate.toString("pem"), @@ -274,6 +454,8 @@ export const gatewayV2ServiceFactory = ({ }; return { - registerGateway + listGateways, + registerGateway, + getPlatformConnectionDetailsByGatewayId }; }; diff --git a/backend/src/ee/services/proxy/proxy-service.ts b/backend/src/ee/services/proxy/proxy-service.ts index 9f6c45fe4..2576ff1fe 100644 --- a/backend/src/ee/services/proxy/proxy-service.ts +++ b/backend/src/ee/services/proxy/proxy-service.ts @@ -4,6 +4,7 @@ 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 { @@ -89,7 +90,7 @@ export const proxyServiceFactory = ({ x509.KeyUsageFlags.keyEncipherment, true ), - new x509.BasicConstraintsExtension(true, 0, true), + new x509.BasicConstraintsExtension(true, 2, true), await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), await x509.SubjectKeyIdentifierExtension.create(orgProxyCaKeys.publicKey) ] @@ -120,7 +121,7 @@ export const proxyServiceFactory = ({ x509.KeyUsageFlags.keyEncipherment, true ), - new x509.BasicConstraintsExtension(true, 0, true), + new x509.BasicConstraintsExtension(true, 1, true), await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), await x509.SubjectKeyIdentifierExtension.create(instanceProxyCaKeys.publicKey) ] @@ -587,88 +588,28 @@ export const proxyServiceFactory = ({ }; }; - const getCredentialsForGateway = async ({ proxyName, orgId }: { proxyName: string; orgId: string }) => { - let proxy: TProxies | null; - if (isInstanceProxy(proxyName)) { - proxy = await proxyDAL.findOne({ - name: proxyName - }); - } else { - proxy = await proxyDAL.findOne({ - orgId, - name: proxyName - }); - } - - if (!proxy) { - throw new NotFoundError({ - message: "Proxy not found" - }); - } - - const keyAlgorithm = SshCertKeyAlgorithm.RSA_2048; - const { publicKey: proxyClientSshPublicKey, privateKey: proxyClientSshPrivateKey } = - await createSshKeyPair(keyAlgorithm); - - if (isInstanceProxy(proxyName)) { - const instanceCAs = await $getInstanceCAs(); - const proxyClientSshCert = await createSshCert({ - caPrivateKey: instanceCAs.instanceProxySshServerCaPrivateKey.toString("utf8"), - clientPublicKey: proxyClientSshPublicKey, - keyId: `proxy-client-${proxy.id}`, - principals: ["gateway ID"], // TODO: set gateway ID as principal in SSH certificate - certType: SshCertType.USER, - requestedTtl: "30d" - }); - - return { - proxyIp: proxy.ip, - clientSshCert: proxyClientSshCert.signedPublicKey, - clientSshPrivateKey: proxyClientSshPrivateKey, - serverCAPublicKey: instanceCAs.instanceProxySshServerCaPublicKey.toString("utf8") - }; - } - - const orgCAs = await $getOrgCAs(orgId); - const proxyClientSshCert = await createSshCert({ - caPrivateKey: orgCAs.proxySshServerCaPrivateKey.toString("utf8"), - clientPublicKey: proxyClientSshPublicKey, - keyId: `proxy-client-${proxy.id}`, - principals: [orgId], - certType: SshCertType.USER, - requestedTtl: "30d" - }); - - return { - proxyIp: proxy.ip, - clientSshCert: proxyClientSshCert.signedPublicKey, - clientSshPrivateKey: proxyClientSshPrivateKey, - serverCAPublicKey: orgCAs.proxySshServerCaPublicKey.toString("utf8") - }; - }; - - const $generateProxyCredentials = async ({ + const $generateProxyServerCredentials = async ({ ip, orgId, - rootProxyPkiCaCertificate, proxyPkiServerCaCertificate, proxyPkiServerCaPrivateKey, - proxySshServerCaPrivateKey, - proxyPkiServerCaCertificateChain, - proxySshClientCaPublicKey + proxyPkiClientCaCertificate, + proxyPkiClientCaCertificateChain, + proxySshClientCaPublicKey, + proxySshServerCaPrivateKey }: { ip: string; - rootProxyPkiCaCertificate: Buffer; proxyPkiServerCaCertificate: Buffer; proxyPkiServerCaPrivateKey: Buffer; + proxyPkiClientCaCertificateChain: Buffer; + proxyPkiClientCaCertificate: Buffer; proxySshServerCaPrivateKey: Buffer; - proxyPkiServerCaCertificateChain: Buffer; proxySshClientCaPublicKey: Buffer; orgId?: string; }) => { const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); const proxyServerCaCert = new x509.X509Certificate(proxyPkiServerCaCertificate); - const rootProxyCaCert = new x509.X509Certificate(rootProxyPkiCaCertificate); + const proxyClientCaCert = new x509.X509Certificate(proxyPkiClientCaCertificate); const proxyServerCaSkObj = crypto.nativeCrypto.createPrivateKey({ key: proxyPkiServerCaPrivateKey, format: "der", @@ -733,12 +674,11 @@ export const proxyServiceFactory = ({ return { pki: { serverCertificate: proxyServerCertificate.toString("pem"), - serverCertificateChain: prependCertToPemChain( - proxyServerCaCert, - proxyPkiServerCaCertificateChain.toString("utf8") - ), serverPrivateKey: proxyServerCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(), - clientCA: rootProxyCaCert.toString("pem") + clientCertificateChain: prependCertToPemChain( + proxyClientCaCert, + proxyPkiClientCaCertificateChain.toString("utf8") + ) }, ssh: { serverCertificate: proxyServerSshCert.signedPublicKey, @@ -748,6 +688,205 @@ export const proxyServiceFactory = ({ }; }; + const $generateProxyClientCredentials = async ({ + actor, + gatewayId, + orgId, + proxyPkiClientCaCertificate, + proxyPkiClientCaPrivateKey, + proxyPkiServerCaCertificate, + proxyPkiServerCaCertificateChain + }: { + actor: ActorType; + gatewayId: string; + orgId: string; + proxyPkiClientCaCertificate: Buffer; + proxyPkiClientCaPrivateKey: Buffer; + proxyPkiServerCaCertificate: Buffer; + proxyPkiServerCaCertificateChain: Buffer; + }) => { + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + const proxyClientCaCert = new x509.X509Certificate(proxyPkiClientCaCertificate); + const proxyServerCaCert = new x509.X509Certificate(proxyPkiServerCaCertificate); + const proxyClientCaSkObj = crypto.nativeCrypto.createPrivateKey({ + key: proxyPkiClientCaPrivateKey, + format: "der", + type: "pkcs8" + }); + + const importedProxyClientCaPrivateKey = await crypto.nativeCrypto.subtle.importKey( + "pkcs8", + proxyClientCaSkObj.export({ format: "der", type: "pkcs8" }), + alg, + true, + ["sign"] + ); + + const clientCertIssuedAt = new Date(); + const clientCertExpiration = new Date(new Date().getTime() + 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(); + + const clientCert = await x509.X509CertificateGenerator.create({ + serialNumber: clientCertSerialNumber, + subject: `O=${orgId},OU=proxy-client,CN=${actor}:${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) + ] + }); + + return { + clientCertificate: clientCert.toString("pem"), + clientPrivateKey: clientCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(), + serverCertificateChain: prependCertToPemChain( + proxyServerCaCert, + proxyPkiServerCaCertificateChain.toString("utf8") + ) + }; + }; + + const getCredentialsForGateway = async ({ + proxyName, + orgId, + gatewayId + }: { + proxyName: string; + orgId: string; + gatewayId: string; + }) => { + let proxy: TProxies | null; + if (isInstanceProxy(proxyName)) { + proxy = await proxyDAL.findOne({ + name: proxyName + }); + } else { + proxy = await proxyDAL.findOne({ + orgId, + name: proxyName + }); + } + + if (!proxy) { + throw new NotFoundError({ + message: "Proxy not found" + }); + } + + const keyAlgorithm = SshCertKeyAlgorithm.RSA_2048; + const { publicKey: proxyClientSshPublicKey, privateKey: proxyClientSshPrivateKey } = + await createSshKeyPair(keyAlgorithm); + + if (isInstanceProxy(proxyName)) { + const instanceCAs = await $getInstanceCAs(); + const proxyClientSshCert = await createSshCert({ + caPrivateKey: instanceCAs.instanceProxySshServerCaPrivateKey.toString("utf8"), + clientPublicKey: proxyClientSshPublicKey, + keyId: `proxy-client-${proxy.id}`, + principals: [gatewayId], + certType: SshCertType.USER, + requestedTtl: "30d" + }); + + return { + proxyIp: proxy.ip, + clientSshCert: proxyClientSshCert.signedPublicKey, + clientSshPrivateKey: proxyClientSshPrivateKey, + serverCAPublicKey: instanceCAs.instanceProxySshServerCaPublicKey.toString("utf8") + }; + } + + const orgCAs = await $getOrgCAs(orgId); + const proxyClientSshCert = await createSshCert({ + caPrivateKey: orgCAs.proxySshServerCaPrivateKey.toString("utf8"), + clientPublicKey: proxyClientSshPublicKey, + keyId: `proxy-client-${proxy.id}`, + principals: [gatewayId], + certType: SshCertType.USER, + requestedTtl: "30d" + }); + + return { + proxyIp: proxy.ip, + clientSshCert: proxyClientSshCert.signedPublicKey, + clientSshPrivateKey: proxyClientSshPrivateKey, + serverCAPublicKey: orgCAs.proxySshServerCaPublicKey.toString("utf8") + }; + }; + + const getCredentialsForClient = async ({ + proxyId, + orgId, + gatewayId, + actor + }: { + proxyId: string; + orgId: string; + gatewayId: string; + actor: ActorType; + }) => { + const proxy = await proxyDAL.findOne({ + id: proxyId + }); + + if (!proxy) { + throw new NotFoundError({ + message: "Proxy not found" + }); + } + + if (isInstanceProxy(proxy.name)) { + const instanceCAs = await $getInstanceCAs(); + const proxyCertificateCredentials = await $generateProxyClientCredentials({ + actor, + gatewayId, + orgId, + proxyPkiClientCaCertificate: instanceCAs.instanceProxyPkiClientCaCertificate, + proxyPkiClientCaPrivateKey: instanceCAs.instanceProxyPkiClientCaPrivateKey, + proxyPkiServerCaCertificate: instanceCAs.instanceProxyPkiServerCaCertificate, + proxyPkiServerCaCertificateChain: instanceCAs.instanceProxyPkiServerCaCertificateChain + }); + + return { + ...proxyCertificateCredentials, + proxyIp: proxy.ip + }; + } + + const orgCAs = await $getOrgCAs(orgId); + const proxyCertificateCredentials = await $generateProxyClientCredentials({ + actor, + gatewayId, + orgId, + proxyPkiClientCaCertificate: orgCAs.proxyPkiClientCaCertificate, + proxyPkiClientCaPrivateKey: orgCAs.proxyPkiClientCaPrivateKey, + proxyPkiServerCaCertificate: orgCAs.proxyPkiServerCaCertificate, + proxyPkiServerCaCertificateChain: orgCAs.proxyPkiServerCaCertificateChain + }); + + return { + ...proxyCertificateCredentials, + proxyIp: proxy.ip + }; + }; + const registerProxy = async ({ ip, name, @@ -837,14 +976,12 @@ export const proxyServiceFactory = ({ if (isInstanceProxy(name)) { const instanceCAs = await $getInstanceCAs(); - return $generateProxyCredentials({ + return $generateProxyServerCredentials({ ip, - rootProxyPkiCaCertificate: instanceCAs.rootProxyPkiCaCertificate, - proxyPkiServerCaCertificate: instanceCAs.instanceProxyPkiServerCaCertificate, proxyPkiServerCaPrivateKey: instanceCAs.instanceProxyPkiServerCaPrivateKey, - proxyPkiServerCaCertificateChain: instanceCAs.instanceProxyPkiServerCaCertificateChain, - + proxyPkiClientCaCertificate: instanceCAs.instanceProxyPkiClientCaCertificate, + proxyPkiClientCaCertificateChain: instanceCAs.instanceProxyPkiClientCaCertificateChain, proxySshServerCaPrivateKey: instanceCAs.instanceProxySshServerCaPrivateKey, proxySshClientCaPublicKey: instanceCAs.instanceProxySshClientCaPublicKey }); @@ -852,17 +989,13 @@ export const proxyServiceFactory = ({ if (proxy.orgId) { const orgCAs = await $getOrgCAs(proxy.orgId); - const instanceCAs = await $getInstanceCAs(); - - return $generateProxyCredentials({ + return $generateProxyServerCredentials({ ip, orgId: proxy.orgId, - rootProxyPkiCaCertificate: instanceCAs.rootProxyPkiCaCertificate, - proxyPkiServerCaCertificate: orgCAs.proxyPkiServerCaCertificate, proxyPkiServerCaPrivateKey: orgCAs.proxyPkiServerCaPrivateKey, - proxyPkiServerCaCertificateChain: orgCAs.proxyPkiServerCaCertificateChain, - + proxyPkiClientCaCertificate: orgCAs.proxyPkiClientCaCertificate, + proxyPkiClientCaCertificateChain: orgCAs.proxyPkiClientCaCertificateChain, proxySshServerCaPrivateKey: orgCAs.proxySshServerCaPrivateKey, proxySshClientCaPublicKey: orgCAs.proxySshClientCaPublicKey }); @@ -875,6 +1008,7 @@ export const proxyServiceFactory = ({ return { registerProxy, - getCredentialsForGateway + getCredentialsForGateway, + getCredentialsForClient }; }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 2952b062d..bf8e1d364 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -38,6 +38,9 @@ import { externalKmsServiceFactory } from "@app/ee/services/external-kms/externa import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal"; +import { gatewayV2DalFactory } from "@app/ee/services/gateway-v2/gateway-v2-dal"; +import { gatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; +import { orgGatewayConfigV2DalFactory } from "@app/ee/services/gateway-v2/org-gateway-config-v2-dal"; import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal"; import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { groupDALFactory } from "@app/ee/services/group/group-dal"; @@ -72,6 +75,7 @@ import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/proje import { projectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service"; import { instanceProxyConfigDalFactory } from "@app/ee/services/proxy/instance-proxy-config-dal"; import { orgProxyConfigDalFactory } from "@app/ee/services/proxy/org-proxy-config-dal"; +import { proxyDalFactory } from "@app/ee/services/proxy/proxy-dal"; import { proxyServiceFactory } from "@app/ee/services/proxy/proxy-service"; import { rateLimitDALFactory } from "@app/ee/services/rate-limit/rate-limit-dal"; import { rateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service"; @@ -147,8 +151,6 @@ import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { certificateDALFactory } from "@app/services/certificate/certificate-dal"; import { certificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; -import { gatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; -import { orgGatewayConfigV2DalFactory } from "@app/ee/services/gateway-v2/org-gateway-config-v2-dal"; import { certificateServiceFactory } from "@app/services/certificate/certificate-service"; import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { certificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; @@ -319,7 +321,6 @@ import { registerV1Routes } from "./v1"; import { initializeOauthConfigSync } from "./v1/sso-router"; import { registerV2Routes } from "./v2"; import { registerV3Routes } from "./v3"; -import { proxyDalFactory } from "@app/ee/services/proxy/proxy-dal"; const histogram = monitorEventLoopDelay({ resolution: 20 }); histogram.enable(); @@ -948,6 +949,7 @@ export const registerRoutes = async ( const instanceProxyConfigDAL = instanceProxyConfigDalFactory(db); const orgProxyConfigDAL = orgProxyConfigDalFactory(db); const proxyDAL = proxyDalFactory(db); + const gatewayV2DAL = gatewayV2DalFactory(db); const orgGatewayConfigV2DAL = orgGatewayConfigV2DalFactory(db); @@ -1626,9 +1628,26 @@ export const registerRoutes = async ( identityAuthTemplateDAL }); - const dynamicSecretProviders = buildDynamicSecretProviders({ - gatewayService + const proxyService = proxyServiceFactory({ + instanceProxyConfigDAL, + orgProxyConfigDAL, + proxyDAL, + kmsService }); + + const gatewayV2Service = gatewayV2ServiceFactory({ + kmsService, + proxyService, + orgGatewayConfigV2DAL, + gatewayV2DAL, + proxyDAL + }); + + const dynamicSecretProviders = buildDynamicSecretProviders({ + gatewayService, + gatewayV2Service + }); + const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({ queueService, dynamicSecretLeaseDAL, @@ -1648,6 +1667,7 @@ export const registerRoutes = async ( licenseService, kmsService, gatewayDAL, + gatewayV2DAL, resourceMetadataDAL }); @@ -1972,19 +1992,6 @@ export const registerRoutes = async ( appConnectionDAL }); - const proxyService = proxyServiceFactory({ - instanceProxyConfigDAL, - orgProxyConfigDAL, - proxyDAL, - kmsService - }); - - const gatewayV2Service = gatewayV2ServiceFactory({ - kmsService, - proxyService, - orgGatewayConfigV2DAL - }); - // setup the communication with license key server await licenseService.init(); diff --git a/frontend/src/hooks/api/gateways-v2/index.tsx b/frontend/src/hooks/api/gateways-v2/index.tsx new file mode 100644 index 000000000..c4a4e685c --- /dev/null +++ b/frontend/src/hooks/api/gateways-v2/index.tsx @@ -0,0 +1 @@ +export { gatewaysV2QueryKeys } from "./queries"; diff --git a/frontend/src/hooks/api/gateways-v2/queries.tsx b/frontend/src/hooks/api/gateways-v2/queries.tsx new file mode 100644 index 000000000..8c3184778 --- /dev/null +++ b/frontend/src/hooks/api/gateways-v2/queries.tsx @@ -0,0 +1,18 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TGatewayV2 } from "./types"; + +export const gatewaysV2QueryKeys = { + allKey: () => ["gateways-v2"], + listKey: () => [...gatewaysV2QueryKeys.allKey(), "list"], + list: () => + queryOptions({ + queryKey: gatewaysV2QueryKeys.listKey(), + queryFn: async () => { + const { data } = await apiRequest.get<{ gateways: TGatewayV2[] }>("/api/v2/gateways"); + return data.gateways; + } + }) +}; diff --git a/frontend/src/hooks/api/gateways-v2/types.ts b/frontend/src/hooks/api/gateways-v2/types.ts new file mode 100644 index 000000000..40a0bf5b4 --- /dev/null +++ b/frontend/src/hooks/api/gateways-v2/types.ts @@ -0,0 +1,11 @@ +export type TGatewayV2 = { + id: string; + identityId: string; + name: string; + createdAt: string; + updatedAt: string; + identity: { + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/gateways/queries.tsx b/frontend/src/hooks/api/gateways/queries.tsx index bb05b17a4..64cb18c79 100644 --- a/frontend/src/hooks/api/gateways/queries.tsx +++ b/frontend/src/hooks/api/gateways/queries.tsx @@ -2,6 +2,7 @@ import { queryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; +import { TGatewayV2 } from "../gateways-v2/types"; import { TGateway } from "./types"; export const gatewaysQueryKeys = { @@ -12,7 +13,9 @@ export const gatewaysQueryKeys = { queryKey: gatewaysQueryKeys.listKey(), queryFn: async () => { const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways"); - return data.gateways; + const { data: dataV2 } = await apiRequest.get("/api/v2/gateways"); + + return [...data.gateways, ...dataV2]; } }) };