mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-18 00:03:11 +00:00
feat: switch from AWS KMS to AWS Secrets Manager as secret backend
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
c57bc88aad
commit
68c8388976
Generated
+25
-25
@@ -979,30 +979,6 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-kms"
|
||||
version = "1.101.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1dca55e9417ba817965206945209c061a77cf03380d93ce9556b0ea76499e02"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http 0.63.4",
|
||||
"aws-smithy-json 0.62.4",
|
||||
"aws-smithy-observability",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"fastrand",
|
||||
"http 0.2.12",
|
||||
"http 1.4.0",
|
||||
"regex-lite",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-rds"
|
||||
version = "1.126.0"
|
||||
@@ -1030,6 +1006,30 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-secretsmanager"
|
||||
version = "1.100.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b0b2985427bb081e54e759468d3af89fa2ccb17fb8b9e5b704ae2f8da10a3b"
|
||||
dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws-runtime",
|
||||
"aws-smithy-async",
|
||||
"aws-smithy-http 0.63.4",
|
||||
"aws-smithy-json 0.62.4",
|
||||
"aws-smithy-observability",
|
||||
"aws-smithy-runtime",
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"bytes",
|
||||
"fastrand",
|
||||
"http 0.2.12",
|
||||
"http 1.4.0",
|
||||
"regex-lite",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-sdk-sqs"
|
||||
version = "1.77.0"
|
||||
@@ -16745,8 +16745,8 @@ dependencies = [
|
||||
"aws-config",
|
||||
"aws-credential-types",
|
||||
"aws-sdk-bedrockruntime",
|
||||
"aws-sdk-kms",
|
||||
"aws-sdk-rds",
|
||||
"aws-sdk-secretsmanager",
|
||||
"aws-sdk-sts",
|
||||
"aws-smithy-types",
|
||||
"aws-smithy-types-convert",
|
||||
|
||||
+1
-1
@@ -560,7 +560,7 @@ aws-sdk-bedrock = "1.129.0"
|
||||
aws-sdk-bedrockruntime = "=1.122.0"
|
||||
aws-credential-types = "^1"
|
||||
aws-smithy-types = "^1"
|
||||
aws-sdk-kms = "^1"
|
||||
aws-sdk-secretsmanager = "^1"
|
||||
aws-sdk-sqs = "=1.77.0"
|
||||
aws-sdk-sts = "=1.79.0"
|
||||
aws-sdk-sso = "=1.77.0"
|
||||
|
||||
@@ -1383,6 +1383,42 @@ pub async fn migrate_secrets_from_aws_kms(
|
||||
Ok(Json(report))
|
||||
}
|
||||
|
||||
/// Test connection to AWS Secrets Manager
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
pub async fn test_aws_sm_backend(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
Json(settings): Json<AwsSecretsManagerSettings>,
|
||||
) -> Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
windmill_common::secret_backend::test_aws_sm_connection(&settings).await?;
|
||||
Ok("Successfully connected to AWS Secrets Manager".to_string())
|
||||
}
|
||||
|
||||
/// Migrate existing secrets from database to AWS Secrets Manager
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
pub async fn migrate_secrets_to_aws_sm(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
Json(settings): Json<AwsSecretsManagerSettings>,
|
||||
) -> JsonResult<SecretMigrationReport> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let report = windmill_common::secret_backend::migrate_secrets_to_aws_sm(&db, &settings).await?;
|
||||
Ok(Json(report))
|
||||
}
|
||||
|
||||
/// Migrate secrets from AWS Secrets Manager back to database
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
pub async fn migrate_secrets_from_aws_sm(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
Json(settings): Json<AwsSecretsManagerSettings>,
|
||||
) -> JsonResult<SecretMigrationReport> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
let report = windmill_common::secret_backend::migrate_secrets_from_aws_sm(&db, &settings).await?;
|
||||
Ok(Json(report))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// JWKS Endpoint for Vault JWT Authentication
|
||||
// ============================================================================
|
||||
|
||||
@@ -1866,6 +1866,66 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SecretMigrationReport"
|
||||
|
||||
/settings/test_aws_sm_backend:
|
||||
post:
|
||||
summary: test connection to AWS Secrets Manager
|
||||
operationId: testAwsSmBackend
|
||||
tags:
|
||||
- setting
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AwsSecretsManagerSettings"
|
||||
responses:
|
||||
"200":
|
||||
description: connection test result
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/settings/migrate_secrets_to_aws_sm:
|
||||
post:
|
||||
summary: migrate secrets from database to AWS Secrets Manager
|
||||
operationId: migrateSecretsToAwsSm
|
||||
tags:
|
||||
- setting
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AwsSecretsManagerSettings"
|
||||
responses:
|
||||
"200":
|
||||
description: migration report
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SecretMigrationReport"
|
||||
|
||||
/settings/migrate_secrets_from_aws_sm:
|
||||
post:
|
||||
summary: migrate secrets from AWS Secrets Manager to database
|
||||
operationId: migrateSecretsFromAwsSm
|
||||
tags:
|
||||
- setting
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AwsSecretsManagerSettings"
|
||||
responses:
|
||||
"200":
|
||||
description: migration report
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/SecretMigrationReport"
|
||||
|
||||
/users/email:
|
||||
get:
|
||||
summary: get current user email (if logged in)
|
||||
@@ -19409,6 +19469,27 @@ components:
|
||||
type: string
|
||||
description: Custom endpoint URL for testing (e.g., LocalStack)
|
||||
|
||||
AwsSecretsManagerSettings:
|
||||
type: object
|
||||
required:
|
||||
- region
|
||||
properties:
|
||||
region:
|
||||
type: string
|
||||
description: AWS region (e.g., us-east-1)
|
||||
access_key_id:
|
||||
type: string
|
||||
description: AWS Access Key ID (optional, uses default credential chain if not provided)
|
||||
secret_access_key:
|
||||
type: string
|
||||
description: AWS Secret Access Key (optional)
|
||||
endpoint_url:
|
||||
type: string
|
||||
description: Custom endpoint URL for testing (e.g., LocalStack)
|
||||
prefix:
|
||||
type: string
|
||||
description: Prefix for secret names (e.g., windmill/)
|
||||
|
||||
SecretMigrationFailure:
|
||||
type: object
|
||||
required:
|
||||
|
||||
@@ -6,11 +6,6 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
//! Secret backend extension for the API layer
|
||||
//!
|
||||
//! This module provides helper functions for integrating the SecretBackend
|
||||
//! trait with variable operations in the API.
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -26,8 +21,8 @@ use windmill_common::secret_backend::{database::DatabaseBackend, SecretBackend};
|
||||
use windmill_common::{
|
||||
global_settings::{load_value_from_global_settings, SECRET_BACKEND_SETTING},
|
||||
secret_backend::{
|
||||
AzureKeyVaultBackend, AzureKeyVaultSettings, SecretBackendConfig, VaultBackend,
|
||||
VaultSettings,
|
||||
AwsSecretsManagerBackend, AwsSecretsManagerSettings, AzureKeyVaultBackend,
|
||||
AzureKeyVaultSettings, SecretBackendConfig, VaultBackend, VaultSettings,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -56,13 +51,23 @@ lazy_static::lazy_static! {
|
||||
static ref AZURE_KV_BACKEND_CACHE: RwLock<Option<CachedAzureKvBackend>> = RwLock::new(None);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
struct CachedAwsSmBackend {
|
||||
backend: Arc<dyn SecretBackend>,
|
||||
settings: AwsSecretsManagerSettings,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref AWS_SM_BACKEND_CACHE: RwLock<Option<CachedAwsSmBackend>> = RwLock::new(None);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
async fn get_secret_backend(db: &DB) -> Result<Arc<dyn SecretBackend>> {
|
||||
let config = match load_value_from_global_settings(db, SECRET_BACKEND_SETTING).await? {
|
||||
Some(value) => serde_json::from_value::<SecretBackendConfig>(value).unwrap_or_default(),
|
||||
None => SecretBackendConfig::default(),
|
||||
};
|
||||
|
||||
match config {
|
||||
SecretBackendConfig::Database => Ok(Arc::new(DatabaseBackend::new(db.clone()))),
|
||||
SecretBackendConfig::HashiCorpVault(settings) => {
|
||||
@@ -71,11 +76,8 @@ async fn get_secret_backend(db: &DB) -> Result<Arc<dyn SecretBackend>> {
|
||||
SecretBackendConfig::AzureKeyVault(settings) => {
|
||||
get_or_create_azure_kv_backend(db, settings).await
|
||||
}
|
||||
SecretBackendConfig::AwsKms(_) => {
|
||||
// KMS backend: secrets are in DB encrypted by KMS.
|
||||
// For API-layer operations (rename), we use the database backend
|
||||
// since KMS secrets don't need external store operations.
|
||||
Ok(Arc::new(DatabaseBackend::new(db.clone())))
|
||||
SecretBackendConfig::AwsSecretsManager(settings) => {
|
||||
get_or_create_aws_sm_backend(db, settings).await
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,20 +90,13 @@ async fn get_or_create_vault_backend(
|
||||
{
|
||||
let cache = VAULT_BACKEND_CACHE.read().await;
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.settings == settings {
|
||||
return Ok(cached.backend.clone());
|
||||
}
|
||||
if cached.settings == settings { return Ok(cached.backend.clone()); }
|
||||
}
|
||||
}
|
||||
|
||||
let mut cache = VAULT_BACKEND_CACHE.write().await;
|
||||
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.settings == settings {
|
||||
return Ok(cached.backend.clone());
|
||||
}
|
||||
if cached.settings == settings { return Ok(cached.backend.clone()); }
|
||||
}
|
||||
|
||||
let backend: Arc<dyn SecretBackend> = {
|
||||
#[cfg(feature = "openidconnect")]
|
||||
if settings.token.is_none() {
|
||||
@@ -109,13 +104,10 @@ async fn get_or_create_vault_backend(
|
||||
} else {
|
||||
Arc::new(VaultBackend::new(settings.clone()))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "openidconnect"))]
|
||||
Arc::new(VaultBackend::new(settings.clone()))
|
||||
};
|
||||
|
||||
*cache = Some(CachedVaultBackend { backend: backend.clone(), settings });
|
||||
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
@@ -127,24 +119,36 @@ async fn get_or_create_azure_kv_backend(
|
||||
{
|
||||
let cache = AZURE_KV_BACKEND_CACHE.read().await;
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.settings == settings {
|
||||
return Ok(cached.backend.clone());
|
||||
}
|
||||
if cached.settings == settings { return Ok(cached.backend.clone()); }
|
||||
}
|
||||
}
|
||||
|
||||
let mut cache = AZURE_KV_BACKEND_CACHE.write().await;
|
||||
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.settings == settings {
|
||||
return Ok(cached.backend.clone());
|
||||
if cached.settings == settings { return Ok(cached.backend.clone()); }
|
||||
}
|
||||
let backend: Arc<dyn SecretBackend> = Arc::new(AzureKeyVaultBackend::new(settings.clone()));
|
||||
*cache = Some(CachedAzureKvBackend { backend: backend.clone(), settings });
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
async fn get_or_create_aws_sm_backend(
|
||||
_db: &DB,
|
||||
settings: AwsSecretsManagerSettings,
|
||||
) -> Result<Arc<dyn SecretBackend>> {
|
||||
{
|
||||
let cache = AWS_SM_BACKEND_CACHE.read().await;
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.settings == settings { return Ok(cached.backend.clone()); }
|
||||
}
|
||||
}
|
||||
|
||||
let backend: Arc<dyn SecretBackend> = Arc::new(AzureKeyVaultBackend::new(settings.clone()));
|
||||
|
||||
*cache = Some(CachedAzureKvBackend { backend: backend.clone(), settings });
|
||||
|
||||
let mut cache = AWS_SM_BACKEND_CACHE.write().await;
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.settings == settings { return Ok(cached.backend.clone()); }
|
||||
}
|
||||
let backend: Arc<dyn SecretBackend> =
|
||||
Arc::new(AwsSecretsManagerBackend::new_with_client(settings.clone()).await?);
|
||||
*cache = Some(CachedAwsSmBackend { backend: backend.clone(), settings });
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
@@ -154,35 +158,31 @@ async fn is_vault_backend_configured(db: &DB) -> Result<bool> {
|
||||
Some(value) => serde_json::from_value::<SecretBackendConfig>(value).unwrap_or_default(),
|
||||
None => SecretBackendConfig::default(),
|
||||
};
|
||||
|
||||
// AwsKms intentionally excluded: KMS secrets are in the DB, no external rename needed
|
||||
Ok(matches!(
|
||||
config,
|
||||
SecretBackendConfig::HashiCorpVault(_) | SecretBackendConfig::AzureKeyVault(_)
|
||||
SecretBackendConfig::HashiCorpVault(_)
|
||||
| SecretBackendConfig::AzureKeyVault(_)
|
||||
| SecretBackendConfig::AwsSecretsManager(_)
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
fn is_vault_stored_value(value: &str) -> bool {
|
||||
value.starts_with("$vault:")
|
||||
}
|
||||
fn is_vault_stored_value(value: &str) -> bool { value.starts_with("$vault:") }
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
fn is_azure_kv_stored_value(value: &str) -> bool {
|
||||
value.starts_with("$azure_kv:")
|
||||
}
|
||||
fn is_azure_kv_stored_value(value: &str) -> bool { value.starts_with("$azure_kv:") }
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
fn is_aws_sm_stored_value(value: &str) -> bool { value.starts_with("$aws_sm:") }
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
fn is_external_stored_value(value: &str) -> bool {
|
||||
is_vault_stored_value(value) || is_azure_kv_stored_value(value)
|
||||
is_vault_stored_value(value) || is_azure_kv_stored_value(value) || is_aws_sm_stored_value(value)
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "private", feature = "enterprise")))]
|
||||
pub async fn rename_vault_secrets_with_prefix(
|
||||
_db: &DB,
|
||||
_workspace_id: &str,
|
||||
_old_prefix: &str,
|
||||
_new_prefix: &str,
|
||||
_db: &DB, _workspace_id: &str, _old_prefix: &str, _new_prefix: &str,
|
||||
_variables: Vec<(String, String)>,
|
||||
) -> Result<Vec<(String, String)>> {
|
||||
Ok(vec![])
|
||||
@@ -190,35 +190,27 @@ pub async fn rename_vault_secrets_with_prefix(
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
pub async fn rename_vault_secrets_with_prefix(
|
||||
db: &DB,
|
||||
workspace_id: &str,
|
||||
old_prefix: &str,
|
||||
new_prefix: &str,
|
||||
db: &DB, workspace_id: &str, old_prefix: &str, new_prefix: &str,
|
||||
variables: Vec<(String, String)>,
|
||||
) -> Result<Vec<(String, String)>> {
|
||||
if !is_vault_backend_configured(db).await? {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
if !is_vault_backend_configured(db).await? { return Ok(vec![]); }
|
||||
let backend = get_secret_backend(db).await?;
|
||||
let mut updates = Vec::new();
|
||||
|
||||
for (old_path, value) in variables {
|
||||
if !is_external_stored_value(&value) {
|
||||
continue;
|
||||
}
|
||||
if !is_external_stored_value(&value) { continue; }
|
||||
|
||||
let marker_prefix = if is_azure_kv_stored_value(&value) {
|
||||
let marker_prefix = if value.starts_with("$azure_kv:") {
|
||||
"$azure_kv:"
|
||||
} else if value.starts_with("$aws_sm:") {
|
||||
"$aws_sm:"
|
||||
} else {
|
||||
"$vault:"
|
||||
};
|
||||
|
||||
let new_path = if old_path.starts_with(old_prefix) {
|
||||
format!("{}{}", new_prefix, &old_path[old_prefix.len()..])
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
} else { continue; };
|
||||
|
||||
let secret_value = match backend.get_secret(workspace_id, &old_path).await {
|
||||
Ok(v) => v,
|
||||
@@ -227,37 +219,21 @@ pub async fn rename_vault_secrets_with_prefix(
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to read secret at {} during bulk rename: {}",
|
||||
old_path,
|
||||
e
|
||||
);
|
||||
tracing::error!("Failed to read secret at {} during bulk rename: {}", old_path, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = backend
|
||||
.set_secret(workspace_id, &new_path, &secret_value)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to write secret to {} during bulk rename: {}",
|
||||
new_path,
|
||||
e
|
||||
);
|
||||
if let Err(e) = backend.set_secret(workspace_id, &new_path, &secret_value).await {
|
||||
tracing::error!("Failed to write secret to {} during bulk rename: {}", new_path, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = backend.delete_secret(workspace_id, &old_path).await {
|
||||
tracing::warn!(
|
||||
"Failed to delete old secret at {} after rename: {}",
|
||||
old_path,
|
||||
e
|
||||
);
|
||||
tracing::warn!("Failed to delete old secret at {} after rename: {}", old_path, e);
|
||||
}
|
||||
|
||||
updates.push((old_path, format!("{}{}", marker_prefix, new_path)));
|
||||
}
|
||||
|
||||
Ok(updates)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ default = []
|
||||
enterprise = ["dep:aws-config"]
|
||||
instance_config_schema = ["dep:schemars"]
|
||||
local_reports = ["dep:rsa", "dep:aes-gcm"]
|
||||
private = ["dep:aws-sdk-rds", "dep:aws-sdk-kms"]
|
||||
private = ["dep:aws-sdk-rds", "dep:aws-sdk-secretsmanager"]
|
||||
jemalloc = ["dep:tikv-jemalloc-ctl"]
|
||||
tantivy = []
|
||||
prometheus = ["dep:prometheus"]
|
||||
@@ -80,7 +80,7 @@ postgres-native-tls.workspace = true
|
||||
native-tls.workspace = true
|
||||
|
||||
aws-smithy-types-convert = { workspace = true, optional = true }
|
||||
aws-sdk-kms = { workspace = true, optional = true }
|
||||
aws-sdk-secretsmanager = { workspace = true, optional = true }
|
||||
aws-sdk-rds = { workspace = true, optional = true }
|
||||
indexmap.workspace = true
|
||||
bytes.workspace = true
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Author: Windmill Labs, Inc
|
||||
* Copyright (C) Windmill Labs, Inc - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited.
|
||||
*/
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::db::DB;
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
use super::{AwsKmsSettings, SecretBackend, SecretMigrationReport};
|
||||
|
||||
pub struct AwsKmsBackend;
|
||||
|
||||
impl AwsKmsBackend {
|
||||
pub fn new(_settings: AwsKmsSettings) -> Self {
|
||||
AwsKmsBackend
|
||||
}
|
||||
|
||||
pub async fn encrypt_value(&self, _plaintext: &str) -> Result<String> {
|
||||
Err(Error::internal_err(
|
||||
"AWS KMS integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn decrypt_value(&self, _ciphertext_b64: &str) -> Result<String> {
|
||||
Err(Error::internal_err(
|
||||
"AWS KMS integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SecretBackend for AwsKmsBackend {
|
||||
async fn get_secret(&self, _workspace_id: &str, _path: &str) -> Result<String> {
|
||||
Err(Error::internal_err(
|
||||
"AWS KMS integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn set_secret(&self, _workspace_id: &str, _path: &str, _value: &str) -> Result<()> {
|
||||
Err(Error::internal_err(
|
||||
"AWS KMS integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn delete_secret(&self, _workspace_id: &str, _path: &str) -> Result<()> {
|
||||
// No-op for KMS: secrets are stored in the database, not externally
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn backend_name(&self) -> &'static str {
|
||||
"aws_kms"
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_aws_kms_connection(_settings: &AwsKmsSettings) -> Result<()> {
|
||||
Err(Error::internal_err(
|
||||
"AWS KMS integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn migrate_secrets_to_aws_kms(
|
||||
_db: &DB,
|
||||
_settings: &AwsKmsSettings,
|
||||
) -> Result<SecretMigrationReport> {
|
||||
Err(Error::internal_err(
|
||||
"AWS KMS integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn migrate_secrets_from_aws_kms(
|
||||
_db: &DB,
|
||||
_settings: &AwsKmsSettings,
|
||||
) -> Result<SecretMigrationReport> {
|
||||
Err(Error::internal_err(
|
||||
"AWS KMS integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn decrypt_aws_kms_value(_db: &DB, _value: &str) -> Result<String> {
|
||||
Err(Error::internal_err(
|
||||
"AWS KMS integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Author: Windmill Labs, Inc
|
||||
* Copyright (C) Windmill Labs, Inc - All Rights Reserved
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited.
|
||||
*/
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::db::DB;
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
use super::{AwsSecretsManagerSettings, SecretBackend, SecretMigrationReport};
|
||||
|
||||
pub struct AwsSecretsManagerBackend;
|
||||
|
||||
impl AwsSecretsManagerBackend {
|
||||
pub fn new(_settings: AwsSecretsManagerSettings) -> Self {
|
||||
AwsSecretsManagerBackend
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SecretBackend for AwsSecretsManagerBackend {
|
||||
async fn get_secret(&self, _workspace_id: &str, _path: &str) -> Result<String> {
|
||||
Err(Error::internal_err(
|
||||
"AWS Secrets Manager integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn set_secret(&self, _workspace_id: &str, _path: &str, _value: &str) -> Result<()> {
|
||||
Err(Error::internal_err(
|
||||
"AWS Secrets Manager integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn delete_secret(&self, _workspace_id: &str, _path: &str) -> Result<()> {
|
||||
Err(Error::internal_err(
|
||||
"AWS Secrets Manager integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn backend_name(&self) -> &'static str {
|
||||
"aws_secrets_manager"
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn test_aws_sm_connection(_settings: &AwsSecretsManagerSettings) -> Result<()> {
|
||||
Err(Error::internal_err(
|
||||
"AWS Secrets Manager integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn migrate_secrets_to_aws_sm(
|
||||
_db: &DB,
|
||||
_settings: &AwsSecretsManagerSettings,
|
||||
) -> Result<SecretMigrationReport> {
|
||||
Err(Error::internal_err(
|
||||
"AWS Secrets Manager integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn migrate_secrets_from_aws_sm(
|
||||
_db: &DB,
|
||||
_settings: &AwsSecretsManagerSettings,
|
||||
) -> Result<SecretMigrationReport> {
|
||||
Err(Error::internal_err(
|
||||
"AWS Secrets Manager integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
@@ -23,8 +23,8 @@ pub mod azure_kv_ee;
|
||||
pub mod azure_kv_oss;
|
||||
|
||||
#[cfg(feature = "private")]
|
||||
pub mod aws_kms_ee;
|
||||
pub mod aws_kms_oss;
|
||||
pub mod aws_sm_ee;
|
||||
pub mod aws_sm_oss;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -42,10 +42,10 @@ pub use azure_kv_ee::*;
|
||||
pub use azure_kv_oss::*;
|
||||
|
||||
#[cfg(feature = "private")]
|
||||
pub use aws_kms_ee::*;
|
||||
pub use aws_sm_ee::*;
|
||||
|
||||
#[cfg(not(feature = "private"))]
|
||||
pub use aws_kms_oss::*;
|
||||
pub use aws_sm_oss::*;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -53,56 +53,22 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::error::Result;
|
||||
|
||||
/// Trait for secret storage backends
|
||||
///
|
||||
/// Implementations of this trait handle the storage and retrieval of secrets.
|
||||
/// The default implementation stores secrets encrypted in the database.
|
||||
/// Enterprise Edition supports HashiCorp Vault as an alternative backend.
|
||||
#[async_trait]
|
||||
pub trait SecretBackend: Send + Sync {
|
||||
/// Retrieve a secret value
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `workspace_id` - The workspace identifier
|
||||
/// * `path` - The path/name of the secret variable
|
||||
///
|
||||
/// # Returns
|
||||
/// The decrypted secret value
|
||||
async fn get_secret(&self, workspace_id: &str, path: &str) -> Result<String>;
|
||||
|
||||
/// Store a secret value
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `workspace_id` - The workspace identifier
|
||||
/// * `path` - The path/name of the secret variable
|
||||
/// * `value` - The plaintext secret value to store
|
||||
async fn set_secret(&self, workspace_id: &str, path: &str, value: &str) -> Result<()>;
|
||||
|
||||
/// Delete a secret
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `workspace_id` - The workspace identifier
|
||||
/// * `path` - The path/name of the secret variable
|
||||
async fn delete_secret(&self, workspace_id: &str, path: &str) -> Result<()>;
|
||||
|
||||
/// Get the name of this backend for logging/debugging
|
||||
fn backend_name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
/// Configuration for secret storage backend
|
||||
///
|
||||
/// This enum is stored in global_settings and determines which backend
|
||||
/// is used for secret storage at the instance level.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum SecretBackendConfig {
|
||||
/// Store secrets encrypted in the database (default behavior)
|
||||
Database,
|
||||
/// Store secrets in HashiCorp Vault (Enterprise Edition only)
|
||||
HashiCorpVault(VaultSettings),
|
||||
/// Store secrets in Azure Key Vault (Enterprise Edition only)
|
||||
AzureKeyVault(AzureKeyVaultSettings),
|
||||
/// Encrypt secrets with AWS KMS, store in database (Enterprise Edition only)
|
||||
AwsKms(AwsKmsSettings),
|
||||
AwsSecretsManager(AwsSecretsManagerSettings),
|
||||
}
|
||||
|
||||
impl Default for SecretBackendConfig {
|
||||
@@ -114,78 +80,59 @@ impl Default for SecretBackendConfig {
|
||||
/// Settings for HashiCorp Vault integration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct VaultSettings {
|
||||
/// Vault server address (e.g., "https://vault.company.com:8200")
|
||||
pub address: String,
|
||||
/// KV v2 mount path (e.g., "windmill")
|
||||
pub mount_path: 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")]
|
||||
pub jwt_role: Option<String>,
|
||||
/// Vault Enterprise namespace (optional)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub namespace: Option<String>,
|
||||
/// Static Vault token for testing/development (optional)
|
||||
/// If provided, this is used instead of JWT authentication
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AzureKeyVaultSettings {
|
||||
/// Azure Key Vault URL (e.g., "https://myvault.vault.azure.net")
|
||||
pub vault_url: String,
|
||||
/// Azure AD tenant ID
|
||||
pub tenant_id: String,
|
||||
/// Azure AD application (client) ID
|
||||
pub client_id: String,
|
||||
/// Azure AD client secret
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub client_secret: Option<String>,
|
||||
/// Static Bearer token for testing/development (optional)
|
||||
/// If provided, this is used instead of OAuth2 client credentials authentication
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
/// Settings for AWS KMS integration
|
||||
/// Settings for AWS Secrets Manager integration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AwsKmsSettings {
|
||||
/// KMS Key ID, Key ARN, Alias name, or Alias ARN
|
||||
pub key_id: String,
|
||||
pub struct AwsSecretsManagerSettings {
|
||||
/// AWS region (e.g., "us-east-1")
|
||||
pub region: String,
|
||||
/// AWS Access Key ID (optional - uses default credential chain if not provided)
|
||||
/// Static AWS access key ID (optional - uses default credential chain if not provided)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub access_key_id: Option<String>,
|
||||
/// AWS Secret Access Key (optional)
|
||||
/// Static AWS secret access key (optional)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub secret_access_key: Option<String>,
|
||||
/// Custom endpoint URL for LocalStack/testing (optional)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub endpoint_url: Option<String>,
|
||||
/// Prefix for secret names in AWS Secrets Manager (e.g., "windmill/")
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub prefix: Option<String>,
|
||||
}
|
||||
|
||||
/// Result of a secret migration operation
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SecretMigrationReport {
|
||||
/// Total number of secrets found
|
||||
pub total_secrets: usize,
|
||||
/// Number of secrets successfully migrated
|
||||
pub migrated_count: usize,
|
||||
/// Number of secrets that failed to migrate
|
||||
pub failed_count: usize,
|
||||
/// Details of any failures
|
||||
pub failures: Vec<SecretMigrationFailure>,
|
||||
}
|
||||
|
||||
/// Details of a failed secret migration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SecretMigrationFailure {
|
||||
/// Workspace ID where the secret is located
|
||||
pub workspace_id: String,
|
||||
/// Path of the secret that failed to migrate
|
||||
pub path: String,
|
||||
/// Error message
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
@@ -6,11 +6,6 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
//! HashiCorp Vault secret backend stubs (Open Source Edition)
|
||||
//!
|
||||
//! This module provides stub implementations for Vault integration.
|
||||
//! The actual Vault integration requires Enterprise Edition.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::db::DB;
|
||||
@@ -21,7 +16,6 @@ use super::{
|
||||
VaultSettings,
|
||||
};
|
||||
|
||||
/// Stub VaultBackend for OSS - all operations return EE required error
|
||||
pub struct VaultBackend;
|
||||
|
||||
impl VaultBackend {
|
||||
@@ -55,10 +49,6 @@ impl SecretBackend for VaultBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the appropriate secret backend based on configuration
|
||||
///
|
||||
/// In OSS, always returns DatabaseBackend regardless of config.
|
||||
/// Vault configuration is ignored with a warning.
|
||||
pub async fn create_secret_backend(
|
||||
db: DB,
|
||||
config: &SecretBackendConfig,
|
||||
@@ -79,9 +69,9 @@ pub async fn create_secret_backend(
|
||||
);
|
||||
Ok(Arc::new(DatabaseBackend::new(db)))
|
||||
}
|
||||
SecretBackendConfig::AwsKms(_) => {
|
||||
SecretBackendConfig::AwsSecretsManager(_) => {
|
||||
tracing::warn!(
|
||||
"AWS KMS is configured but requires Enterprise Edition. \
|
||||
"AWS Secrets Manager is configured but requires Enterprise Edition. \
|
||||
Falling back to database backend."
|
||||
);
|
||||
Ok(Arc::new(DatabaseBackend::new(db)))
|
||||
@@ -89,14 +79,12 @@ pub async fn create_secret_backend(
|
||||
}
|
||||
}
|
||||
|
||||
/// Test connection to Vault (OSS stub)
|
||||
pub async fn test_vault_connection(_settings: &VaultSettings, _db: Option<&DB>) -> Result<()> {
|
||||
Err(Error::internal_err(
|
||||
"HashiCorp Vault integration requires Enterprise Edition".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Migrate secrets from database to Vault (OSS stub)
|
||||
pub async fn migrate_secrets_to_vault(
|
||||
_db: &DB,
|
||||
_settings: &VaultSettings,
|
||||
@@ -106,7 +94,6 @@ pub async fn migrate_secrets_to_vault(
|
||||
))
|
||||
}
|
||||
|
||||
/// Migrate secrets from Vault back to database (OSS stub)
|
||||
pub async fn migrate_secrets_to_database(
|
||||
_db: &DB,
|
||||
_settings: &VaultSettings,
|
||||
@@ -116,7 +103,6 @@ pub async fn migrate_secrets_to_database(
|
||||
))
|
||||
}
|
||||
|
||||
/// Generate a JWT for Vault authentication (OSS stub)
|
||||
pub async fn generate_vault_jwt(_db: &DB, _vault_address: &str) -> Result<String> {
|
||||
Err(Error::internal_err(
|
||||
"HashiCorp Vault integration requires Enterprise Edition".to_string(),
|
||||
|
||||
@@ -188,17 +188,13 @@ pub async fn get_secret_value_as_admin(
|
||||
let r = if variable.is_secret {
|
||||
let value = variable.value;
|
||||
if !value.is_empty() {
|
||||
if value.starts_with("$aws_kms:") {
|
||||
crate::secret_backend::decrypt_aws_kms_value(db, &value).await?
|
||||
} else {
|
||||
let mc = build_crypt(db, w_id).await?;
|
||||
decrypt(&mc, value).map_err(|e| {
|
||||
crate::error::Error::internal_err(format!(
|
||||
"Error decrypting variable {}: {}",
|
||||
variable.path, e
|
||||
))
|
||||
})?
|
||||
}
|
||||
let mc = build_crypt(db, w_id).await?;
|
||||
decrypt(&mc, value).map_err(|e| {
|
||||
crate::error::Error::internal_err(format!(
|
||||
"Error decrypting variable {}: {}",
|
||||
variable.path, e
|
||||
))
|
||||
})?
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
@@ -6,11 +6,6 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
//! Secret backend extension for the store layer
|
||||
//!
|
||||
//! This module provides helper functions for integrating the SecretBackend
|
||||
//! trait with variable operations in the store.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use windmill_common::{
|
||||
@@ -23,7 +18,10 @@ use windmill_common::{
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
use windmill_common::{
|
||||
global_settings::{load_value_from_global_settings, SECRET_BACKEND_SETTING},
|
||||
secret_backend::{AzureKeyVaultBackend, AzureKeyVaultSettings, AwsKmsBackend, AwsKmsSettings, SecretBackendConfig, VaultBackend, VaultSettings},
|
||||
secret_backend::{
|
||||
AwsSecretsManagerBackend, AwsSecretsManagerSettings, AzureKeyVaultBackend,
|
||||
AzureKeyVaultSettings, SecretBackendConfig, VaultBackend, VaultSettings,
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
@@ -52,14 +50,14 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
struct CachedAwsKmsBackend {
|
||||
backend: Arc<AwsKmsBackend>,
|
||||
settings: AwsKmsSettings,
|
||||
struct CachedAwsSmBackend {
|
||||
backend: Arc<dyn SecretBackend>,
|
||||
settings: AwsSecretsManagerSettings,
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
lazy_static::lazy_static! {
|
||||
static ref AWS_KMS_BACKEND_CACHE: RwLock<Option<CachedAwsKmsBackend>> = RwLock::new(None);
|
||||
static ref AWS_SM_BACKEND_CACHE: RwLock<Option<CachedAwsSmBackend>> = RwLock::new(None);
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "private", feature = "enterprise")))]
|
||||
@@ -82,9 +80,8 @@ pub async fn get_secret_backend(db: &DB) -> Result<Arc<dyn SecretBackend>> {
|
||||
SecretBackendConfig::AzureKeyVault(settings) => {
|
||||
get_or_create_azure_kv_backend(db, settings).await
|
||||
}
|
||||
SecretBackendConfig::AwsKms(settings) => {
|
||||
let kms = get_or_create_aws_kms_backend(settings).await?;
|
||||
Ok(kms as Arc<dyn SecretBackend>)
|
||||
SecretBackendConfig::AwsSecretsManager(settings) => {
|
||||
get_or_create_aws_sm_backend(db, settings).await
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,15 +99,12 @@ async fn get_or_create_vault_backend(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut cache = VAULT_BACKEND_CACHE.write().await;
|
||||
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.settings == settings {
|
||||
return Ok(cached.backend.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let backend: Arc<dyn SecretBackend> = {
|
||||
#[cfg(feature = "openidconnect")]
|
||||
if settings.token.is_none() {
|
||||
@@ -118,13 +112,10 @@ async fn get_or_create_vault_backend(
|
||||
} else {
|
||||
Arc::new(VaultBackend::new(settings.clone()))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "openidconnect"))]
|
||||
Arc::new(VaultBackend::new(settings.clone()))
|
||||
};
|
||||
|
||||
*cache = Some(CachedVaultBackend { backend: backend.clone(), settings });
|
||||
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
@@ -141,47 +132,39 @@ async fn get_or_create_azure_kv_backend(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut cache = AZURE_KV_BACKEND_CACHE.write().await;
|
||||
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.settings == settings {
|
||||
return Ok(cached.backend.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let backend: Arc<dyn SecretBackend> = Arc::new(AzureKeyVaultBackend::new(settings.clone()));
|
||||
|
||||
*cache = Some(CachedAzureKvBackend { backend: backend.clone(), settings });
|
||||
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
async fn get_or_create_aws_kms_backend(
|
||||
settings: AwsKmsSettings,
|
||||
) -> Result<Arc<AwsKmsBackend>> {
|
||||
async fn get_or_create_aws_sm_backend(
|
||||
_db: &DB,
|
||||
settings: AwsSecretsManagerSettings,
|
||||
) -> Result<Arc<dyn SecretBackend>> {
|
||||
{
|
||||
let cache = AWS_KMS_BACKEND_CACHE.read().await;
|
||||
let cache = AWS_SM_BACKEND_CACHE.read().await;
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.settings == settings {
|
||||
return Ok(cached.backend.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut cache = AWS_KMS_BACKEND_CACHE.write().await;
|
||||
|
||||
let mut cache = AWS_SM_BACKEND_CACHE.write().await;
|
||||
if let Some(ref cached) = *cache {
|
||||
if cached.settings == settings {
|
||||
return Ok(cached.backend.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let backend = Arc::new(AwsKmsBackend::new(settings.clone()).await?);
|
||||
|
||||
*cache = Some(CachedAwsKmsBackend { backend: backend.clone(), settings });
|
||||
|
||||
let backend: Arc<dyn SecretBackend> =
|
||||
Arc::new(AwsSecretsManagerBackend::new_with_client(settings.clone()).await?);
|
||||
*cache = Some(CachedAwsSmBackend { backend: backend.clone(), settings });
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
@@ -196,8 +179,12 @@ pub async fn is_vault_backend_configured(db: &DB) -> Result<bool> {
|
||||
Some(value) => serde_json::from_value::<SecretBackendConfig>(value).unwrap_or_default(),
|
||||
None => SecretBackendConfig::default(),
|
||||
};
|
||||
|
||||
Ok(matches!(config, SecretBackendConfig::HashiCorpVault(_) | SecretBackendConfig::AzureKeyVault(_)))
|
||||
Ok(matches!(
|
||||
config,
|
||||
SecretBackendConfig::HashiCorpVault(_)
|
||||
| SecretBackendConfig::AzureKeyVault(_)
|
||||
| SecretBackendConfig::AwsSecretsManager(_)
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_secret_value(
|
||||
@@ -206,33 +193,7 @@ pub async fn get_secret_value(
|
||||
path: &str,
|
||||
encrypted_value: &str,
|
||||
) -> Result<String> {
|
||||
if is_aws_kms_stored_value(encrypted_value) {
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
{
|
||||
let config =
|
||||
match load_value_from_global_settings(db, SECRET_BACKEND_SETTING).await? {
|
||||
Some(value) => {
|
||||
serde_json::from_value::<SecretBackendConfig>(value).unwrap_or_default()
|
||||
}
|
||||
None => SecretBackendConfig::default(),
|
||||
};
|
||||
if let SecretBackendConfig::AwsKms(settings) = config {
|
||||
let kms = get_or_create_aws_kms_backend(settings).await?;
|
||||
let ciphertext_b64 = &encrypted_value["$aws_kms:".len()..];
|
||||
return kms.decrypt_value(ciphertext_b64).await;
|
||||
}
|
||||
return Err(Error::internal_err(
|
||||
"Value has $aws_kms: prefix but AWS KMS is not configured".to_string(),
|
||||
));
|
||||
}
|
||||
#[cfg(not(all(feature = "private", feature = "enterprise")))]
|
||||
return Err(Error::internal_err(
|
||||
"AWS KMS requires Enterprise Edition".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let backend = get_secret_backend(db).await?;
|
||||
|
||||
match backend.backend_name() {
|
||||
"database" => {
|
||||
let mc = build_crypt(db, workspace_id).await?;
|
||||
@@ -240,8 +201,9 @@ pub async fn get_secret_value(
|
||||
Error::internal_err(format!("Error decrypting variable {}: {}", path, e))
|
||||
})
|
||||
}
|
||||
"hashicorp_vault" => backend.get_secret(workspace_id, path).await,
|
||||
"azure_key_vault" => backend.get_secret(workspace_id, path).await,
|
||||
"hashicorp_vault" | "azure_key_vault" | "aws_secrets_manager" => {
|
||||
backend.get_secret(workspace_id, path).await
|
||||
}
|
||||
_ => Err(Error::internal_err(format!(
|
||||
"Unknown backend: {}",
|
||||
backend.backend_name()
|
||||
@@ -256,7 +218,6 @@ pub async fn store_secret_value(
|
||||
plain_value: &str,
|
||||
) -> Result<String> {
|
||||
let backend = get_secret_backend(db).await?;
|
||||
|
||||
match backend.backend_name() {
|
||||
"database" => {
|
||||
let mc = build_crypt(db, workspace_id).await?;
|
||||
@@ -270,30 +231,9 @@ pub async fn store_secret_value(
|
||||
backend.set_secret(workspace_id, path, plain_value).await?;
|
||||
Ok(format!("$azure_kv:{}", path))
|
||||
}
|
||||
"aws_kms" => {
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
{
|
||||
let config =
|
||||
match load_value_from_global_settings(db, SECRET_BACKEND_SETTING).await? {
|
||||
Some(value) => {
|
||||
serde_json::from_value::<SecretBackendConfig>(value)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
None => SecretBackendConfig::default(),
|
||||
};
|
||||
if let SecretBackendConfig::AwsKms(settings) = config {
|
||||
let kms = get_or_create_aws_kms_backend(settings).await?;
|
||||
let ciphertext_b64 = kms.encrypt_value(plain_value).await?;
|
||||
return Ok(format!("$aws_kms:{}", ciphertext_b64));
|
||||
}
|
||||
Err(Error::internal_err(
|
||||
"AWS KMS backend active but settings not found".to_string(),
|
||||
))
|
||||
}
|
||||
#[cfg(not(all(feature = "private", feature = "enterprise")))]
|
||||
Err(Error::internal_err(
|
||||
"AWS KMS requires Enterprise Edition".to_string(),
|
||||
))
|
||||
"aws_secrets_manager" => {
|
||||
backend.set_secret(workspace_id, path, plain_value).await?;
|
||||
Ok(format!("$aws_sm:{}", path))
|
||||
}
|
||||
_ => Err(Error::internal_err(format!(
|
||||
"Unknown backend: {}",
|
||||
@@ -323,12 +263,12 @@ pub fn is_azure_kv_stored_value(value: &str) -> bool {
|
||||
value.starts_with("$azure_kv:")
|
||||
}
|
||||
|
||||
pub fn is_aws_kms_stored_value(value: &str) -> bool {
|
||||
value.starts_with("$aws_kms:")
|
||||
pub fn is_aws_sm_stored_value(value: &str) -> bool {
|
||||
value.starts_with("$aws_sm:")
|
||||
}
|
||||
|
||||
pub fn is_external_stored_value(value: &str) -> bool {
|
||||
is_vault_stored_value(value) || is_azure_kv_stored_value(value)
|
||||
is_vault_stored_value(value) || is_azure_kv_stored_value(value) || is_aws_sm_stored_value(value)
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "private", feature = "enterprise")))]
|
||||
@@ -340,21 +280,14 @@ pub async fn rename_vault_secret(
|
||||
current_value: &str,
|
||||
) -> Result<Option<String>> {
|
||||
if is_vault_stored_value(current_value) {
|
||||
tracing::warn!(
|
||||
"Variable has $vault: prefix but Vault requires Enterprise Edition. \
|
||||
Updating DB reference to {}",
|
||||
new_path
|
||||
);
|
||||
return Ok(Some(format!("$vault:{}", new_path)));
|
||||
}
|
||||
if is_azure_kv_stored_value(current_value) {
|
||||
tracing::warn!(
|
||||
"Variable has $azure_kv: prefix but Azure Key Vault requires Enterprise Edition. \
|
||||
Updating DB reference to {}",
|
||||
new_path
|
||||
);
|
||||
return Ok(Some(format!("$azure_kv:{}", new_path)));
|
||||
}
|
||||
if is_aws_sm_stored_value(current_value) {
|
||||
return Ok(Some(format!("$aws_sm:{}", new_path)));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -372,6 +305,8 @@ pub async fn rename_vault_secret(
|
||||
|
||||
let marker_prefix = if current_value.starts_with("$azure_kv:") {
|
||||
"$azure_kv:"
|
||||
} else if current_value.starts_with("$aws_sm:") {
|
||||
"$aws_sm:"
|
||||
} else {
|
||||
"$vault:"
|
||||
};
|
||||
@@ -380,9 +315,7 @@ pub async fn rename_vault_secret(
|
||||
tracing::warn!(
|
||||
"Variable value has {} prefix but external secret backend is not configured. \
|
||||
Updating DB reference from {} to {}",
|
||||
marker_prefix,
|
||||
old_path,
|
||||
new_path
|
||||
marker_prefix, old_path, new_path
|
||||
);
|
||||
return Ok(Some(format!("{}{}", marker_prefix, new_path)));
|
||||
}
|
||||
@@ -394,24 +327,19 @@ pub async fn rename_vault_secret(
|
||||
Err(Error::NotFound(_)) => {
|
||||
tracing::warn!(
|
||||
"Secret not found in backend at path {} during rename to {}",
|
||||
old_path,
|
||||
new_path
|
||||
old_path, new_path
|
||||
);
|
||||
return Ok(Some(format!("{}{}", marker_prefix, new_path)));
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
backend
|
||||
.set_secret(workspace_id, new_path, &secret_value)
|
||||
.await?;
|
||||
backend.set_secret(workspace_id, new_path, &secret_value).await?;
|
||||
|
||||
if let Err(e) = backend.delete_secret(workspace_id, old_path).await {
|
||||
tracing::warn!(
|
||||
"Failed to delete old secret at {} after rename to {}: {}",
|
||||
old_path,
|
||||
new_path,
|
||||
e
|
||||
old_path, new_path, e
|
||||
);
|
||||
}
|
||||
|
||||
@@ -451,6 +379,8 @@ pub async fn rename_vault_secrets_with_prefix(
|
||||
|
||||
let marker_prefix = if value.starts_with("$azure_kv:") {
|
||||
"$azure_kv:"
|
||||
} else if value.starts_with("$aws_sm:") {
|
||||
"$aws_sm:"
|
||||
} else {
|
||||
"$vault:"
|
||||
};
|
||||
@@ -468,33 +398,18 @@ pub async fn rename_vault_secrets_with_prefix(
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"Failed to read secret at {} during bulk rename: {}",
|
||||
old_path,
|
||||
e
|
||||
);
|
||||
tracing::error!("Failed to read secret at {} during bulk rename: {}", old_path, e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = backend
|
||||
.set_secret(workspace_id, &new_path, &secret_value)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"Failed to write secret to {} during bulk rename: {}",
|
||||
new_path,
|
||||
e
|
||||
);
|
||||
if let Err(e) = backend.set_secret(workspace_id, &new_path, &secret_value).await {
|
||||
tracing::error!("Failed to write secret to {} during bulk rename: {}", new_path, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = backend.delete_secret(workspace_id, &old_path).await {
|
||||
tracing::warn!(
|
||||
"Failed to delete old secret at {} after rename: {}",
|
||||
old_path,
|
||||
e
|
||||
);
|
||||
tracing::warn!("Failed to delete old secret at {} after rename: {}", old_path, e);
|
||||
}
|
||||
|
||||
updates.push((old_path, format!("{}{}", marker_prefix, new_path)));
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { SettingService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import TextInput from '../text_input/TextInput.svelte'
|
||||
import { Database, Lock, Server, ArrowLeft, ArrowRight, Cloud, KeyRound } from 'lucide-svelte'
|
||||
import { Database, Lock, Server, ArrowLeft, ArrowRight, Cloud } from 'lucide-svelte'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
@@ -19,14 +19,13 @@
|
||||
|
||||
let { values, disabled = false }: Props = $props()
|
||||
|
||||
// Initialize default values if not set
|
||||
$effect(() => {
|
||||
if (!$values['secret_backend']) {
|
||||
$values['secret_backend'] = { type: 'Database' }
|
||||
}
|
||||
})
|
||||
|
||||
let selectedType: 'Database' | 'HashiCorpVault' | 'AzureKeyVault' | 'AwsKms' = $derived(
|
||||
let selectedType: 'Database' | 'HashiCorpVault' | 'AzureKeyVault' | 'AwsSecretsManager' = $derived(
|
||||
$values['secret_backend']?.type ?? 'Database'
|
||||
)
|
||||
|
||||
@@ -48,20 +47,17 @@
|
||||
let migrateToAzureKvModalOpen = $state(false)
|
||||
let migrateFromAzureKvModalOpen = $state(false)
|
||||
|
||||
let testingAwsKmsConnection = $state(false)
|
||||
let migratingToAwsKms = $state(false)
|
||||
let migratingFromAwsKms = $state(false)
|
||||
let migrateToAwsKmsModalOpen = $state(false)
|
||||
let migrateFromAwsKmsModalOpen = $state(false)
|
||||
let testingAwsSmConnection = $state(false)
|
||||
let migratingToAwsSm = $state(false)
|
||||
let migratingFromAwsSm = $state(false)
|
||||
let migrateToAwsSmModalOpen = $state(false)
|
||||
let migrateFromAwsSmModalOpen = $state(false)
|
||||
|
||||
let vaultDisabled = $derived(!$enterpriseLicense)
|
||||
|
||||
function setBackendType(type: string | undefined) {
|
||||
if (!type) return
|
||||
if (type === 'HashiCorpVault' && vaultDisabled) return
|
||||
if (type === 'AzureKeyVault' && vaultDisabled) return
|
||||
if (type === 'AwsKms' && vaultDisabled) return
|
||||
|
||||
if ((type === 'HashiCorpVault' || type === 'AzureKeyVault' || type === 'AwsSecretsManager') && vaultDisabled) return
|
||||
if (type === 'Database') {
|
||||
$values['secret_backend'] = { type: 'Database' }
|
||||
} else if (type === 'HashiCorpVault') {
|
||||
@@ -82,38 +78,24 @@
|
||||
client_secret: $values['secret_backend']?.client_secret ?? null,
|
||||
token: $values['secret_backend']?.token ?? null
|
||||
}
|
||||
} else if (type === 'AwsKms') {
|
||||
} else if (type === 'AwsSecretsManager') {
|
||||
$values['secret_backend'] = {
|
||||
type: 'AwsKms',
|
||||
key_id: $values['secret_backend']?.key_id ?? '',
|
||||
type: 'AwsSecretsManager',
|
||||
region: $values['secret_backend']?.region ?? 'us-east-1',
|
||||
access_key_id: $values['secret_backend']?.access_key_id ?? null,
|
||||
secret_access_key: $values['secret_backend']?.secret_access_key ?? null,
|
||||
endpoint_url: $values['secret_backend']?.endpoint_url ?? null
|
||||
endpoint_url: $values['secret_backend']?.endpoint_url ?? null,
|
||||
prefix: $values['secret_backend']?.prefix ?? 'windmill/'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setAuthMethod(method: string | undefined) {
|
||||
if (
|
||||
!method ||
|
||||
!$values['secret_backend'] ||
|
||||
$values['secret_backend'].type !== 'HashiCorpVault'
|
||||
)
|
||||
return
|
||||
|
||||
if (!method || !$values['secret_backend'] || $values['secret_backend'].type !== 'HashiCorpVault') return
|
||||
if (method === 'token') {
|
||||
$values['secret_backend'] = {
|
||||
...$values['secret_backend'],
|
||||
jwt_role: null,
|
||||
token: $values['secret_backend'].token ?? ''
|
||||
}
|
||||
$values['secret_backend'] = { ...$values['secret_backend'], jwt_role: null, token: $values['secret_backend'].token ?? '' }
|
||||
} else if (method === 'jwt') {
|
||||
$values['secret_backend'] = {
|
||||
...$values['secret_backend'],
|
||||
token: null,
|
||||
jwt_role: $values['secret_backend'].jwt_role ?? 'windmill-secrets'
|
||||
}
|
||||
$values['secret_backend'] = { ...$values['secret_backend'], token: null, jwt_role: $values['secret_backend'].jwt_role ?? 'windmill-secrets' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,9 +117,7 @@
|
||||
sendUserToast('Successfully connected to HashiCorp Vault')
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to connect to Vault: ' + error.message, true)
|
||||
} finally {
|
||||
testingConnection = false
|
||||
}
|
||||
} finally { testingConnection = false }
|
||||
}
|
||||
|
||||
async function migrateSecretsToVault() {
|
||||
@@ -145,17 +125,10 @@
|
||||
migratingToVault = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsToVault({ requestBody: getVaultSettings() })
|
||||
if (report.failed_count > 0) {
|
||||
sendUserToast(`Migration completed with errors: ${report.migrated_count}/${report.total_secrets} secrets migrated, ${report.failed_count} failed`, true)
|
||||
} else {
|
||||
sendUserToast(`Successfully migrated ${report.migrated_count}/${report.total_secrets} secrets to Vault`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to migrate secrets to Vault: ' + error.message, true)
|
||||
} finally {
|
||||
migratingToVault = false
|
||||
migrateToVaultModalOpen = false
|
||||
}
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to Vault`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingToVault = false; migrateToVaultModalOpen = false }
|
||||
}
|
||||
|
||||
async function migrateSecretsToDatabase() {
|
||||
@@ -163,17 +136,10 @@
|
||||
migratingToDatabase = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsToDatabase({ requestBody: getVaultSettings() })
|
||||
if (report.failed_count > 0) {
|
||||
sendUserToast(`Migration completed with errors: ${report.migrated_count}/${report.total_secrets} secrets migrated, ${report.failed_count} failed`, true)
|
||||
} else {
|
||||
sendUserToast(`Successfully migrated ${report.migrated_count}/${report.total_secrets} secrets to database`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to migrate secrets to database: ' + error.message, true)
|
||||
} finally {
|
||||
migratingToDatabase = false
|
||||
migrateToDatabaseModalOpen = false
|
||||
}
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to database`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingToDatabase = false; migrateToDatabaseModalOpen = false }
|
||||
}
|
||||
|
||||
function isVaultConfigValid(): boolean {
|
||||
@@ -201,11 +167,8 @@
|
||||
try {
|
||||
await SettingService.testAzureKvBackend({ requestBody: getAzureKvSettings() })
|
||||
sendUserToast('Successfully connected to Azure Key Vault')
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to connect to Azure Key Vault: ' + error.message, true)
|
||||
} finally {
|
||||
testingAzureKvConnection = false
|
||||
}
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { testingAzureKvConnection = false }
|
||||
}
|
||||
|
||||
async function migrateSecretsToAzureKv() {
|
||||
@@ -213,17 +176,10 @@
|
||||
migratingToAzureKv = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsToAzureKv({ requestBody: getAzureKvSettings() })
|
||||
if (report.failed_count > 0) {
|
||||
sendUserToast(`Migration completed with errors: ${report.migrated_count}/${report.total_secrets} secrets migrated, ${report.failed_count} failed`, true)
|
||||
} else {
|
||||
sendUserToast(`Successfully migrated ${report.migrated_count}/${report.total_secrets} secrets to Azure Key Vault`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to migrate secrets to Azure Key Vault: ' + error.message, true)
|
||||
} finally {
|
||||
migratingToAzureKv = false
|
||||
migrateToAzureKvModalOpen = false
|
||||
}
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to Azure Key Vault`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingToAzureKv = false; migrateToAzureKvModalOpen = false }
|
||||
}
|
||||
|
||||
async function migrateSecretsFromAzureKv() {
|
||||
@@ -231,17 +187,10 @@
|
||||
migratingFromAzureKv = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsFromAzureKv({ requestBody: getAzureKvSettings() })
|
||||
if (report.failed_count > 0) {
|
||||
sendUserToast(`Migration completed with errors: ${report.migrated_count}/${report.total_secrets} secrets migrated, ${report.failed_count} failed`, true)
|
||||
} else {
|
||||
sendUserToast(`Successfully migrated ${report.migrated_count}/${report.total_secrets} secrets to database`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to migrate secrets to database: ' + error.message, true)
|
||||
} finally {
|
||||
migratingFromAzureKv = false
|
||||
migrateFromAzureKvModalOpen = false
|
||||
}
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to database`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingFromAzureKv = false; migrateFromAzureKvModalOpen = false }
|
||||
}
|
||||
|
||||
function isAzureKvConfigValid(): boolean {
|
||||
@@ -254,119 +203,69 @@
|
||||
)
|
||||
}
|
||||
|
||||
function getAwsKmsSettings() {
|
||||
function getAwsSmSettings() {
|
||||
return {
|
||||
key_id: $values['secret_backend'].key_id,
|
||||
region: $values['secret_backend'].region,
|
||||
access_key_id: $values['secret_backend'].access_key_id || undefined,
|
||||
secret_access_key: $values['secret_backend'].secret_access_key || undefined,
|
||||
endpoint_url: $values['secret_backend'].endpoint_url || undefined
|
||||
endpoint_url: $values['secret_backend'].endpoint_url || undefined,
|
||||
prefix: $values['secret_backend'].prefix || undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function testAwsKmsConnection() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsKms') return
|
||||
testingAwsKmsConnection = true
|
||||
async function testAwsSmConnection() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsSecretsManager') return
|
||||
testingAwsSmConnection = true
|
||||
try {
|
||||
await SettingService.testAwsKmsBackend({ requestBody: getAwsKmsSettings() })
|
||||
sendUserToast('Successfully connected to AWS KMS')
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to connect to AWS KMS: ' + error.message, true)
|
||||
} finally {
|
||||
testingAwsKmsConnection = false
|
||||
}
|
||||
await SettingService.testAwsSmBackend({ requestBody: getAwsSmSettings() })
|
||||
sendUserToast('Successfully connected to AWS Secrets Manager')
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { testingAwsSmConnection = false }
|
||||
}
|
||||
|
||||
async function migrateSecretsToAwsKms() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsKms') return
|
||||
migratingToAwsKms = true
|
||||
async function migrateSecretsToAwsSm() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsSecretsManager') return
|
||||
migratingToAwsSm = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsToAwsKms({ requestBody: getAwsKmsSettings() })
|
||||
if (report.failed_count > 0) {
|
||||
sendUserToast(`Migration completed with errors: ${report.migrated_count}/${report.total_secrets} secrets migrated, ${report.failed_count} failed`, true)
|
||||
} else {
|
||||
sendUserToast(`Successfully migrated ${report.migrated_count}/${report.total_secrets} secrets to AWS KMS`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to migrate secrets to AWS KMS: ' + error.message, true)
|
||||
} finally {
|
||||
migratingToAwsKms = false
|
||||
migrateToAwsKmsModalOpen = false
|
||||
}
|
||||
const report = await SettingService.migrateSecretsToAwsSm({ requestBody: getAwsSmSettings() })
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to AWS Secrets Manager`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingToAwsSm = false; migrateToAwsSmModalOpen = false }
|
||||
}
|
||||
|
||||
async function migrateSecretsFromAwsKms() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsKms') return
|
||||
migratingFromAwsKms = true
|
||||
async function migrateSecretsFromAwsSm() {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsSecretsManager') return
|
||||
migratingFromAwsSm = true
|
||||
try {
|
||||
const report = await SettingService.migrateSecretsFromAwsKms({ requestBody: getAwsKmsSettings() })
|
||||
if (report.failed_count > 0) {
|
||||
sendUserToast(`Migration completed with errors: ${report.migrated_count}/${report.total_secrets} secrets migrated, ${report.failed_count} failed`, true)
|
||||
} else {
|
||||
sendUserToast(`Successfully migrated ${report.migrated_count}/${report.total_secrets} secrets to database`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
sendUserToast('Failed to migrate secrets from AWS KMS: ' + error.message, true)
|
||||
} finally {
|
||||
migratingFromAwsKms = false
|
||||
migrateFromAwsKmsModalOpen = false
|
||||
}
|
||||
const report = await SettingService.migrateSecretsFromAwsSm({ requestBody: getAwsSmSettings() })
|
||||
if (report.failed_count > 0) sendUserToast(`Migration: ${report.migrated_count}/${report.total_secrets} migrated, ${report.failed_count} failed`, true)
|
||||
else sendUserToast(`Migrated ${report.migrated_count}/${report.total_secrets} secrets to database`)
|
||||
} catch (error: any) { sendUserToast('Failed: ' + error.message, true) }
|
||||
finally { migratingFromAwsSm = false; migrateFromAwsSmModalOpen = false }
|
||||
}
|
||||
|
||||
function isAwsKmsConfigValid(): boolean {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsKms') return false
|
||||
return (
|
||||
$values['secret_backend'].key_id?.trim() !== '' &&
|
||||
$values['secret_backend'].region?.trim() !== ''
|
||||
)
|
||||
function isAwsSmConfigValid(): boolean {
|
||||
if (!$values['secret_backend'] || $values['secret_backend'].type !== 'AwsSecretsManager') return false
|
||||
return $values['secret_backend'].region?.trim() !== ''
|
||||
}
|
||||
|
||||
let baseUrl = $derived($values['base_url'] ?? 'https://your-windmill-instance.com')
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Backend Type Selector -->
|
||||
<div class="flex flex-col gap-2 mt-1">
|
||||
<ToggleButtonGroup selected={selectedType} onSelected={(v) => setBackendType(v)}>
|
||||
{#snippet children({ item: toggleButton })}
|
||||
<ToggleButton
|
||||
value="Database"
|
||||
label="Database"
|
||||
tooltip="Store secrets encrypted in the database (default)"
|
||||
item={toggleButton}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="HashiCorpVault"
|
||||
label="HashiCorp Vault (Beta)"
|
||||
tooltip={vaultDisabled
|
||||
? 'HashiCorp Vault integration requires Enterprise Edition'
|
||||
: 'Store secrets in HashiCorp Vault (Beta feature)'}
|
||||
item={toggleButton}
|
||||
disabled={vaultDisabled}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="AzureKeyVault"
|
||||
label="Azure Key Vault"
|
||||
tooltip={vaultDisabled
|
||||
? 'Azure Key Vault integration requires Enterprise Edition'
|
||||
: 'Store secrets in Azure Key Vault'}
|
||||
item={toggleButton}
|
||||
disabled={vaultDisabled}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="AwsKms"
|
||||
label="AWS KMS"
|
||||
tooltip={vaultDisabled
|
||||
? 'AWS KMS integration requires Enterprise Edition'
|
||||
: 'Encrypt secrets with AWS KMS (stored in database)'}
|
||||
item={toggleButton}
|
||||
disabled={vaultDisabled}
|
||||
/>
|
||||
<ToggleButton value="Database" label="Database" tooltip="Store secrets encrypted in the database (default)" item={toggleButton} />
|
||||
<ToggleButton value="HashiCorpVault" label="HashiCorp Vault (Beta)" tooltip={vaultDisabled ? 'Requires Enterprise Edition' : 'Store secrets in HashiCorp Vault'} item={toggleButton} disabled={vaultDisabled} />
|
||||
<ToggleButton value="AzureKeyVault" label="Azure Key Vault" tooltip={vaultDisabled ? 'Requires Enterprise Edition' : 'Store secrets in Azure Key Vault'} item={toggleButton} disabled={vaultDisabled} />
|
||||
<ToggleButton value="AwsSecretsManager" label="AWS Secrets Manager" tooltip={vaultDisabled ? 'Requires Enterprise Edition' : 'Store secrets in AWS Secrets Manager'} item={toggleButton} disabled={vaultDisabled} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
{#if vaultDisabled}
|
||||
<div class="flex items-center gap-1">
|
||||
<EEOnly>HashiCorp Vault, Azure Key Vault, and AWS KMS integrations require Enterprise Edition</EEOnly>
|
||||
<EEOnly>External secret store integrations require Enterprise Edition</EEOnly>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -376,124 +275,54 @@
|
||||
<Database class="text-primary" size={20} />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-emphasis">Database Storage (Default)</p>
|
||||
<p class="text-xs text-secondary">
|
||||
Secrets are encrypted using workspace-specific keys and stored in the PostgreSQL database.
|
||||
</p>
|
||||
<p class="text-xs text-secondary">Secrets are encrypted using workspace-specific keys and stored in the PostgreSQL database.</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if selectedType === 'HashiCorpVault'}
|
||||
<!-- Vault Configuration -->
|
||||
<div class="space-y-4 p-4 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Lock class="text-primary" size={20} />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-emphasis">
|
||||
HashiCorp Vault Configuration
|
||||
<span
|
||||
class="ml-2 px-1.5 py-0.5 text-2xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 rounded"
|
||||
>Beta</span
|
||||
>
|
||||
</p>
|
||||
<p class="text-xs text-secondary">
|
||||
Store secrets in an external HashiCorp Vault instance.
|
||||
</p>
|
||||
<p class="text-sm font-medium text-emphasis">HashiCorp Vault Configuration <span class="ml-2 px-1.5 py-0.5 text-2xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 rounded">Beta</span></p>
|
||||
<p class="text-xs text-secondary">Store secrets in an external HashiCorp Vault instance.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="vault_address" class="block text-xs font-semibold text-emphasis"
|
||||
>Vault Address</label
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'vault_address',
|
||||
placeholder: 'https://vault.company.com:8200',
|
||||
disabled: disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].address}
|
||||
/>
|
||||
<label for="vault_address" class="block text-xs font-semibold text-emphasis">Vault Address</label>
|
||||
<TextInput inputProps={{ type: 'text', id: 'vault_address', placeholder: 'https://vault.company.com:8200', disabled }} bind:value={$values['secret_backend'].address} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="vault_mount_path" class="block text-xs font-semibold text-emphasis"
|
||||
>KV Mount Path</label
|
||||
>
|
||||
<label for="vault_mount_path" class="block text-xs font-semibold text-emphasis">KV Mount Path</label>
|
||||
<span class="text-2xs text-secondary">The KV v2 secrets engine mount path in Vault</span>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'vault_mount_path',
|
||||
placeholder: 'windmill',
|
||||
disabled: disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].mount_path}
|
||||
/>
|
||||
<TextInput inputProps={{ type: 'text', id: 'vault_mount_path', placeholder: 'windmill', disabled }} bind:value={$values['secret_backend'].mount_path} />
|
||||
</div>
|
||||
|
||||
<!-- Authentication Method Toggle -->
|
||||
<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)}>
|
||||
{#snippet children({ item: toggleButton })}
|
||||
<ToggleButton
|
||||
value="jwt"
|
||||
label="JWT Auth"
|
||||
tooltip="Authenticate using Windmill-signed JWTs (recommended for production)"
|
||||
item={toggleButton}
|
||||
{disabled}
|
||||
/>
|
||||
<ToggleButton
|
||||
value="token"
|
||||
label="Static Token"
|
||||
tooltip="Use a static Vault token (for testing/development)"
|
||||
item={toggleButton}
|
||||
{disabled}
|
||||
/>
|
||||
<ToggleButton value="jwt" label="JWT Auth" tooltip="Authenticate using Windmill-signed JWTs" item={toggleButton} {disabled} />
|
||||
<ToggleButton value="token" label="Static Token" tooltip="Use a static Vault token" item={toggleButton} {disabled} />
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
{#if authMethod === 'token'}
|
||||
<div class="flex flex-col gap-1 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="vault_token" class="block text-xs font-semibold text-emphasis"
|
||||
>Vault Token</label
|
||||
>
|
||||
<span class="text-2xs text-secondary"
|
||||
>Static token for authentication. Recommended only for testing/development.</span
|
||||
>
|
||||
<label for="vault_token" class="block text-xs font-semibold text-emphasis">Vault Token</label>
|
||||
<span class="text-2xs text-secondary">Static token. Recommended only for testing/development.</span>
|
||||
<Password bind:password={$values['secret_backend'].token} small {disabled} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-2 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="vault_jwt_role" class="block text-xs font-semibold text-emphasis"
|
||||
>JWT Auth Role</label
|
||||
>
|
||||
<span class="text-2xs text-secondary"
|
||||
>The JWT authentication role configured in Vault.</span
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'vault_jwt_role',
|
||||
placeholder: 'windmill-secrets',
|
||||
disabled: disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].jwt_role}
|
||||
/>
|
||||
|
||||
<label for="vault_jwt_role" class="block text-xs font-semibold text-emphasis">JWT Auth Role</label>
|
||||
<span class="text-2xs text-secondary">The JWT authentication role configured in Vault.</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'vault_jwt_role', placeholder: 'windmill-secrets', disabled }} bind:value={$values['secret_backend'].jwt_role} />
|
||||
<details class="mt-2">
|
||||
<summary class="text-xs font-medium text-secondary cursor-pointer hover:text-primary"
|
||||
>Vault JWT Setup Instructions</summary
|
||||
>
|
||||
<summary class="text-xs font-medium text-secondary cursor-pointer hover:text-primary">Vault JWT Setup Instructions</summary>
|
||||
<div class="mt-2 p-2 bg-surface rounded text-2xs text-secondary space-y-2">
|
||||
<p>Configure Vault to accept JWTs from Windmill:</p>
|
||||
<div
|
||||
class="bg-gray-100 dark:bg-gray-800 p-2 rounded font-mono text-2xs overflow-x-auto"
|
||||
>
|
||||
<pre
|
||||
># Enable JWT auth method
|
||||
<div class="bg-gray-100 dark:bg-gray-800 p-2 rounded font-mono text-2xs overflow-x-auto">
|
||||
<pre># Enable JWT auth method
|
||||
vault auth enable jwt
|
||||
|
||||
# Configure JWT auth with Windmill's JWKS endpoint
|
||||
@@ -517,84 +346,39 @@ vault write auth/jwt/role/windmill-secrets \
|
||||
bound_audiences="{baseUrl}" \
|
||||
user_claim="email" \
|
||||
policies="windmill-secrets" \
|
||||
ttl="1h"</pre
|
||||
>
|
||||
ttl="1h"</pre>
|
||||
</div>
|
||||
<p class="text-yellow-600 dark:text-yellow-400">
|
||||
Replace <code>windmill-secrets</code> with your role name if different.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="vault_namespace" class="block text-xs font-semibold text-emphasis"
|
||||
>Namespace (optional)</label
|
||||
>
|
||||
<span class="text-2xs text-secondary"
|
||||
>Vault Enterprise namespace (leave empty if not using namespaces)</span
|
||||
>
|
||||
<TextInput
|
||||
inputProps={{
|
||||
type: 'text',
|
||||
id: 'vault_namespace',
|
||||
placeholder: 'admin/my-namespace',
|
||||
disabled: disabled
|
||||
}}
|
||||
bind:value={$values['secret_backend'].namespace}
|
||||
/>
|
||||
<label for="vault_namespace" class="block text-xs font-semibold text-emphasis">Namespace (optional)</label>
|
||||
<span class="text-2xs text-secondary">Vault Enterprise namespace</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'vault_namespace', placeholder: 'admin/my-namespace', disabled }} bind:value={$values['secret_backend'].namespace} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
variant="accent"
|
||||
onclick={testVaultConnection}
|
||||
disabled={disabled || !isVaultConfigValid() || testingConnection}
|
||||
loading={testingConnection}
|
||||
startIcon={{ icon: Server }}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button unifiedSize="md" variant="accent" onclick={testVaultConnection} disabled={disabled || !isVaultConfigValid() || testingConnection} loading={testingConnection} startIcon={{ icon: Server }}>Test Connection</Button>
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<span class="block text-xs font-semibold text-emphasis">Secret Migration</span>
|
||||
<span class="text-2xs text-secondary">
|
||||
Migrate secrets between the database and HashiCorp Vault. Original values are NOT
|
||||
deleted to allow for rollback.
|
||||
</span>
|
||||
<span class="text-2xs text-secondary">Original values are NOT deleted to allow for rollback.</span>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Database size={16} /><ArrowRight size={16} /><Lock size={16} />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-2"><Database size={16} /><ArrowRight size={16} /><Lock size={16} /></div>
|
||||
<p class="text-xs font-medium mb-2">Database → Vault</p>
|
||||
<p class="text-2xs text-secondary mb-3">Decrypt secrets from database and store in Vault</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToVaultModalOpen = true)} disabled={disabled || !isVaultConfigValid() || migratingToVault} startIcon={{ icon: ArrowRight }}>
|
||||
Migrate to Vault
|
||||
</Button>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToVaultModalOpen = true)} disabled={disabled || !isVaultConfigValid() || migratingToVault} startIcon={{ icon: ArrowRight }}>Migrate to Vault</Button>
|
||||
</div>
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Lock size={16} /><ArrowLeft size={16} /><Database size={16} />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 mb-2"><Lock size={16} /><ArrowLeft size={16} /><Database size={16} /></div>
|
||||
<p class="text-xs font-medium mb-2">Vault → Database</p>
|
||||
<p class="text-2xs text-secondary mb-3">Read secrets from Vault and encrypt in database</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToDatabaseModalOpen = true)} disabled={disabled || !isVaultConfigValid() || migratingToDatabase} startIcon={{ icon: ArrowLeft }}>
|
||||
Migrate to Database
|
||||
</Button>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToDatabaseModalOpen = true)} disabled={disabled || !isVaultConfigValid() || migratingToDatabase} startIcon={{ icon: ArrowLeft }}>Migrate to Database</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if selectedType === 'AzureKeyVault'}
|
||||
<!-- Azure Key Vault Configuration -->
|
||||
<div class="space-y-4 p-4 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<Cloud class="text-primary" size={20} />
|
||||
@@ -603,7 +387,6 @@ vault write auth/jwt/role/windmill-secrets \
|
||||
<p class="text-xs text-secondary">Store secrets in an Azure Key Vault instance.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="azure_vault_url" class="block text-xs font-semibold text-emphasis">Vault URL</label>
|
||||
@@ -611,137 +394,91 @@ vault write auth/jwt/role/windmill-secrets \
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="azure_tenant_id" class="block text-xs font-semibold text-emphasis">Tenant ID</label>
|
||||
<span class="text-2xs text-secondary">The Azure Active Directory tenant ID</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'azure_tenant_id', placeholder: '00000000-0000-0000-0000-000000000000', disabled }} bind:value={$values['secret_backend'].tenant_id} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="azure_client_id" class="block text-xs font-semibold text-emphasis">Client ID</label>
|
||||
<span class="text-2xs text-secondary">The Azure AD application (service principal) client ID</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'azure_client_id', placeholder: '00000000-0000-0000-0000-000000000000', disabled }} bind:value={$values['secret_backend'].client_id} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="azure_client_secret" class="block text-xs font-semibold text-emphasis">Client Secret</label>
|
||||
<span class="text-2xs text-secondary">The Azure AD application client secret for authentication</span>
|
||||
<Password bind:password={$values['secret_backend'].client_secret} small {disabled} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="azure_token" class="block text-xs font-semibold text-emphasis">Token (optional)</label>
|
||||
<span class="text-2xs text-secondary">Static Bearer token for testing/development. If provided, OAuth2 authentication is skipped.</span>
|
||||
<span class="text-2xs text-secondary">Static Bearer token for testing. If provided, OAuth2 is skipped.</span>
|
||||
<Password bind:password={$values['secret_backend'].token} small {disabled} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<div class="flex gap-2">
|
||||
<Button unifiedSize="md" variant="accent" onclick={testAzureKvConnection} disabled={disabled || !isAzureKvConfigValid() || testingAzureKvConnection} loading={testingAzureKvConnection} startIcon={{ icon: Server }}>
|
||||
Test Connection
|
||||
</Button>
|
||||
</div>
|
||||
<Button unifiedSize="md" variant="accent" onclick={testAzureKvConnection} disabled={disabled || !isAzureKvConfigValid() || testingAzureKvConnection} loading={testingAzureKvConnection} startIcon={{ icon: Server }}>Test Connection</Button>
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<span class="block text-xs font-semibold text-emphasis">Secret Migration</span>
|
||||
<span class="text-2xs text-secondary">Migrate secrets between the database and Azure Key Vault. Original values are NOT deleted to allow for rollback.</span>
|
||||
<span class="text-2xs text-secondary">Original values are NOT deleted to allow for rollback.</span>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2"><Database size={16} /><ArrowRight size={16} /><Cloud size={16} /></div>
|
||||
<p class="text-xs font-medium mb-2">Database → Azure Key Vault</p>
|
||||
<p class="text-2xs text-secondary mb-3">Decrypt secrets from database and store in Azure Key Vault</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToAzureKvModalOpen = true)} disabled={disabled || !isAzureKvConfigValid() || migratingToAzureKv} startIcon={{ icon: ArrowRight }}>Migrate to Azure Key Vault</Button>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToAzureKvModalOpen = true)} disabled={disabled || !isAzureKvConfigValid() || migratingToAzureKv} startIcon={{ icon: ArrowRight }}>Migrate to Azure KV</Button>
|
||||
</div>
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2"><Cloud size={16} /><ArrowLeft size={16} /><Database size={16} /></div>
|
||||
<p class="text-xs font-medium mb-2">Azure Key Vault → Database</p>
|
||||
<p class="text-2xs text-secondary mb-3">Read secrets from Azure Key Vault and encrypt in database</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateFromAzureKvModalOpen = true)} disabled={disabled || !isAzureKvConfigValid() || migratingFromAzureKv} startIcon={{ icon: ArrowLeft }}>Migrate to Database</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if selectedType === 'AwsKms'}
|
||||
<!-- AWS KMS Configuration -->
|
||||
{:else if selectedType === 'AwsSecretsManager'}
|
||||
<div class="space-y-4 p-4 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<KeyRound class="text-primary" size={20} />
|
||||
<Cloud class="text-primary" size={20} />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-emphasis">AWS KMS Configuration</p>
|
||||
<p class="text-xs text-secondary">
|
||||
Encrypt secrets using AWS Key Management Service. Secrets remain stored in the database
|
||||
but are encrypted/decrypted by KMS instead of Windmill's built-in encryption.
|
||||
</p>
|
||||
<p class="text-sm font-medium text-emphasis">AWS Secrets Manager Configuration</p>
|
||||
<p class="text-xs text-secondary">Store secrets in AWS Secrets Manager.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="aws_kms_key_id" class="block text-xs font-semibold text-emphasis">KMS Key ID</label>
|
||||
<span class="text-2xs text-secondary">Key ID, Key ARN, Alias name (e.g. alias/my-key), or Alias ARN</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_kms_key_id', placeholder: 'alias/windmill-secrets', disabled }} bind:value={$values['secret_backend'].key_id} />
|
||||
<label for="aws_sm_region" class="block text-xs font-semibold text-emphasis">Region</label>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_sm_region', placeholder: 'us-east-1', disabled }} bind:value={$values['secret_backend'].region} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="aws_kms_region" class="block text-xs font-semibold text-emphasis">Region</label>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_kms_region', placeholder: 'us-east-1', disabled }} bind:value={$values['secret_backend'].region} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="aws_kms_access_key_id" class="block text-xs font-semibold text-emphasis">Access Key ID (optional)</label>
|
||||
<label for="aws_sm_access_key_id" class="block text-xs font-semibold text-emphasis">Access Key ID (optional)</label>
|
||||
<span class="text-2xs text-secondary">If not provided, the default AWS credential chain is used (env vars, instance profile, EKS pod identity)</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_kms_access_key_id', placeholder: 'AKIA...', disabled }} bind:value={$values['secret_backend'].access_key_id} />
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_sm_access_key_id', placeholder: 'AKIA...', disabled }} bind:value={$values['secret_backend'].access_key_id} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1 p-3 bg-surface-secondary rounded-lg">
|
||||
<label for="aws_kms_secret_access_key" class="block text-xs font-semibold text-emphasis">Secret Access Key (optional)</label>
|
||||
<label for="aws_sm_secret_access_key" class="block text-xs font-semibold text-emphasis">Secret Access Key (optional)</label>
|
||||
<Password bind:password={$values['secret_backend'].secret_access_key} small {disabled} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="aws_kms_endpoint_url" class="block text-xs font-semibold text-emphasis">Endpoint URL (optional)</label>
|
||||
<span class="text-2xs text-secondary">Custom endpoint for LocalStack or other KMS-compatible services</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_kms_endpoint_url', placeholder: 'http://localhost:4566', disabled }} bind:value={$values['secret_backend'].endpoint_url} />
|
||||
<label for="aws_sm_prefix" class="block text-xs font-semibold text-emphasis">Secret Name Prefix (optional)</label>
|
||||
<span class="text-2xs text-secondary">Prefix for secret names in AWS Secrets Manager (default: windmill/)</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_sm_prefix', placeholder: 'windmill/', disabled }} bind:value={$values['secret_backend'].prefix} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="aws_sm_endpoint_url" class="block text-xs font-semibold text-emphasis">Endpoint URL (optional)</label>
|
||||
<span class="text-2xs text-secondary">Custom endpoint for LocalStack or other compatible services</span>
|
||||
<TextInput inputProps={{ type: 'text', id: 'aws_sm_endpoint_url', placeholder: 'http://localhost:4566', disabled }} bind:value={$values['secret_backend'].endpoint_url} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
unifiedSize="md"
|
||||
variant="accent"
|
||||
onclick={testAwsKmsConnection}
|
||||
disabled={disabled || !isAwsKmsConfigValid() || testingAwsKmsConnection}
|
||||
loading={testingAwsKmsConnection}
|
||||
startIcon={{ icon: Server }}
|
||||
>
|
||||
Test Connection
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button unifiedSize="md" variant="accent" onclick={testAwsSmConnection} disabled={disabled || !isAwsSmConfigValid() || testingAwsSmConnection} loading={testingAwsSmConnection} startIcon={{ icon: Server }}>Test Connection</Button>
|
||||
<div class="flex flex-col gap-4 pt-4 border-t">
|
||||
<span class="block text-xs font-semibold text-emphasis">Secret Migration</span>
|
||||
<span class="text-2xs text-secondary">
|
||||
Migrate secrets between database encryption (workspace keys) and AWS KMS encryption.
|
||||
Secrets stay in the database; only the encryption method changes.
|
||||
</span>
|
||||
<span class="text-2xs text-secondary">Original values are NOT deleted to allow for rollback.</span>
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Database size={16} /><ArrowRight size={16} /><KeyRound size={16} />
|
||||
</div>
|
||||
<p class="text-xs font-medium mb-2">Database → AWS KMS</p>
|
||||
<p class="text-2xs text-secondary mb-3">Re-encrypt secrets from workspace keys to AWS KMS</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToAwsKmsModalOpen = true)} disabled={disabled || !isAwsKmsConfigValid() || migratingToAwsKms} startIcon={{ icon: ArrowRight }}>
|
||||
Migrate to AWS KMS
|
||||
</Button>
|
||||
<div class="flex items-center gap-2 mb-2"><Database size={16} /><ArrowRight size={16} /><Cloud size={16} /></div>
|
||||
<p class="text-xs font-medium mb-2">Database → AWS Secrets Manager</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateToAwsSmModalOpen = true)} disabled={disabled || !isAwsSmConfigValid() || migratingToAwsSm} startIcon={{ icon: ArrowRight }}>Migrate to AWS SM</Button>
|
||||
</div>
|
||||
<div class="flex-1 p-3 border rounded-lg">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<KeyRound size={16} /><ArrowLeft size={16} /><Database size={16} />
|
||||
</div>
|
||||
<p class="text-xs font-medium mb-2">AWS KMS → Database</p>
|
||||
<p class="text-2xs text-secondary mb-3">Re-encrypt secrets from AWS KMS to workspace keys</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateFromAwsKmsModalOpen = true)} disabled={disabled || !isAwsKmsConfigValid() || migratingFromAwsKms} startIcon={{ icon: ArrowLeft }}>
|
||||
Migrate to Database
|
||||
</Button>
|
||||
<div class="flex items-center gap-2 mb-2"><Cloud size={16} /><ArrowLeft size={16} /><Database size={16} /></div>
|
||||
<p class="text-xs font-medium mb-2">AWS Secrets Manager → Database</p>
|
||||
<Button unifiedSize="sm" variant="default" onclick={() => (migrateFromAwsSmModalOpen = true)} disabled={disabled || !isAwsSmConfigValid() || migratingFromAwsSm} startIcon={{ icon: ArrowLeft }}>Migrate to Database</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -750,102 +487,21 @@ vault write auth/jwt/role/windmill-secrets \
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Migrate to AWS KMS Modal -->
|
||||
<ConfirmationModal
|
||||
title="Migrate Secrets to AWS KMS"
|
||||
confirmationText="Migrate"
|
||||
open={migrateToAwsKmsModalOpen}
|
||||
loading={migratingToAwsKms}
|
||||
type="reload"
|
||||
onCanceled={() => { migrateToAwsKmsModalOpen = false }}
|
||||
onConfirmed={migrateSecretsToAwsKms}
|
||||
>
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>This will re-encrypt all existing secrets using AWS KMS. The process will:</p>
|
||||
<ol class="list-decimal list-inside text-sm space-y-1">
|
||||
<li>Read all encrypted secrets from the database</li>
|
||||
<li>Decrypt them using the workspace encryption keys</li>
|
||||
<li>Re-encrypt them using AWS KMS</li>
|
||||
<li>Update the database values in-place</li>
|
||||
</ol>
|
||||
<p class="text-yellow-600 dark:text-yellow-400 text-sm mt-2">
|
||||
Note: Secrets remain in the database. The encryption method changes from workspace keys to AWS KMS.
|
||||
</p>
|
||||
<p>Are you sure you want to proceed?</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
<ConfirmationModal title="Migrate to AWS Secrets Manager" confirmationText="Migrate" open={migrateToAwsSmModalOpen} loading={migratingToAwsSm} type="reload" onCanceled={() => { migrateToAwsSmModalOpen = false }} onConfirmed={migrateSecretsToAwsSm}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets from the database to AWS Secrets Manager.</p><p class="text-yellow-600 dark:text-yellow-400 text-sm">Database values are NOT deleted automatically.</p></div>{/snippet}
|
||||
</ConfirmationModal>
|
||||
|
||||
<!-- Migrate from AWS KMS to Database Modal -->
|
||||
<ConfirmationModal
|
||||
title="Migrate Secrets to Database"
|
||||
confirmationText="Migrate"
|
||||
open={migrateFromAwsKmsModalOpen}
|
||||
loading={migratingFromAwsKms}
|
||||
type="reload"
|
||||
onCanceled={() => { migrateFromAwsKmsModalOpen = false }}
|
||||
onConfirmed={migrateSecretsFromAwsKms}
|
||||
>
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>This will re-encrypt all AWS KMS-encrypted secrets using workspace keys. The process will:</p>
|
||||
<ol class="list-decimal list-inside text-sm space-y-1">
|
||||
<li>Find all secrets encrypted with AWS KMS</li>
|
||||
<li>Decrypt them using AWS KMS</li>
|
||||
<li>Re-encrypt them using workspace encryption keys</li>
|
||||
<li>Update the database values in-place</li>
|
||||
</ol>
|
||||
<p>Are you sure you want to proceed?</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
<ConfirmationModal title="Migrate to Database" confirmationText="Migrate" open={migrateFromAwsSmModalOpen} loading={migratingFromAwsSm} type="reload" onCanceled={() => { migrateFromAwsSmModalOpen = false }} onConfirmed={migrateSecretsFromAwsSm}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets from AWS Secrets Manager back to the database.</p><p class="text-yellow-600 dark:text-yellow-400 text-sm">AWS Secrets Manager values are NOT deleted automatically.</p></div>{/snippet}
|
||||
</ConfirmationModal>
|
||||
|
||||
<!-- Migrate to Azure Key Vault Modal -->
|
||||
<ConfirmationModal title="Migrate Secrets to Azure Key Vault" confirmationText="Migrate" open={migrateToAzureKvModalOpen} loading={migratingToAzureKv} type="reload" onCanceled={() => { migrateToAzureKvModalOpen = false }} onConfirmed={migrateSecretsToAzureKv}>
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>This will migrate all existing secrets from the database to Azure Key Vault. The process will:</p>
|
||||
<ol class="list-decimal list-inside text-sm space-y-1">
|
||||
<li>Read all encrypted secrets from the database</li>
|
||||
<li>Decrypt them using the workspace encryption keys</li>
|
||||
<li>Store them in Azure Key Vault</li>
|
||||
</ol>
|
||||
<p class="text-yellow-600 dark:text-yellow-400 text-sm mt-2">Note: Database values are NOT deleted automatically.</p>
|
||||
<p>Are you sure you want to proceed?</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
<ConfirmationModal title="Migrate to Azure Key Vault" confirmationText="Migrate" open={migrateToAzureKvModalOpen} loading={migratingToAzureKv} type="reload" onCanceled={() => { migrateToAzureKvModalOpen = false }} onConfirmed={migrateSecretsToAzureKv}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets to Azure Key Vault.</p><p class="text-yellow-600 dark:text-yellow-400 text-sm">Database values are NOT deleted automatically.</p></div>{/snippet}
|
||||
</ConfirmationModal>
|
||||
|
||||
<!-- Migrate from Azure Key Vault to Database Modal -->
|
||||
<ConfirmationModal title="Migrate Secrets to Database" confirmationText="Migrate" open={migrateFromAzureKvModalOpen} loading={migratingFromAzureKv} type="reload" onCanceled={() => { migrateFromAzureKvModalOpen = false }} onConfirmed={migrateSecretsFromAzureKv}>
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>This will migrate all secrets from Azure Key Vault back to the database.</p>
|
||||
<p class="text-yellow-600 dark:text-yellow-400 text-sm mt-2">Note: Azure Key Vault values are NOT deleted automatically.</p>
|
||||
<p>Are you sure you want to proceed?</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
<ConfirmationModal title="Migrate to Database" confirmationText="Migrate" open={migrateFromAzureKvModalOpen} loading={migratingFromAzureKv} type="reload" onCanceled={() => { migrateFromAzureKvModalOpen = false }} onConfirmed={migrateSecretsFromAzureKv}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets from Azure Key Vault back to the database.</p></div>{/snippet}
|
||||
</ConfirmationModal>
|
||||
|
||||
<!-- Migrate to Vault Modal -->
|
||||
<ConfirmationModal title="Migrate Secrets to Vault" confirmationText="Migrate" open={migrateToVaultModalOpen} loading={migratingToVault} type="reload" onCanceled={() => { migrateToVaultModalOpen = false }} onConfirmed={migrateSecretsToVault}>
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>This will migrate all existing secrets from the database to HashiCorp Vault.</p>
|
||||
<p class="text-yellow-600 dark:text-yellow-400 text-sm mt-2">Note: Database values are NOT deleted automatically.</p>
|
||||
<p>Are you sure you want to proceed?</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
<ConfirmationModal title="Migrate to Vault" confirmationText="Migrate" open={migrateToVaultModalOpen} loading={migratingToVault} type="reload" onCanceled={() => { migrateToVaultModalOpen = false }} onConfirmed={migrateSecretsToVault}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets to HashiCorp Vault.</p><p class="text-yellow-600 dark:text-yellow-400 text-sm">Database values are NOT deleted automatically.</p></div>{/snippet}
|
||||
</ConfirmationModal>
|
||||
|
||||
<!-- Migrate to Database Modal -->
|
||||
<ConfirmationModal title="Migrate Secrets to Database" confirmationText="Migrate" open={migrateToDatabaseModalOpen} loading={migratingToDatabase} type="reload" onCanceled={() => { migrateToDatabaseModalOpen = false }} onConfirmed={migrateSecretsToDatabase}>
|
||||
{#snippet children()}
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>This will migrate all secrets from HashiCorp Vault back to the database.</p>
|
||||
<p class="text-yellow-600 dark:text-yellow-400 text-sm mt-2">Note: Vault values are NOT deleted automatically.</p>
|
||||
<p>Are you sure you want to proceed?</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
<ConfirmationModal title="Migrate to Database" confirmationText="Migrate" open={migrateToDatabaseModalOpen} loading={migratingToDatabase} type="reload" onCanceled={() => { migrateToDatabaseModalOpen = false }} onConfirmed={migrateSecretsToDatabase}>
|
||||
{#snippet children()}<div class="flex flex-col gap-2"><p>This will copy all secrets from Vault back to the database.</p></div>{/snippet}
|
||||
</ConfirmationModal>
|
||||
|
||||
Reference in New Issue
Block a user