diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index eca66d68a..88791f8e1 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1860,6 +1860,7 @@ export const AppConnections = { }, 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." diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts index a6bea104a..48695a2a5 100644 --- a/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts @@ -49,7 +49,12 @@ export const getHCVaultAccessToken = async (connection: TValidateHCVaultConnecti const tokenResp = await request.post( `${removeTrailingSlash(instanceUrl)}/v1/auth/approle/login`, { role_id: roleId, secret_id: secretId }, - { headers: { "Content-Type": "application/json" } } + { + headers: { + "Content-Type": "application/json", + ...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {}) + } + } ); if (tokenResp.status !== 200) { @@ -95,7 +100,10 @@ export const listHCVaultMounts = async (appConnection: THCVaultConnection) => { const accessToken = await getHCVaultAccessToken(appConnection); const { data } = await request.get(`${instanceUrl}/v1/sys/mounts`, { - headers: { "X-Vault-Token": accessToken } + headers: { + "X-Vault-Token": accessToken, + ...(appConnection.credentials.namespace ? { "X-Vault-Namespace": appConnection.credentials.namespace } : {}) + } }); const mounts: string[] = []; 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 index ffc5f3d18..a6db4d0eb 100644 --- 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 @@ -17,8 +17,11 @@ const InstanceUrlSchema = z .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() @@ -28,6 +31,7 @@ export const HCVaultConnectionAccessTokenCredentialsSchema = z.object({ 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) }); diff --git a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts index 6764d0afd..f82207086 100644 --- a/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts +++ b/backend/src/services/secret-sync/hc-vault/hc-vault-sync-fns.ts @@ -10,12 +10,13 @@ import { import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors"; import { TSecretMap } from "@app/services/secret-sync/secret-sync-types"; -const listHCVaultVariables = async ({ instanceUrl, mount, accessToken, path }: THCVaultListVariables) => { +const listHCVaultVariables = async ({ instanceUrl, namespace, mount, accessToken, path }: THCVaultListVariables) => { const { data } = await request.get( `${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`, { headers: { - "X-Vault-Token": accessToken + "X-Vault-Token": accessToken, + ...(namespace ? { "X-Vault-Namespace": namespace } : {}) } } ); @@ -24,7 +25,14 @@ const listHCVaultVariables = async ({ instanceUrl, mount, accessToken, path }: T }; // Hashicorp Vault updates all variables in one batch. This is to respect their versioning -const updateHCVaultVariables = async ({ path, instanceUrl, accessToken, mount, data }: TPostHCVaultVariable) => +const updateHCVaultVariables = async ({ + path, + instanceUrl, + namespace, + accessToken, + mount, + data +}: TPostHCVaultVariable) => request.post( `${instanceUrl}/v1/${removeTrailingSlash(mount)}/data/${path}`, { @@ -33,6 +41,7 @@ const updateHCVaultVariables = async ({ path, instanceUrl, accessToken, mount, d { headers: { "X-Vault-Token": accessToken, + ...(namespace ? { "X-Vault-Namespace": namespace } : {}), "Content-Type": "application/json" } } @@ -46,10 +55,17 @@ export const HCVaultSyncFns = { syncOptions: { disableSecretDeletion } } = secretSync; + const { namespace } = connection.credentials; const accessToken = await getHCVaultAccessToken(connection); const instanceUrl = await getHCVaultInstanceUrl(connection); - const variables = await listHCVaultVariables({ instanceUrl, accessToken, mount, path }); + const variables = await listHCVaultVariables({ + instanceUrl, + accessToken, + namespace, + mount, + path + }); let tainted = false; for (const entry of Object.entries(secretMap)) { @@ -73,7 +89,7 @@ export const HCVaultSyncFns = { if (!tainted) return; try { - await updateHCVaultVariables({ accessToken, instanceUrl, mount, path, data: variables }); + await updateHCVaultVariables({ accessToken, instanceUrl, namespace, mount, path, data: variables }); } catch (error) { throw new SecretSyncError({ error @@ -86,10 +102,11 @@ export const HCVaultSyncFns = { destinationConfig: { mount, path } } = secretSync; + const { namespace } = connection.credentials; const accessToken = await getHCVaultAccessToken(connection); const instanceUrl = await getHCVaultInstanceUrl(connection); - const variables = await listHCVaultVariables({ instanceUrl, accessToken, mount, path }); + const variables = await listHCVaultVariables({ instanceUrl, namespace, accessToken, mount, path }); for await (const [key] of Object.entries(variables)) { if (key in secretMap) { @@ -98,7 +115,7 @@ export const HCVaultSyncFns = { } try { - await updateHCVaultVariables({ accessToken, instanceUrl, mount, path, data: variables }); + await updateHCVaultVariables({ accessToken, instanceUrl, namespace, mount, path, data: variables }); } catch (error) { throw new SecretSyncError({ error @@ -111,11 +128,13 @@ export const HCVaultSyncFns = { 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 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 index 73ecc3b5f..4da823a76 100644 --- 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 @@ -25,6 +25,7 @@ export type THCVaultListVariablesResponse = { export type THCVaultListVariables = { accessToken: string; instanceUrl: string; + namespace?: string; mount: string; path: string; }; diff --git a/docs/images/app-connections/hashicorp-vault/vault-cluster-urls.png b/docs/images/app-connections/hashicorp-vault/vault-cluster-urls.png index 619764b54..57af4514f 100644 Binary files a/docs/images/app-connections/hashicorp-vault/vault-cluster-urls.png and b/docs/images/app-connections/hashicorp-vault/vault-cluster-urls.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 index e7abf62c4..872d11cab 100644 Binary files a/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-modal.png and b/docs/images/app-connections/hashicorp-vault/vault-infisical-connect-modal.png differ diff --git a/docs/integrations/app-connections/hashicorp-vault.mdx b/docs/integrations/app-connections/hashicorp-vault.mdx index eb4420d5d..d56a2370c 100644 --- a/docs/integrations/app-connections/hashicorp-vault.mdx +++ b/docs/integrations/app-connections/hashicorp-vault.mdx @@ -4,7 +4,7 @@ description: "Learn how to configure a Hashicorp Vault Connection for Infisical. --- - The Hashicorp Vault UI may vary based on whether you're self-hosting or using HCP, but the written directions should still be universal. + 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. @@ -114,10 +114,14 @@ Infisical supports two methods for connecting to Hashicorp Vault. Save this value 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. + @@ -144,6 +148,7 @@ Infisical supports two methods for connecting to Hashicorp Vault. - **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 (if using Access Token authentication method). - **Role ID**: The Role ID generated in the steps above (if using AppRole authentication method). - **Secret ID**: The Secret ID generated in the steps above (if using AppRole authentication method). diff --git a/frontend/src/hooks/api/appConnections/types/hc-vault-connection.ts b/frontend/src/hooks/api/appConnections/types/hc-vault-connection.ts index 2993dc432..f3cddfd96 100644 --- a/frontend/src/hooks/api/appConnections/types/hc-vault-connection.ts +++ b/frontend/src/hooks/api/appConnections/types/hc-vault-connection.ts @@ -11,6 +11,7 @@ export type THCVaultConnection = TRootAppConnection & { app: AppConnection.HCVau method: HCVaultConnectionMethod.AccessToken; credentials: { instanceUrl: string; + namespace?: string; accessToken: string; }; } @@ -18,6 +19,7 @@ export type THCVaultConnection = TRootAppConnection & { app: AppConnection.HCVau method: HCVaultConnectionMethod.AppRole; credentials: { instanceUrl: string; + namespace?: string; roleId: string; secretId: string; }; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx index 9559ae819..af3954b45 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/HCVaultConnectionForm.tsx @@ -35,11 +35,14 @@ const InstanceUrlSchema = z .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") }) }), @@ -47,6 +50,7 @@ const formSchema = z.discriminatedUnion("method", [ 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") }) @@ -127,6 +131,23 @@ export const HCVaultConnectionForm = ({ appConnection, onSubmit }: Props) => { )} /> + ( + + + + )} + /> {selectedMethod === HCVaultConnectionMethod.AccessToken ? (