diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index cabc8e1db..0bcee6405 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -2144,6 +2144,7 @@ export const SecretSyncs = { const destinationName = SECRET_SYNC_NAME_MAP[destination]; return { initialSyncBehavior: `Specify how Infisical should resolve the initial sync to the ${destinationName} destination.`, + keySchema: `Specify the format to use for structuring secret keys in the ${destinationName} destination.`, disableSecretDeletion: `Enable this flag to prevent removal of secrets from the ${destinationName} destination when syncing.` }; }, diff --git a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts index abc4dcf82..a73bc81c9 100644 --- a/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts +++ b/backend/src/services/secret-sync/aws-parameter-store/aws-parameter-store-sync-fns.ts @@ -2,6 +2,7 @@ import AWS, { AWSError } from "aws-sdk"; import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TAwsParameterStoreSyncWithCredentials } from "./aws-parameter-store-sync-types"; @@ -389,6 +390,9 @@ export const AwsParameterStoreSyncFns = { for (const entry of Object.entries(awsParameterStoreSecretsRecord)) { const [key, parameter] = entry; + // eslint-disable-next-line no-continue + if (!matchesSchema(key, syncOptions.keySchema)) continue; + if (!(key in secretMap) || !secretMap[key].value) { parametersToDelete.push(parameter); } diff --git a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts index 7cea12d1b..cef1bee15 100644 --- a/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts +++ b/backend/src/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-fns.ts @@ -27,6 +27,7 @@ import { import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns"; import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TAwsSecretsManagerSyncWithCredentials } from "./aws-secrets-manager-sync-types"; @@ -399,6 +400,9 @@ export const AwsSecretsManagerSyncFns = { if (syncOptions.disableSecretDeletion) return; for await (const secretKey of Object.keys(awsSecretsRecord)) { + // eslint-disable-next-line no-continue + if (!matchesSchema(secretKey, syncOptions.keySchema)) continue; + if (!(secretKey in secretMap) || !secretMap[secretKey].value) { try { await deleteSecret(client, secretKey); diff --git a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts index 64d82c125..dce509fac 100644 --- a/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts +++ b/backend/src/services/secret-sync/azure-app-configuration/azure-app-configuration-sync-fns.ts @@ -7,6 +7,7 @@ import { TAppConnectionDALFactory } from "@app/services/app-connection/app-conne import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault"; import { isAzureKeyVaultReference } from "@app/services/integration-auth/integration-sync-secret-fns"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TAzureAppConfigurationSyncWithCredentials } from "./azure-app-configuration-sync-types"; @@ -139,6 +140,9 @@ export const azureAppConfigurationSyncFactory = ({ if (secretSync.syncOptions.disableSecretDeletion) return; for await (const key of Object.keys(azureAppConfigSecrets)) { + // eslint-disable-next-line no-continue + if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue; + const azureSecret = azureAppConfigSecrets[key]; if ( !(key in secretMap) || diff --git a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts index 12f1f2aff..fd1e2bd78 100644 --- a/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts +++ b/backend/src/services/secret-sync/azure-key-vault/azure-key-vault-sync-fns.ts @@ -5,6 +5,7 @@ import { request } from "@app/lib/config/request"; import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { SecretSyncError } from "../secret-sync-errors"; @@ -192,7 +193,9 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur if (secretSync.syncOptions.disableSecretDeletion) return; for await (const deleteSecretKey of deleteSecrets.filter( - (secret) => !setSecrets.find((setSecret) => setSecret.key === secret) + (secret) => + matchesSchema(secret, secretSync.syncOptions.keySchema) && + !setSecrets.find((setSecret) => setSecret.key === secret) )) { await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${deleteSecretKey}?api-version=7.3`, { headers: { diff --git a/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts b/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts index 3a52a4939..256ae4644 100644 --- a/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts +++ b/backend/src/services/secret-sync/camunda/camunda-sync-fns.ts @@ -12,6 +12,7 @@ import { TCamundaSyncWithCredentials } from "@app/services/secret-sync/camunda/camunda-sync-types"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "../secret-sync-types"; @@ -116,6 +117,9 @@ export const camundaSyncFactory = ({ kmsService, appConnectionDAL }: TCamundaSec if (secretSync.syncOptions.disableSecretDeletion) return; for await (const secret of Object.keys(camundaSecrets)) { + // eslint-disable-next-line no-continue + if (!matchesSchema(secret, secretSync.syncOptions.keySchema)) continue; + if (!(secret in secretMap) || !secretMap[secret].value) { try { await deleteCamundaSecret({ diff --git a/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts index 2ee7977a4..11143e24d 100644 --- a/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts +++ b/backend/src/services/secret-sync/databricks/databricks-sync-fns.ts @@ -11,6 +11,7 @@ import { TDatabricksSyncWithCredentials } from "@app/services/secret-sync/databricks/databricks-sync-types"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { TSecretMap } from "../secret-sync-types"; @@ -115,6 +116,9 @@ export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabri if (secretSync.syncOptions.disableSecretDeletion) return; for await (const secret of databricksSecretKeys) { + // eslint-disable-next-line no-continue + if (!matchesSchema(secret.key, secretSync.syncOptions.keySchema)) continue; + if (!(secret.key in secretMap)) { await deleteDatabricksSecrets({ key: secret.key, diff --git a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts index a71e29ae4..97da66a48 100644 --- a/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts +++ b/backend/src/services/secret-sync/gcp/gcp-sync-fns.ts @@ -4,6 +4,7 @@ import { request } from "@app/lib/config/request"; import { logger } from "@app/lib/logger"; import { getGcpConnectionAuthToken } from "@app/services/app-connection/gcp"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { SecretSyncError } from "../secret-sync-errors"; import { TSecretMap } from "../secret-sync-types"; @@ -153,6 +154,9 @@ export const GcpSyncFns = { } for await (const key of Object.keys(gcpSecrets)) { + // eslint-disable-next-line no-continue + if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue; + try { if (!(key in secretMap) || !secretMap[key].value) { // eslint-disable-next-line no-continue diff --git a/backend/src/services/secret-sync/github/github-sync-fns.ts b/backend/src/services/secret-sync/github/github-sync-fns.ts index 1fe922de5..952f4b512 100644 --- a/backend/src/services/secret-sync/github/github-sync-fns.ts +++ b/backend/src/services/secret-sync/github/github-sync-fns.ts @@ -4,6 +4,7 @@ import sodium from "libsodium-wrappers"; import { getGitHubClient } from "@app/services/app-connection/github"; import { GitHubSyncScope, GitHubSyncVisibility } from "@app/services/secret-sync/github/github-sync-enums"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; @@ -222,6 +223,9 @@ export const GithubSyncFns = { if (secretSync.syncOptions.disableSecretDeletion) return; for await (const encryptedSecret of encryptedSecrets) { + // eslint-disable-next-line no-continue + if (!matchesSchema(encryptedSecret.name, secretSync.syncOptions.keySchema)) continue; + if (!(encryptedSecret.name in secretMap)) { await deleteSecret(client, secretSync, encryptedSecret); } diff --git a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts index db35df292..6331cd91f 100644 --- a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts @@ -11,6 +11,7 @@ import { TPostHCVaultVariable } from "@app/services/secret-sync/hc-vault/hc-vault-sync-types"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; const listHCVaultVariables = async ({ instanceUrl, namespace, mount, accessToken, path }: THCVaultListVariables) => { @@ -68,7 +69,7 @@ export const HCVaultSyncFns = { const { connection, destinationConfig: { mount, path }, - syncOptions: { disableSecretDeletion } + syncOptions: { disableSecretDeletion, keySchema } } = secretSync; const { namespace } = connection.credentials; @@ -95,6 +96,9 @@ export const HCVaultSyncFns = { if (disableSecretDeletion) return; for await (const [key] of Object.entries(variables)) { + // eslint-disable-next-line no-continue + if (!matchesSchema(key, keySchema)) continue; + if (!(key in secretMap)) { delete variables[key]; tainted = true; diff --git a/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts index 5fa0a3d63..2fcf488aa 100644 --- a/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts +++ b/backend/src/services/secret-sync/humanitec/humanitec-sync-fns.ts @@ -2,6 +2,7 @@ import { request } from "@app/lib/config/request"; import { logger } from "@app/lib/logger"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; @@ -199,6 +200,9 @@ export const HumanitecSyncFns = { if (secretSync.syncOptions.disableSecretDeletion) return; for await (const humanitecSecret of humanitecSecrets) { + // eslint-disable-next-line no-continue + if (!matchesSchema(humanitecSecret.key, secretSync.syncOptions.keySchema)) continue; + if (!secretMap[humanitecSecret.key]) { await deleteSecret(secretSync, humanitecSecret); } diff --git a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts b/backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts index 95045e1e9..e270f2e02 100644 --- a/backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts +++ b/backend/src/services/secret-sync/oci-vault/oci-vault-sync-fns.ts @@ -11,6 +11,7 @@ import { TUpdateOCIVaultVariable } from "@app/services/secret-sync/oci-vault/oci-vault-sync-types"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; const listOCIVaultVariables = async ({ provider, compartmentId, vaultId, onlyActive }: TOCIVaultListVariables) => { @@ -211,6 +212,9 @@ export const OCIVaultSyncFns = { // Update and delete secrets for await (const [key, variable] of Object.entries(variables)) { + // eslint-disable-next-line no-continue + if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue; + // Only update / delete active secrets if (variable.lifecycleState === vault.models.SecretSummary.LifecycleState.Active) { if (key in secretMap && secretMap[key].value.length > 0) { diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index d029a000a..1bb4da9db 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -1,4 +1,5 @@ import { AxiosError } from "axios"; +import RE2 from "re2"; import { AWS_PARAMETER_STORE_SYNC_LIST_OPTION, @@ -61,45 +62,63 @@ type TSyncSecretDeps = { kmsService: Pick; }; -// const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { -// let secretMap = { ...unprocessedSecretMap }; -// -// const { appendSuffix, prependPrefix } = secretSync.syncOptions; -// -// if (appendSuffix || prependPrefix) { -// secretMap = {}; -// Object.entries(unprocessedSecretMap).forEach(([key, value]) => { -// secretMap[`${prependPrefix || ""}${key}${appendSuffix || ""}`] = value; -// }); -// } -// -// return secretMap; -// }; -// -// const stripAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => { -// let secretMap = { ...unprocessedSecretMap }; -// -// const { appendSuffix, prependPrefix } = secretSync.syncOptions; -// -// if (appendSuffix || prependPrefix) { -// secretMap = {}; -// Object.entries(unprocessedSecretMap).forEach(([key, value]) => { -// let processedKey = key; -// -// if (prependPrefix && processedKey.startsWith(prependPrefix)) { -// processedKey = processedKey.slice(prependPrefix.length); -// } -// -// if (appendSuffix && processedKey.endsWith(appendSuffix)) { -// processedKey = processedKey.slice(0, -appendSuffix.length); -// } -// -// secretMap[processedKey] = value; -// }); -// } -// -// return secretMap; -// }; +// Add schema to secret keys +const addSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => { + if (!schema) return unprocessedSecretMap; + + const processedSecretMap: TSecretMap = {}; + + for (const [key, value] of Object.entries(unprocessedSecretMap)) { + const newKey = new RE2("{{secretKey}}").replace(schema, key); + processedSecretMap[newKey] = value; + } + + return processedSecretMap; +}; + +// Strip schema from secret keys +const stripSchema = (unprocessedSecretMap: TSecretMap, schema?: string): TSecretMap => { + if (!schema) return unprocessedSecretMap; + + const [prefix, suffix] = schema.split("{{secretKey}}"); + + const strippedMap: TSecretMap = {}; + + for (const [key, value] of Object.entries(unprocessedSecretMap)) { + if (!key.startsWith(prefix) || !key.endsWith(suffix)) { + // eslint-disable-next-line no-continue + continue; + } + + const strippedKey = key.slice(prefix.length, key.length - suffix.length); + strippedMap[strippedKey] = value; + } + + return strippedMap; +}; + +// Checks if a key matches a schema +export const matchesSchema = (key: string, schema?: string): boolean => { + if (!schema) return true; + + const [prefix, suffix] = schema.split("{{secretKey}}"); + if (prefix === undefined || suffix === undefined) return true; + + return key.startsWith(prefix) && key.endsWith(suffix); +}; + +// Filter only for secrets with keys that match the schema +const filterForSchema = (secretMap: TSecretMap, schema?: string): TSecretMap => { + const filteredMap: TSecretMap = {}; + + for (const [key, value] of Object.entries(secretMap)) { + if (matchesSchema(key, schema)) { + filteredMap[key] = value; + } + } + + return filteredMap; +}; export const SecretSyncFns = { syncSecrets: ( @@ -107,51 +126,51 @@ export const SecretSyncFns = { secretMap: TSecretMap, { kmsService, appConnectionDAL }: TSyncSecretDeps ): Promise => { - // const affixedSecretMap = addAffixes(secretSync, secretMap); + const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema); switch (secretSync.destination) { case SecretSync.AWSParameterStore: - return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap); + return AwsParameterStoreSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.AWSSecretsManager: - return AwsSecretsManagerSyncFns.syncSecrets(secretSync, secretMap); + return AwsSecretsManagerSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.GitHub: - return GithubSyncFns.syncSecrets(secretSync, secretMap); + return GithubSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.GCPSecretManager: - return GcpSyncFns.syncSecrets(secretSync, secretMap); + return GcpSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.AzureKeyVault: return azureKeyVaultSyncFactory({ appConnectionDAL, kmsService - }).syncSecrets(secretSync, secretMap); + }).syncSecrets(secretSync, schemaSecretMap); case SecretSync.AzureAppConfiguration: return azureAppConfigurationSyncFactory({ appConnectionDAL, kmsService - }).syncSecrets(secretSync, secretMap); + }).syncSecrets(secretSync, schemaSecretMap); case SecretSync.Databricks: return databricksSyncFactory({ appConnectionDAL, kmsService - }).syncSecrets(secretSync, secretMap); + }).syncSecrets(secretSync, schemaSecretMap); case SecretSync.Humanitec: - return HumanitecSyncFns.syncSecrets(secretSync, secretMap); + return HumanitecSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.TerraformCloud: - return TerraformCloudSyncFns.syncSecrets(secretSync, secretMap); + return TerraformCloudSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Camunda: return camundaSyncFactory({ appConnectionDAL, kmsService - }).syncSecrets(secretSync, secretMap); + }).syncSecrets(secretSync, schemaSecretMap); case SecretSync.Vercel: - return VercelSyncFns.syncSecrets(secretSync, secretMap); + return VercelSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.Windmill: - return WindmillSyncFns.syncSecrets(secretSync, secretMap); + return WindmillSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.HCVault: - return HCVaultSyncFns.syncSecrets(secretSync, secretMap); + return HCVaultSyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.TeamCity: - return TeamCitySyncFns.syncSecrets(secretSync, secretMap); + return TeamCitySyncFns.syncSecrets(secretSync, schemaSecretMap); case SecretSync.OCIVault: - return OCIVaultSyncFns.syncSecrets(secretSync, secretMap); + return OCIVaultSyncFns.syncSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` @@ -226,59 +245,58 @@ export const SecretSyncFns = { ); } - return secretMap; - // return stripAffixes(secretSync, secretMap); + return stripSchema(filterForSchema(secretMap), secretSync.syncOptions.keySchema); }, removeSecrets: ( secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap, { kmsService, appConnectionDAL }: TSyncSecretDeps ): Promise => { - // const affixedSecretMap = addAffixes(secretSync, secretMap); + const schemaSecretMap = addSchema(secretMap, secretSync.syncOptions.keySchema); switch (secretSync.destination) { case SecretSync.AWSParameterStore: - return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap); + return AwsParameterStoreSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.AWSSecretsManager: - return AwsSecretsManagerSyncFns.removeSecrets(secretSync, secretMap); + return AwsSecretsManagerSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.GitHub: - return GithubSyncFns.removeSecrets(secretSync, secretMap); + return GithubSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.GCPSecretManager: - return GcpSyncFns.removeSecrets(secretSync, secretMap); + return GcpSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.AzureKeyVault: return azureKeyVaultSyncFactory({ appConnectionDAL, kmsService - }).removeSecrets(secretSync, secretMap); + }).removeSecrets(secretSync, schemaSecretMap); case SecretSync.AzureAppConfiguration: return azureAppConfigurationSyncFactory({ appConnectionDAL, kmsService - }).removeSecrets(secretSync, secretMap); + }).removeSecrets(secretSync, schemaSecretMap); case SecretSync.Databricks: return databricksSyncFactory({ appConnectionDAL, kmsService - }).removeSecrets(secretSync, secretMap); + }).removeSecrets(secretSync, schemaSecretMap); case SecretSync.Humanitec: - return HumanitecSyncFns.removeSecrets(secretSync, secretMap); + return HumanitecSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.TerraformCloud: - return TerraformCloudSyncFns.removeSecrets(secretSync, secretMap); + return TerraformCloudSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Camunda: return camundaSyncFactory({ appConnectionDAL, kmsService - }).removeSecrets(secretSync, secretMap); + }).removeSecrets(secretSync, schemaSecretMap); case SecretSync.Vercel: - return VercelSyncFns.removeSecrets(secretSync, secretMap); + return VercelSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.Windmill: - return WindmillSyncFns.removeSecrets(secretSync, secretMap); + return WindmillSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.HCVault: - return HCVaultSyncFns.removeSecrets(secretSync, secretMap); + return HCVaultSyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.TeamCity: - return TeamCitySyncFns.removeSecrets(secretSync, secretMap); + return TeamCitySyncFns.removeSecrets(secretSync, schemaSecretMap); case SecretSync.OCIVault: - return OCIVaultSyncFns.removeSecrets(secretSync, secretMap); + return OCIVaultSyncFns.removeSecrets(secretSync, schemaSecretMap); default: throw new Error( `Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}` diff --git a/backend/src/services/secret-sync/secret-sync-schemas.ts b/backend/src/services/secret-sync/secret-sync-schemas.ts index 50ff3f307..9dab3c121 100644 --- a/backend/src/services/secret-sync/secret-sync-schemas.ts +++ b/backend/src/services/secret-sync/secret-sync-schemas.ts @@ -1,3 +1,4 @@ +import RE2 from "re2"; import { AnyZodObject, z } from "zod"; import { SecretSyncsSchema } from "@app/db/schemas/secret-syncs"; @@ -24,6 +25,14 @@ const BaseSyncOptionsSchema = ({ ? z.nativeEnum(SecretSyncInitialSyncBehavior) : z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination) ).describe(SecretSyncs.SYNC_OPTIONS(destination).initialSyncBehavior), + keySchema: z + .string() + .optional() + .refine((val) => !val || new RE2(/^(?:[a-zA-Z0-9\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9\-/]*)$/).test(val), { + message: + "Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, slashes, and the {{secretKey}} placeholder." + }) + .describe(SecretSyncs.SYNC_OPTIONS(destination).keySchema), disableSecretDeletion: z.boolean().optional().describe(SecretSyncs.SYNC_OPTIONS(destination).disableSecretDeletion) }); diff --git a/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts b/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts index 6dbd9bdd7..0afe29beb 100644 --- a/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts +++ b/backend/src/services/secret-sync/teamcity/teamcity-sync-fns.ts @@ -1,6 +1,7 @@ import { request } from "@app/lib/config/request"; import { getTeamCityInstanceUrl } from "@app/services/app-connection/teamcity"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { TDeleteTeamCityVariable, @@ -125,6 +126,9 @@ export const TeamCitySyncFns = { const variables = await listTeamCityVariables({ instanceUrl, accessToken, project, buildConfig }); for await (const [key, variable] of Object.entries(variables)) { + // eslint-disable-next-line no-continue + if (!matchesSchema(key, secretSync.syncOptions.keySchema)) continue; + if (!(key in secretMap)) { try { await deleteTeamCityVariable({ diff --git a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts index 4cfd7ec05..a58ec213c 100644 --- a/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts +++ b/backend/src/services/secret-sync/terraform-cloud/terraform-cloud-sync-fns.ts @@ -4,6 +4,7 @@ import { AxiosResponse } from "axios"; import { request } from "@app/lib/config/request"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { SECRET_SYNC_NAME_MAP } from "../secret-sync-maps"; @@ -231,6 +232,9 @@ export const TerraformCloudSyncFns = { if (secretSync.syncOptions.disableSecretDeletion) return; for (const terraformCloudVariable of terraformCloudVariables) { + // eslint-disable-next-line no-continue + if (!matchesSchema(terraformCloudVariable.key, secretSync.syncOptions.keySchema)) continue; + if (!Object.prototype.hasOwnProperty.call(secretMap, terraformCloudVariable.key)) { await deleteVariable(secretSync, terraformCloudVariable); } diff --git a/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts b/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts index 713971283..90e9327e5 100644 --- a/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts +++ b/backend/src/services/secret-sync/vercel/vercel-sync-fns.ts @@ -2,6 +2,7 @@ import { request } from "@app/lib/config/request"; import { IntegrationUrls } from "@app/services/integration-auth/integration-list"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; import { VercelEnvironmentType } from "./vercel-sync-enums"; @@ -290,6 +291,9 @@ export const VercelSyncFns = { if (secretSync.syncOptions.disableSecretDeletion) return; for await (const vercelSecret of vercelSecrets) { + // eslint-disable-next-line no-continue + if (!matchesSchema(vercelSecret.key, secretSync.syncOptions.keySchema)) continue; + if (!secretMap[vercelSecret.key]) { await deleteSecret(secretSync, vercelSecret); } diff --git a/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts b/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts index 2e2c36740..a09706581 100644 --- a/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts +++ b/backend/src/services/secret-sync/windmill/windmill-sync-fns.ts @@ -1,6 +1,7 @@ import { request } from "@app/lib/config/request"; import { getWindmillInstanceUrl } from "@app/services/app-connection/windmill"; import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns"; import { TDeleteWindmillVariable, TPostWindmillVariable, @@ -128,7 +129,7 @@ export const WindmillSyncFns = { const { connection, destinationConfig: { path }, - syncOptions: { disableSecretDeletion } + syncOptions: { disableSecretDeletion, keySchema } } = secretSync; // url needs to be lowercase @@ -169,6 +170,9 @@ export const WindmillSyncFns = { if (disableSecretDeletion) return; for await (const [key, variable] of Object.entries(variables)) { + // eslint-disable-next-line no-continue + if (!matchesSchema(key, keySchema)) continue; + if (!(key in secretMap)) { try { await deleteWindmillVariable({ diff --git a/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-options.png b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-options.png index 6a4a68f2c..30a74eac0 100644 Binary files a/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-options.png and b/docs/images/secret-syncs/aws-parameter-store/aws-parameter-store-options.png differ diff --git a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png index 89ec35e4d..7e3cd5ea9 100644 Binary files a/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png and b/docs/images/secret-syncs/aws-secrets-manager/aws-secrets-manager-options.png differ diff --git a/docs/integrations/cloud/heroku.mdx b/docs/integrations/cloud/heroku.mdx index a63c3f381..75cf8c106 100644 --- a/docs/integrations/cloud/heroku.mdx +++ b/docs/integrations/cloud/heroku.mdx @@ -22,11 +22,11 @@ description: "How to sync secrets from Infisical to Heroku" Select which Infisical environment secrets you want to sync to which Heroku app and press create integration to start syncing secrets to Heroku. - + ![integrations heroku](../../images/integrations/heroku/integrations-heroku-create.png) Here's some guidance on each field: - + - Project Environment: The environment in the current Infisical project from which you want to sync secrets from. - Secrets Path: The path in the current Infisical project from which you want to sync secrets from such as `/` (for secrets that do not reside in a folder) or `/foo/bar` (for secrets nested in a folder, in this case a folder called `bar` in another folder called `foo`). - Heroku App: The application in Heroku that you want to sync secrets to. @@ -34,7 +34,7 @@ description: "How to sync secrets from Infisical to Heroku" - **No Import - Overwrite all values in Heroku**: Sync secrets and overwrite any existing secrets in Heroku. - **Import - Prefer values from Infisical**: Import secrets from Heroku to Infisical; if a secret with the same name already exists in Infisical, do nothing. Afterwards, sync secrets to Heroku. - **Import - Prefer values from Heroku**: Import secrets from Heroku to Infisical; if a secret with the same name already exists in Infisical, replace its value with the one from Heroku. Afterwards, sync secrets to Heroku. - + ![integrations heroku](../../images/integrations/heroku/integrations-heroku.png) @@ -46,27 +46,26 @@ description: "How to sync secrets from Infisical to Heroku" Navigate to your user Account settings > Applications to create a new API client. - ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-settings.png) - ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-applications.png) - ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-new-app.png) - + ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-settings.png) + ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-applications.png) + ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-new-app.png) + Create the API client. As part of the form, set the **OAuth callback URL** to `https://your-domain.com/integrations/heroku/oauth2/callback`. - ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-new-app-form.png) + ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-new-app-form.png) Obtain the **Client ID** and **Client Secret** for your Heroku API client. - - ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-credentials.png) - + + ![integrations Heroku config](../../images/integrations/heroku/integrations-heroku-config-credentials.png) + Back in your Infisical instance, add two new environment variables for the credentials of your Heroku API client. - `CLIENT_ID_HEROKU`: The **Client ID** of your Heroku API client. - `CLIENT_SECRET_HEROKU`: The **Client Secret** of your Heroku API client. - + Once added, restart your Infisical instance and use the Heroku integration. - diff --git a/docs/integrations/secret-syncs/aws-parameter-store.mdx b/docs/integrations/secret-syncs/aws-parameter-store.mdx index fad37265a..11f0c94ad 100644 --- a/docs/integrations/secret-syncs/aws-parameter-store.mdx +++ b/docs/integrations/secret-syncs/aws-parameter-store.mdx @@ -40,6 +40,10 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Parameter Store when keys conflict. - **Import Secrets (Prioritize AWS Parameter Store)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Parameter Store over Infisical when keys conflict. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **KMS Key**: The AWS KMS key ID or alias to encrypt parameters with. - **Tags**: Optional resource tags to add to parameters synced by Infisical. - **Sync Secret Metadata as Resource Tags**: If enabled, metadata attached to secrets will be added as resource tags to parameters synced by Infisical. diff --git a/docs/integrations/secret-syncs/aws-secrets-manager.mdx b/docs/integrations/secret-syncs/aws-secrets-manager.mdx index 8ed85be25..f7654eeae 100644 --- a/docs/integrations/secret-syncs/aws-secrets-manager.mdx +++ b/docs/integrations/secret-syncs/aws-secrets-manager.mdx @@ -43,6 +43,10 @@ description: "Learn how to configure an AWS Secrets Manager Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. - **Import Secrets (Prioritize AWS Secrets Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **KMS Key**: The AWS KMS key ID or alias to encrypt secrets with. - **Tags**: Optional tags to add to secrets synced by Infisical. - **Sync Secret Metadata as Tags**: If enabled, metadata attached to secrets will be added as tags to secrets synced by Infisical. diff --git a/docs/integrations/secret-syncs/azure-app-configuration.mdx b/docs/integrations/secret-syncs/azure-app-configuration.mdx index 35a577872..ee47504bc 100644 --- a/docs/integrations/secret-syncs/azure-app-configuration.mdx +++ b/docs/integrations/secret-syncs/azure-app-configuration.mdx @@ -48,7 +48,10 @@ description: "Learn how to configure an Azure App Configuration Sync for Infisic - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. - **Import Secrets (Prioritize Azure App Configuration)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. - + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. diff --git a/docs/integrations/secret-syncs/azure-key-vault.mdx b/docs/integrations/secret-syncs/azure-key-vault.mdx index 5f55a73ae..609ba8b8d 100644 --- a/docs/integrations/secret-syncs/azure-key-vault.mdx +++ b/docs/integrations/secret-syncs/azure-key-vault.mdx @@ -51,6 +51,10 @@ description: "Learn how to configure a Azure Key Vault Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Secrets Manager when keys conflict. - **Import Secrets (Prioritize Azure Key Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Secrets Manager over Infisical when keys conflict. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. diff --git a/docs/integrations/secret-syncs/camunda.mdx b/docs/integrations/secret-syncs/camunda.mdx index 5ed2cd9ae..df57a5b7d 100644 --- a/docs/integrations/secret-syncs/camunda.mdx +++ b/docs/integrations/secret-syncs/camunda.mdx @@ -39,6 +39,10 @@ description: "Learn how to configure a Camunda Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Camunda when keys conflict. - **Import Secrets (Prioritize Camunda)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Camunda over Infisical when keys conflict. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. diff --git a/docs/integrations/secret-syncs/databricks.mdx b/docs/integrations/secret-syncs/databricks.mdx index c9db5f88a..225bad5b1 100644 --- a/docs/integrations/secret-syncs/databricks.mdx +++ b/docs/integrations/secret-syncs/databricks.mdx @@ -46,6 +46,10 @@ description: "Learn how to configure a Databricks Sync for Infisical." Databricks does not support importing secrets. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. diff --git a/docs/integrations/secret-syncs/gcp-secret-manager.mdx b/docs/integrations/secret-syncs/gcp-secret-manager.mdx index 72c932116..ace63787d 100644 --- a/docs/integrations/secret-syncs/gcp-secret-manager.mdx +++ b/docs/integrations/secret-syncs/gcp-secret-manager.mdx @@ -42,6 +42,10 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over GCP Secret Manager when keys conflict. - **Import Secrets (Prioritize GCP Secret Manager)**: Imports secrets from the destination endpoint before syncing, prioritizing values from GCP Secret Manager over Infisical when keys conflict. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. diff --git a/docs/integrations/secret-syncs/github.mdx b/docs/integrations/secret-syncs/github.mdx index d55ec3d0b..7786567cc 100644 --- a/docs/integrations/secret-syncs/github.mdx +++ b/docs/integrations/secret-syncs/github.mdx @@ -62,6 +62,10 @@ description: "Learn how to configure a GitHub Sync for Infisical." GitHub does not support importing secrets. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. diff --git a/docs/integrations/secret-syncs/hashicorp-vault.mdx b/docs/integrations/secret-syncs/hashicorp-vault.mdx index 0d6c0d644..48e4d8dfd 100644 --- a/docs/integrations/secret-syncs/hashicorp-vault.mdx +++ b/docs/integrations/secret-syncs/hashicorp-vault.mdx @@ -54,6 +54,10 @@ description: "Learn how to configure a Hashicorp Vault Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Hashicorp Vault when keys conflict. - **Import Secrets (Prioritize Hashicorp Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Hashicorp Vault over Infisical when keys conflict. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. diff --git a/docs/integrations/secret-syncs/humanitec.mdx b/docs/integrations/secret-syncs/humanitec.mdx index e8cd7eafc..ec36bd4da 100644 --- a/docs/integrations/secret-syncs/humanitec.mdx +++ b/docs/integrations/secret-syncs/humanitec.mdx @@ -55,6 +55,10 @@ description: "Learn how to configure a Humanitec Sync for Infisical." Humanitec does not support importing secrets. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. diff --git a/docs/integrations/secret-syncs/oci-vault.mdx b/docs/integrations/secret-syncs/oci-vault.mdx index 7f3c5fd19..67a3426aa 100644 --- a/docs/integrations/secret-syncs/oci-vault.mdx +++ b/docs/integrations/secret-syncs/oci-vault.mdx @@ -47,10 +47,13 @@ description: "Learn how to configure an Oracle Cloud Infrastructure Vault Sync f ![Configure Sync Options](/images/secret-syncs/oci-vault/configure-sync-options.png) - **Initial Sync Behavior**: Determines how Infisical should resolve the initial sync. - - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over OCI Vault when keys conflict. - - **Import Secrets (Prioritize OCI Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from OCI Vault over Infisical when keys conflict. - + - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. + - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over OCI Vault when keys conflict. + - **Import Secrets (Prioritize OCI Vault)**: Imports secrets from the destination endpoint before syncing, prioritizing values from OCI Vault over Infisical when keys conflict. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. diff --git a/docs/integrations/secret-syncs/overview.mdx b/docs/integrations/secret-syncs/overview.mdx index 0df04cbb7..f097f0503 100644 --- a/docs/integrations/secret-syncs/overview.mdx +++ b/docs/integrations/secret-syncs/overview.mdx @@ -93,4 +93,28 @@ via the UI or API for the third-party service you intend to sync secrets to. Infisical is continuously expanding it's Secret Sync third-party service support. If the service you need isn't available, you can still use our Native Integrations in the interim, or contact us at team@infisical.com to make a request . - \ No newline at end of file + + +## Key Schemas + +Key Schemas let you control how Infisical names your secret keys when syncing to external destinations. This makes it clear which secrets are managed by Infisical and prevents accidental changes to unrelated secrets. + +A Key Schema adds a prefix, suffix, or format to your secrets before they reach the destination. + +This example demonstrates key behavior if the schema was set to `INFISICAL_{{secretKey}}`: + +
+ ```mermaid + graph LR + A[SECRET_1] --> T["Syncs as"] --> B[INFISICAL_SECRET_1] + + style A fill:#E6F4FF,stroke:#0096D6,stroke-width:2px,color:black,rx:15px + style B fill:#F4FFE6,stroke:#96D600,stroke-width:2px,color:black,rx:15px + style T fill:#FFF2B2,stroke:#E6C34A,stroke-width:2px,color:black,rx:2px,font-size:12px + + ``` +
+ + + When importing secrets from the destination into infisical, the schema is stripped from imported secret keys. + diff --git a/docs/integrations/secret-syncs/teamcity.mdx b/docs/integrations/secret-syncs/teamcity.mdx index af4c8d76a..3482101ca 100644 --- a/docs/integrations/secret-syncs/teamcity.mdx +++ b/docs/integrations/secret-syncs/teamcity.mdx @@ -48,7 +48,10 @@ description: "Learn how to configure a TeamCity Sync for Infisical." Infisical only syncs secrets from within the target scope; inherited secrets will not be imported. - + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. diff --git a/docs/integrations/secret-syncs/terraform-cloud.mdx b/docs/integrations/secret-syncs/terraform-cloud.mdx index 80a087d2b..d2f762ef1 100644 --- a/docs/integrations/secret-syncs/terraform-cloud.mdx +++ b/docs/integrations/secret-syncs/terraform-cloud.mdx @@ -56,6 +56,10 @@ description: "Learn how to configure a Terraform Cloud Sync for Infisical." Terraform Cloud does not support importing secrets. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. diff --git a/docs/integrations/secret-syncs/vercel.mdx b/docs/integrations/secret-syncs/vercel.mdx index 593874dee..c903d3faa 100644 --- a/docs/integrations/secret-syncs/vercel.mdx +++ b/docs/integrations/secret-syncs/vercel.mdx @@ -43,6 +43,10 @@ description: "Learn how to configure a Vercel Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Vercel when keys conflict. - **Import Secrets (Prioritize Vercel)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Vercel over Infisical when keys conflict. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. diff --git a/docs/integrations/secret-syncs/windmill.mdx b/docs/integrations/secret-syncs/windmill.mdx index 90d35f8b8..e98a2c7b6 100644 --- a/docs/integrations/secret-syncs/windmill.mdx +++ b/docs/integrations/secret-syncs/windmill.mdx @@ -44,6 +44,10 @@ description: "Learn how to configure a Windmill Sync for Infisical." - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over Windmill when keys conflict. - **Import Secrets (Prioritize Windmill)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Windmill over Infisical when keys conflict. + - **Key Schema**: Template that determines how secret names are transformed when syncing, using `{{secretKey}}` as a placeholder for the original secret name. + + We highly recommend using a Key Schema to ensure that Infisical only manages the specific keys you intend, keeping everything else untouched. + - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. diff --git a/frontend/src/components/secret-syncs/SecretSyncImportSecretsModal.tsx b/frontend/src/components/secret-syncs/SecretSyncImportSecretsModal.tsx index 8b587c62f..c1b21a771 100644 --- a/frontend/src/components/secret-syncs/SecretSyncImportSecretsModal.tsx +++ b/frontend/src/components/secret-syncs/SecretSyncImportSecretsModal.tsx @@ -44,7 +44,9 @@ const Content = ({ secretSync, onComplete }: ContentProps) => { handleSubmit, control, formState: { isSubmitting, isDirty } - } = useForm({ resolver: zodResolver(FormSchema) }); + } = useForm({ + resolver: zodResolver(FormSchema) + }); const triggerImportSecrets = useTriggerSecretSyncImportSecrets(); diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index 9eec461cd..a497c3ba9 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -1,9 +1,13 @@ import { ReactNode } from "react"; import { Controller, useFormContext } from "react-hook-form"; -import { faQuestionCircle, faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; +import { + faCircleInfo, + faQuestionCircle, + faTriangleExclamation +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { FormControl, Select, SelectItem, Switch, Tooltip } from "@app/components/v2"; +import { FormControl, Input, Select, SelectItem, Switch, Tooltip } from "@app/components/v2"; import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs"; import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs"; @@ -122,6 +126,45 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { )} )} + ( + + We highly recommend using a{" "} + + Key Schema + {" "} + to ensure that Infisical only manages the specific keys you intend, keeping + everything else untouched. + + } + > +
+ Infisical strongly advises setting a Key Schema{" "} + +
+ + } + > + +
+ )} + control={control} + name="syncOptions.keySchema" + /> {AdditionalSyncOptionsFieldsComponent} { ); }} /> - {/* ( - - - - )} - control={control} - name="syncOptions.prependPrefix" - /> - ( - - - - )} - control={control} - name="syncOptions.appendSuffix" - /> */} ); }; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index b7f62c139..144ccb2a8 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -41,11 +41,7 @@ export const SecretSyncReviewFields = () => { connection, environment, secretPath, - syncOptions: { - // appendSuffix, prependPrefix, - disableSecretDeletion, - initialSyncBehavior - }, + syncOptions: { disableSecretDeletion, initialSyncBehavior, keySchema }, destination, isAutoSyncEnabled } = watch(); @@ -137,8 +133,7 @@ export const SecretSyncReviewFields = () => { {SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destinationName).name} - {/* {prependPrefix} - {appendSuffix} */} + {keySchema} {AdditionalSyncOptionsFieldsComponent} {disableSecretDeletion && ( diff --git a/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts index bf72321ce..4e858ac0d 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/base-secret-sync-schema.ts @@ -8,18 +8,17 @@ export const BaseSecretSyncSchema = { const baseSyncOptionsSchema = z.object({ initialSyncBehavior: z.nativeEnum(SecretSyncInitialSyncBehavior), - disableSecretDeletion: z.boolean().optional().default(false) - // scott: removed temporarily for evaluation of template formatting - // prependPrefix: z - // .string() - // .trim() - // .transform((str) => str.toUpperCase()) - // .optional(), - // appendSuffix: z - // .string() - // .trim() - // .transform((str) => str.toUpperCase()) - // .optional() + disableSecretDeletion: z.boolean().optional().default(false), + keySchema: z + .string() + .optional() + .refine( + (val) => !val || /^(?:[a-zA-Z0-9\-/]*)(?:\{\{secretKey\}\})(?:[a-zA-Z0-9\-/]*)$/.test(val), + { + message: + "Key schema must include one {{secretKey}} and only contain letters, numbers, dashes, slashes, and the {{secretKey}} placeholder." + } + ) }); const syncOptionsSchema = additionalSyncOptions diff --git a/frontend/src/hooks/api/secretSyncs/types/root-sync.ts b/frontend/src/hooks/api/secretSyncs/types/root-sync.ts index dfbbcd063..38c9efdf2 100644 --- a/frontend/src/hooks/api/secretSyncs/types/root-sync.ts +++ b/frontend/src/hooks/api/secretSyncs/types/root-sync.ts @@ -4,8 +4,7 @@ import { SecretSyncInitialSyncBehavior, SecretSyncStatus } from "@app/hooks/api/ export type RootSyncOptions = { initialSyncBehavior: SecretSyncInitialSyncBehavior; disableSecretDeletion?: boolean; - // prependPrefix?: string; - // appendSuffix?: string; + keySchema?: string; }; export type TRootSecretSync = { diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx index ca229b55c..a53016d53 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -21,12 +21,7 @@ type Props = { export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) => { const { destination, - syncOptions: { - // appendSuffix, - // prependPrefix, - initialSyncBehavior, - disableSecretDeletion - } + syncOptions: { initialSyncBehavior, disableSecretDeletion, keySchema } } = secretSync; let AdditionalSyncOptionsComponent: ReactNode; @@ -88,8 +83,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = {SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP[initialSyncBehavior](destination).name} - {/* {prependPrefix} - {appendSuffix} */} + {keySchema} {AdditionalSyncOptionsComponent} {disableSecretDeletion && (