From cc34b92d56c8e59bd5ecf37aca42179bafac7e23 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 26 Aug 2025 03:02:34 +0800 Subject: [PATCH] feat: pki and ssh setup for instance proxy --- backend/src/@types/fastify.d.ts | 2 + backend/src/@types/knex.d.ts | 16 + ...1627_add-gateway-v2-pki-and-ssh-configs.ts | 78 +++ backend/src/db/schemas/index.ts | 2 + .../src/db/schemas/instance-proxy-config.ts | 38 ++ backend/src/db/schemas/models.ts | 6 +- backend/src/db/schemas/org-proxy-config.ts | 31 ++ backend/src/ee/routes/v1/index.ts | 2 + backend/src/ee/routes/v1/proxy-router.ts | 24 + .../proxy/instance-proxy-config-dal.ts | 11 + .../ee/services/proxy/org-proxy-config-dal.ts | 11 + .../src/ee/services/proxy/proxy-service.ts | 464 ++++++++++++++++++ backend/src/keystore/keystore.ts | 3 +- backend/src/server/routes/index.ts | 15 +- .../services/certificate/certificate-fns.ts | 3 + 15 files changed, 703 insertions(+), 3 deletions(-) create mode 100644 backend/src/db/migrations/20250825131627_add-gateway-v2-pki-and-ssh-configs.ts create mode 100644 backend/src/db/schemas/instance-proxy-config.ts create mode 100644 backend/src/db/schemas/org-proxy-config.ts create mode 100644 backend/src/ee/routes/v1/proxy-router.ts create mode 100644 backend/src/ee/services/proxy/instance-proxy-config-dal.ts create mode 100644 backend/src/ee/services/proxy/org-proxy-config-dal.ts create mode 100644 backend/src/ee/services/proxy/proxy-service.ts diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index c25d8d4d1..977970f14 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -31,6 +31,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio import { TPitServiceFactory } from "@app/ee/services/pit/pit-service"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-types"; import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; +import { TProxyServiceFactory } from "@app/ee/services/proxy/proxy-service"; import { RateLimitConfiguration, TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-types"; import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-types"; import { TScimServiceFactory } from "@app/ee/services/scim/scim-types"; @@ -303,6 +304,7 @@ declare module "fastify" { bus: TEventBusService; sse: TServerSentEventsService; identityAuthTemplate: TIdentityAuthTemplateServiceFactory; + proxy: TProxyServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index f645cb8f2..9fdc94aca 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -179,6 +179,9 @@ import { TIncidentContacts, TIncidentContactsInsert, TIncidentContactsUpdate, + TInstanceProxyConfig, + TInstanceProxyConfigInsert, + TInstanceProxyConfigUpdate, TIntegrationAuths, TIntegrationAuthsInsert, TIntegrationAuthsUpdate, @@ -233,6 +236,9 @@ import { TOrgMemberships, TOrgMembershipsInsert, TOrgMembershipsUpdate, + TOrgProxyConfig, + TOrgProxyConfigInsert, + TOrgProxyConfigUpdate, TOrgRoles, TOrgRolesInsert, TOrgRolesUpdate, @@ -1254,5 +1260,15 @@ declare module "knex/types/tables" { TRemindersRecipientsInsert, TRemindersRecipientsUpdate >; + [TableName.InstanceProxyConfig]: KnexOriginal.CompositeTableType< + TInstanceProxyConfig, + TInstanceProxyConfigInsert, + TInstanceProxyConfigUpdate + >; + [TableName.OrgProxyConfig]: KnexOriginal.CompositeTableType< + TOrgProxyConfig, + TOrgProxyConfigInsert, + TOrgProxyConfigUpdate + >; } } 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 new file mode 100644 index 000000000..c242d0f58 --- /dev/null +++ b/backend/src/db/migrations/20250825131627_add-gateway-v2-pki-and-ssh-configs.ts @@ -0,0 +1,78 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.InstanceProxyConfig))) { + await knex.schema.createTable(TableName.InstanceProxyConfig, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + + // Root CA for proxy PKI + t.binary("encryptedRootProxyPkiCaPrivateKey").notNullable(); + t.binary("encryptedRootProxyPkiCaCertificate").notNullable(); + + // Instance CA for proxy PKI + t.binary("encryptedInstanceProxyPkiCaPrivateKey").notNullable(); + t.binary("encryptedInstanceProxyPkiCaCertificate").notNullable(); + t.binary("encryptedInstanceProxyPkiCaCertificateChain").notNullable(); + + // Instance client/server intermediates for proxy PKI + t.binary("encryptedInstanceProxyPkiClientCaPrivateKey").notNullable(); + t.binary("encryptedInstanceProxyPkiClientCaCertificate").notNullable(); + t.binary("encryptedInstanceProxyPkiClientCaCertificateChain").notNullable(); + t.binary("encryptedInstanceProxyPkiServerCaPrivateKey").notNullable(); + t.binary("encryptedInstanceProxyPkiServerCaCertificate").notNullable(); + t.binary("encryptedInstanceProxyPkiServerCaCertificateChain").notNullable(); + + // Org Parent CAs for proxy + t.binary("encryptedOrgProxyPkiCaPrivateKey").notNullable(); + t.binary("encryptedOrgProxyPkiCaCertificate").notNullable(); + t.binary("encryptedOrgProxyPkiCaCertificateChain").notNullable(); + + // Instance SSH CAs for proxy + t.binary("encryptedInstanceProxySshClientCaPrivateKey").notNullable(); + t.binary("encryptedInstanceProxySshClientCaPublicKey").notNullable(); + t.binary("encryptedInstanceProxySshServerCaPrivateKey").notNullable(); + t.binary("encryptedInstanceProxySshServerCaPublicKey").notNullable(); + }); + + await createOnUpdateTrigger(knex, TableName.InstanceProxyConfig); + } + + // Org-level proxy configuration (one-to-one with organization) + if (!(await knex.schema.hasTable(TableName.OrgProxyConfig))) { + await knex.schema.createTable(TableName.OrgProxyConfig, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + + t.uuid("orgId").notNullable().unique(); + t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + + // Org-scoped proxy PKI (client + server) + t.binary("encryptedProxyPkiClientCaPrivateKey").notNullable(); + t.binary("encryptedProxyPkiClientCaCertificate").notNullable(); + t.binary("encryptedProxyPkiClientCaCertificateChain").notNullable(); + t.binary("encryptedProxyPkiServerCaPrivateKey").notNullable(); + t.binary("encryptedProxyPkiServerCaCertificate").notNullable(); + t.binary("encryptedProxyPkiServerCaCertificateChain").notNullable(); + + // Org-scoped proxy SSH (client + server) + t.binary("encryptedProxySshClientCaPrivateKey").notNullable(); + t.binary("encryptedProxySshClientCaPublicKey").notNullable(); + t.binary("encryptedProxySshServerCaPrivateKey").notNullable(); + t.binary("encryptedProxySshServerCaPublicKey").notNullable(); + }); + + await createOnUpdateTrigger(knex, TableName.OrgProxyConfig); + } +} + +export async function down(knex: Knex): Promise { + await dropOnUpdateTrigger(knex, TableName.OrgProxyConfig); + await knex.schema.dropTableIfExists(TableName.OrgProxyConfig); + + await dropOnUpdateTrigger(knex, TableName.InstanceProxyConfig); + await knex.schema.dropTableIfExists(TableName.InstanceProxyConfig); +} diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 1642c3555..01c066035 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -57,6 +57,7 @@ export * from "./identity-token-auths"; export * from "./identity-ua-client-secrets"; export * from "./identity-universal-auths"; export * from "./incident-contacts"; +export * from "./instance-proxy-config"; export * from "./integration-auths"; export * from "./integrations"; export * from "./internal-certificate-authorities"; @@ -76,6 +77,7 @@ export * from "./oidc-configs"; export * from "./org-bots"; export * from "./org-gateway-config"; export * from "./org-memberships"; +export * from "./org-proxy-config"; export * from "./org-roles"; export * from "./organizations"; export * from "./pki-alerts"; diff --git a/backend/src/db/schemas/instance-proxy-config.ts b/backend/src/db/schemas/instance-proxy-config.ts new file mode 100644 index 000000000..369ae381a --- /dev/null +++ b/backend/src/db/schemas/instance-proxy-config.ts @@ -0,0 +1,38 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const InstanceProxyConfigSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + encryptedRootProxyPkiCaPrivateKey: zodBuffer, + encryptedRootProxyPkiCaCertificate: zodBuffer, + encryptedInstanceProxyPkiCaPrivateKey: zodBuffer, + encryptedInstanceProxyPkiCaCertificate: zodBuffer, + encryptedInstanceProxyPkiCaCertificateChain: zodBuffer, + encryptedInstanceProxyPkiClientCaPrivateKey: zodBuffer, + encryptedInstanceProxyPkiClientCaCertificate: zodBuffer, + encryptedInstanceProxyPkiClientCaCertificateChain: zodBuffer, + encryptedInstanceProxyPkiServerCaPrivateKey: zodBuffer, + encryptedInstanceProxyPkiServerCaCertificate: zodBuffer, + encryptedInstanceProxyPkiServerCaCertificateChain: zodBuffer, + encryptedOrgProxyPkiCaPrivateKey: zodBuffer, + encryptedOrgProxyPkiCaCertificate: zodBuffer, + encryptedOrgProxyPkiCaCertificateChain: zodBuffer, + encryptedInstanceProxySshClientCaPrivateKey: zodBuffer, + encryptedInstanceProxySshClientCaPublicKey: zodBuffer, + encryptedInstanceProxySshServerCaPrivateKey: zodBuffer, + encryptedInstanceProxySshServerCaPublicKey: zodBuffer +}); + +export type TInstanceProxyConfig = z.infer; +export type TInstanceProxyConfigInsert = Omit, TImmutableDBKeys>; +export type TInstanceProxyConfigUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 855934b28..dd1526011 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -178,7 +178,11 @@ export enum TableName { SecretScanningConfig = "secret_scanning_configs", // reminders Reminder = "reminders", - ReminderRecipient = "reminders_recipients" + ReminderRecipient = "reminders_recipients", + + // gateway v2 + InstanceProxyConfig = "instance_proxy_config", + OrgProxyConfig = "org_proxy_config" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId"; diff --git a/backend/src/db/schemas/org-proxy-config.ts b/backend/src/db/schemas/org-proxy-config.ts new file mode 100644 index 000000000..8b854ffc2 --- /dev/null +++ b/backend/src/db/schemas/org-proxy-config.ts @@ -0,0 +1,31 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { zodBuffer } from "@app/lib/zod"; + +import { TImmutableDBKeys } from "./models"; + +export const OrgProxyConfigSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + orgId: z.string().uuid(), + encryptedProxyPkiClientCaPrivateKey: zodBuffer, + encryptedProxyPkiClientCaCertificate: zodBuffer, + encryptedProxyPkiClientCaCertificateChain: zodBuffer, + encryptedProxyPkiServerCaPrivateKey: zodBuffer, + encryptedProxyPkiServerCaCertificate: zodBuffer, + encryptedProxyPkiServerCaCertificateChain: zodBuffer, + encryptedProxySshClientCaPrivateKey: zodBuffer, + encryptedProxySshClientCaPublicKey: zodBuffer, + encryptedProxySshServerCaPrivateKey: zodBuffer, + encryptedProxySshServerCaPublicKey: zodBuffer +}); + +export type TOrgProxyConfig = z.infer; +export type TOrgProxyConfigInsert = Omit, TImmutableDBKeys>; +export type TOrgProxyConfigUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index ab9503f58..d1232e5e8 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -23,6 +23,7 @@ import { registerOrgRoleRouter } from "./org-role-router"; import { registerPITRouter } from "./pit-router"; import { registerProjectRoleRouter } from "./project-role-router"; import { registerProjectRouter } from "./project-router"; +import { registerProxyRouter } from "./proxy-router"; import { registerRateLimitRouter } from "./rate-limit-router"; import { registerSamlRouter } from "./saml-router"; import { registerScimRouter } from "./scim-router"; @@ -79,6 +80,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { ); await server.register(registerGatewayRouter, { prefix: "/gateways" }); + await server.register(registerProxyRouter, { prefix: "/proxies" }); await server.register(registerGithubOrgSyncRouter, { prefix: "/github-org-sync-config" }); await server.register( diff --git a/backend/src/ee/routes/v1/proxy-router.ts b/backend/src/ee/routes/v1/proxy-router.ts new file mode 100644 index 000000000..d2c580708 --- /dev/null +++ b/backend/src/ee/routes/v1/proxy-router.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +import { writeLimit } from "@app/server/config/rateLimiter"; + +export const registerProxyRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + ip: z.string() + }), + response: { + 200: z.any() + } + }, + handler: async (req) => { + return server.services.proxy.registerProxy(req.body); + } + }); +}; diff --git a/backend/src/ee/services/proxy/instance-proxy-config-dal.ts b/backend/src/ee/services/proxy/instance-proxy-config-dal.ts new file mode 100644 index 000000000..4a128daf3 --- /dev/null +++ b/backend/src/ee/services/proxy/instance-proxy-config-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 TInstanceProxyConfigDALFactory = ReturnType; + +export const instanceProxyConfigDalFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.InstanceProxyConfig); + + return orm; +}; diff --git a/backend/src/ee/services/proxy/org-proxy-config-dal.ts b/backend/src/ee/services/proxy/org-proxy-config-dal.ts new file mode 100644 index 000000000..f15dd823b --- /dev/null +++ b/backend/src/ee/services/proxy/org-proxy-config-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 TOrgProxyConfigDALFactory = ReturnType; + +export const orgProxyConfigDalFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.OrgProxyConfig); + + return orm; +}; diff --git a/backend/src/ee/services/proxy/proxy-service.ts b/backend/src/ee/services/proxy/proxy-service.ts new file mode 100644 index 000000000..e3e5fb921 --- /dev/null +++ b/backend/src/ee/services/proxy/proxy-service.ts @@ -0,0 +1,464 @@ +import * as x509 from "@peculiar/x509"; + +import { PgSqlLock } from "@app/keystore/keystore"; +import { crypto } from "@app/lib/crypto"; +import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns"; +import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; +import { + createSerialNumber, + keyAlgorithmToAlgCfg +} from "@app/services/certificate-authority/certificate-authority-fns"; +import { TKmsServiceFactory } from "@app/services/kms/kms-service"; + +import { createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns"; +import { SshCertType } from "../ssh/ssh-certificate-authority-types"; +import { SshCertKeyAlgorithm } from "../ssh-certificate/ssh-certificate-types"; +import { TInstanceProxyConfigDALFactory } from "./instance-proxy-config-dal"; +import { TOrgProxyConfigDALFactory } from "./org-proxy-config-dal"; + +export type TProxyServiceFactory = ReturnType; + +const INSTANCE_PROXY_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"; + +export const proxyServiceFactory = ({ + instanceProxyConfigDAL, + orgProxyConfigDAL, + kmsService +}: { + instanceProxyConfigDAL: TInstanceProxyConfigDALFactory; + orgProxyConfigDAL: TOrgProxyConfigDALFactory; + kmsService: TKmsServiceFactory; +}) => { + const $getInstanceCAs = async () => { + const instanceConfig = await instanceProxyConfigDAL.transaction(async (tx) => { + const existingInstanceProxyConfig = await instanceProxyConfigDAL.findById(INSTANCE_PROXY_CONFIG_UUID); + if (existingInstanceProxyConfig) return existingInstanceProxyConfig; + + await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.InstanceProxyConfigInit()]); + + const rootCaKeyAlgorithm = CertKeyAlgorithm.RSA_2048; + const alg = keyAlgorithmToAlgCfg(rootCaKeyAlgorithm); + const rootCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + + // generate root CA + const rootCaSerialNumber = createSerialNumber(); + const rootCaSkObj = crypto.nativeCrypto.KeyObject.from(rootCaKeys.privateKey); + const rootCaIssuedAt = new Date(); + const rootCaExpiration = new Date(new Date().setFullYear(2045)); + const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({ + name: `O=Infisical,CN=Infisical Instance Root Proxy CA`, + serialNumber: rootCaSerialNumber, + notBefore: rootCaIssuedAt, + notAfter: rootCaExpiration, + signingAlgorithm: alg, + keys: rootCaKeys, + extensions: [ + // eslint-disable-next-line no-bitwise + new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), + await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey) + ] + }); + + // generate org proxy CA + const orgProxyCaSerialNumber = createSerialNumber(); + const orgProxyCaIssuedAt = new Date(); + const orgProxyCaExpiration = new Date(new Date().setFullYear(2045)); + const orgProxyCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const orgProxyCaSkObj = crypto.nativeCrypto.KeyObject.from(orgProxyCaKeys.privateKey); + const orgProxyCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: orgProxyCaSerialNumber, + subject: `O=Infisical,CN=Infisical Organization Proxy CA`, + issuer: rootCaCert.subject, + notBefore: orgProxyCaIssuedAt, + notAfter: orgProxyCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: orgProxyCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(orgProxyCaKeys.publicKey) + ] + }); + const orgProxyCaChain = constructPemChainFromCerts([rootCaCert]); + + // generate instance proxy CA + const instanceProxyCaSerialNumber = createSerialNumber(); + const instanceProxyCaIssuedAt = new Date(); + const instanceProxyCaExpiration = new Date(new Date().setFullYear(2045)); + const instanceProxyCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const instanceProxyCaSkObj = crypto.nativeCrypto.KeyObject.from(instanceProxyCaKeys.privateKey); + const instanceProxyCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: instanceProxyCaSerialNumber, + subject: `O=Infisical,CN=Infisical Instance Proxy CA`, + issuer: rootCaCert.subject, + notBefore: instanceProxyCaIssuedAt, + notAfter: instanceProxyCaExpiration, + signingKey: rootCaKeys.privateKey, + publicKey: instanceProxyCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(instanceProxyCaKeys.publicKey) + ] + }); + const instanceProxyCaChain = constructPemChainFromCerts([rootCaCert]); + + // generate instance proxy client CA + const instanceProxyClientCaSerialNumber = createSerialNumber(); + const instanceProxyClientCaIssuedAt = new Date(); + const instanceProxyClientCaExpiration = new Date(new Date().setFullYear(2045)); + const instanceProxyClientCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const instanceProxyClientCaSkObj = crypto.nativeCrypto.KeyObject.from(instanceProxyClientCaKeys.privateKey); + const instanceProxyClientCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: instanceProxyClientCaSerialNumber, + subject: `O=Infisical,CN=Infisical Instance Proxy Client CA`, + issuer: instanceProxyCaCert.subject, + notBefore: instanceProxyClientCaIssuedAt, + notAfter: instanceProxyClientCaExpiration, + signingKey: instanceProxyCaKeys.privateKey, + publicKey: instanceProxyClientCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(instanceProxyCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(instanceProxyClientCaKeys.publicKey) + ] + }); + const instanceProxyClientCaChain = constructPemChainFromCerts([instanceProxyCaCert, rootCaCert]); + + // generate instance proxy server CA + const instanceProxyServerCaSerialNumber = createSerialNumber(); + const instanceProxyServerCaIssuedAt = new Date(); + const instanceProxyServerCaExpiration = new Date(new Date().setFullYear(2045)); + const instanceProxyServerCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const instanceProxyServerCaSkObj = crypto.nativeCrypto.KeyObject.from(instanceProxyServerCaKeys.privateKey); + const instanceProxyServerCaCert = await x509.X509CertificateGenerator.create({ + serialNumber: instanceProxyServerCaSerialNumber, + subject: `O=Infisical,CN=Infisical Instance Proxy Server CA`, + issuer: instanceProxyCaCert.subject, + notBefore: instanceProxyServerCaIssuedAt, + notAfter: instanceProxyServerCaExpiration, + signingKey: instanceProxyCaKeys.privateKey, + publicKey: instanceProxyServerCaKeys.publicKey, + signingAlgorithm: alg, + extensions: [ + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags.keyCertSign | + x509.KeyUsageFlags.cRLSign | + x509.KeyUsageFlags.digitalSignature | + x509.KeyUsageFlags.keyEncipherment, + true + ), + new x509.BasicConstraintsExtension(true, 0, true), + await x509.AuthorityKeyIdentifierExtension.create(instanceProxyCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(instanceProxyServerCaKeys.publicKey) + ] + }); + const instanceProxyServerCaChain = constructPemChainFromCerts([instanceProxyCaCert, rootCaCert]); + + const instanceSshServerCaKeyPair = await createSshKeyPair(SshCertKeyAlgorithm.RSA_2048); + const instanceSshClientCaKeyPair = await createSshKeyPair(SshCertKeyAlgorithm.RSA_2048); + + const encryptWithRoot = kmsService.encryptWithRootKey(); + + // root proxy CA + const encryptedRootProxyPkiCaPrivateKey = encryptWithRoot( + Buffer.from( + rootCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + ); + const encryptedRootProxyPkiCaCertificate = encryptWithRoot(Buffer.from(rootCaCert.rawData)); + + // org proxy CA + const encryptedOrgProxyPkiCaPrivateKey = encryptWithRoot( + Buffer.from( + orgProxyCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + ); + const encryptedOrgProxyPkiCaCertificate = encryptWithRoot(Buffer.from(orgProxyCaCert.rawData)); + const encryptedOrgProxyPkiCaCertificateChain = encryptWithRoot(Buffer.from(orgProxyCaChain)); + + // instance proxy CA + const encryptedInstanceProxyPkiCaPrivateKey = encryptWithRoot( + Buffer.from( + instanceProxyCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + ); + const encryptedInstanceProxyPkiCaCertificate = encryptWithRoot(Buffer.from(instanceProxyCaCert.rawData)); + const encryptedInstanceProxyPkiCaCertificateChain = encryptWithRoot(Buffer.from(instanceProxyCaChain)); + + // instance proxy client CA + const encryptedInstanceProxyPkiClientCaPrivateKey = encryptWithRoot( + Buffer.from( + instanceProxyClientCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + ); + const encryptedInstanceProxyPkiClientCaCertificate = encryptWithRoot( + Buffer.from(instanceProxyClientCaCert.rawData) + ); + const encryptedInstanceProxyPkiClientCaCertificateChain = encryptWithRoot( + Buffer.from(instanceProxyClientCaChain) + ); + + // instance proxy server CA + const encryptedInstanceProxyPkiServerCaPrivateKey = encryptWithRoot( + Buffer.from( + instanceProxyServerCaSkObj.export({ + type: "pkcs8", + format: "der" + }) + ) + ); + const encryptedInstanceProxyPkiServerCaCertificate = encryptWithRoot( + Buffer.from(instanceProxyServerCaCert.rawData) + ); + const encryptedInstanceProxyPkiServerCaCertificateChain = encryptWithRoot( + Buffer.from(instanceProxyServerCaChain) + ); + + const encryptedInstanceProxySshClientCaPublicKey = encryptWithRoot( + Buffer.from(instanceSshClientCaKeyPair.publicKey) + ); + const encryptedInstanceProxySshClientCaPrivateKey = encryptWithRoot( + Buffer.from(instanceSshClientCaKeyPair.privateKey) + ); + + const encryptedInstanceProxySshServerCaPublicKey = encryptWithRoot( + Buffer.from(instanceSshServerCaKeyPair.publicKey) + ); + const encryptedInstanceProxySshServerCaPrivateKey = encryptWithRoot( + Buffer.from(instanceSshServerCaKeyPair.privateKey) + ); + + return instanceProxyConfigDAL.create({ + // @ts-expect-error id is kept as fixed for idempotence and to avoid race condition + id: INSTANCE_PROXY_CONFIG_UUID, + encryptedRootProxyPkiCaPrivateKey, + encryptedRootProxyPkiCaCertificate, + encryptedInstanceProxyPkiCaPrivateKey, + encryptedInstanceProxyPkiCaCertificate, + encryptedInstanceProxyPkiCaCertificateChain, + encryptedInstanceProxyPkiClientCaPrivateKey, + encryptedInstanceProxyPkiClientCaCertificate, + encryptedInstanceProxyPkiClientCaCertificateChain, + encryptedInstanceProxyPkiServerCaPrivateKey, + encryptedInstanceProxyPkiServerCaCertificate, + encryptedInstanceProxyPkiServerCaCertificateChain, + encryptedOrgProxyPkiCaPrivateKey, + encryptedOrgProxyPkiCaCertificate, + encryptedOrgProxyPkiCaCertificateChain, + encryptedInstanceProxySshClientCaPublicKey, + encryptedInstanceProxySshClientCaPrivateKey, + encryptedInstanceProxySshServerCaPublicKey, + encryptedInstanceProxySshServerCaPrivateKey + }); + }); + + // decrypt the instance config + const decryptWithRoot = kmsService.decryptWithRootKey(); + + // decrypt root proxy CA + const rootProxyPkiCaPrivateKey = decryptWithRoot(instanceConfig.encryptedRootProxyPkiCaPrivateKey); + const rootProxyPkiCaCertificate = decryptWithRoot(instanceConfig.encryptedRootProxyPkiCaCertificate); + + // decrypt org proxy CA + const orgProxyPkiCaPrivateKey = decryptWithRoot(instanceConfig.encryptedOrgProxyPkiCaPrivateKey); + const orgProxyPkiCaCertificate = decryptWithRoot(instanceConfig.encryptedOrgProxyPkiCaCertificate); + const orgProxyPkiCaCertificateChain = decryptWithRoot(instanceConfig.encryptedOrgProxyPkiCaCertificateChain); + + // decrypt instance proxy CA + const instanceProxyPkiCaPrivateKey = decryptWithRoot(instanceConfig.encryptedInstanceProxyPkiCaPrivateKey); + const instanceProxyPkiCaCertificate = decryptWithRoot(instanceConfig.encryptedInstanceProxyPkiCaCertificate); + const instanceProxyPkiCaCertificateChain = decryptWithRoot( + instanceConfig.encryptedInstanceProxyPkiCaCertificateChain + ); + + // decrypt instance proxy client CA + const instanceProxyPkiClientCaPrivateKey = decryptWithRoot( + instanceConfig.encryptedInstanceProxyPkiClientCaPrivateKey + ); + const instanceProxyPkiClientCaCertificate = decryptWithRoot( + instanceConfig.encryptedInstanceProxyPkiClientCaCertificate + ); + const instanceProxyPkiClientCaCertificateChain = decryptWithRoot( + instanceConfig.encryptedInstanceProxyPkiClientCaCertificateChain + ); + + // decrypt instance proxy server CA + const instanceProxyPkiServerCaPrivateKey = decryptWithRoot( + instanceConfig.encryptedInstanceProxyPkiServerCaPrivateKey + ); + const instanceProxyPkiServerCaCertificate = decryptWithRoot( + instanceConfig.encryptedInstanceProxyPkiServerCaCertificate + ); + const instanceProxyPkiServerCaCertificateChain = decryptWithRoot( + instanceConfig.encryptedInstanceProxyPkiServerCaCertificateChain + ); + + // decrypt SSH keys + const instanceProxySshClientCaPublicKey = decryptWithRoot( + instanceConfig.encryptedInstanceProxySshClientCaPublicKey + ); + const instanceProxySshClientCaPrivateKey = decryptWithRoot( + instanceConfig.encryptedInstanceProxySshClientCaPrivateKey + ); + const instanceProxySshServerCaPublicKey = decryptWithRoot( + instanceConfig.encryptedInstanceProxySshServerCaPublicKey + ); + const instanceProxySshServerCaPrivateKey = decryptWithRoot( + instanceConfig.encryptedInstanceProxySshServerCaPrivateKey + ); + + return { + rootProxyPkiCaPrivateKey, + rootProxyPkiCaCertificate, + orgProxyPkiCaPrivateKey, + orgProxyPkiCaCertificate, + orgProxyPkiCaCertificateChain, + instanceProxyPkiCaPrivateKey, + instanceProxyPkiCaCertificate, + instanceProxyPkiCaCertificateChain, + instanceProxyPkiClientCaPrivateKey, + instanceProxyPkiClientCaCertificate, + instanceProxyPkiClientCaCertificateChain, + instanceProxyPkiServerCaPrivateKey, + instanceProxyPkiServerCaCertificate, + instanceProxyPkiServerCaCertificateChain, + instanceProxySshClientCaPublicKey, + instanceProxySshClientCaPrivateKey, + instanceProxySshServerCaPublicKey, + instanceProxySshServerCaPrivateKey + }; + }; + + const registerProxy = async ({ ip }: { ip: string }) => { + // initialize instance CAs if not yet initialized + const instanceCAs = await $getInstanceCAs(); + + // TODO: check if identity used already has an existing proxy. If the same IP, return the existing proxy. If not, create a new proxy and overwrite + + // generate proxy server PKI certificate + const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048); + const proxyServerCaCert = new x509.X509Certificate(instanceCAs.instanceProxyPkiServerCaCertificate); + const rootProxyCaCert = new x509.X509Certificate(instanceCAs.rootProxyPkiCaCertificate); + const proxyServerCaSkObj = crypto.nativeCrypto.createPrivateKey({ + key: instanceCAs.instanceProxyPkiServerCaPrivateKey, + format: "der", + type: "pkcs8" + }); + const proxyServerCaPrivateKey = await crypto.nativeCrypto.subtle.importKey( + "pkcs8", + proxyServerCaSkObj.export({ format: "der", type: "pkcs8" }), + alg, + true, + ["sign"] + ); + + const proxyServerKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]); + const proxyServerCertIssuedAt = new Date(); + const proxyServerCertExpireAt = new Date(new Date().setMonth(new Date().getMonth() + 1)); + const proxyServerCertPrivateKey = crypto.nativeCrypto.KeyObject.from(proxyServerKeys.privateKey); + + const proxyServerCertExtensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(false), + await x509.AuthorityKeyIdentifierExtension.create(proxyServerCaCert, false), + await x509.SubjectKeyIdentifierExtension.create(proxyServerKeys.publicKey), + new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy + new x509.KeyUsagesExtension( + // eslint-disable-next-line no-bitwise + x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT], + true + ), + new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true), + // san + new x509.SubjectAlternativeNameExtension([{ type: "ip", value: ip }], false) + ]; + + const proxyServerSerialNumber = createSerialNumber(); + const proxyServerCertificate = await x509.X509CertificateGenerator.create({ + serialNumber: proxyServerSerialNumber, + subject: `CN=${ip},O=Infisical,OU=Proxy`, + issuer: proxyServerCaCert.subject, + notBefore: proxyServerCertIssuedAt, + notAfter: proxyServerCertExpireAt, + signingKey: proxyServerCaPrivateKey, + publicKey: proxyServerKeys.publicKey, + signingAlgorithm: alg, + extensions: proxyServerCertExtensions + }); + + // generate proxy server SSH certificate + const keyAlgorithm = SshCertKeyAlgorithm.RSA_2048; + const { publicKey: proxyServerSshPublicKey, privateKey: proxyServerSshPrivateKey } = + await createSshKeyPair(keyAlgorithm); + + const proxyServerSshCert = await createSshCert({ + caPrivateKey: instanceCAs.instanceProxySshServerCaPrivateKey.toString("utf8"), + clientPublicKey: proxyServerSshPublicKey, + keyId: "proxy-server", + principals: [ip], + certType: SshCertType.HOST, + requestedTtl: "30d" + }); + + return { + pki: { + serverCertificate: proxyServerCertificate.toString("pem"), + serverCertificateChain: prependCertToPemChain( + proxyServerCaCert, + instanceCAs.instanceProxyPkiServerCaCertificateChain.toString("utf8") + ), + serverPrivateKey: proxyServerCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(), + clientCA: rootProxyCaCert.toString("pem") + }, + ssh: { + serverCertificate: proxyServerSshCert.signedPublicKey, + serverPrivateKey: proxyServerSshPrivateKey, + clientCAPublicKey: instanceCAs.instanceProxySshClientCaPublicKey.toString("utf8") + } + }; + }; + + return { + registerProxy + }; +}; diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 26aff767e..c4583b574 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -13,7 +13,8 @@ export const PgSqlLock = { SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`), CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`), CreateFolder: (envId: string, projectId: string) => pgAdvisoryLockHashText(`create-folder:${envId}-${projectId}`), - SshInit: (projectId: string) => pgAdvisoryLockHashText(`ssh-bootstrap:${projectId}`) + SshInit: (projectId: string) => pgAdvisoryLockHashText(`ssh-bootstrap:${projectId}`), + InstanceProxyConfigInit: () => pgAdvisoryLockHashText("instance-proxy-config-init") } as const; // all the key prefixes used must be set here to avoid conflict diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 6dd7d190d..ce2e5dac6 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -70,6 +70,9 @@ import { projectTemplateDALFactory } from "@app/ee/services/project-template/pro import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; 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 { 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"; import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal"; @@ -939,6 +942,9 @@ export const registerRoutes = async ( const pkiSubscriberDAL = pkiSubscriberDALFactory(db); const pkiTemplatesDAL = pkiTemplatesDALFactory(db); + const instanceProxyConfigDAL = instanceProxyConfigDalFactory(db); + const orgProxyConfigDAL = orgProxyConfigDalFactory(db); + const certificateService = certificateServiceFactory({ certificateDAL, certificateBodyDAL, @@ -1960,6 +1966,12 @@ export const registerRoutes = async ( appConnectionDAL }); + const proxyService = proxyServiceFactory({ + instanceProxyConfigDAL, + orgProxyConfigDAL, + kmsService + }); + // setup the communication with license key server await licenseService.init(); @@ -2091,7 +2103,8 @@ export const registerRoutes = async ( secretScanningV2: secretScanningV2Service, reminder: reminderService, bus: eventBusService, - sse: sseService + sse: sseService, + proxy: proxyService }); const cronJobs: CronJob[] = []; diff --git a/backend/src/services/certificate/certificate-fns.ts b/backend/src/services/certificate/certificate-fns.ts index eee220ce9..b2bc4df41 100644 --- a/backend/src/services/certificate/certificate-fns.ts +++ b/backend/src/services/certificate/certificate-fns.ts @@ -52,6 +52,9 @@ export const constructPemChainFromCerts = (certificates: x509.X509Certificate[]) .join("\n") .trim(); +export const prependCertToPemChain = (cert: x509.X509Certificate, pemChain: string) => + `${cert.toString("pem")}\n${pemChain}`; + export const splitPemChain = (pemText: string) => { const re2Pattern = new RE2("-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----", "g");