diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 37a3d3679..dfb23a6c3 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1862,6 +1862,13 @@ export const AppConnections = { instanceUrl: "The Windmill instance URL to connect with (defaults to https://app.windmill.dev).", accessToken: "The access token to use to connect with Windmill." }, + HC_VAULT: { + instanceUrl: "The Hashicrop Vault instance URL to connect with.", + namespace: "The Hashicrop Vault namespace to connect with.", + accessToken: "The access token used to connect with Hashicorp Vault.", + roleId: "The Role ID used to connect with Hashicorp Vault.", + secretId: "The Secret ID used to connect with Hashicorp Vault." + }, LDAP: { provider: "The type of LDAP provider. Determines provider-specific behaviors.", url: "The LDAP/LDAPS URL to connect to (e.g., 'ldap://domain-or-ip:389' or 'ldaps://domain-or-ip:636').", @@ -2019,6 +2026,10 @@ export const SecretSyncs = { workspace: "The Windmill workspace to sync secrets to.", path: "The Windmill workspace path to sync secrets to." }, + HC_VAULT: { + mount: "The Hashicorp Vault Secrets Engine Mount to sync secrets to.", + path: "The Hashicorp Vault path to sync secrets to." + }, TEAMCITY: { project: "The TeamCity project to sync secrets to.", buildConfig: "The TeamCity build configuration to sync secrets to." diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index f6180b9b6..f6c260ea5 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -28,6 +28,10 @@ import { } from "@app/services/app-connection/databricks"; import { GcpConnectionListItemSchema, SanitizedGcpConnectionSchema } from "@app/services/app-connection/gcp"; import { GitHubConnectionListItemSchema, SanitizedGitHubConnectionSchema } from "@app/services/app-connection/github"; +import { + HCVaultConnectionListItemSchema, + SanitizedHCVaultConnectionSchema +} from "@app/services/app-connection/hc-vault"; import { HumanitecConnectionListItemSchema, SanitizedHumanitecConnectionSchema @@ -68,6 +72,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedMsSqlConnectionSchema.options, ...SanitizedCamundaConnectionSchema.options, ...SanitizedAuth0ConnectionSchema.options, + ...SanitizedHCVaultConnectionSchema.options, ...SanitizedAzureClientSecretsConnectionSchema.options, ...SanitizedWindmillConnectionSchema.options, ...SanitizedLdapConnectionSchema.options, @@ -88,6 +93,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ MsSqlConnectionListItemSchema, CamundaConnectionListItemSchema, Auth0ConnectionListItemSchema, + HCVaultConnectionListItemSchema, AzureClientSecretsConnectionListItemSchema, WindmillConnectionListItemSchema, LdapConnectionListItemSchema, diff --git a/backend/src/server/routes/v1/app-connection-routers/hc-vault-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/hc-vault-connection-router.ts new file mode 100644 index 000000000..061c02777 --- /dev/null +++ b/backend/src/server/routes/v1/app-connection-routers/hc-vault-connection-router.ts @@ -0,0 +1,47 @@ +import z from "zod"; + +import { readLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + CreateHCVaultConnectionSchema, + SanitizedHCVaultConnectionSchema, + UpdateHCVaultConnectionSchema +} from "@app/services/app-connection/hc-vault"; +import { AuthMode } from "@app/services/auth/auth-type"; + +import { registerAppConnectionEndpoints } from "./app-connection-endpoints"; + +export const registerHCVaultConnectionRouter = async (server: FastifyZodProvider) => { + registerAppConnectionEndpoints({ + app: AppConnection.HCVault, + server, + sanitizedResponseSchema: SanitizedHCVaultConnectionSchema, + createSchema: CreateHCVaultConnectionSchema, + updateSchema: UpdateHCVaultConnectionSchema + }); + + // The following endpoints are for internal Infisical App use only and not part of the public API + server.route({ + method: "GET", + url: `/:connectionId/mounts`, + config: { + rateLimit: readLimit + }, + schema: { + params: z.object({ + connectionId: z.string().uuid() + }), + response: { + 200: z.string().array() + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { connectionId } = req.params; + + const mounts = await server.services.appConnection.hcvault.listMounts(connectionId, req.permission); + return mounts; + } + }); +}; diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index 194f32290..eeae5e5e3 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -9,6 +9,7 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; +import { registerHCVaultConnectionRouter } from "./hc-vault-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; @@ -37,6 +38,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record + registerSyncSecretsEndpoints({ + destination: SecretSync.HCVault, + server, + responseSchema: HCVaultSyncSchema, + createSchema: CreateHCVaultSyncSchema, + updateSchema: UpdateHCVaultSyncSchema + }); diff --git a/backend/src/server/routes/v1/secret-sync-routers/index.ts b/backend/src/server/routes/v1/secret-sync-routers/index.ts index a54777727..75b3ac68e 100644 --- a/backend/src/server/routes/v1/secret-sync-routers/index.ts +++ b/backend/src/server/routes/v1/secret-sync-routers/index.ts @@ -8,6 +8,7 @@ import { registerCamundaSyncRouter } from "./camunda-sync-router"; import { registerDatabricksSyncRouter } from "./databricks-sync-router"; import { registerGcpSyncRouter } from "./gcp-sync-router"; import { registerGitHubSyncRouter } from "./github-sync-router"; +import { registerHCVaultSyncRouter } from "./hc-vault-sync-router"; import { registerHumanitecSyncRouter } from "./humanitec-sync-router"; import { registerTeamCitySyncRouter } from "./teamcity-sync-router"; import { registerTerraformCloudSyncRouter } from "./terraform-cloud-sync-router"; @@ -29,5 +30,6 @@ export const SECRET_SYNC_REGISTER_ROUTER_MAP: Record { getAzureClientSecretsConnectionListItem(), getWindmillConnectionListItem(), getAuth0ConnectionListItem(), + getHCVaultConnectionListItem(), getLdapConnectionListItem(), getTeamCityConnectionListItem() ].sort((a, b) => a.name.localeCompare(b.name)); @@ -152,6 +158,7 @@ export const validateAppConnectionCredentials = async ( [AppConnection.TerraformCloud]: validateTerraformCloudConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Auth0]: validateAuth0ConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Windmill]: validateWindmillConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.HCVault]: validateHCVaultConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.LDAP]: validateLdapConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.TeamCity]: validateTeamCityConnectionCredentials as TAppConnectionCredentialsValidator }; @@ -186,10 +193,13 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case MsSqlConnectionMethod.UsernameAndPassword: return "Username & Password"; case WindmillConnectionMethod.AccessToken: + case HCVaultConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken: return "Access Token"; case Auth0ConnectionMethod.ClientCredentials: return "Client Credentials"; + case HCVaultConnectionMethod.AppRole: + return "App Role"; case LdapConnectionMethod.SimpleBind: return "Simple Bind"; default: @@ -238,6 +248,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.AzureClientSecrets]: platformManagedCredentialsNotSupported, [AppConnection.Windmill]: platformManagedCredentialsNotSupported, [AppConnection.Auth0]: platformManagedCredentialsNotSupported, + [AppConnection.HCVault]: platformManagedCredentialsNotSupported, [AppConnection.LDAP]: platformManagedCredentialsNotSupported, // we could support this in the future [AppConnection.TeamCity]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 594c4c734..05e00446c 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -16,6 +16,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Camunda]: "Camunda", [AppConnection.Windmill]: "Windmill", [AppConnection.Auth0]: "Auth0", + [AppConnection.HCVault]: "Hashicorp Vault", [AppConnection.LDAP]: "LDAP", [AppConnection.TeamCity]: "TeamCity" }; diff --git a/backend/src/services/app-connection/app-connection-service.ts b/backend/src/services/app-connection/app-connection-service.ts index 9669de0c8..7a8b1a09c 100644 --- a/backend/src/services/app-connection/app-connection-service.ts +++ b/backend/src/services/app-connection/app-connection-service.ts @@ -43,6 +43,8 @@ import { ValidateGcpConnectionCredentialsSchema } from "./gcp"; import { gcpConnectionService } from "./gcp/gcp-connection-service"; import { ValidateGitHubConnectionCredentialsSchema } from "./github"; import { githubConnectionService } from "./github/github-connection-service"; +import { ValidateHCVaultConnectionCredentialsSchema } from "./hc-vault"; +import { hcVaultConnectionService } from "./hc-vault/hc-vault-connection-service"; import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec"; import { humanitecConnectionService } from "./humanitec/humanitec-connection-service"; import { ValidateLdapConnectionCredentialsSchema } from "./ldap"; @@ -81,6 +83,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record { + const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl); + + await blockLocalAndPrivateIpAddresses(instanceUrl); + + return instanceUrl; +}; + +export const getHCVaultConnectionListItem = () => ({ + name: "HCVault" as const, + app: AppConnection.HCVault as const, + methods: Object.values(HCVaultConnectionMethod) as [ + HCVaultConnectionMethod.AccessToken, + HCVaultConnectionMethod.AppRole + ] +}); + +type TokenRespData = { + auth: { + client_token: string; + }; +}; + +export const getHCVaultAccessToken = async (connection: TValidateHCVaultConnectionCredentials) => { + // Return access token directly if not using AppRole method + if (connection.method !== HCVaultConnectionMethod.AppRole) { + return connection.credentials.accessToken; + } + + // Generate temporary token for AppRole method + try { + const { instanceUrl, roleId, secretId } = connection.credentials; + const tokenResp = await request.post( + `${removeTrailingSlash(instanceUrl)}/v1/auth/approle/login`, + { role_id: roleId, secret_id: secretId }, + { + headers: { + "Content-Type": "application/json", + ...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {}) + } + } + ); + + if (tokenResp.status !== 200) { + throw new BadRequestError({ + message: `Unable to validate credentials: Hashicorp Vault responded with a status code of ${tokenResp.status} (${tokenResp.statusText}). Verify credentials and try again.` + }); + } + + return tokenResp.data.auth.client_token; + } catch (e: unknown) { + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } +}; + +export const validateHCVaultConnectionCredentials = async (config: THCVaultConnectionConfig) => { + const instanceUrl = await getHCVaultInstanceUrl(config); + + try { + const accessToken = await getHCVaultAccessToken(config); + + // Verify token + await request.get(`${instanceUrl}/v1/auth/token/lookup-self`, { + headers: { "X-Vault-Token": accessToken } + }); + + return config.credentials; + } catch (error: unknown) { + if (error instanceof AxiosError) { + throw new BadRequestError({ + message: `Failed to validate credentials: ${error.message || "Unknown error"}` + }); + } + throw new BadRequestError({ + message: "Unable to validate connection: verify credentials" + }); + } +}; + +export const listHCVaultMounts = async (appConnection: THCVaultConnection) => { + const instanceUrl = await getHCVaultInstanceUrl(appConnection); + const accessToken = await getHCVaultAccessToken(appConnection); + + const { data } = await request.get(`${instanceUrl}/v1/sys/mounts`, { + headers: { + "X-Vault-Token": accessToken, + ...(appConnection.credentials.namespace ? { "X-Vault-Namespace": appConnection.credentials.namespace } : {}) + } + }); + + const mounts: string[] = []; + + // Filter for "kv" version 2 type only + Object.entries(data.data).forEach(([path, mount]) => { + if (mount.type === "kv" && mount.options?.version === "2") { + mounts.push(path); + } + }); + + return mounts; +}; diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-schemas.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-schemas.ts new file mode 100644 index 000000000..a6db4d0eb --- /dev/null +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-schemas.ts @@ -0,0 +1,100 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { HCVaultConnectionMethod } from "./hc-vault-connection-enums"; + +const InstanceUrlSchema = z + .string() + .trim() + .min(1, "Instance URL required") + .url("Invalid Instance URL") + .describe(AppConnections.CREDENTIALS.HC_VAULT.instanceUrl); + +const NamespaceSchema = z.string().trim().optional().describe(AppConnections.CREDENTIALS.HC_VAULT.namespace); + +export const HCVaultConnectionAccessTokenCredentialsSchema = z.object({ + instanceUrl: InstanceUrlSchema, + namespace: NamespaceSchema, + accessToken: z + .string() + .trim() + .min(1, "Access Token required") + .describe(AppConnections.CREDENTIALS.HC_VAULT.accessToken) +}); + +export const HCVaultConnectionAppRoleCredentialsSchema = z.object({ + instanceUrl: InstanceUrlSchema, + namespace: NamespaceSchema, + roleId: z.string().trim().min(1, "Role ID required").describe(AppConnections.CREDENTIALS.HC_VAULT.roleId), + secretId: z.string().trim().min(1, "Secret ID required").describe(AppConnections.CREDENTIALS.HC_VAULT.secretId) +}); + +const BaseHCVaultConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.HCVault) }); + +export const HCVaultConnectionSchema = z.intersection( + BaseHCVaultConnectionSchema, + z.discriminatedUnion("method", [ + z.object({ + method: z.literal(HCVaultConnectionMethod.AccessToken), + credentials: HCVaultConnectionAccessTokenCredentialsSchema + }), + z.object({ + method: z.literal(HCVaultConnectionMethod.AppRole), + credentials: HCVaultConnectionAppRoleCredentialsSchema + }) + ]) +); + +export const SanitizedHCVaultConnectionSchema = z.discriminatedUnion("method", [ + BaseHCVaultConnectionSchema.extend({ + method: z.literal(HCVaultConnectionMethod.AccessToken), + credentials: HCVaultConnectionAccessTokenCredentialsSchema.pick({}) + }), + BaseHCVaultConnectionSchema.extend({ + method: z.literal(HCVaultConnectionMethod.AppRole), + credentials: HCVaultConnectionAppRoleCredentialsSchema.pick({}) + }) +]); + +export const ValidateHCVaultConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(HCVaultConnectionMethod.AccessToken) + .describe(AppConnections.CREATE(AppConnection.HCVault).method), + credentials: HCVaultConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.HCVault).credentials + ) + }), + z.object({ + method: z.literal(HCVaultConnectionMethod.AppRole).describe(AppConnections.CREATE(AppConnection.HCVault).method), + credentials: HCVaultConnectionAppRoleCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.HCVault).credentials + ) + }) +]); + +export const CreateHCVaultConnectionSchema = ValidateHCVaultConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.HCVault) +); + +export const UpdateHCVaultConnectionSchema = z + .object({ + credentials: z + .union([HCVaultConnectionAccessTokenCredentialsSchema, HCVaultConnectionAppRoleCredentialsSchema]) + .optional() + .describe(AppConnections.UPDATE(AppConnection.HCVault).credentials) + }) + .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.HCVault)); + +export const HCVaultConnectionListItemSchema = z.object({ + name: z.literal("HCVault"), + app: z.literal(AppConnection.HCVault), + methods: z.nativeEnum(HCVaultConnectionMethod).array() +}); diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-service.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-service.ts new file mode 100644 index 000000000..b5cee6fdd --- /dev/null +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-service.ts @@ -0,0 +1,30 @@ +import { logger } from "@app/lib/logger"; +import { OrgServiceActor } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { listHCVaultMounts } from "./hc-vault-connection-fns"; +import { THCVaultConnection } from "./hc-vault-connection-types"; + +type TGetAppConnectionFunc = ( + app: AppConnection, + connectionId: string, + actor: OrgServiceActor +) => Promise; + +export const hcVaultConnectionService = (getAppConnection: TGetAppConnectionFunc) => { + const listMounts = async (connectionId: string, actor: OrgServiceActor) => { + const appConnection = await getAppConnection(AppConnection.HCVault, connectionId, actor); + + try { + const mounts = await listHCVaultMounts(appConnection); + return mounts; + } catch (error) { + logger.error(error, "Failed to establish connection with Hashicorp Vault"); + return []; + } + }; + + return { + listMounts + }; +}; diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-types.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-types.ts new file mode 100644 index 000000000..6f254eda0 --- /dev/null +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-types.ts @@ -0,0 +1,35 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateHCVaultConnectionSchema, + HCVaultConnectionSchema, + ValidateHCVaultConnectionCredentialsSchema +} from "./hc-vault-connection-schemas"; + +export type THCVaultConnection = z.infer; + +export type THCVaultConnectionInput = z.infer & { + app: AppConnection.HCVault; +}; + +export type TValidateHCVaultConnectionCredentialsSchema = typeof ValidateHCVaultConnectionCredentialsSchema; + +export type TValidateHCVaultConnectionCredentials = z.infer; + +export type THCVaultConnectionConfig = DiscriminativePick & { + orgId: string; +}; + +export type THCVaultMountResponse = { + data: { + [key: string]: { + options: { + version?: string | null; + } | null; + type: string; // We're only interested in "kv" types + }; + }; +}; diff --git a/backend/src/services/app-connection/hc-vault/index.ts b/backend/src/services/app-connection/hc-vault/index.ts new file mode 100644 index 000000000..161c2b51c --- /dev/null +++ b/backend/src/services/app-connection/hc-vault/index.ts @@ -0,0 +1,4 @@ +export * from "./hc-vault-connection-enums"; +export * from "./hc-vault-connection-fns"; +export * from "./hc-vault-connection-schemas"; +export * from "./hc-vault-connection-types"; diff --git a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-constants.ts b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-constants.ts new file mode 100644 index 000000000..4210307d2 --- /dev/null +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-constants.ts @@ -0,0 +1,10 @@ +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types"; + +export const HC_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = { + name: "Hashicorp Vault", + destination: SecretSync.HCVault, + connection: AppConnection.HCVault, + canImportSecrets: true +}; 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 new file mode 100644 index 000000000..db35df292 --- /dev/null +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts @@ -0,0 +1,161 @@ +import { isAxiosError } from "axios"; + +import { request } from "@app/lib/config/request"; +import { removeTrailingSlash } from "@app/lib/fn"; +import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; +import { getHCVaultAccessToken, getHCVaultInstanceUrl } from "@app/services/app-connection/hc-vault"; +import { + THCVaultListVariables, + THCVaultListVariablesResponse, + THCVaultSyncWithCredentials, + TPostHCVaultVariable +} from "@app/services/secret-sync/hc-vault/hc-vault-sync-types"; +import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; +import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; + +const listHCVaultVariables = async ({ instanceUrl, namespace, mount, accessToken, path }: THCVaultListVariables) => { + await blockLocalAndPrivateIpAddresses(instanceUrl); + + try { + const { data } = await request.get( + `${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`, + { + headers: { + "X-Vault-Token": accessToken, + ...(namespace ? { "X-Vault-Namespace": namespace } : {}) + } + } + ); + + return data.data.data; + } catch (error: unknown) { + // Returning an empty set when a path isn't found allows that path to be created by a later POST request + if (isAxiosError(error) && error.response?.status === 404) { + return {}; + } + throw error; + } +}; + +// Hashicorp Vault updates all variables in one batch. This is to respect their versioning +const updateHCVaultVariables = async ({ + path, + instanceUrl, + namespace, + accessToken, + mount, + data +}: TPostHCVaultVariable) => { + await blockLocalAndPrivateIpAddresses(instanceUrl); + + return request.post( + `${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`, + { + data + }, + { + headers: { + "X-Vault-Token": accessToken, + ...(namespace ? { "X-Vault-Namespace": namespace } : {}), + "Content-Type": "application/json" + } + } + ); +}; + +export const HCVaultSyncFns = { + syncSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { mount, path }, + syncOptions: { disableSecretDeletion } + } = secretSync; + + const { namespace } = connection.credentials; + const accessToken = await getHCVaultAccessToken(connection); + const instanceUrl = await getHCVaultInstanceUrl(connection); + + const variables = await listHCVaultVariables({ + instanceUrl, + accessToken, + namespace, + mount, + path + }); + let tainted = false; + + for (const entry of Object.entries(secretMap)) { + const [key, { value }] = entry; + if (value !== variables[key]) { + variables[key] = value; + tainted = true; + } + } + + if (disableSecretDeletion) return; + + for await (const [key] of Object.entries(variables)) { + if (!(key in secretMap)) { + delete variables[key]; + tainted = true; + } + } + + // Only update variables if there was a change detected + if (!tainted) return; + + try { + await updateHCVaultVariables({ accessToken, instanceUrl, namespace, mount, path, data: variables }); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + }, + removeSecrets: async (secretSync: THCVaultSyncWithCredentials, secretMap: TSecretMap) => { + const { + connection, + destinationConfig: { mount, path } + } = secretSync; + + const { namespace } = connection.credentials; + const accessToken = await getHCVaultAccessToken(connection); + const instanceUrl = await getHCVaultInstanceUrl(connection); + + const variables = await listHCVaultVariables({ instanceUrl, namespace, accessToken, mount, path }); + + for await (const [key] of Object.entries(variables)) { + if (key in secretMap) { + delete variables[key]; + } + } + + try { + await updateHCVaultVariables({ accessToken, instanceUrl, namespace, mount, path, data: variables }); + } catch (error) { + throw new SecretSyncError({ + error + }); + } + }, + getSecrets: async (secretSync: THCVaultSyncWithCredentials) => { + const { + connection, + destinationConfig: { mount, path } + } = secretSync; + + const { namespace } = connection.credentials; + const accessToken = await getHCVaultAccessToken(connection); + const instanceUrl = await getHCVaultInstanceUrl(connection); + + const variables = await listHCVaultVariables({ + instanceUrl, + namespace, + accessToken, + mount, + path + }); + + return Object.fromEntries(Object.entries(variables).map(([key, value]) => [key, { value }])); + } +}; diff --git a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-schemas.ts b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-schemas.ts new file mode 100644 index 000000000..d0f2a9f65 --- /dev/null +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-schemas.ts @@ -0,0 +1,58 @@ +import RE2 from "re2"; +import { z } from "zod"; + +import { SecretSyncs } from "@app/lib/api-docs"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; +import { SecretSync } from "@app/services/secret-sync/secret-sync-enums"; +import { + BaseSecretSyncSchema, + GenericCreateSecretSyncFieldsSchema, + GenericUpdateSecretSyncFieldsSchema +} from "@app/services/secret-sync/secret-sync-schemas"; +import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"; + +const HCVaultSyncDestinationConfigSchema = z.object({ + mount: z + .string() + .trim() + .min(1, "Secrets Engine Mount required") + .describe(SecretSyncs.DESTINATION_CONFIG.HC_VAULT.mount), + path: z + .string() + .trim() + .min(1, "Path required") + .transform((val) => val.replace(/^\/+|\/+$/g, "")) // removes leading/trailing slashes + .refine((val) => new RE2("^([a-zA-Z0-9._-]+/)*[a-zA-Z0-9._-]+$").test(val), { + message: + "Invalid Vault path format. Use alphanumerics, dots, dashes, underscores, and single slashes between segments." + }) + .describe(SecretSyncs.DESTINATION_CONFIG.HC_VAULT.path) +}); + +const HCVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true }; + +export const HCVaultSyncSchema = BaseSecretSyncSchema(SecretSync.HCVault, HCVaultSyncOptionsConfig).extend({ + destination: z.literal(SecretSync.HCVault), + destinationConfig: HCVaultSyncDestinationConfigSchema +}); + +export const CreateHCVaultSyncSchema = GenericCreateSecretSyncFieldsSchema( + SecretSync.HCVault, + HCVaultSyncOptionsConfig +).extend({ + destinationConfig: HCVaultSyncDestinationConfigSchema +}); + +export const UpdateHCVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema( + SecretSync.HCVault, + HCVaultSyncOptionsConfig +).extend({ + destinationConfig: HCVaultSyncDestinationConfigSchema.optional() +}); + +export const HCVaultSyncListItemSchema = z.object({ + name: z.literal("Hashicorp Vault"), + connection: z.literal(AppConnection.HCVault), + destination: z.literal(SecretSync.HCVault), + canImportSecrets: z.literal(true) +}); diff --git a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-types.ts b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-types.ts new file mode 100644 index 000000000..4da823a76 --- /dev/null +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-types.ts @@ -0,0 +1,39 @@ +import { z } from "zod"; + +import { THCVaultConnection } from "@app/services/app-connection/hc-vault"; + +import { CreateHCVaultSyncSchema, HCVaultSyncListItemSchema, HCVaultSyncSchema } from "./hc-vault-sync-schemas"; + +export type THCVaultSync = z.infer; + +export type THCVaultSyncInput = z.infer; + +export type THCVaultSyncListItem = z.infer; + +export type THCVaultSyncWithCredentials = THCVaultSync & { + connection: THCVaultConnection; +}; + +export type THCVaultListVariablesResponse = { + data: { + data: { + [key: string]: string; + }; + }; +}; + +export type THCVaultListVariables = { + accessToken: string; + instanceUrl: string; + namespace?: string; + mount: string; + path: string; +}; + +export type TPostHCVaultVariable = THCVaultListVariables & { + data: { + [key: string]: string; + }; +}; + +export type TDeleteHCVaultVariable = THCVaultListVariables; diff --git a/backend/src/services/secret-sync/hc-vault/index.ts b/backend/src/services/secret-sync/hc-vault/index.ts new file mode 100644 index 000000000..6fd9f4310 --- /dev/null +++ b/backend/src/services/secret-sync/hc-vault/index.ts @@ -0,0 +1,4 @@ +export * from "./hc-vault-sync-constants"; +export * from "./hc-vault-sync-fns"; +export * from "./hc-vault-sync-schemas"; +export * from "./hc-vault-sync-types"; diff --git a/backend/src/services/secret-sync/secret-sync-enums.ts b/backend/src/services/secret-sync/secret-sync-enums.ts index 687d76f33..9d59ebb76 100644 --- a/backend/src/services/secret-sync/secret-sync-enums.ts +++ b/backend/src/services/secret-sync/secret-sync-enums.ts @@ -11,6 +11,7 @@ export enum SecretSync { Camunda = "camunda", Vercel = "vercel", Windmill = "windmill", + HCVault = "hashicorp-vault", TeamCity = "teamcity" } diff --git a/backend/src/services/secret-sync/secret-sync-fns.ts b/backend/src/services/secret-sync/secret-sync-fns.ts index 143fa4622..f5737edb3 100644 --- a/backend/src/services/secret-sync/secret-sync-fns.ts +++ b/backend/src/services/secret-sync/secret-sync-fns.ts @@ -25,6 +25,7 @@ import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./az import { CAMUNDA_SYNC_LIST_OPTION, camundaSyncFactory } from "./camunda"; import { GCP_SYNC_LIST_OPTION } from "./gcp"; import { GcpSyncFns } from "./gcp/gcp-sync-fns"; +import { HC_VAULT_SYNC_LIST_OPTION, HCVaultSyncFns } from "./hc-vault"; import { HUMANITEC_SYNC_LIST_OPTION } from "./humanitec"; import { HumanitecSyncFns } from "./humanitec/humanitec-sync-fns"; import { TEAMCITY_SYNC_LIST_OPTION, TeamCitySyncFns } from "./teamcity"; @@ -45,6 +46,7 @@ const SECRET_SYNC_LIST_OPTIONS: Record = { [SecretSync.Camunda]: CAMUNDA_SYNC_LIST_OPTION, [SecretSync.Vercel]: VERCEL_SYNC_LIST_OPTION, [SecretSync.Windmill]: WINDMILL_SYNC_LIST_OPTION, + [SecretSync.HCVault]: HC_VAULT_SYNC_LIST_OPTION, [SecretSync.TeamCity]: TEAMCITY_SYNC_LIST_OPTION }; @@ -142,6 +144,8 @@ export const SecretSyncFns = { return VercelSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.Windmill: return WindmillSyncFns.syncSecrets(secretSync, secretMap); + case SecretSync.HCVault: + return HCVaultSyncFns.syncSecrets(secretSync, secretMap); case SecretSync.TeamCity: return TeamCitySyncFns.syncSecrets(secretSync, secretMap); default: @@ -203,6 +207,9 @@ export const SecretSyncFns = { case SecretSync.Windmill: secretMap = await WindmillSyncFns.getSecrets(secretSync); break; + case SecretSync.HCVault: + secretMap = await HCVaultSyncFns.getSecrets(secretSync); + break; case SecretSync.TeamCity: secretMap = await TeamCitySyncFns.getSecrets(secretSync); break; @@ -259,6 +266,8 @@ export const SecretSyncFns = { return VercelSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.Windmill: return WindmillSyncFns.removeSecrets(secretSync, secretMap); + case SecretSync.HCVault: + return HCVaultSyncFns.removeSecrets(secretSync, secretMap); case SecretSync.TeamCity: return TeamCitySyncFns.removeSecrets(secretSync, secretMap); default: diff --git a/backend/src/services/secret-sync/secret-sync-maps.ts b/backend/src/services/secret-sync/secret-sync-maps.ts index fdf4dfabb..c6d7adc8c 100644 --- a/backend/src/services/secret-sync/secret-sync-maps.ts +++ b/backend/src/services/secret-sync/secret-sync-maps.ts @@ -14,6 +14,7 @@ export const SECRET_SYNC_NAME_MAP: Record = { [SecretSync.Camunda]: "Camunda", [SecretSync.Vercel]: "Vercel", [SecretSync.Windmill]: "Windmill", + [SecretSync.HCVault]: "Hashicorp Vault", [SecretSync.TeamCity]: "TeamCity" }; @@ -30,5 +31,6 @@ export const SECRET_SYNC_CONNECTION_MAP: Record = { [SecretSync.Camunda]: AppConnection.Camunda, [SecretSync.Vercel]: AppConnection.Vercel, [SecretSync.Windmill]: AppConnection.Windmill, + [SecretSync.HCVault]: AppConnection.HCVault, [SecretSync.TeamCity]: AppConnection.TeamCity }; diff --git a/backend/src/services/secret-sync/secret-sync-types.ts b/backend/src/services/secret-sync/secret-sync-types.ts index e99b31c20..e88174cc6 100644 --- a/backend/src/services/secret-sync/secret-sync-types.ts +++ b/backend/src/services/secret-sync/secret-sync-types.ts @@ -55,6 +55,12 @@ import { TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault"; import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp"; +import { + THCVaultSync, + THCVaultSyncInput, + THCVaultSyncListItem, + THCVaultSyncWithCredentials +} from "./hc-vault/hc-vault-sync-types"; import { THumanitecSync, THumanitecSyncInput, @@ -88,6 +94,7 @@ export type TSecretSync = | TCamundaSync | TVercelSync | TWindmillSync + | THCVaultSync | TTeamCitySync; export type TSecretSyncWithCredentials = @@ -103,6 +110,7 @@ export type TSecretSyncWithCredentials = | TCamundaSyncWithCredentials | TVercelSyncWithCredentials | TWindmillSyncWithCredentials + | THCVaultSyncWithCredentials | TTeamCitySyncWithCredentials; export type TSecretSyncInput = @@ -118,6 +126,7 @@ export type TSecretSyncInput = | TCamundaSyncInput | TVercelSyncInput | TWindmillSyncInput + | THCVaultSyncInput | TTeamCitySyncInput; export type TSecretSyncListItem = @@ -133,6 +142,7 @@ export type TSecretSyncListItem = | TCamundaSyncListItem | TVercelSyncListItem | TWindmillSyncListItem + | THCVaultSyncListItem | TTeamCitySyncListItem; export type TSyncOptionsConfig = { diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 6ab348520..cd2773172 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -7,6 +7,7 @@ import { ProjectType, SecretsV2Schema, SecretType, TableName, TSecretsV2, TSecre import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { generateCacheKeyFromData } from "@app/lib/crypto/cache"; +import { applyJitter } from "@app/lib/dates"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { buildFindFilter, @@ -22,7 +23,6 @@ import type { TFindSecretsByFolderIdsFilter, TGetSecretsDTO } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; -import { applyJitter } from "@app/lib/dates"; export const SecretServiceCacheKeys = { get productKey() { diff --git a/docs/api-reference/endpoints/app-connections/hashicorp-vault/available.mdx b/docs/api-reference/endpoints/app-connections/hashicorp-vault/available.mdx new file mode 100644 index 000000000..ff52a325a --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/hashicorp-vault/available.mdx @@ -0,0 +1,4 @@ +--- +title: "Available" +openapi: "GET /api/v1/app-connections/hashicorp-vault/available" +--- diff --git a/docs/api-reference/endpoints/app-connections/hashicorp-vault/create.mdx b/docs/api-reference/endpoints/app-connections/hashicorp-vault/create.mdx new file mode 100644 index 000000000..6a978e780 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/hashicorp-vault/create.mdx @@ -0,0 +1,8 @@ +--- +title: "Create" +openapi: "POST /api/v1/app-connections/hashicorp-vault" +--- + + + Check out the configuration docs for [Hashicorp Vault Connections](/integrations/app-connections/hashicorp-vault) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/app-connections/hashicorp-vault/delete.mdx b/docs/api-reference/endpoints/app-connections/hashicorp-vault/delete.mdx new file mode 100644 index 000000000..aaf378fb5 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/hashicorp-vault/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/app-connections/hashicorp-vault/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/hashicorp-vault/get-by-id.mdx b/docs/api-reference/endpoints/app-connections/hashicorp-vault/get-by-id.mdx new file mode 100644 index 000000000..39366f508 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/hashicorp-vault/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/app-connections/hashicorp-vault/{connectionId}" +--- diff --git a/docs/api-reference/endpoints/app-connections/hashicorp-vault/get-by-name.mdx b/docs/api-reference/endpoints/app-connections/hashicorp-vault/get-by-name.mdx new file mode 100644 index 000000000..9f3c41783 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/hashicorp-vault/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/app-connections/hashicorp-vault/connection-name/{connectionName}" +--- diff --git a/docs/api-reference/endpoints/app-connections/hashicorp-vault/list.mdx b/docs/api-reference/endpoints/app-connections/hashicorp-vault/list.mdx new file mode 100644 index 000000000..474c70fd8 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/hashicorp-vault/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/app-connections/hashicorp-vault" +--- diff --git a/docs/api-reference/endpoints/app-connections/hashicorp-vault/update.mdx b/docs/api-reference/endpoints/app-connections/hashicorp-vault/update.mdx new file mode 100644 index 000000000..e155c8e67 --- /dev/null +++ b/docs/api-reference/endpoints/app-connections/hashicorp-vault/update.mdx @@ -0,0 +1,8 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/app-connections/hashicorp-vault/{connectionId}" +--- + + + Check out the configuration docs for [Hashicorp Vault Connections](/integrations/app-connections/hashicorp-vault) to learn how to obtain the required credentials. + diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/create.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/create.mdx new file mode 100644 index 000000000..ab9171f7b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/create.mdx @@ -0,0 +1,4 @@ +--- +title: "Create" +openapi: "POST /api/v1/secret-syncs/hashicorp-vault" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/delete.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/delete.mdx new file mode 100644 index 000000000..700438ba5 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/secret-syncs/hashicorp-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-id.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-id.mdx new file mode 100644 index 000000000..7017416c1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by ID" +openapi: "GET /api/v1/secret-syncs/hashicorp-vault/{syncId}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-name.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-name.mdx new file mode 100644 index 000000000..a817732f1 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-name.mdx @@ -0,0 +1,4 @@ +--- +title: "Get by Name" +openapi: "GET /api/v1/secret-syncs/hashicorp-vault/sync-name/{syncName}" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/import-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/import-secrets.mdx new file mode 100644 index 000000000..3ee2c479c --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/import-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Import Secrets" +openapi: "POST /api/v1/secret-syncs/hashicorp-vault/{syncId}/import-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/list.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/list.mdx new file mode 100644 index 000000000..e3c08f125 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/secret-syncs/hashicorp-vault" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/remove-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/remove-secrets.mdx new file mode 100644 index 000000000..7b54e94d6 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/remove-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Remove Secrets" +openapi: "POST /api/v1/secret-syncs/hashicorp-vault/{syncId}/remove-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/sync-secrets.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/sync-secrets.mdx new file mode 100644 index 000000000..24f58d802 --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/sync-secrets.mdx @@ -0,0 +1,4 @@ +--- +title: "Sync Secrets" +openapi: "POST /api/v1/secret-syncs/hashicorp-vault/{syncId}/sync-secrets" +--- diff --git a/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/update.mdx b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/update.mdx new file mode 100644 index 000000000..58ddc5a2b --- /dev/null +++ b/docs/api-reference/endpoints/secret-syncs/hashicorp-vault/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/secret-syncs/hashicorp-vault/{syncId}" +--- diff --git a/docs/images/app-connections/hashicorp-vault/vault-access.png b/docs/images/app-connections/hashicorp-vault/vault-access.png new file mode 100644 index 000000000..b6b6504fe Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-access.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-approle.png b/docs/images/app-connections/hashicorp-vault/vault-approle.png new file mode 100644 index 000000000..89cff90fe Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-approle.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-authentication-methods.png b/docs/images/app-connections/hashicorp-vault/vault-authentication-methods.png new file mode 100644 index 000000000..fd9d607b9 Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-authentication-methods.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-cluster-urls.png b/docs/images/app-connections/hashicorp-vault/vault-cluster-urls.png new file mode 100644 index 000000000..57af4514f Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-cluster-urls.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-create-policy.png b/docs/images/app-connections/hashicorp-vault/vault-create-policy.png new file mode 100644 index 000000000..5d429fcc3 Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-create-policy.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-enable-method.png b/docs/images/app-connections/hashicorp-vault/vault-enable-method.png new file mode 100644 index 000000000..c034995ac Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-enable-method.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-modal.png b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-modal.png new file mode 100644 index 000000000..872d11cab Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-modal.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-page.png b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-page.png new file mode 100644 index 000000000..4a91f8dbb Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-page.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-success.png b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-success.png new file mode 100644 index 000000000..90d562e11 Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-success.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-policies-navigate.png b/docs/images/app-connections/hashicorp-vault/vault-policies-navigate.png new file mode 100644 index 000000000..05cab30eb Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-policies-navigate.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-policies-page.png b/docs/images/app-connections/hashicorp-vault/vault-policies-page.png new file mode 100644 index 000000000..3a84b86ee Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-policies-page.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-profile-token.png b/docs/images/app-connections/hashicorp-vault/vault-profile-token.png new file mode 100644 index 000000000..aca3b7f0a Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-profile-token.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-shell-output.png b/docs/images/app-connections/hashicorp-vault/vault-shell-output.png new file mode 100644 index 000000000..a5d654110 Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-shell-output.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-shell.png b/docs/images/app-connections/hashicorp-vault/vault-shell.png new file mode 100644 index 000000000..ca3ff0bc4 Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-shell.png differ diff --git a/docs/images/app-connections/hashicorp-vault/vault-token.png b/docs/images/app-connections/hashicorp-vault/vault-token.png new file mode 100644 index 000000000..aca3b7f0a Binary files /dev/null and b/docs/images/app-connections/hashicorp-vault/vault-token.png differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-1.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-1.png deleted file mode 100644 index 367386709..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-1.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-2.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-2.png deleted file mode 100644 index 80de8df26..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-2.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-3.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-3.png deleted file mode 100644 index d51142541..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-3.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-auth.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-auth.png deleted file mode 100644 index b587777f9..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-auth.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-cluster-url.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-cluster-url.png deleted file mode 100644 index 619764b54..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-cluster-url.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-create.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-create.png deleted file mode 100644 index 7fdef0d4a..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-create.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-1.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-1.png deleted file mode 100644 index a65870b44..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-1.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-2.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-2.png deleted file mode 100644 index 34b03768f..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-2.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-3.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-3.png deleted file mode 100644 index 624fdc574..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-3.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-1.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-1.png deleted file mode 100644 index e2a77654a..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-1.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-2.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-2.png deleted file mode 100644 index 719659014..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-2.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-3.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-3.png deleted file mode 100644 index 76fe2de35..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-3.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-shell.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-shell.png deleted file mode 100644 index 7d63bde40..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault-shell.png and /dev/null differ diff --git a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault.png b/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault.png deleted file mode 100644 index e556ffc6b..000000000 Binary files a/docs/images/integrations/hashicorp-vault/integrations-hashicorp-vault.png and /dev/null differ diff --git a/docs/images/secret-syncs/hashicorp-vault/select-option.png b/docs/images/secret-syncs/hashicorp-vault/select-option.png new file mode 100644 index 000000000..3ed6ce079 Binary files /dev/null and b/docs/images/secret-syncs/hashicorp-vault/select-option.png differ diff --git a/docs/images/secret-syncs/hashicorp-vault/sync-created.png b/docs/images/secret-syncs/hashicorp-vault/sync-created.png new file mode 100644 index 000000000..6fc827f2c Binary files /dev/null and b/docs/images/secret-syncs/hashicorp-vault/sync-created.png differ diff --git a/docs/images/secret-syncs/hashicorp-vault/sync-destination.png b/docs/images/secret-syncs/hashicorp-vault/sync-destination.png new file mode 100644 index 000000000..42ec57273 Binary files /dev/null and b/docs/images/secret-syncs/hashicorp-vault/sync-destination.png differ diff --git a/docs/images/secret-syncs/hashicorp-vault/sync-details.png b/docs/images/secret-syncs/hashicorp-vault/sync-details.png new file mode 100644 index 000000000..61f045db0 Binary files /dev/null and b/docs/images/secret-syncs/hashicorp-vault/sync-details.png differ diff --git a/docs/images/secret-syncs/hashicorp-vault/sync-options.png b/docs/images/secret-syncs/hashicorp-vault/sync-options.png new file mode 100644 index 000000000..fd96843f0 Binary files /dev/null and b/docs/images/secret-syncs/hashicorp-vault/sync-options.png differ diff --git a/docs/images/secret-syncs/hashicorp-vault/sync-review.png b/docs/images/secret-syncs/hashicorp-vault/sync-review.png new file mode 100644 index 000000000..f95a12e0c Binary files /dev/null and b/docs/images/secret-syncs/hashicorp-vault/sync-review.png differ diff --git a/docs/images/secret-syncs/hashicorp-vault/sync-source.png b/docs/images/secret-syncs/hashicorp-vault/sync-source.png new file mode 100644 index 000000000..b3440a8df Binary files /dev/null and b/docs/images/secret-syncs/hashicorp-vault/sync-source.png differ diff --git a/docs/integrations/app-connections/hashicorp-vault.mdx b/docs/integrations/app-connections/hashicorp-vault.mdx new file mode 100644 index 000000000..6e2ff68bf --- /dev/null +++ b/docs/integrations/app-connections/hashicorp-vault.mdx @@ -0,0 +1,214 @@ +--- +title: "Hashicorp Vault Connection" +description: "Learn how to configure a Hashicorp Vault Connection for Infisical." +--- + + + Infisical is compatible with Vault Self-hosted, HCP Vault Dedicated, and HCP Vault Enterprise deployments. Please note that HCP Generic Secrets are currently not supported. + + +Infisical supports two methods for connecting to Hashicorp Vault. + + + + + + ![Vault Access](/images/app-connections/hashicorp-vault/vault-access.png) + + + In the **Authentication Methods** tab, click on **Enable new method**. + + ![Vault Enable Method](/images/app-connections/hashicorp-vault/vault-authentication-methods.png) + + + ![Vault AppRole](/images/app-connections/hashicorp-vault/vault-approle.png) + + + You may change the name of the method, but we suggest keeping it as `approle`. + + ![Vault Enable Method](/images/app-connections/hashicorp-vault/vault-enable-method.png) + + + From the home page, navigate to **Policies**. + + ![Vault Policies Navigate](/images/app-connections/hashicorp-vault/vault-policies-navigate.png) + + + ![Vault Policies Page](/images/app-connections/hashicorp-vault/vault-policies-page.png) + + + You may name your policy whatever you want, but remember the name as it will be used in future steps. + + Depending on your use case, you may have different policy configurations: + + + + ```hcl + path "demo_mount/data/*" { + capabilities = [ "create", "read", "update" ] + } + + path "sys/mounts" { + capabilities = ["read"] + } + ``` + + - **demo_mount**: The name of the target secrets engine (e.g., 'secret', 'kv'). + - **data/\***: The path within the secrets engine used for storing secrets. The wildcard (*) grants access to all secrets within this mount point. + + + Make sure to replace the policy path with the specific path where you intend to sync your secrets. For better security and control, it's recommended to use a more granular path instead of a wildcard (*). You can also specify a path that doesn’t yet exist—Infisical will automatically create it for you during the sync process. + + + + + ![Vault Create Policy](/images/app-connections/hashicorp-vault/vault-create-policy.png) + + + **Open Vault Shell** + + ![Vault Shell](/images/app-connections/hashicorp-vault/vault-shell.png) + + + If you used custom approle or policy names in previous steps, you'll need to customize the following commands. + + + **Create Infisical Role** + + ```hcl + vault write auth/approle/role/infisical token_policies="infisical-policy" token_ttl=30s token_max_ttl=2m + ``` + + **Read RoleID** + + ```hcl + vault read auth/approle/role/infisical/role-id + ``` + + **Generate New SecretID** + + ```hcl + vault write -force auth/approle/role/infisical/secret-id + ``` + + Your shell output should look similar to the image below. Save the RoleID and SecretID values for later steps. + + ![Vault Shell Output](/images/app-connections/hashicorp-vault/vault-shell-output.png) + + + + + ## Get a Hashicorp Vault Access Token + + Open your profile dropdown and click **Copy token**. This token will be used in later steps. + + ![Vault Profile Copy Token](/images/app-connections/hashicorp-vault/vault-profile-token.png) + + + +## Getting Vault Instance URL + + + + For self-hosted instances, locate and copy your vault's base URL (for example: `https://vault.example.com`). + + Save the URL for later steps. + + + On HCP instances, you may need to navigate to **Cluster Overview** to see your cluster URL. Save this value for later steps. + + ![Vault Cluster URLs](/images/app-connections/hashicorp-vault/vault-cluster-urls.png) + + + Cluster Overview is found in the HCP dashboard, not in your cluster's web UI. + + + + +## Setup Vault Connection in Infisical + + + + + + In your Infisical dashboard, go to **Organization Settings** and select the **App Connections** tab. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click the **+ Add Connection** button and select the **Hashicorp Vault Connection** option. + + ![Select Vault Connection](/images/app-connections/hashicorp-vault/vault-infisical-connect-page.png) + + + Configure your Vault Connection using the Instance URL and credentials from the steps above. **Depending on if you chose to authenticate with an Access Token or AppRole, you may need to input different information.** + + ![Vault Configure Connection](/images/app-connections/hashicorp-vault/vault-infisical-connect-modal.png) + + + + - **Name**: The name of the connection being created. Must be slug-friendly. + - **Description**: An optional description to provide details about this connection. + - **Instance URL**: The URL of your Hashicorp Vault instance. + - **Namespace (optional)**: The namespace within your vault. Self-hosted and enterprise clusters may not use namespaces. + - **Role ID**: The Role ID generated in the steps above. + - **Secret ID**: The Secret ID generated in the steps above. + + + - **Name**: The name of the connection being created. Must be slug-friendly. + - **Description**: An optional description to provide details about this connection. + - **Instance URL**: The URL of your Hashicorp Vault instance. + - **Namespace (optional)**: The namespace within your vault. Self-hosted and enterprise clusters may not use namespaces. + - **Access Token**: The Access Token generated in the steps above. + + + + + Your Vault Connection is now available for use. + ![Vault Connection Created](/images/app-connections/hashicorp-vault/vault-infisical-connect-success.png) + + + + + To create a Vault Connection, make an API request to the [Create Hashicorp Vault + Connection](/api-reference/endpoints/app-connections/hashicorp-vault/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/hashicorp-vault \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-vault-connection", + "method": "app-role", + "credentials": { + "instanceUrl": "https://vault.example.com", + "roleId": "4797c4fa-7794-71f0-c8b1-7c87759df5bf", + "secretId": "ad24df93-19c8-c865-9997-6b8513253d3a" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-vault-connection", + "version": 1, + "orgId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2025-04-01T05:31:56Z", + "updatedAt": "2025-04-01T05:31:56Z", + "app": "hashicorp-vault", + "method": "app-role", + "credentials": { + "instanceUrl": "https://vault.example.com", + "roleId": "4797c4fa-7794-71f0-c8b1-7c87759df5bf" + } + } + } + ``` + + diff --git a/docs/integrations/cloud/hashicorp-vault.mdx b/docs/integrations/cloud/hashicorp-vault.mdx index 51a66f3ff..df2542ce7 100644 --- a/docs/integrations/cloud/hashicorp-vault.mdx +++ b/docs/integrations/cloud/hashicorp-vault.mdx @@ -4,158 +4,5 @@ description: "How to sync secrets from Infisical to HashiCorp Vault" --- - Infisical connects to Vault via the AppRole auth method. - - Currently, each Infisical project can only point and sync secrets to one Vault cluster / namespace - but with unlimited integrations to different paths within it. - - This tutorial makes use of Vault's UI but, in principle, instructions can executed via - Vault CLI or API call. - - Lastly, you should note that we provide a simple use-case and, in practice, you should adapt and extend it to your own Vault use-case and follow best practices, for instance when defining fine-grained ACL policies. + The Hashicorp Vault Native Integration will be deprecated in 2026. Please migrate to our new [Hashicorp Vault Sync](../secret-syncs/hashicorp-vault). - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) -- Have experience with [HashiCorp Vault](https://www.vaultproject.io/). - -## Navigate to your project's integrations tab - -![integrations](../../images/integrations.png) - -## Prepare Vault - -This section mirrors the latter parts of the [Vault quickstart](https://developer.hashicorp.com/vault/tutorials/cloud/getting-started-intro) provided by HashiCorp and uses sample names/values for demonstration. - -To begin, navigate to the cluster / namespace that you want to sync secrets to in Vault; we'll use the default `admin` namespace (in practice, we recommend creating a namespace and not using the default `admin` namespace). - -### Enable KV Secrets Engine - -In Secrets, enable a KV Secrets Engine at a path for Infisical to sync secrets to; we'll use the path `kv`. - -![integrations hashicorp vault secrets engine](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-1.png) -![integrations hashicorp vault secrets engine](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-2.png) -![integrations hashicorp vault secrets engine](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-engine-3.png) - -### Enable the AppRole auth method - -In Access > Auth Methods, enable the AppRole auth method. - -![integrations hashicorp vault access](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-1.png) -![integrations hashicorp vault access](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-2.png) -![integrations hashicorp vault access](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-access-3.png) - -### Create an ACL Policy - -Now in Policies, create a new ACL policy scoped to the path(s) you wish Infisical to be able to sync secrets to. - -We'll call the policy `test` and have it grant access to the `dev` path in the KV Secrets Engine where we will be syncing secrets to from Infisical. - -```console -path "kv/data/dev" { - capabilities = [ "create", "read", "update" ] -} - -path "sys/namespaces/*" { - capabilities = [ "create", "read", "update", "delete", "list" ] -} -``` - - - `kv` comes from the path of the KV Secrets Engine that we enabled and `dev` is the chosen path within it - that we want to sync secrets to. - - -![integrations hashicorp vault policy](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-1.png) -![integrations hashicorp vault policy](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-2.png) -![integrations hashicorp vault policy](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-policy-3.png) - -### Create a role with the policy attached - -We now create a `infisical` role with the generated token's time-to-live (TTL) set to 1 hour and can be renewed for up to 4 hours from the time of its creation. - -1. Click the Vault CLI shell icon (`>_`) to open a command shell in the browser. - -![integrations hashicorp vault shell](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-shell.png) - -2. Copy the command below. - -```console -vault write auth/approle/role/infisical token_policies="test" token_ttl=1h token_max_ttl=4h -``` - -3. Paste the command into the command shell in the browser and press the enter button. - -### Generate a RoleID and SecretID - -Finally, we need to generate a **RoleID** and **SecretID** (like a username and password) that Infisical can use -to authenticate with Vault. - -1. Click the Vault CLI shell icon (>_) again to open a command shell. - -2. Read the RoleID. - -```console -vault read auth/approle/role/infisical/role-id -``` - -Example output: - -```console -Key Value -role_id b6ccdcca-183b-ce9c-6b98-b556b9a0edb9 -``` - -3. Generate a new SecretID of the `infisical` role. - -```console -vault write -force auth/approle/role/infisical/secret-id -``` - -Example output: - - -```console -Key Value -secret_id 735a47cc-7a98-77cc-0128-12b1e96a4157 -secret_id_accessor 3ab305d1-1eab-df4b-4079-ef7135635c49 -...snip... -``` - -Great. We're now ready to connect Infisical to Vault! - -## Enter your Vault instance and authentication details - -Back in Infisical, press on the HashiCorp Vault tile and input your Vault instance and `infisical` role RoleID and SecretID. - -![integrations hashicorp vault authorization](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-auth.png) - -For additional details on each field: - -- Vault Cluster URL: The address of your cluster, either HCP or self-hosted. - -If using HCP, you can copy your Cluster URL in the Cluster Overview: - -![integrations hashicorp vault cluster URL](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-cluster-url.png) - -- Vault Namespace: The Vault namespace you wish to connect to. -- Vault RoleID: The RoleID previously created for the `infisical` role. -- Vault SecretID: The SecretID previously created for the `infisical` role. - -## Start integration - -Select which Infisical environment secrets you want to sync to Vault. - -For additional details on each field: - -- Vault KV Secrets Engine Path: the path at which you enabled the intended KV Secrets Engine; in this demonstration, we used `kv`. -- Vault Secret(s) Path: the path in the KV Secrets Engine that you wish to sync secrets to. - -Press create integration to start syncing secrets to Vault. - -![integrations hashicorp vault](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault-create.png) -![integrations hashicorp vault](../../images/integrations/hashicorp-vault/integrations-hashicorp-vault.png) - - - diff --git a/docs/integrations/secret-syncs/hashicorp-vault.mdx b/docs/integrations/secret-syncs/hashicorp-vault.mdx new file mode 100644 index 000000000..0d6c0d644 --- /dev/null +++ b/docs/integrations/secret-syncs/hashicorp-vault.mdx @@ -0,0 +1,160 @@ +--- +title: "Hashicorp Vault Sync" +description: "Learn how to configure a Hashicorp Vault Sync for Infisical." +--- + +**Prerequisites:** + - Set up and add secrets to [Infisical Cloud](https://app.infisical.com) + - Create a [Hashicorp Vault Connection](/integrations/app-connections/hashicorp-vault) + + + + + + Navigate to **Project** > **Integrations** and select the **Secret Syncs** tab. Click on the **Add Sync** button. + + ![Secret Syncs Tab](/images/secret-syncs/general/secret-sync-tab.png) + + + ![Select Hashicorp Vault](/images/secret-syncs/hashicorp-vault/select-option.png) + + + Configure the **Source** from where secrets should be retrieved, then click **Next**. + + ![Configure Source](/images/secret-syncs/hashicorp-vault/sync-source.png) + + - **Environment**: The project environment to retrieve secrets from. + - **Secret Path**: The folder path to retrieve secrets from. + + + If you need to sync secrets from multiple folder locations, check out [secret imports](/documentation/platform/secret-reference#secret-imports). + + + + Configure the **Destination** to where secrets should be deployed. + + ![Configure Destination](/images/secret-syncs/hashicorp-vault/sync-destination.png) + + - **Hashicorp Vault Connection**: The Vault Connection to authenticate with. + - **Secrets Engine Mount**: The secrets engine to sync secrets with (e.g., 'secret', 'kv'). + - **Path**: The specific path within the secrets engine where secrets will be stored. + + After configuring these parameters, click the **Next** button to continue to the Sync Options step. + + + If the **path** you provide does not exist in Vault, it will be created. + + + + Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. + + ![Configure Options](/images/secret-syncs/hashicorp-vault/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 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. + - **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. + + + Configure the **Details** of your Hashicorp Vault Sync, then click **Next**. + + ![Configure Details](/images/secret-syncs/hashicorp-vault/sync-details.png) + + - **Name**: The name of your sync. Must be slug-friendly. + - **Description**: An optional description for your sync. + + + Review your Hashicorp Vault Sync configuration, then click **Create Sync**. + + ![Confirm Configuration](/images/secret-syncs/hashicorp-vault/sync-review.png) + + + If enabled, your Hashicorp Vault Sync will begin syncing your secrets to the destination endpoint. + + ![Sync Created](/images/secret-syncs/hashicorp-vault/sync-created.png) + + + + + To create an **Hashicorp Vault Sync**, make an API request to the [Create Hashicorp Vault Sync](/api-reference/endpoints/secret-syncs/hashicorp-vault/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/secret-syncs/hashicorp-vault \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-vault-sync", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "an example sync", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "environment": "dev", + "secretPath": "/", + "isEnabled": true, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "destinationConfig": { + "mount": "secret", + "path": "dev/nested" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretSync": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-vault-sync", + "description": "an example sync", + "isEnabled": true, + "version": 1, + "folderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connectionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "syncStatus": "succeeded", + "lastSyncJobId": "123", + "lastSyncMessage": null, + "lastSyncedAt": "2023-11-07T05:31:56Z", + "importStatus": null, + "lastImportJobId": null, + "lastImportMessage": null, + "lastImportedAt": null, + "removeStatus": null, + "lastRemoveJobId": null, + "lastRemoveMessage": null, + "lastRemovedAt": null, + "syncOptions": { + "initialSyncBehavior": "overwrite-destination" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "connection": { + "app": "hashicorp-vault", + "name": "my-vault-connection", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a" + }, + "folder": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "path": "/" + }, + "destination": "hashicorp-vault", + "destinationConfig": { + "mount": "secret", + "path": "dev/nested" + } + } + } + ``` + + diff --git a/docs/mint.json b/docs/mint.json index 06091b281..0040e739b 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -438,6 +438,7 @@ "integrations/app-connections/databricks", "integrations/app-connections/gcp", "integrations/app-connections/github", + "integrations/app-connections/hashicorp-vault", "integrations/app-connections/humanitec", "integrations/app-connections/ldap", "integrations/app-connections/mssql", @@ -465,6 +466,7 @@ "integrations/secret-syncs/databricks", "integrations/secret-syncs/gcp-secret-manager", "integrations/secret-syncs/github", + "integrations/secret-syncs/hashicorp-vault", "integrations/secret-syncs/humanitec", "integrations/secret-syncs/teamcity", "integrations/secret-syncs/terraform-cloud", @@ -1068,6 +1070,18 @@ "api-reference/endpoints/app-connections/github/delete" ] }, + { + "group": "Hashicorp Vault", + "pages": [ + "api-reference/endpoints/app-connections/hashicorp-vault/list", + "api-reference/endpoints/app-connections/hashicorp-vault/available", + "api-reference/endpoints/app-connections/hashicorp-vault/get-by-id", + "api-reference/endpoints/app-connections/hashicorp-vault/get-by-name", + "api-reference/endpoints/app-connections/hashicorp-vault/create", + "api-reference/endpoints/app-connections/hashicorp-vault/update", + "api-reference/endpoints/app-connections/hashicorp-vault/delete" + ] + }, { "group": "Humanitec", "pages": [ @@ -1280,6 +1294,20 @@ "api-reference/endpoints/secret-syncs/github/remove-secrets" ] }, + { + "group": "Hashicorp Vault", + "pages": [ + "api-reference/endpoints/secret-syncs/hashicorp-vault/list", + "api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-id", + "api-reference/endpoints/secret-syncs/hashicorp-vault/get-by-name", + "api-reference/endpoints/secret-syncs/hashicorp-vault/create", + "api-reference/endpoints/secret-syncs/hashicorp-vault/update", + "api-reference/endpoints/secret-syncs/hashicorp-vault/delete", + "api-reference/endpoints/secret-syncs/hashicorp-vault/sync-secrets", + "api-reference/endpoints/secret-syncs/hashicorp-vault/import-secrets", + "api-reference/endpoints/secret-syncs/hashicorp-vault/remove-secrets" + ] + }, { "group": "Humanitec", "pages": [ diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HCVaultSyncFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HCVaultSyncFields.tsx new file mode 100644 index 000000000..76e14b6d7 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/HCVaultSyncFields.tsx @@ -0,0 +1,86 @@ +import { Controller, useFormContext, useWatch } from "react-hook-form"; +import { SingleValue } from "react-select"; +import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { SecretSyncConnectionField } from "@app/components/secret-syncs/forms/SecretSyncConnectionField"; +import { FilterableSelect, FormControl, Input, Tooltip } from "@app/components/v2"; +import { useHCVaultConnectionListMounts } from "@app/hooks/api/appConnections/hc-vault"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +import { TSecretSyncForm } from "../schemas"; + +export const HCVaultSyncFields = () => { + const { control, setValue } = useFormContext< + TSecretSyncForm & { destination: SecretSync.HCVault } + >(); + + const connectionId = useWatch({ name: "connection.id", control }); + + const { data: mounts, isLoading: isMountsLoading } = useHCVaultConnectionListMounts( + connectionId, + { + enabled: Boolean(connectionId) + } + ); + + return ( + <> + { + setValue("destinationConfig.mount", ""); + setValue("destinationConfig.path", ""); + }} + /> + + ( + +
+ Don't see the mount you're looking for?{" "} + +
+ + } + > + + onChange((option as SingleValue<{ value: string }>)?.value ?? null) + } + options={mounts?.map((v) => ({ label: v, value: v }))} + placeholder="Select a Secrets Engine Mount..." + /> +
+ )} + /> + ( + + + + )} + /> + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx index 47afc6f04..1d7a1dd55 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncDestinationFields/SecretSyncDestinationFields.tsx @@ -11,6 +11,7 @@ import { CamundaSyncFields } from "./CamundaSyncFields"; import { DatabricksSyncFields } from "./DatabricksSyncFields"; import { GcpSyncFields } from "./GcpSyncFields"; import { GitHubSyncFields } from "./GitHubSyncFields"; +import { HCVaultSyncFields } from "./HCVaultSyncFields"; import { HumanitecSyncFields } from "./HumanitecSyncFields"; import { TeamCitySyncFields } from "./TeamCitySyncFields"; import { TerraformCloudSyncFields } from "./TerraformCloudSyncFields"; @@ -47,6 +48,8 @@ export const SecretSyncDestinationFields = () => { return ; case SecretSync.Windmill: return ; + case SecretSync.HCVault: + return ; case SecretSync.TeamCity: return ; default: diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx index e2c285b6f..e4aa4ad65 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncOptionsFields/SecretSyncOptionsFields.tsx @@ -43,6 +43,7 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => { case SecretSync.Camunda: case SecretSync.Vercel: case SecretSync.Windmill: + case SecretSync.HCVault: case SecretSync.TeamCity: AdditionalSyncOptionsFieldsComponent = null; break; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HCVaultSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HCVaultSyncReviewFields.tsx new file mode 100644 index 000000000..2e1abac21 --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/HCVaultSyncReviewFields.tsx @@ -0,0 +1,18 @@ +import { useFormContext } from "react-hook-form"; + +import { TSecretSyncForm } from "@app/components/secret-syncs/forms/schemas"; +import { GenericFieldLabel } from "@app/components/v2"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const HCVaultSyncReviewFields = () => { + const { watch } = useFormContext(); + const mount = watch("destinationConfig.mount"); + const path = watch("destinationConfig.path"); + + return ( + <> + {mount} + {path} + + ); +}; diff --git a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx index da9651535..62402e540 100644 --- a/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx +++ b/frontend/src/components/secret-syncs/forms/SecretSyncReviewFields/SecretSyncReviewFields.tsx @@ -21,6 +21,7 @@ import { CamundaSyncReviewFields } from "./CamundaSyncReviewFields"; import { DatabricksSyncReviewFields } from "./DatabricksSyncReviewFields"; import { GcpSyncReviewFields } from "./GcpSyncReviewFields"; import { GitHubSyncReviewFields } from "./GitHubSyncReviewFields"; +import { HCVaultSyncReviewFields } from "./HCVaultSyncReviewFields"; import { HumanitecSyncReviewFields } from "./HumanitecSyncReviewFields"; import { TeamCitySyncReviewFields } from "./TeamCitySyncReviewFields"; import { TerraformCloudSyncReviewFields } from "./TerraformCloudSyncReviewFields"; @@ -89,6 +90,9 @@ export const SecretSyncReviewFields = () => { case SecretSync.Windmill: DestinationFieldsComponent = ; break; + case SecretSync.HCVault: + DestinationFieldsComponent = ; + break; case SecretSync.TeamCity: DestinationFieldsComponent = ; break; diff --git a/frontend/src/components/secret-syncs/forms/schemas/hc-vault-sync-destination-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/hc-vault-sync-destination-schema.ts new file mode 100644 index 000000000..a02778aaf --- /dev/null +++ b/frontend/src/components/secret-syncs/forms/schemas/hc-vault-sync-destination-schema.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +import { BaseSecretSyncSchema } from "@app/components/secret-syncs/forms/schemas/base-secret-sync-schema"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; + +export const HCVaultSyncDestinationSchema = BaseSecretSyncSchema().merge( + z.object({ + destination: z.literal(SecretSync.HCVault), + destinationConfig: z.object({ + mount: z.string().trim().min(1, "Secrets Engine Mount required"), + path: z + .string() + .trim() + .min(1, "Path required") + .transform((val) => val.trim().replace(/^\/+|\/+$/g, "")) // removes leading/trailing slashes + .refine((val) => /^([a-zA-Z0-9._-]+\/)*[a-zA-Z0-9._-]+$/.test(val), { + message: + "Invalid Vault path format. Use alphanumerics, dots, dashes, underscores, and single slashes between segments." + }) + }) + }) +); diff --git a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts index 50221dc39..bc6184bc7 100644 --- a/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts +++ b/frontend/src/components/secret-syncs/forms/schemas/secret-sync-schema.ts @@ -8,6 +8,7 @@ import { CamundaSyncDestinationSchema } from "./camunda-sync-destination-schema" import { DatabricksSyncDestinationSchema } from "./databricks-sync-destination-schema"; import { GcpSyncDestinationSchema } from "./gcp-sync-destination-schema"; import { GitHubSyncDestinationSchema } from "./github-sync-destination-schema"; +import { HCVaultSyncDestinationSchema } from "./hc-vault-sync-destination-schema"; import { HumanitecSyncDestinationSchema } from "./humanitec-sync-destination-schema"; import { TeamCitySyncDestinationSchema } from "./teamcity-sync-destination-schema"; import { TerraformCloudSyncDestinationSchema } from "./terraform-cloud-destination-schema"; @@ -27,6 +28,7 @@ const SecretSyncUnionSchema = z.discriminatedUnion("destination", [ CamundaSyncDestinationSchema, VercelSyncDestinationSchema, WindmillSyncDestinationSchema, + HCVaultSyncDestinationSchema, TeamCitySyncDestinationSchema ]); diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 35d481b82..68715f9a0 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -19,6 +19,7 @@ import { DatabricksConnectionMethod, GcpConnectionMethod, GitHubConnectionMethod, + HCVaultConnectionMethod, HumanitecConnectionMethod, LdapConnectionMethod, MsSqlConnectionMethod, @@ -58,6 +59,7 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Camunda]: { name: "Camunda", image: "Camunda.png" }, [AppConnection.Windmill]: { name: "Windmill", image: "Windmill.png" }, [AppConnection.Auth0]: { name: "Auth0", image: "Auth0.png", size: 40 }, + [AppConnection.HCVault]: { name: "Hashicorp Vault", image: "Vault.png", size: 65 }, [AppConnection.LDAP]: { name: "LDAP", image: "LDAP.png", size: 65 }, [AppConnection.TeamCity]: { name: "TeamCity", image: "TeamCity.png" } }; @@ -88,11 +90,14 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case PostgresConnectionMethod.UsernameAndPassword: case MsSqlConnectionMethod.UsernameAndPassword: return { name: "Username & Password", icon: faLock }; + case HCVaultConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken: case WindmillConnectionMethod.AccessToken: return { name: "Access Token", icon: faKey }; case Auth0ConnectionMethod.ClientCredentials: return { name: "Client Credentials", icon: faServer }; + case HCVaultConnectionMethod.AppRole: + return { name: "App Role", icon: faUser }; case LdapConnectionMethod.SimpleBind: return { name: "Simple Bind", icon: faLink }; default: diff --git a/frontend/src/helpers/secretSyncs.ts b/frontend/src/helpers/secretSyncs.ts index 009c2804b..58d9f3e48 100644 --- a/frontend/src/helpers/secretSyncs.ts +++ b/frontend/src/helpers/secretSyncs.ts @@ -40,6 +40,10 @@ export const SECRET_SYNC_MAP: Record = { [SecretSync.Camunda]: AppConnection.Camunda, [SecretSync.Vercel]: AppConnection.Vercel, [SecretSync.Windmill]: AppConnection.Windmill, + [SecretSync.HCVault]: AppConnection.HCVault, [SecretSync.TeamCity]: AppConnection.TeamCity }; diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index daa0eb1c3..5e1f84cb4 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -14,6 +14,7 @@ export enum AppConnection { Camunda = "camunda", Windmill = "windmill", Auth0 = "auth0", + HCVault = "hashicorp-vault", LDAP = "ldap", TeamCity = "teamcity" } diff --git a/frontend/src/hooks/api/appConnections/hc-vault/index.ts b/frontend/src/hooks/api/appConnections/hc-vault/index.ts new file mode 100644 index 000000000..b69c25120 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/hc-vault/index.ts @@ -0,0 +1 @@ +export * from "./queries"; diff --git a/frontend/src/hooks/api/appConnections/hc-vault/queries.tsx b/frontend/src/hooks/api/appConnections/hc-vault/queries.tsx new file mode 100644 index 000000000..6f1b6ae46 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/hc-vault/queries.tsx @@ -0,0 +1,36 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { appConnectionKeys } from "../queries"; + +const hcVaultConnectionKeys = { + all: [...appConnectionKeys.all, "hcvault"] as const, + listMounts: (connectionId: string) => + [...hcVaultConnectionKeys.all, "mounts", connectionId] as const +}; + +export const useHCVaultConnectionListMounts = ( + connectionId: string, + options?: Omit< + UseQueryOptions< + string[], + unknown, + string[], + ReturnType + >, + "queryKey" | "queryFn" + > +) => { + return useQuery({ + queryKey: hcVaultConnectionKeys.listMounts(connectionId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/app-connections/hashicorp-vault/${connectionId}/mounts` + ); + + return data; + }, + ...options + }); +}; diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index 9e9100d57..910716c02 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -72,6 +72,10 @@ export type TAuth0ConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Auth0; }; +export type THCVaultConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.HCVault; +}; + export type TLdapConnectionOption = TAppConnectionOptionBase & { app: AppConnection.LDAP; }; @@ -96,6 +100,7 @@ export type TAppConnectionOption = | TCamundaConnectionOption | TWindmillConnectionOption | TAuth0ConnectionOption + | THCVaultConnectionOption | TTeamCityConnectionOption; export type TAppConnectionOptionMap = { @@ -114,6 +119,7 @@ export type TAppConnectionOptionMap = { [AppConnection.Camunda]: TCamundaConnectionOption; [AppConnection.Windmill]: TWindmillConnectionOption; [AppConnection.Auth0]: TAuth0ConnectionOption; + [AppConnection.HCVault]: THCVaultConnectionOption; [AppConnection.LDAP]: TLdapConnectionOption; [AppConnection.TeamCity]: TTeamCityConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/hc-vault-connection.ts b/frontend/src/hooks/api/appConnections/types/hc-vault-connection.ts new file mode 100644 index 000000000..f3cddfd96 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/hc-vault-connection.ts @@ -0,0 +1,27 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum HCVaultConnectionMethod { + AccessToken = "access-token", + AppRole = "app-role" +} + +export type THCVaultConnection = TRootAppConnection & { app: AppConnection.HCVault } & ( + | { + method: HCVaultConnectionMethod.AccessToken; + credentials: { + instanceUrl: string; + namespace?: string; + accessToken: string; + }; + } + | { + method: HCVaultConnectionMethod.AppRole; + credentials: { + instanceUrl: string; + namespace?: string; + roleId: string; + secretId: string; + }; + } + ); diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index d441e26e0..00c0c3f3a 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -9,6 +9,7 @@ import { TCamundaConnection } from "./camunda-connection"; import { TDatabricksConnection } from "./databricks-connection"; import { TGcpConnection } from "./gcp-connection"; import { TGitHubConnection } from "./github-connection"; +import { THCVaultConnection } from "./hc-vault-connection"; import { THumanitecConnection } from "./humanitec-connection"; import { TLdapConnection } from "./ldap-connection"; import { TMsSqlConnection } from "./mssql-connection"; @@ -27,6 +28,7 @@ export * from "./camunda-connection"; export * from "./databricks-connection"; export * from "./gcp-connection"; export * from "./github-connection"; +export * from "./hc-vault-connection"; export * from "./humanitec-connection"; export * from "./ldap-connection"; export * from "./mssql-connection"; @@ -52,6 +54,7 @@ export type TAppConnection = | TCamundaConnection | TWindmillConnection | TAuth0Connection + | THCVaultConnection | TLdapConnection | TTeamCityConnection; @@ -96,6 +99,7 @@ export type TAppConnectionMap = { [AppConnection.Camunda]: TCamundaConnection; [AppConnection.Windmill]: TWindmillConnection; [AppConnection.Auth0]: TAuth0Connection; + [AppConnection.HCVault]: THCVaultConnection; [AppConnection.LDAP]: TLdapConnection; [AppConnection.TeamCity]: TTeamCityConnection; }; diff --git a/frontend/src/hooks/api/secretSyncs/enums.ts b/frontend/src/hooks/api/secretSyncs/enums.ts index 450df773a..d078765bc 100644 --- a/frontend/src/hooks/api/secretSyncs/enums.ts +++ b/frontend/src/hooks/api/secretSyncs/enums.ts @@ -11,6 +11,7 @@ export enum SecretSync { Camunda = "camunda", Vercel = "vercel", Windmill = "windmill", + HCVault = "hashicorp-vault", TeamCity = "teamcity" } diff --git a/frontend/src/hooks/api/secretSyncs/types/hc-vault-sync.ts b/frontend/src/hooks/api/secretSyncs/types/hc-vault-sync.ts new file mode 100644 index 000000000..388698ef8 --- /dev/null +++ b/frontend/src/hooks/api/secretSyncs/types/hc-vault-sync.ts @@ -0,0 +1,16 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretSync } from "@app/hooks/api/secretSyncs"; +import { TRootSecretSync } from "@app/hooks/api/secretSyncs/types/root-sync"; + +export type THCVaultSync = TRootSecretSync & { + destination: SecretSync.HCVault; + destinationConfig: { + mount: string; + path: string; + }; + connection: { + app: AppConnection.HCVault; + name: string; + id: string; + }; +}; diff --git a/frontend/src/hooks/api/secretSyncs/types/index.ts b/frontend/src/hooks/api/secretSyncs/types/index.ts index e9ac538fe..2dba65649 100644 --- a/frontend/src/hooks/api/secretSyncs/types/index.ts +++ b/frontend/src/hooks/api/secretSyncs/types/index.ts @@ -9,6 +9,7 @@ import { TCamundaSync } from "./camunda-sync"; import { TDatabricksSync } from "./databricks-sync"; import { TGcpSync } from "./gcp-sync"; import { TGitHubSync } from "./github-sync"; +import { THCVaultSync } from "./hc-vault-sync"; import { THumanitecSync } from "./humanitec-sync"; import { TTeamCitySync } from "./teamcity-sync"; import { TTerraformCloudSync } from "./terraform-cloud-sync"; @@ -34,6 +35,7 @@ export type TSecretSync = | TCamundaSync | TVercelSync | TWindmillSync + | THCVaultSync | TTeamCitySync; export type TListSecretSyncs = { secretSyncs: TSecretSync[] }; diff --git a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx index a862c31a3..8533d7007 100644 --- a/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx +++ b/frontend/src/layouts/ProjectLayout/ProjectLayout.tsx @@ -13,9 +13,9 @@ import { TBreadcrumbFormat } from "@app/components/v2"; import { - useProjectPermission, ProjectPermissionActions, ProjectPermissionSub, + useProjectPermission, useSubscription, useWorkspace } from "@app/context"; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index fc4477813..5c004ce96 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -18,6 +18,7 @@ import { CamundaConnectionForm } from "./CamundaConnectionForm"; import { DatabricksConnectionForm } from "./DatabricksConnectionForm"; import { GcpConnectionForm } from "./GcpConnectionForm"; import { GitHubConnectionForm } from "./GitHubConnectionForm"; +import { HCVaultConnectionForm } from "./HCVaultConnectionForm"; import { HumanitecConnectionForm } from "./HumanitecConnectionForm"; import { LdapConnectionForm } from "./LdapConnectionForm"; import { MsSqlConnectionForm } from "./MsSqlConnectionForm"; @@ -94,6 +95,8 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => { return ; case AppConnection.Auth0: return ; + case AppConnection.HCVault: + return ; case AppConnection.LDAP: return ; case AppConnection.TeamCity: @@ -164,6 +167,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Auth0: return ; + case AppConnection.HCVault: + return ; case AppConnection.LDAP: return ; case AppConnection.TeamCity: diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx new file mode 100644 index 000000000..af3954b45 --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx @@ -0,0 +1,224 @@ +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { HCVaultConnectionMethod, THCVaultConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: THCVaultConnection; + onSubmit: (formData: FormData) => Promise; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.HCVault) +}); + +const InstanceUrlSchema = z + .string() + .trim() + .min(1, "Instance URL required") + .url("Invalid Instance URL"); + +const NamespaceSchema = z.string().trim().optional(); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(HCVaultConnectionMethod.AccessToken), + credentials: z.object({ + instanceUrl: InstanceUrlSchema, + namespace: NamespaceSchema, + accessToken: z.string().trim().min(1, "Access Token required") + }) + }), + rootSchema.extend({ + method: z.literal(HCVaultConnectionMethod.AppRole), + credentials: z.object({ + instanceUrl: InstanceUrlSchema, + namespace: NamespaceSchema, + roleId: z.string().trim().min(1, "Role ID required"), + secretId: z.string().trim().min(1, "Secret ID required") + }) + }) +]); + +type FormData = z.infer; + +export const HCVaultConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.HCVault, + method: HCVaultConnectionMethod.AppRole + } + }); + + const { + handleSubmit, + control, + watch, + formState: { isSubmitting, isDirty } + } = form; + + const selectedMethod = watch("method"); + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + {selectedMethod === HCVaultConnectionMethod.AccessToken ? ( + ( + + + + )} + /> + ) : ( + <> + ( + + + + )} + /> + ( + + + + )} + /> + + )} +
+ + + + +
+ +
+ ); +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/HCVaultSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/HCVaultSyncDestinationCol.tsx new file mode 100644 index 000000000..8011234e1 --- /dev/null +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/HCVaultSyncDestinationCol.tsx @@ -0,0 +1,14 @@ +import { THCVaultSync } from "@app/hooks/api/secretSyncs/types/hc-vault-sync"; + +import { getSecretSyncDestinationColValues } from "../helpers"; +import { SecretSyncTableCell } from "../SecretSyncTableCell"; + +type Props = { + secretSync: THCVaultSync; +}; + +export const HCVaultSyncDestinationCol = ({ secretSync }: Props) => { + const { primaryText, secondaryText } = getSecretSyncDestinationColValues(secretSync); + + return ; +}; diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx index 776f4665e..abfbf100c 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/SecretSyncDestinationCol/SecretSyncDestinationCol.tsx @@ -8,6 +8,7 @@ import { CamundaSyncDestinationCol } from "./CamundaSyncDestinationCol"; import { DatabricksSyncDestinationCol } from "./DatabricksSyncDestinationCol"; import { GcpSyncDestinationCol } from "./GcpSyncDestinationCol"; import { GitHubSyncDestinationCol } from "./GitHubSyncDestinationCol"; +import { HCVaultSyncDestinationCol } from "./HCVaultSyncDestinationCol"; import { HumanitecSyncDestinationCol } from "./HumanitecSyncDestinationCol"; import { TeamCitySyncDestinationCol } from "./TeamCitySyncDestinationCol"; import { TerraformCloudSyncDestinationCol } from "./TerraformCloudSyncDestinationCol"; @@ -44,6 +45,8 @@ export const SecretSyncDestinationCol = ({ secretSync }: Props) => { return ; case SecretSync.Windmill: return ; + case SecretSync.HCVault: + return ; case SecretSync.TeamCity: return ; default: diff --git a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts index 27dc9d5a1..b1fe387e5 100644 --- a/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts +++ b/frontend/src/pages/secret-manager/IntegrationsListPage/components/SecretSyncsTab/SecretSyncTable/helpers/index.ts @@ -94,6 +94,10 @@ export const getSecretSyncDestinationColValues = (secretSync: TSecretSync) => { primaryText = destinationConfig.workspace; secondaryText = destinationConfig.path; break; + case SecretSync.HCVault: + primaryText = destinationConfig.mount; + secondaryText = destinationConfig.path; + break; case SecretSync.TeamCity: primaryText = destinationConfig.project; secondaryText = destinationConfig.buildConfig; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/HCVaultSyncDestinationSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/HCVaultSyncDestinationSection.tsx new file mode 100644 index 000000000..423e56cf0 --- /dev/null +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/HCVaultSyncDestinationSection.tsx @@ -0,0 +1,19 @@ +import { GenericFieldLabel } from "@app/components/secret-syncs"; +import { THCVaultSync } from "@app/hooks/api/secretSyncs/types/hc-vault-sync"; + +type Props = { + secretSync: THCVaultSync; +}; + +export const HCVaultSyncDestinationSection = ({ secretSync }: Props) => { + const { + destinationConfig: { path, mount } + } = secretSync; + + return ( + <> + {mount} + {path} + + ); +}; diff --git a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx index 505f3ac3b..4ea5798d9 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncDestinationSection/SecretSyncDestinatonSection.tsx @@ -18,6 +18,7 @@ import { CamundaSyncDestinationSection } from "./CamundaSyncDestinationSection"; import { DatabricksSyncDestinationSection } from "./DatabricksSyncDestinationSection"; import { GcpSyncDestinationSection } from "./GcpSyncDestinationSection"; import { GitHubSyncDestinationSection } from "./GitHubSyncDestinationSection"; +import { HCVaultSyncDestinationSection } from "./HCVaultSyncDestinationSection"; import { HumanitecSyncDestinationSection } from "./HumanitecSyncDestinationSection"; import { TeamCitySyncDestinationSection } from "./TeamCitySyncDestinationSection"; import { TerraformCloudSyncDestinationSection } from "./TerraformCloudSyncDestinationSection"; @@ -35,7 +36,7 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: const app = APP_CONNECTION_MAP[connection.app].name; let DestinationComponents: ReactNode; - switch (secretSync.destination) { + switch (destination) { case SecretSync.AWSParameterStore: DestinationComponents = ; break; @@ -74,6 +75,9 @@ export const SecretSyncDestinationSection = ({ secretSync, onEditDestination }: case SecretSync.Windmill: DestinationComponents = ; break; + case SecretSync.HCVault: + DestinationComponents = ; + break; case SecretSync.TeamCity: DestinationComponents = ; break; 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 bc6713d3d..5995e7cd1 100644 --- a/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx +++ b/frontend/src/pages/secret-manager/SecretSyncDetailsByIDPage/components/SecretSyncOptionsSection/SecretSyncOptionsSection.tsx @@ -52,6 +52,7 @@ export const SecretSyncOptionsSection = ({ secretSync, onEditOptions }: Props) = case SecretSync.Camunda: case SecretSync.Vercel: case SecretSync.Windmill: + case SecretSync.HCVault: case SecretSync.TeamCity: AdditionalSyncOptionsComponent = null; break;