feat(vault): optional KV secret path prefix setting (WIN-1960) (#9249)

* feat(vault): add optional KV secret path prefix setting (WIN-1960)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update ee-repo-ref to 0189ba6504fd70eb4929e4881d624d48efd14aee

This commit updates the EE repository reference after PR #581 was merged in windmill-ee-private.

Previous ee-repo-ref: e32e8d6483550c67897e09b6f900dff1034bdae8

New ee-repo-ref: 0189ba6504fd70eb4929e4881d624d48efd14aee

Automated by sync-ee-ref workflow.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
Ruben Fiszel
2026-05-20 06:09:26 +00:00
committed by GitHub
co-authored by Claude Opus 4.7 windmill-internal-app[bot]
parent 457a78cc6a
commit d08f72b3e1
7 changed files with 45 additions and 3 deletions
+1 -1
View File
@@ -1 +1 @@
017d36418a65ce5c840c502e3174df0c393612ba
0189ba6504fd70eb4929e4881d624d48efd14aee
+3
View File
@@ -21076,6 +21076,9 @@ components:
mount_path:
type: string
description: KV v2 secrets engine mount path (e.g., windmill)
kv_secret_path_prefix:
type: string
description: Optional path prefix inserted between the KV data/metadata segment and the workspace id (e.g., "apps/windmill"). When set, secrets are stored at `<mount>/data/<prefix>/<workspace>/<secret>`, allowing a Vault policy scoped to exactly `<mount>/data/<prefix>/*`.
jwt_role:
type: string
description: Vault JWT auth role name for Windmill (optional, if not provided token auth is used)
@@ -118,6 +118,13 @@ pub struct VaultSettings {
pub address: String,
/// KV v2 mount path (e.g., "windmill")
pub mount_path: String,
/// Optional path prefix inserted between the KV `data`/`metadata` segment
/// and the workspace id, e.g. "apps/windmill". When set, secrets live at
/// `<mount>/data/<prefix>/<workspace>/<secret>`, so a Vault policy can be
/// scoped to exactly `<mount>/data/<prefix>/*`. Surrounding slashes are
/// trimmed.
#[serde(skip_serializing_if = "Option::is_none")]
pub kv_secret_path_prefix: Option<String>,
/// JWT auth role name configured in Vault (used for JWT/OIDC auth)
/// Optional - if not provided, token auth is used
#[serde(skip_serializing_if = "Option::is_none")]
@@ -25,6 +25,7 @@ mod tests {
VaultSettings {
address: "http://127.0.0.1:8200".to_string(),
mount_path: "windmill".to_string(),
kv_secret_path_prefix: None,
jwt_role: Some("windmill-secrets".to_string()),
jwt_mount_path: None,
namespace: None,
@@ -90,6 +90,7 @@ mod tests {
address: std::env::var("VAULT_ADDR")
.unwrap_or_else(|_| "http://127.0.0.1:8200".to_string()),
mount_path: "windmill".to_string(),
kv_secret_path_prefix: None,
jwt_role: None, // Static token mode
jwt_mount_path: None,
namespace: None,
@@ -106,6 +107,7 @@ mod tests {
address: std::env::var("VAULT_ADDR")
.unwrap_or_else(|_| "http://127.0.0.1:8200".to_string()),
mount_path: "windmill".to_string(),
kv_secret_path_prefix: None,
jwt_role: Some("windmill-secrets".to_string()), // JWT mode
jwt_mount_path: None,
namespace: None,
@@ -34,6 +34,7 @@ fn test_vault_settings() -> VaultSettings {
address: std::env::var("VAULT_ADDR")
.unwrap_or_else(|_| "http://127.0.0.1:8200".to_string()),
mount_path: "windmill".to_string(),
kv_secret_path_prefix: None,
jwt_role: Some("windmill-secrets".to_string()),
jwt_mount_path: None,
namespace: None,
@@ -69,6 +69,7 @@
type: 'HashiCorpVault',
address: $values['secret_backend']?.address ?? '',
mount_path: $values['secret_backend']?.mount_path ?? 'windmill',
kv_secret_path_prefix: $values['secret_backend']?.kv_secret_path_prefix ?? null,
jwt_role: $values['secret_backend']?.jwt_role ?? 'windmill-secrets',
jwt_mount_path: $values['secret_backend']?.jwt_mount_path ?? null,
namespace: $values['secret_backend']?.namespace ?? null,
@@ -122,6 +123,7 @@
return {
address: $values['secret_backend'].address,
mount_path: $values['secret_backend'].mount_path,
kv_secret_path_prefix: $values['secret_backend'].kv_secret_path_prefix || undefined,
jwt_role: $values['secret_backend'].jwt_role,
jwt_mount_path: $values['secret_backend'].jwt_mount_path || undefined,
namespace: $values['secret_backend'].namespace || undefined,
@@ -355,6 +357,11 @@
let baseUrl = $derived($values['base_url'] ?? 'https://your-windmill-instance.com')
let jwtMount = $derived(($values['secret_backend']?.jwt_mount_path?.trim() || 'jwt') as string)
let kvPrefix = $derived(
($values['secret_backend']?.kv_secret_path_prefix?.trim().replace(/^\/+|\/+$/g, '') ||
'') as string
)
let kvPolicyPath = $derived(kvPrefix ? `${kvPrefix}/*` : '*')
let vaultAudience = $derived(
($values['secret_backend']?.address?.trim() || 'https://vault.example.com:8200') as string
)
@@ -453,6 +460,27 @@
bind:value={$values['secret_backend'].mount_path}
/>
</div>
<div class="flex flex-col gap-1">
<label for="vault_kv_secret_path_prefix" class="block text-xs font-semibold text-emphasis"
>KV Secret Path Prefix (optional)</label
>
<span class="text-2xs text-secondary"
>Optional prefix inserted before the workspace id. When set, secrets are stored at
<code>&lt;mount&gt;/data/&lt;prefix&gt;/&lt;workspace&gt;/&lt;secret&gt;</code>, so you
can keep an existing layout and scope a Vault policy to exactly
<code>{$values['secret_backend']?.mount_path ?? 'windmill'}/data/{kvPolicyPath}</code
>.</span
>
<TextInput
inputProps={{
type: 'text',
id: 'vault_kv_secret_path_prefix',
placeholder: 'apps/windmill',
disabled
}}
bind:value={$values['secret_backend'].kv_secret_path_prefix}
/>
</div>
<div class="flex flex-col gap-2">
<span class="block text-xs font-semibold text-emphasis">Authentication Method</span>
<ToggleButtonGroup selected={authMethod} onSelected={(v) => setAuthMethod(v)}>
@@ -541,10 +569,10 @@ vault write auth/{jwtMount}/config \
# Create a policy for Windmill secrets
vault policy write windmill-secrets - &lt;&lt;EOF
path "{$values['secret_backend']?.mount_path ?? 'windmill'}/data/*" &#123;
path "{$values['secret_backend']?.mount_path ?? 'windmill'}/data/{kvPolicyPath}" &#123;
capabilities = ["create", "read", "update", "delete"]
&#125;
path "{$values['secret_backend']?.mount_path ?? 'windmill'}/metadata/*" &#123;
path "{$values['secret_backend']?.mount_path ?? 'windmill'}/metadata/{kvPolicyPath}" &#123;
capabilities = ["list", "delete"]
&#125;
EOF