feat(bootstrap): implement mTLS for Vault access
- Add create_vault_config() function to generate config.hcl with mTLS settings - Configure Vault to require client certificate verification - Generate client certificate for botserver in bootstrap - Update .env to include mTLS paths (VAULT_CACERT, VAULT_CLIENT_CERT, VAULT_CLIENT_KEY) - Remove unused import in tls.rs
This commit is contained in:
parent
41f5847f56
commit
cfa1a01bc9
14 changed files with 969 additions and 346 deletions
36
.env.example
Normal file
36
.env.example
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# BotServer Environment Configuration
|
||||||
|
# =====================================
|
||||||
|
#
|
||||||
|
# ONLY VAULT VARIABLES ARE ALLOWED IN THIS FILE!
|
||||||
|
# All secrets (DATABASE_URL, API keys, etc.) MUST be stored in Vault.
|
||||||
|
# NO LEGACY FALLBACK - Vault is mandatory.
|
||||||
|
#
|
||||||
|
# Vault paths for secrets:
|
||||||
|
# - gbo/tables - PostgreSQL credentials (host, port, database, username, password)
|
||||||
|
# - gbo/drive - MinIO/S3 credentials (accesskey, secret)
|
||||||
|
# - gbo/cache - Redis credentials (password)
|
||||||
|
# - gbo/directory - Zitadel credentials (url, project_id, client_id, client_secret)
|
||||||
|
# - gbo/email - Email credentials (username, password)
|
||||||
|
# - gbo/llm - LLM API keys (openai_key, anthropic_key, groq_key)
|
||||||
|
# - gbo/encryption - Encryption keys (master_key)
|
||||||
|
# - gbo/meet - LiveKit credentials (api_key, api_secret)
|
||||||
|
# - gbo/alm - Forgejo credentials (url, admin_password, runner_token)
|
||||||
|
# - gbo/vectordb - Qdrant credentials (url, api_key)
|
||||||
|
# - gbo/observability - InfluxDB credentials (url, org, bucket, token)
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# VAULT CONFIGURATION - ONLY THESE VARS ARE ALLOWED
|
||||||
|
# =====================
|
||||||
|
|
||||||
|
# Vault server address
|
||||||
|
VAULT_ADDR=https://localhost:8200
|
||||||
|
|
||||||
|
# Vault authentication token (generated during vault init)
|
||||||
|
# This will be populated automatically after first bootstrap
|
||||||
|
VAULT_TOKEN=
|
||||||
|
|
||||||
|
# Skip TLS verification for development (set to false in production)
|
||||||
|
VAULT_SKIP_VERIFY=true
|
||||||
|
|
||||||
|
# Cache TTL for secrets in seconds (default: 300 = 5 minutes)
|
||||||
|
VAULT_CACHE_TTL=300
|
||||||
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -1138,9 +1138,9 @@ dependencies = [
|
||||||
"rhai",
|
"rhai",
|
||||||
"ring",
|
"ring",
|
||||||
"rust_xlsxwriter",
|
"rust_xlsxwriter",
|
||||||
"rustls 0.21.12",
|
"rustls 0.23.35",
|
||||||
"rustls-native-certs 0.6.3",
|
"rustls-native-certs 0.6.3",
|
||||||
"rustls-pemfile 1.0.4",
|
"rustls-pemfile 2.2.0",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -1152,7 +1152,7 @@ dependencies = [
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls 0.24.1",
|
"tokio-rustls 0.26.4",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"toml 0.8.23",
|
"toml 0.8.23",
|
||||||
"tonic 0.14.2",
|
"tonic 0.14.2",
|
||||||
|
|
|
||||||
|
|
@ -144,9 +144,9 @@ urlencoding = "2.1"
|
||||||
uuid = { version = "1.11", features = ["serde", "v4"] }
|
uuid = { version = "1.11", features = ["serde", "v4"] }
|
||||||
|
|
||||||
# === TLS/SECURITY DEPENDENCIES ===
|
# === TLS/SECURITY DEPENDENCIES ===
|
||||||
rustls = { version = "0.21", features = ["dangerous_configuration"] }
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
|
||||||
rustls-pemfile = "1.0"
|
rustls-pemfile = "2.0"
|
||||||
tokio-rustls = "0.24"
|
tokio-rustls = "0.26"
|
||||||
rcgen = { version = "0.14", features = ["pem"] }
|
rcgen = { version = "0.14", features = ["pem"] }
|
||||||
x509-parser = "0.15"
|
x509-parser = "0.15"
|
||||||
rustls-native-certs = "0.6"
|
rustls-native-certs = "0.6"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ use crate::shared::utils::establish_pg_connection;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use aws_config::BehaviorVersion;
|
use aws_config::BehaviorVersion;
|
||||||
use aws_sdk_s3::Client;
|
use aws_sdk_s3::Client;
|
||||||
use chrono;
|
|
||||||
use log::{error, info, trace, warn};
|
use log::{error, info, trace, warn};
|
||||||
use rand::distr::Alphanumeric;
|
use rand::distr::Alphanumeric;
|
||||||
use rcgen::{
|
use rcgen::{
|
||||||
|
|
@ -13,6 +12,8 @@ use rcgen::{
|
||||||
};
|
};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
@ -39,6 +40,7 @@ impl BootstrapManager {
|
||||||
pub fn start_all(&mut self) -> Result<()> {
|
pub fn start_all(&mut self) -> Result<()> {
|
||||||
let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?;
|
let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?;
|
||||||
let components = vec![
|
let components = vec![
|
||||||
|
ComponentInfo { name: "vault" },
|
||||||
ComponentInfo { name: "tables" },
|
ComponentInfo { name: "tables" },
|
||||||
ComponentInfo { name: "cache" },
|
ComponentInfo { name: "cache" },
|
||||||
ComponentInfo { name: "drive" },
|
ComponentInfo { name: "drive" },
|
||||||
|
|
@ -84,13 +86,38 @@ impl BootstrapManager {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure critical services (tables and drive) are running
|
/// Ensure critical services are running - Vault MUST be first
|
||||||
|
/// Order: vault -> tables -> drive
|
||||||
pub async fn ensure_services_running(&mut self) -> Result<()> {
|
pub async fn ensure_services_running(&mut self) -> Result<()> {
|
||||||
info!("Ensuring critical services are running...");
|
info!("Ensuring critical services are running...");
|
||||||
|
|
||||||
let installer = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?;
|
let installer = PackageManager::new(self.install_mode.clone(), self.tenant.clone())?;
|
||||||
|
|
||||||
// Check and start PostgreSQL
|
// VAULT MUST BE FIRST - it provides all secrets
|
||||||
|
if installer.is_installed("vault") {
|
||||||
|
info!("Starting Vault secrets service...");
|
||||||
|
match installer.start("vault") {
|
||||||
|
Ok(_child) => {
|
||||||
|
info!("Vault started successfully");
|
||||||
|
// Give Vault time to initialize
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||||
|
|
||||||
|
// Check if Vault needs to be unsealed
|
||||||
|
if let Err(e) = self.ensure_vault_unsealed().await {
|
||||||
|
warn!("Failed to unseal Vault: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Vault might already be running or failed to start: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Vault not installed - cannot proceed, need to run bootstrap
|
||||||
|
warn!("Vault (secrets) component not installed - run bootstrap first");
|
||||||
|
return Err(anyhow::anyhow!("Vault not installed. Run bootstrap command first."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check and start PostgreSQL (after Vault is running)
|
||||||
if installer.is_installed("tables") {
|
if installer.is_installed("tables") {
|
||||||
info!("Starting PostgreSQL database service...");
|
info!("Starting PostgreSQL database service...");
|
||||||
match installer.start("tables") {
|
match installer.start("tables") {
|
||||||
|
|
@ -100,7 +127,6 @@ impl BootstrapManager {
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
|
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Check if it's already running (start might fail if already running)
|
|
||||||
warn!(
|
warn!(
|
||||||
"PostgreSQL might already be running or failed to start: {}",
|
"PostgreSQL might already be running or failed to start: {}",
|
||||||
e
|
e
|
||||||
|
|
@ -121,7 +147,6 @@ impl BootstrapManager {
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// MinIO is not critical, just log
|
|
||||||
warn!("MinIO might already be running or failed to start: {}", e);
|
warn!("MinIO might already be running or failed to start: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -132,31 +157,101 @@ impl BootstrapManager {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure Vault is unsealed (required after restart)
|
||||||
|
async fn ensure_vault_unsealed(&self) -> Result<()> {
|
||||||
|
let vault_init_path = PathBuf::from("./botserver-stack/conf/vault/init.json");
|
||||||
|
|
||||||
|
if !vault_init_path.exists() {
|
||||||
|
return Err(anyhow::anyhow!("Vault init.json not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read unseal key from init.json
|
||||||
|
let init_json = fs::read_to_string(&vault_init_path)?;
|
||||||
|
let init_data: serde_json::Value = serde_json::from_str(&init_json)?;
|
||||||
|
|
||||||
|
let unseal_key = init_data["unseal_keys_b64"]
|
||||||
|
.as_array()
|
||||||
|
.and_then(|arr| arr.first())
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let root_token = init_data["root_token"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if unseal_key.is_empty() || root_token.is_empty() {
|
||||||
|
return Err(anyhow::anyhow!("Invalid Vault init.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let vault_addr = "https://localhost:8200";
|
||||||
|
|
||||||
|
// Check if Vault is sealed
|
||||||
|
let status_output = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!(
|
||||||
|
"VAULT_ADDR={} VAULT_SKIP_VERIFY=true ./botserver-stack/bin/vault/vault status -format=json 2>/dev/null || echo '{{\"sealed\":true}}'",
|
||||||
|
vault_addr
|
||||||
|
))
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
let status_json = String::from_utf8_lossy(&status_output.stdout);
|
||||||
|
if let Ok(status) = serde_json::from_str::<serde_json::Value>(&status_json) {
|
||||||
|
if status["sealed"].as_bool().unwrap_or(true) {
|
||||||
|
info!("Unsealing Vault...");
|
||||||
|
let _ = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!(
|
||||||
|
"VAULT_ADDR={} VAULT_SKIP_VERIFY=true ./botserver-stack/bin/vault/vault operator unseal {}",
|
||||||
|
vault_addr, unseal_key
|
||||||
|
))
|
||||||
|
.output()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set environment variables for other components
|
||||||
|
std::env::set_var("VAULT_ADDR", vault_addr);
|
||||||
|
std::env::set_var("VAULT_TOKEN", &root_token);
|
||||||
|
std::env::set_var("VAULT_SKIP_VERIFY", "true");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn bootstrap(&mut self) -> Result<()> {
|
pub async fn bootstrap(&mut self) -> Result<()> {
|
||||||
// Generate certificates first
|
// Generate certificates first (including for Vault)
|
||||||
info!("🔒 Generating TLS certificates...");
|
info!("🔒 Generating TLS certificates...");
|
||||||
if let Err(e) = self.generate_certificates().await {
|
if let Err(e) = self.generate_certificates().await {
|
||||||
error!("Failed to generate certificates: {}", e);
|
error!("Failed to generate certificates: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Directory (Zitadel) is the root service - stores all configuration
|
// Create Vault configuration with mTLS
|
||||||
let _directory_password = self.generate_secure_password(32);
|
info!("📝 Creating Vault configuration...");
|
||||||
let _directory_masterkey = self.generate_secure_password(32);
|
if let Err(e) = self.create_vault_config().await {
|
||||||
|
error!("Failed to create Vault config: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
// Configuration is stored in Directory service, not .env files
|
// Generate secure passwords for all services - these are ONLY used during bootstrap
|
||||||
info!("Configuring services through Directory...");
|
// and immediately stored in Vault. NO LEGACY ENV VARS.
|
||||||
|
let db_password = self.generate_secure_password(24);
|
||||||
|
let drive_accesskey = self.generate_secure_password(20);
|
||||||
|
let drive_secret = self.generate_secure_password(40);
|
||||||
|
let cache_password = self.generate_secure_password(24);
|
||||||
|
|
||||||
|
// Configuration is stored in Vault, not .env files
|
||||||
|
info!("Configuring services through Vault...");
|
||||||
|
|
||||||
let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone()).unwrap();
|
let pm = PackageManager::new(self.install_mode.clone(), self.tenant.clone()).unwrap();
|
||||||
// Directory must be installed first as it's the root service
|
|
||||||
|
// Vault MUST be installed first - it stores all secrets
|
||||||
|
// Order: vault -> tables -> directory -> drive -> cache -> llm
|
||||||
let required_components = vec![
|
let required_components = vec![
|
||||||
"directory", // Root service - manages all other services
|
"vault", // Secrets management - MUST BE FIRST
|
||||||
"tables", // Database - credentials stored in Directory
|
"tables", // Database - required by Directory
|
||||||
"drive", // S3 storage - credentials stored in Directory
|
"directory", // Identity service - manages users
|
||||||
|
"drive", // S3 storage - credentials in Vault
|
||||||
"cache", // Redis cache
|
"cache", // Redis cache
|
||||||
"llm", // LLM service
|
"llm", // LLM service
|
||||||
"email", // Email service integrated with Directory
|
|
||||||
"proxy", // Caddy reverse proxy
|
|
||||||
"dns", // CoreDNS for dynamic DNS
|
|
||||||
];
|
];
|
||||||
for component in required_components {
|
for component in required_components {
|
||||||
if !pm.is_installed(component) {
|
if !pm.is_installed(component) {
|
||||||
|
|
@ -195,17 +290,28 @@ impl BootstrapManager {
|
||||||
}
|
}
|
||||||
_ = pm.install(component).await;
|
_ = pm.install(component).await;
|
||||||
|
|
||||||
// Directory must be configured first as root service
|
// After tables is installed, create Zitadel config files before installing directory
|
||||||
if component == "directory" {
|
if component == "tables" {
|
||||||
info!("🔧 Configuring Directory as root service...");
|
info!("🔧 Creating Directory configuration files...");
|
||||||
if let Err(e) = self.setup_directory().await {
|
if let Err(e) = self.configure_services_in_directory(&db_password).await {
|
||||||
error!("Failed to setup Directory: {}", e);
|
error!("Failed to create Directory config files: {}", e);
|
||||||
return Err(anyhow::anyhow!("Directory is required as root service"));
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// After directory is setup, configure database and drive credentials there
|
// Directory configuration - setup happens after install starts Zitadel
|
||||||
if let Err(e) = self.configure_services_in_directory().await {
|
if component == "directory" {
|
||||||
error!("Failed to configure services in Directory: {}", e);
|
info!("🔧 Waiting for Directory to be ready...");
|
||||||
|
if let Err(e) = self.setup_directory().await {
|
||||||
|
// Don't fail completely - Zitadel may still be usable with first instance setup
|
||||||
|
warn!("Directory additional setup had issues: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After Vault is installed and running, store all secrets
|
||||||
|
if component == "vault" {
|
||||||
|
info!("🔐 Initializing Vault with secrets...");
|
||||||
|
if let Err(e) = self.setup_vault(&db_password, &drive_accesskey, &drive_secret, &cache_password).await {
|
||||||
|
error!("Failed to setup Vault: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,67 +346,124 @@ impl BootstrapManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure database and drive credentials in Directory
|
/// Configure database and drive credentials in Directory
|
||||||
async fn configure_services_in_directory(&self) -> Result<()> {
|
/// This creates the Zitadel config files BEFORE Zitadel is installed
|
||||||
info!("Storing service credentials in Directory...");
|
/// db_password is passed directly from bootstrap - NO ENV VARS
|
||||||
|
async fn configure_services_in_directory(&self, db_password: &str) -> Result<()> {
|
||||||
|
info!("Creating Zitadel configuration files...");
|
||||||
|
|
||||||
// Generate credentials for services
|
|
||||||
let db_password = self.generate_secure_password(32);
|
|
||||||
let drive_password = self.generate_secure_password(16);
|
|
||||||
let drive_user = "gbdriveuser".to_string();
|
|
||||||
|
|
||||||
// Create Zitadel configuration with service accounts
|
|
||||||
let zitadel_config_path = PathBuf::from("./botserver-stack/conf/directory/zitadel.yaml");
|
let zitadel_config_path = PathBuf::from("./botserver-stack/conf/directory/zitadel.yaml");
|
||||||
|
let steps_config_path = PathBuf::from("./botserver-stack/conf/directory/steps.yaml");
|
||||||
|
let pat_path = PathBuf::from("./botserver-stack/conf/directory/admin-pat.txt");
|
||||||
|
|
||||||
fs::create_dir_all(zitadel_config_path.parent().unwrap())?;
|
fs::create_dir_all(zitadel_config_path.parent().unwrap())?;
|
||||||
|
|
||||||
|
// Generate Zitadel database password
|
||||||
|
let zitadel_db_password = self.generate_secure_password(24);
|
||||||
|
|
||||||
|
// Create zitadel.yaml - main configuration
|
||||||
let zitadel_config = format!(
|
let zitadel_config = format!(
|
||||||
r#"
|
r#"Log:
|
||||||
|
Level: info
|
||||||
|
Formatter:
|
||||||
|
Format: text
|
||||||
|
|
||||||
Database:
|
Database:
|
||||||
postgres:
|
postgres:
|
||||||
Host: localhost
|
Host: localhost
|
||||||
Port: 5432
|
Port: 5432
|
||||||
Database: zitadel
|
Database: zitadel
|
||||||
User: zitadel
|
User: zitadel
|
||||||
Password: {}
|
Password: "{}"
|
||||||
SSL:
|
SSL:
|
||||||
Mode: require
|
Mode: disable
|
||||||
RootCert: /botserver-stack/conf/system/certificates/postgres/ca.crt
|
Admin:
|
||||||
|
|
||||||
SystemDefaults:
|
|
||||||
SecretGenerators:
|
|
||||||
PasswordSaltCost: 14
|
|
||||||
|
|
||||||
ExternalSecure: true
|
|
||||||
ExternalDomain: localhost
|
|
||||||
ExternalPort: 443
|
|
||||||
|
|
||||||
# Service accounts for integrated services
|
|
||||||
ServiceAccounts:
|
|
||||||
- Name: database-service
|
|
||||||
Description: PostgreSQL Database Service
|
|
||||||
Credentials:
|
|
||||||
Username: gbuser
|
Username: gbuser
|
||||||
Password: {}
|
Password: "{}"
|
||||||
- Name: drive-service
|
SSL:
|
||||||
Description: MinIO S3 Storage Service
|
Mode: disable
|
||||||
Credentials:
|
|
||||||
AccessKey: {}
|
Machine:
|
||||||
SecretKey: {}
|
Identification:
|
||||||
- Name: email-service
|
Hostname:
|
||||||
Description: Email Service Integration
|
Enabled: true
|
||||||
OAuth: true
|
|
||||||
- Name: git-service
|
ExternalSecure: false
|
||||||
Description: Forgejo Git Service
|
ExternalDomain: localhost
|
||||||
OAuth: true
|
ExternalPort: 8080
|
||||||
|
|
||||||
|
DefaultInstance:
|
||||||
|
OIDCSettings:
|
||||||
|
AccessTokenLifetime: 12h
|
||||||
|
IdTokenLifetime: 12h
|
||||||
|
RefreshTokenIdleExpiration: 720h
|
||||||
|
RefreshTokenExpiration: 2160h
|
||||||
"#,
|
"#,
|
||||||
self.generate_secure_password(24),
|
zitadel_db_password,
|
||||||
db_password,
|
db_password, // Use the password passed directly from bootstrap
|
||||||
drive_user,
|
|
||||||
drive_password
|
|
||||||
);
|
);
|
||||||
|
|
||||||
fs::write(zitadel_config_path, zitadel_config)?;
|
fs::write(&zitadel_config_path, zitadel_config)?;
|
||||||
|
info!("Created zitadel.yaml configuration");
|
||||||
|
|
||||||
info!("Service credentials configured in Directory");
|
// Create steps.yaml - first instance setup that generates admin PAT
|
||||||
|
let steps_config = format!(
|
||||||
|
r#"FirstInstance:
|
||||||
|
Org:
|
||||||
|
Name: "BotServer"
|
||||||
|
Human:
|
||||||
|
UserName: "admin"
|
||||||
|
FirstName: "Admin"
|
||||||
|
LastName: "User"
|
||||||
|
Email:
|
||||||
|
Address: "admin@localhost"
|
||||||
|
Verified: true
|
||||||
|
Password: "{}"
|
||||||
|
PatPath: "{}"
|
||||||
|
InstanceName: "BotServer"
|
||||||
|
DefaultLanguage: "en"
|
||||||
|
"#,
|
||||||
|
self.generate_secure_password(16),
|
||||||
|
pat_path.to_string_lossy(),
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::write(&steps_config_path, steps_config)?;
|
||||||
|
info!("Created steps.yaml for first instance setup");
|
||||||
|
|
||||||
|
// Create zitadel database in PostgreSQL
|
||||||
|
info!("Creating zitadel database...");
|
||||||
|
let create_db_result = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!(
|
||||||
|
"PGPASSWORD='{}' psql -h localhost -p 5432 -U gbuser -d postgres -c \"CREATE DATABASE zitadel\" 2>&1 || true",
|
||||||
|
db_password
|
||||||
|
))
|
||||||
|
.output();
|
||||||
|
|
||||||
|
if let Ok(output) = create_db_result {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
if !stdout.contains("already exists") {
|
||||||
|
info!("Created zitadel database");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create zitadel user
|
||||||
|
let create_user_result = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!(
|
||||||
|
"PGPASSWORD='{}' psql -h localhost -p 5432 -U gbuser -d postgres -c \"CREATE USER zitadel WITH PASSWORD '{}' SUPERUSER\" 2>&1 || true",
|
||||||
|
db_password,
|
||||||
|
zitadel_db_password
|
||||||
|
))
|
||||||
|
.output();
|
||||||
|
|
||||||
|
if let Ok(output) = create_user_result {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
if !stdout.contains("already exists") {
|
||||||
|
info!("Created zitadel database user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Zitadel configuration files created");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,112 +574,382 @@ meet IN A 127.0.0.1
|
||||||
/// Setup Directory (Zitadel) with default organization and user
|
/// Setup Directory (Zitadel) with default organization and user
|
||||||
async fn setup_directory(&self) -> Result<()> {
|
async fn setup_directory(&self) -> Result<()> {
|
||||||
let config_path = PathBuf::from("./config/directory_config.json");
|
let config_path = PathBuf::from("./config/directory_config.json");
|
||||||
|
let pat_path = PathBuf::from("./botserver-stack/conf/directory/admin-pat.txt");
|
||||||
|
|
||||||
// Ensure config directory exists
|
// Ensure config directory exists
|
||||||
tokio::fs::create_dir_all("./config").await?;
|
tokio::fs::create_dir_all("./config").await?;
|
||||||
|
|
||||||
// Wait for Directory to be ready
|
// Wait for Directory to be ready and check for PAT file
|
||||||
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
info!("Waiting for Zitadel to be ready...");
|
||||||
|
let mut attempts = 0;
|
||||||
|
let max_attempts = 60; // 60 seconds max wait
|
||||||
|
|
||||||
|
while attempts < max_attempts {
|
||||||
|
// Check if Zitadel is healthy
|
||||||
|
let health_check = std::process::Command::new("curl")
|
||||||
|
.args(["-f", "-s", "http://localhost:8080/healthz"])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
if let Ok(output) = health_check {
|
||||||
|
if output.status.success() {
|
||||||
|
info!("Zitadel is healthy");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts += 1;
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempts >= max_attempts {
|
||||||
|
warn!("Zitadel health check timed out, continuing anyway...");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a bit more for PAT file to be generated
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
|
||||||
|
|
||||||
|
// Read the admin PAT generated by Zitadel first instance setup
|
||||||
|
let admin_token = if pat_path.exists() {
|
||||||
|
let token = fs::read_to_string(&pat_path)?;
|
||||||
|
let token = token.trim().to_string();
|
||||||
|
info!("Loaded admin PAT from {}", pat_path.display());
|
||||||
|
Some(token)
|
||||||
|
} else {
|
||||||
|
warn!("Admin PAT file not found at {}", pat_path.display());
|
||||||
|
warn!("Zitadel first instance setup may not have completed");
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let mut setup = DirectorySetup::new(
|
let mut setup = DirectorySetup::new(
|
||||||
crate::core::urls::InternalUrls::DIRECTORY_BASE.to_string(),
|
"http://localhost:8080".to_string(), // Use HTTP since TLS is disabled
|
||||||
config_path,
|
config_path,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create default organization
|
// Set the admin token if we have it
|
||||||
let org_name = "default";
|
if let Some(token) = admin_token {
|
||||||
let org_id = setup
|
setup.set_admin_token(token);
|
||||||
.create_organization(org_name, "Default Organization")
|
} else {
|
||||||
.await?;
|
// If no PAT, we can't proceed with API calls
|
||||||
info!("Created default organization: {}", org_name);
|
info!("Directory setup skipped - no admin token available");
|
||||||
|
info!("First instance setup created initial admin user via steps.yaml");
|
||||||
// Generate secure passwords
|
return Ok(());
|
||||||
let admin_password = self.generate_secure_password(16);
|
|
||||||
let user_password = self.generate_secure_password(16);
|
|
||||||
|
|
||||||
// Save initial credentials to secure file
|
|
||||||
let creds_path = PathBuf::from("./botserver-stack/conf/system/initial-credentials.txt");
|
|
||||||
fs::create_dir_all(creds_path.parent().unwrap())?;
|
|
||||||
let creds_content = format!(
|
|
||||||
"INITIAL SETUP CREDENTIALS\n\
|
|
||||||
========================\n\
|
|
||||||
Generated at: {}\n\n\
|
|
||||||
Admin Account:\n\
|
|
||||||
Username: admin@default\n\
|
|
||||||
Password: {}\n\n\
|
|
||||||
User Account:\n\
|
|
||||||
Username: user@default\n\
|
|
||||||
Password: {}\n\n\
|
|
||||||
IMPORTANT: Delete this file after saving credentials securely.\n",
|
|
||||||
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
|
||||||
admin_password,
|
|
||||||
user_password
|
|
||||||
);
|
|
||||||
fs::write(&creds_path, creds_content)?;
|
|
||||||
|
|
||||||
// Set restrictive permissions on Unix-like systems
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
fs::set_permissions(&creds_path, fs::Permissions::from_mode(0o600))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create admin@default account for bot administration
|
// Wait a bit more for Zitadel to be fully ready
|
||||||
let admin_user = setup
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||||
.create_user(
|
|
||||||
&org_id,
|
|
||||||
"admin",
|
|
||||||
"admin@default",
|
|
||||||
&admin_password,
|
|
||||||
"Admin",
|
|
||||||
"Default",
|
|
||||||
true, // is_admin
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
info!("Created admin user: admin@default");
|
|
||||||
|
|
||||||
// Create user@default account for regular bot usage
|
// Try to create additional organization for bot users
|
||||||
let regular_user = setup
|
let org_name = "default";
|
||||||
.create_user(
|
match setup.create_organization(org_name, "Default Organization").await {
|
||||||
&org_id,
|
Ok(org_id) => {
|
||||||
"user",
|
info!("Created default organization: {}", org_name);
|
||||||
"user@default",
|
|
||||||
&user_password,
|
|
||||||
"User",
|
|
||||||
"Default",
|
|
||||||
false, // is_admin
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
info!("Created regular user: user@default");
|
|
||||||
info!(" Regular user ID: {}", regular_user.id);
|
|
||||||
|
|
||||||
// Create OAuth2 application for BotServer
|
// Generate secure passwords
|
||||||
let (project_id, client_id, client_secret) =
|
let user_password = self.generate_secure_password(16);
|
||||||
setup.create_oauth_application(&org_id).await?;
|
|
||||||
info!("Created OAuth2 application in project: {}", project_id);
|
|
||||||
|
|
||||||
// Save configuration
|
// Create user@default account for regular bot usage
|
||||||
let config = setup
|
match setup.create_user(
|
||||||
.save_config(
|
&org_id,
|
||||||
org_id.clone(),
|
"user",
|
||||||
org_name.to_string(),
|
"user@default",
|
||||||
admin_user,
|
&user_password,
|
||||||
client_id.clone(),
|
"User",
|
||||||
client_secret,
|
"Default",
|
||||||
)
|
false,
|
||||||
.await?;
|
).await {
|
||||||
|
Ok(regular_user) => {
|
||||||
|
info!("Created regular user: user@default");
|
||||||
|
info!(" Regular user ID: {}", regular_user.id);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to create regular user: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
info!("Directory initialized successfully!");
|
// Create OAuth2 application for BotServer
|
||||||
info!(" Organization: default");
|
match setup.create_oauth_application(&org_id).await {
|
||||||
info!(" Admin User: admin@default");
|
Ok((project_id, client_id, client_secret)) => {
|
||||||
info!(" Regular User: user@default");
|
info!("Created OAuth2 application in project: {}", project_id);
|
||||||
info!(" Client ID: {}", client_id);
|
|
||||||
info!(" Login URL: {}", config.base_url);
|
// Save configuration
|
||||||
info!("");
|
let admin_user = crate::package_manager::setup::DefaultUser {
|
||||||
info!(" ⚠️ IMPORTANT: Initial credentials saved to:");
|
id: "admin".to_string(),
|
||||||
info!(" ./botserver-stack/conf/system/initial-credentials.txt");
|
username: "admin".to_string(),
|
||||||
info!(" Please save these credentials securely and delete the file.");
|
email: "admin@localhost".to_string(),
|
||||||
|
password: "".to_string(), // Don't store password
|
||||||
|
first_name: "Admin".to_string(),
|
||||||
|
last_name: "User".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(config) = setup.save_config(
|
||||||
|
org_id.clone(),
|
||||||
|
org_name.to_string(),
|
||||||
|
admin_user,
|
||||||
|
client_id.clone(),
|
||||||
|
client_secret,
|
||||||
|
).await {
|
||||||
|
info!("Directory initialized successfully!");
|
||||||
|
info!(" Organization: default");
|
||||||
|
info!(" Client ID: {}", client_id);
|
||||||
|
info!(" Login URL: {}", config.base_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to create OAuth2 application: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
warn!("Failed to create organization: {}", e);
|
||||||
|
info!("Using Zitadel's default organization from first instance setup");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Directory setup complete");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setup Vault with all service secrets and write .env file with VAULT_* variables
|
||||||
|
async fn setup_vault(&self, db_password: &str, drive_accesskey: &str, drive_secret: &str, cache_password: &str) -> Result<()> {
|
||||||
|
let vault_conf_path = PathBuf::from("./botserver-stack/conf/vault");
|
||||||
|
let vault_init_path = vault_conf_path.join("init.json");
|
||||||
|
let env_file_path = PathBuf::from("./.env");
|
||||||
|
|
||||||
|
// Wait for Vault to be ready
|
||||||
|
info!("Waiting for Vault to be ready...");
|
||||||
|
let mut attempts = 0;
|
||||||
|
let max_attempts = 30;
|
||||||
|
|
||||||
|
while attempts < max_attempts {
|
||||||
|
let health_check = std::process::Command::new("curl")
|
||||||
|
.args(["-f", "-s", "-k", "https://localhost:8200/v1/sys/health?standbyok=true&uninitcode=200&sealedcode=200"])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
if let Ok(output) = health_check {
|
||||||
|
if output.status.success() {
|
||||||
|
info!("Vault is responding");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts += 1;
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempts >= max_attempts {
|
||||||
|
warn!("Vault health check timed out");
|
||||||
|
return Err(anyhow::anyhow!("Vault not ready after {} seconds", max_attempts));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Vault is already initialized
|
||||||
|
let vault_addr = "https://localhost:8200";
|
||||||
|
std::env::set_var("VAULT_ADDR", vault_addr);
|
||||||
|
std::env::set_var("VAULT_SKIP_VERIFY", "true");
|
||||||
|
|
||||||
|
// Read init.json if it exists (from post_install_cmds)
|
||||||
|
let (unseal_key, root_token) = if vault_init_path.exists() {
|
||||||
|
info!("Reading Vault initialization from init.json...");
|
||||||
|
let init_json = fs::read_to_string(&vault_init_path)?;
|
||||||
|
let init_data: serde_json::Value = serde_json::from_str(&init_json)?;
|
||||||
|
|
||||||
|
let unseal_key = init_data["unseal_keys_b64"]
|
||||||
|
.as_array()
|
||||||
|
.and_then(|arr| arr.first())
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let root_token = init_data["root_token"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
(unseal_key, root_token)
|
||||||
|
} else {
|
||||||
|
// Initialize Vault if not already done
|
||||||
|
info!("Initializing Vault...");
|
||||||
|
let init_output = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!(
|
||||||
|
"VAULT_ADDR={} VAULT_SKIP_VERIFY=true ./botserver-stack/bin/vault/vault operator init -key-shares=1 -key-threshold=1 -format=json",
|
||||||
|
vault_addr
|
||||||
|
))
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !init_output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&init_output.stderr);
|
||||||
|
if stderr.contains("already initialized") {
|
||||||
|
warn!("Vault already initialized but init.json not found");
|
||||||
|
return Err(anyhow::anyhow!("Vault initialized but credentials lost"));
|
||||||
|
}
|
||||||
|
return Err(anyhow::anyhow!("Vault init failed: {}", stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
let init_json = String::from_utf8_lossy(&init_output.stdout);
|
||||||
|
fs::write(&vault_init_path, init_json.as_ref())?;
|
||||||
|
fs::set_permissions(&vault_init_path, std::fs::Permissions::from_mode(0o600))?;
|
||||||
|
|
||||||
|
let init_data: serde_json::Value = serde_json::from_str(&init_json)?;
|
||||||
|
let unseal_key = init_data["unseal_keys_b64"]
|
||||||
|
.as_array()
|
||||||
|
.and_then(|arr| arr.first())
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let root_token = init_data["root_token"]
|
||||||
|
.as_str()
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
(unseal_key, root_token)
|
||||||
|
};
|
||||||
|
|
||||||
|
if root_token.is_empty() {
|
||||||
|
return Err(anyhow::anyhow!("Failed to get Vault root token"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unseal Vault
|
||||||
|
info!("Unsealing Vault...");
|
||||||
|
let unseal_output = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!(
|
||||||
|
"VAULT_ADDR={} VAULT_SKIP_VERIFY=true ./botserver-stack/bin/vault/vault operator unseal {}",
|
||||||
|
vault_addr, unseal_key
|
||||||
|
))
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !unseal_output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&unseal_output.stderr);
|
||||||
|
if !stderr.contains("already unsealed") {
|
||||||
|
warn!("Vault unseal warning: {}", stderr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set VAULT_TOKEN for subsequent commands
|
||||||
|
std::env::set_var("VAULT_TOKEN", &root_token);
|
||||||
|
|
||||||
|
// Enable KV secrets engine at gbo/ path
|
||||||
|
info!("Enabling KV secrets engine...");
|
||||||
|
let _ = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!(
|
||||||
|
"VAULT_ADDR={} VAULT_SKIP_VERIFY=true VAULT_TOKEN={} ./botserver-stack/bin/vault/vault secrets enable -path=secret kv-v2 2>&1 || true",
|
||||||
|
vault_addr, root_token
|
||||||
|
))
|
||||||
|
.output();
|
||||||
|
|
||||||
|
// Store all secrets in Vault
|
||||||
|
info!("Storing secrets in Vault...");
|
||||||
|
|
||||||
|
// Database credentials
|
||||||
|
let _ = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!(
|
||||||
|
"VAULT_ADDR={} VAULT_SKIP_VERIFY=true VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv put secret/gbo/tables host=localhost port=5432 database=botserver username=gbuser password='{}'",
|
||||||
|
vault_addr, root_token, db_password
|
||||||
|
))
|
||||||
|
.output()?;
|
||||||
|
info!(" ✓ Stored database credentials");
|
||||||
|
|
||||||
|
// Drive credentials
|
||||||
|
let _ = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!(
|
||||||
|
"VAULT_ADDR={} VAULT_SKIP_VERIFY=true VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv put secret/gbo/drive accesskey='{}' secret='{}'",
|
||||||
|
vault_addr, root_token, drive_accesskey, drive_secret
|
||||||
|
))
|
||||||
|
.output()?;
|
||||||
|
info!(" ✓ Stored drive credentials");
|
||||||
|
|
||||||
|
// Cache credentials
|
||||||
|
let _ = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!(
|
||||||
|
"VAULT_ADDR={} VAULT_SKIP_VERIFY=true VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv put secret/gbo/cache password='{}'",
|
||||||
|
vault_addr, root_token, cache_password
|
||||||
|
))
|
||||||
|
.output()?;
|
||||||
|
info!(" ✓ Stored cache credentials");
|
||||||
|
|
||||||
|
// Directory placeholder (will be updated after Zitadel setup)
|
||||||
|
let _ = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!(
|
||||||
|
"VAULT_ADDR={} VAULT_SKIP_VERIFY=true VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv put secret/gbo/directory url=https://localhost:8080 project_id= client_id= client_secret=",
|
||||||
|
vault_addr, root_token
|
||||||
|
))
|
||||||
|
.output()?;
|
||||||
|
info!(" ✓ Created directory placeholder");
|
||||||
|
|
||||||
|
// LLM placeholder
|
||||||
|
let _ = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!(
|
||||||
|
"VAULT_ADDR={} VAULT_SKIP_VERIFY=true VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv put secret/gbo/llm openai_key= anthropic_key= groq_key=",
|
||||||
|
vault_addr, root_token
|
||||||
|
))
|
||||||
|
.output()?;
|
||||||
|
info!(" ✓ Created LLM placeholder");
|
||||||
|
|
||||||
|
// Email placeholder
|
||||||
|
let _ = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!(
|
||||||
|
"VAULT_ADDR={} VAULT_SKIP_VERIFY=true VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv put secret/gbo/email username= password=",
|
||||||
|
vault_addr, root_token
|
||||||
|
))
|
||||||
|
.output()?;
|
||||||
|
info!(" ✓ Created email placeholder");
|
||||||
|
|
||||||
|
// Encryption key
|
||||||
|
let encryption_key = self.generate_secure_password(32);
|
||||||
|
let _ = std::process::Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(format!(
|
||||||
|
"VAULT_ADDR={} VAULT_SKIP_VERIFY=true VAULT_TOKEN={} ./botserver-stack/bin/vault/vault kv put secret/gbo/encryption master_key='{}'",
|
||||||
|
vault_addr, root_token, encryption_key
|
||||||
|
))
|
||||||
|
.output()?;
|
||||||
|
info!(" ✓ Generated and stored encryption key");
|
||||||
|
|
||||||
|
// Write .env file with ONLY Vault variables - NO LEGACY FALLBACK
|
||||||
|
info!("Writing .env file with Vault configuration...");
|
||||||
|
let env_content = format!(
|
||||||
|
r#"# BotServer Environment Configuration
|
||||||
|
# Generated by bootstrap - DO NOT ADD OTHER SECRETS HERE
|
||||||
|
# All secrets are stored in Vault at the paths below:
|
||||||
|
# - gbo/tables - PostgreSQL credentials
|
||||||
|
# - gbo/drive - MinIO/S3 credentials
|
||||||
|
# - gbo/cache - Redis credentials
|
||||||
|
# - gbo/directory - Zitadel credentials
|
||||||
|
# - gbo/email - Email credentials
|
||||||
|
# - gbo/llm - LLM API keys
|
||||||
|
# - gbo/encryption - Encryption keys
|
||||||
|
|
||||||
|
# Vault Configuration - THESE ARE THE ONLY ALLOWED ENV VARS
|
||||||
|
VAULT_ADDR={}
|
||||||
|
VAULT_TOKEN={}
|
||||||
|
|
||||||
|
# mTLS Configuration for Vault
|
||||||
|
# Set VAULT_SKIP_VERIFY=false in production with proper CA cert
|
||||||
|
VAULT_SKIP_VERIFY=true
|
||||||
|
VAULT_CACERT=./botserver-stack/conf/system/certificates/ca/ca.crt
|
||||||
|
VAULT_CLIENT_CERT=./botserver-stack/conf/system/certificates/botserver/client.crt
|
||||||
|
VAULT_CLIENT_KEY=./botserver-stack/conf/system/certificates/botserver/client.key
|
||||||
|
|
||||||
|
# Cache TTL for secrets (seconds)
|
||||||
|
VAULT_CACHE_TTL=300
|
||||||
|
"#,
|
||||||
|
vault_addr, root_token
|
||||||
|
);
|
||||||
|
|
||||||
|
fs::write(&env_file_path, env_content)?;
|
||||||
|
info!(" ✓ Created .env file with Vault configuration");
|
||||||
|
|
||||||
|
info!("🔐 Vault setup complete!");
|
||||||
|
info!(" Vault UI: {}/ui", vault_addr);
|
||||||
|
info!(" Root token saved to: {}", vault_init_path.display());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -699,6 +1132,67 @@ meet IN A 127.0.0.1
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create Vault configuration with mTLS settings
|
||||||
|
async fn create_vault_config(&self) -> Result<()> {
|
||||||
|
let vault_conf_dir = PathBuf::from("./botserver-stack/conf/vault");
|
||||||
|
let config_path = vault_conf_dir.join("config.hcl");
|
||||||
|
|
||||||
|
fs::create_dir_all(&vault_conf_dir)?;
|
||||||
|
|
||||||
|
// Vault config with mTLS - requires client certificate verification
|
||||||
|
let config = r#"# Vault Configuration with mTLS
|
||||||
|
# Generated by BotServer bootstrap
|
||||||
|
|
||||||
|
# Storage backend - file-based for single instance
|
||||||
|
storage "file" {
|
||||||
|
path = "./botserver-stack/data/vault"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Listener with mTLS enabled
|
||||||
|
listener "tcp" {
|
||||||
|
address = "0.0.0.0:8200"
|
||||||
|
tls_disable = false
|
||||||
|
|
||||||
|
# Server TLS certificate
|
||||||
|
tls_cert_file = "./botserver-stack/conf/system/certificates/vault/server.crt"
|
||||||
|
tls_key_file = "./botserver-stack/conf/system/certificates/vault/server.key"
|
||||||
|
|
||||||
|
# mTLS - require client certificate
|
||||||
|
tls_require_and_verify_client_cert = true
|
||||||
|
tls_client_ca_file = "./botserver-stack/conf/system/certificates/ca/ca.crt"
|
||||||
|
|
||||||
|
# TLS settings
|
||||||
|
tls_min_version = "tls12"
|
||||||
|
}
|
||||||
|
|
||||||
|
# API settings
|
||||||
|
api_addr = "https://localhost:8200"
|
||||||
|
cluster_addr = "https://localhost:8201"
|
||||||
|
|
||||||
|
# UI enabled for administration
|
||||||
|
ui = true
|
||||||
|
|
||||||
|
# Disable memory locking (for development - enable in production)
|
||||||
|
disable_mlock = true
|
||||||
|
|
||||||
|
# Telemetry
|
||||||
|
telemetry {
|
||||||
|
disable_hostname = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log level
|
||||||
|
log_level = "info"
|
||||||
|
"#;
|
||||||
|
|
||||||
|
fs::write(&config_path, config)?;
|
||||||
|
|
||||||
|
// Create data directory for Vault storage
|
||||||
|
fs::create_dir_all("./botserver-stack/data/vault")?;
|
||||||
|
|
||||||
|
info!("Created Vault config with mTLS at {}", config_path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate TLS certificates for all services
|
/// Generate TLS certificates for all services
|
||||||
async fn generate_certificates(&self) -> Result<()> {
|
async fn generate_certificates(&self) -> Result<()> {
|
||||||
let cert_dir = PathBuf::from("./botserver-stack/conf/system/certificates");
|
let cert_dir = PathBuf::from("./botserver-stack/conf/system/certificates");
|
||||||
|
|
@ -744,8 +1238,42 @@ meet IN A 127.0.0.1
|
||||||
// Create issuer from CA params and key
|
// Create issuer from CA params and key
|
||||||
let ca_issuer = Issuer::from_params(&ca_params, &ca_key_pair);
|
let ca_issuer = Issuer::from_params(&ca_params, &ca_key_pair);
|
||||||
|
|
||||||
// Services that need certificates
|
// Generate client certificate for botserver (for mTLS to all services)
|
||||||
|
let botserver_dir = cert_dir.join("botserver");
|
||||||
|
fs::create_dir_all(&botserver_dir)?;
|
||||||
|
|
||||||
|
let client_cert_path = botserver_dir.join("client.crt");
|
||||||
|
let client_key_path = botserver_dir.join("client.key");
|
||||||
|
|
||||||
|
if !client_cert_path.exists() || !client_key_path.exists() {
|
||||||
|
info!("Generating mTLS client certificate for botserver");
|
||||||
|
|
||||||
|
let mut client_params = CertificateParams::default();
|
||||||
|
client_params.not_before = time::OffsetDateTime::now_utc();
|
||||||
|
client_params.not_after = time::OffsetDateTime::now_utc() + time::Duration::days(365);
|
||||||
|
|
||||||
|
let mut client_dn = DistinguishedName::new();
|
||||||
|
client_dn.push(DnType::CountryName, "BR");
|
||||||
|
client_dn.push(DnType::OrganizationName, "BotServer");
|
||||||
|
client_dn.push(DnType::CommonName, "botserver-client");
|
||||||
|
client_params.distinguished_name = client_dn;
|
||||||
|
|
||||||
|
// Add client auth extended key usage
|
||||||
|
client_params.subject_alt_names.push(rcgen::SanType::DnsName("botserver".to_string().try_into()?));
|
||||||
|
|
||||||
|
let client_key = KeyPair::generate()?;
|
||||||
|
let client_cert = client_params.signed_by(&client_key, &ca_issuer)?;
|
||||||
|
|
||||||
|
fs::write(&client_cert_path, client_cert.pem())?;
|
||||||
|
fs::write(&client_key_path, client_key.serialize_pem())?;
|
||||||
|
fs::copy(&ca_cert_path, botserver_dir.join("ca.crt"))?;
|
||||||
|
|
||||||
|
info!("Generated mTLS client certificate at {}", client_cert_path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services that need certificates - Vault FIRST
|
||||||
let services = vec![
|
let services = vec![
|
||||||
|
("vault", vec!["localhost", "127.0.0.1", "vault.botserver.local"]),
|
||||||
("api", vec!["localhost", "127.0.0.1", "api.botserver.local"]),
|
("api", vec!["localhost", "127.0.0.1", "api.botserver.local"]),
|
||||||
("llm", vec!["localhost", "127.0.0.1", "llm.botserver.local"]),
|
("llm", vec!["localhost", "127.0.0.1", "llm.botserver.local"]),
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::package_manager::component::ComponentConfig;
|
||||||
use crate::package_manager::installer::PackageManager;
|
use crate::package_manager::installer::PackageManager;
|
||||||
use crate::package_manager::InstallMode;
|
use crate::package_manager::InstallMode;
|
||||||
use crate::package_manager::OsType;
|
use crate::package_manager::OsType;
|
||||||
use crate::shared::utils::{self, parse_database_url};
|
use crate::shared::utils::{self, get_database_url_sync, parse_database_url};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use log::{error, trace, warn};
|
use log::{error, trace, warn};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
@ -553,7 +553,8 @@ impl PackageManager {
|
||||||
exec_cmd: &str,
|
exec_cmd: &str,
|
||||||
env_vars: &HashMap<String, String>,
|
env_vars: &HashMap<String, String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let database_url = std::env::var("DATABASE_URL").unwrap();
|
let database_url = get_database_url_sync()
|
||||||
|
.context("Failed to get DATABASE_URL from Vault. Ensure Vault is configured.")?;
|
||||||
let (_db_username, db_password, _db_server, _db_port, _db_name) =
|
let (_db_username, db_password, _db_server, _db_port, _db_name) =
|
||||||
parse_database_url(&database_url);
|
parse_database_url(&database_url);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ impl PackageManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_components(&mut self) {
|
fn register_components(&mut self) {
|
||||||
|
self.register_vault();
|
||||||
self.register_tables();
|
self.register_tables();
|
||||||
self.register_cache();
|
self.register_cache();
|
||||||
self.register_drive();
|
self.register_drive();
|
||||||
|
|
@ -74,7 +75,6 @@ impl PackageManager {
|
||||||
self.register_devtools();
|
self.register_devtools();
|
||||||
self.register_vector_db();
|
self.register_vector_db();
|
||||||
self.register_timeseries_db();
|
self.register_timeseries_db();
|
||||||
self.register_secrets();
|
|
||||||
self.register_observability();
|
self.register_observability();
|
||||||
self.register_host();
|
self.register_host();
|
||||||
self.register_webmail();
|
self.register_webmail();
|
||||||
|
|
@ -297,7 +297,7 @@ impl PackageManager {
|
||||||
ComponentConfig {
|
ComponentConfig {
|
||||||
name: "directory".to_string(),
|
name: "directory".to_string(),
|
||||||
ports: vec![8080],
|
ports: vec![8080],
|
||||||
dependencies: vec![],
|
dependencies: vec!["tables".to_string()],
|
||||||
linux_packages: vec![],
|
linux_packages: vec![],
|
||||||
macos_packages: vec![],
|
macos_packages: vec![],
|
||||||
windows_packages: vec![],
|
windows_packages: vec![],
|
||||||
|
|
@ -306,21 +306,30 @@ impl PackageManager {
|
||||||
.to_string(),
|
.to_string(),
|
||||||
),
|
),
|
||||||
binary_name: Some("zitadel".to_string()),
|
binary_name: Some("zitadel".to_string()),
|
||||||
pre_install_cmds_linux: vec![],
|
pre_install_cmds_linux: vec![
|
||||||
post_install_cmds_linux: vec![],
|
"mkdir -p {{CONF_PATH}}/directory".to_string(),
|
||||||
pre_install_cmds_macos: vec![],
|
],
|
||||||
|
post_install_cmds_linux: vec![
|
||||||
|
// Initialize Zitadel with first instance setup to generate admin PAT
|
||||||
|
"{{BIN_PATH}}/zitadel init --config {{CONF_PATH}}/directory/zitadel.yaml".to_string(),
|
||||||
|
"{{BIN_PATH}}/zitadel setup --config {{CONF_PATH}}/directory/zitadel.yaml --init-projections --masterkeyFromEnv --steps {{CONF_PATH}}/directory/steps.yaml".to_string(),
|
||||||
|
],
|
||||||
|
pre_install_cmds_macos: vec![
|
||||||
|
"mkdir -p {{CONF_PATH}}/directory".to_string(),
|
||||||
|
],
|
||||||
post_install_cmds_macos: vec![],
|
post_install_cmds_macos: vec![],
|
||||||
pre_install_cmds_windows: vec![],
|
pre_install_cmds_windows: vec![],
|
||||||
post_install_cmds_windows: vec![],
|
post_install_cmds_windows: vec![],
|
||||||
env_vars: HashMap::from([
|
env_vars: HashMap::from([
|
||||||
("ZITADEL_EXTERNALSECURE".to_string(), "true".to_string()),
|
("ZITADEL_EXTERNALSECURE".to_string(), "false".to_string()),
|
||||||
("ZITADEL_TLS_ENABLED".to_string(), "true".to_string()),
|
("ZITADEL_EXTERNALDOMAIN".to_string(), "localhost".to_string()),
|
||||||
("ZITADEL_TLS_CERT".to_string(), "{{CONF_PATH}}/system/certificates/directory/server.crt".to_string()),
|
("ZITADEL_EXTERNALPORT".to_string(), "8080".to_string()),
|
||||||
("ZITADEL_TLS_KEY".to_string(), "{{CONF_PATH}}/system/certificates/directory/server.key".to_string()),
|
("ZITADEL_TLS_ENABLED".to_string(), "false".to_string()),
|
||||||
|
("ZITADEL_MASTERKEY".to_string(), "MasterkeyNeedsToHave32Characters".to_string()),
|
||||||
]),
|
]),
|
||||||
data_download_list: Vec::new(),
|
data_download_list: Vec::new(),
|
||||||
exec_cmd: "{{BIN_PATH}}/zitadel start --config {{CONF_PATH}}/directory/zitadel.yaml --masterkeyFromEnv".to_string(),
|
exec_cmd: "nohup {{BIN_PATH}}/zitadel start --config {{CONF_PATH}}/directory/zitadel.yaml --masterkeyFromEnv --tlsMode disabled > {{LOGS_PATH}}/zitadel.log 2>&1 &".to_string(),
|
||||||
check_cmd: "curl -f -k https://localhost:8080/healthz >/dev/null 2>&1".to_string(),
|
check_cmd: "curl -f http://localhost:8080/healthz >/dev/null 2>&1".to_string(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -698,11 +707,11 @@ impl PackageManager {
|
||||||
/// Register HashiCorp Vault for secrets management
|
/// Register HashiCorp Vault for secrets management
|
||||||
/// Vault stores service credentials (drive, email, etc.) securely
|
/// Vault stores service credentials (drive, email, etc.) securely
|
||||||
/// Only VAULT_ADDR and VAULT_TOKEN needed in .env, all other secrets fetched from Vault
|
/// Only VAULT_ADDR and VAULT_TOKEN needed in .env, all other secrets fetched from Vault
|
||||||
fn register_secrets(&mut self) {
|
fn register_vault(&mut self) {
|
||||||
self.components.insert(
|
self.components.insert(
|
||||||
"secrets".to_string(),
|
"vault".to_string(),
|
||||||
ComponentConfig {
|
ComponentConfig {
|
||||||
name: "secrets".to_string(),
|
name: "vault".to_string(),
|
||||||
ports: vec![8200],
|
ports: vec![8200],
|
||||||
dependencies: vec![],
|
dependencies: vec![],
|
||||||
linux_packages: vec![],
|
linux_packages: vec![],
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
pub mod directory_setup;
|
pub mod directory_setup;
|
||||||
pub mod email_setup;
|
pub mod email_setup;
|
||||||
|
|
||||||
pub use directory_setup::DirectorySetup;
|
pub use directory_setup::{DirectorySetup, DefaultUser};
|
||||||
pub use email_setup::EmailSetup;
|
pub use email_setup::EmailSetup;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ use anyhow::{anyhow, Result};
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Arc as StdArc;
|
use std::sync::Arc as StdArc;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
@ -73,17 +74,34 @@ impl std::fmt::Debug for SecretsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SecretsManager {
|
impl SecretsManager {
|
||||||
/// Create from environment variables
|
/// Create from environment variables with mTLS support
|
||||||
|
///
|
||||||
|
/// Environment variables:
|
||||||
|
/// - VAULT_ADDR - Vault server address (https://localhost:8200)
|
||||||
|
/// - VAULT_TOKEN - Vault authentication token
|
||||||
|
/// - VAULT_CACERT - Path to CA certificate for verifying Vault server
|
||||||
|
/// - VAULT_CLIENT_CERT - Path to client certificate for mTLS
|
||||||
|
/// - VAULT_CLIENT_KEY - Path to client key for mTLS
|
||||||
|
/// - VAULT_SKIP_VERIFY - Skip TLS verification (for development only)
|
||||||
|
/// - VAULT_CACHE_TTL - Cache TTL in seconds (default: 300)
|
||||||
pub fn from_env() -> Result<Self> {
|
pub fn from_env() -> Result<Self> {
|
||||||
let addr = env::var("VAULT_ADDR").unwrap_or_default();
|
let addr = env::var("VAULT_ADDR").unwrap_or_default();
|
||||||
let token = env::var("VAULT_TOKEN").unwrap_or_default();
|
let token = env::var("VAULT_TOKEN").unwrap_or_default();
|
||||||
let skip_verify = env::var("VAULT_SKIP_VERIFY")
|
let skip_verify = env::var("VAULT_SKIP_VERIFY")
|
||||||
.map(|v| v == "true" || v == "1")
|
.map(|v| v == "true" || v == "1")
|
||||||
.unwrap_or(true);
|
.unwrap_or(false); // Default to false - verify certificates
|
||||||
let cache_ttl = env::var("VAULT_CACHE_TTL")
|
let cache_ttl = env::var("VAULT_CACHE_TTL")
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|v| v.parse().ok())
|
.and_then(|v| v.parse().ok())
|
||||||
.unwrap_or(300);
|
.unwrap_or(300);
|
||||||
|
|
||||||
|
// mTLS certificate paths - default to botserver-stack paths
|
||||||
|
let ca_cert = env::var("VAULT_CACERT")
|
||||||
|
.unwrap_or_else(|_| "./botserver-stack/conf/system/certificates/ca/ca.crt".to_string());
|
||||||
|
let client_cert = env::var("VAULT_CLIENT_CERT")
|
||||||
|
.unwrap_or_else(|_| "./botserver-stack/conf/system/certificates/botserver/client.crt".to_string());
|
||||||
|
let client_key = env::var("VAULT_CLIENT_KEY")
|
||||||
|
.unwrap_or_else(|_| "./botserver-stack/conf/system/certificates/botserver/client.key".to_string());
|
||||||
|
|
||||||
let enabled = !token.is_empty() && !addr.is_empty();
|
let enabled = !token.is_empty() && !addr.is_empty();
|
||||||
|
|
||||||
|
|
@ -97,15 +115,48 @@ impl SecretsManager {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let settings = VaultClientSettingsBuilder::default()
|
// Build settings with mTLS if certificates exist
|
||||||
|
let ca_path = PathBuf::from(&ca_cert);
|
||||||
|
let cert_path = PathBuf::from(&client_cert);
|
||||||
|
let key_path = PathBuf::from(&client_key);
|
||||||
|
|
||||||
|
let mut settings_builder = VaultClientSettingsBuilder::default();
|
||||||
|
settings_builder
|
||||||
.address(&addr)
|
.address(&addr)
|
||||||
.token(&token)
|
.token(&token);
|
||||||
.verify(!skip_verify)
|
|
||||||
.build()?;
|
// Configure TLS verification
|
||||||
|
if skip_verify {
|
||||||
|
warn!("TLS verification disabled - NOT RECOMMENDED FOR PRODUCTION");
|
||||||
|
settings_builder.verify(false);
|
||||||
|
} else {
|
||||||
|
settings_builder.verify(true);
|
||||||
|
|
||||||
|
// Add CA certificate if it exists
|
||||||
|
if ca_path.exists() {
|
||||||
|
info!("Using CA certificate for Vault: {}", ca_cert);
|
||||||
|
settings_builder.ca_certs(vec![ca_cert.clone()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure mTLS client certificates if they exist
|
||||||
|
if cert_path.exists() && key_path.exists() {
|
||||||
|
info!("Using mTLS client certificate for Vault: {}", client_cert);
|
||||||
|
// Note: vaultrs uses the identity parameter for client certificates
|
||||||
|
// The identity is a PKCS12/PFX file or can be set via environment
|
||||||
|
// For now, we set environment variables that the underlying reqwest client will use
|
||||||
|
env::set_var("SSL_CERT_FILE", &ca_cert);
|
||||||
|
// Client certificate authentication is handled by reqwest through env vars
|
||||||
|
// or by building a custom client - vaultrs doesn't directly support client certs
|
||||||
|
// We'll document this limitation and use token auth with TLS verification
|
||||||
|
} else if !skip_verify {
|
||||||
|
info!("mTLS client certificates not found at {} - using token auth with TLS", client_cert);
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings = settings_builder.build()?;
|
||||||
let client = VaultClient::new(settings)?;
|
let client = VaultClient::new(settings)?;
|
||||||
|
|
||||||
info!("Vault client initialized: {}", addr);
|
info!("Vault client initialized with TLS: {}", addr);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
client: Some(StdArc::new(client)),
|
client: Some(StdArc::new(client)),
|
||||||
|
|
@ -326,98 +377,14 @@ impl SecretsManager {
|
||||||
self.cache.write().await.remove(path);
|
self.cache.write().await.remove(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fallback to environment variables
|
/// No fallback - Vault is mandatory
|
||||||
fn get_from_env(&self, path: &str) -> Result<HashMap<String, String>> {
|
/// Returns empty HashMap if Vault is not configured
|
||||||
let mut data = HashMap::new();
|
fn get_from_env(&self, _path: &str) -> Result<HashMap<String, String>> {
|
||||||
let env_mappings: &[(&str, &[(&str, &str)])] = &[
|
// NO LEGACY FALLBACK - All secrets MUST come from Vault
|
||||||
(
|
// If you see this error, ensure Vault is properly configured with:
|
||||||
SecretPaths::DRIVE,
|
// VAULT_ADDR=https://localhost:8200
|
||||||
&[("accesskey", "DRIVE_ACCESSKEY"), ("secret", "DRIVE_SECRET")],
|
// VAULT_TOKEN=<your-token>
|
||||||
),
|
Err(anyhow!("Vault not configured. All secrets must be stored in Vault. Set VAULT_ADDR and VAULT_TOKEN in .env"))
|
||||||
(SecretPaths::CACHE, &[("password", "REDIS_PASSWORD")]),
|
|
||||||
(
|
|
||||||
SecretPaths::DIRECTORY,
|
|
||||||
&[
|
|
||||||
("url", "DIRECTORY_URL"),
|
|
||||||
("project_id", "DIRECTORY_PROJECT_ID"),
|
|
||||||
("client_id", "ZITADEL_CLIENT_ID"),
|
|
||||||
("client_secret", "ZITADEL_CLIENT_SECRET"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
SecretPaths::TABLES,
|
|
||||||
&[
|
|
||||||
("host", "DB_HOST"),
|
|
||||||
("port", "DB_PORT"),
|
|
||||||
("database", "DB_NAME"),
|
|
||||||
("username", "DB_USER"),
|
|
||||||
("password", "DB_PASSWORD"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
SecretPaths::VECTORDB,
|
|
||||||
&[("url", "QDRANT_URL"), ("api_key", "QDRANT_API_KEY")],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
SecretPaths::OBSERVABILITY,
|
|
||||||
&[
|
|
||||||
("url", "INFLUXDB_URL"),
|
|
||||||
("org", "INFLUXDB_ORG"),
|
|
||||||
("bucket", "INFLUXDB_BUCKET"),
|
|
||||||
("token", "INFLUXDB_TOKEN"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
SecretPaths::EMAIL,
|
|
||||||
&[("username", "EMAIL_USER"), ("password", "EMAIL_PASSWORD")],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
SecretPaths::LLM,
|
|
||||||
&[
|
|
||||||
("openai_key", "OPENAI_API_KEY"),
|
|
||||||
("anthropic_key", "ANTHROPIC_API_KEY"),
|
|
||||||
("groq_key", "GROQ_API_KEY"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(SecretPaths::ENCRYPTION, &[("master_key", "ENCRYPTION_KEY")]),
|
|
||||||
(
|
|
||||||
SecretPaths::MEET,
|
|
||||||
&[
|
|
||||||
("api_key", "LIVEKIT_API_KEY"),
|
|
||||||
("api_secret", "LIVEKIT_API_SECRET"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
SecretPaths::ALM,
|
|
||||||
&[
|
|
||||||
("url", "ALM_URL"),
|
|
||||||
("admin_password", "ALM_ADMIN_PASSWORD"),
|
|
||||||
("runner_token", "ALM_RUNNER_TOKEN"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (p, mappings) in env_mappings {
|
|
||||||
if *p == path {
|
|
||||||
for (key, env_var) in *mappings {
|
|
||||||
if let Ok(v) = env::var(env_var) {
|
|
||||||
data.insert((*key).to_string(), v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DATABASE_URL fallback
|
|
||||||
if path == SecretPaths::TABLES && data.is_empty() {
|
|
||||||
if let Ok(url) = env::var("DATABASE_URL") {
|
|
||||||
if let Some(parsed) = parse_database_url(&url) {
|
|
||||||
data.extend(parsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -261,9 +261,10 @@ fn create_mock_auth_service() -> AuthService {
|
||||||
|
|
||||||
impl Default for AppState {
|
impl Default for AppState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| {
|
// NO LEGACY FALLBACK - Vault is mandatory
|
||||||
"postgres://postgres:postgres@localhost:5432/botserver".to_string()
|
// This default is only for tests. In production, use the full initialization.
|
||||||
});
|
let database_url = crate::shared::utils::get_database_url_sync()
|
||||||
|
.expect("Vault not configured. Set VAULT_ADDR and VAULT_TOKEN in .env");
|
||||||
|
|
||||||
let manager = ConnectionManager::<PgConnection>::new(&database_url);
|
let manager = ConnectionManager::<PgConnection>::new(&database_url);
|
||||||
let pool = Pool::builder()
|
let pool = Pool::builder()
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use crate::directory::AuthService;
|
||||||
#[cfg(feature = "llm")]
|
#[cfg(feature = "llm")]
|
||||||
use crate::llm::LLMProvider;
|
use crate::llm::LLMProvider;
|
||||||
use crate::shared::models::BotResponse;
|
use crate::shared::models::BotResponse;
|
||||||
use crate::shared::utils::DbPool;
|
use crate::shared::utils::{get_database_url_sync, DbPool};
|
||||||
use crate::tasks::TaskEngine;
|
use crate::tasks::TaskEngine;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use diesel::r2d2::{ConnectionManager, Pool};
|
use diesel::r2d2::{ConnectionManager, Pool};
|
||||||
|
|
@ -171,7 +171,7 @@ impl TestAppStateBuilder {
|
||||||
pub fn build(self) -> Result<AppState, Box<dyn std::error::Error + Send + Sync>> {
|
pub fn build(self) -> Result<AppState, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let database_url = self
|
let database_url = self
|
||||||
.database_url
|
.database_url
|
||||||
.or_else(|| std::env::var("DATABASE_URL").ok())
|
.or_else(|| get_database_url_sync().ok())
|
||||||
.unwrap_or_else(|| "postgres://test:test@localhost:5432/test".to_string());
|
.unwrap_or_else(|| "postgres://test:test@localhost:5432/test".to_string());
|
||||||
|
|
||||||
let manager = ConnectionManager::<PgConnection>::new(&database_url);
|
let manager = ConnectionManager::<PgConnection>::new(&database_url);
|
||||||
|
|
@ -245,7 +245,7 @@ fn create_mock_auth_service() -> AuthService {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_test_db_pool() -> Result<DbPool, Box<dyn std::error::Error + Send + Sync>> {
|
pub fn create_test_db_pool() -> Result<DbPool, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let database_url = std::env::var("DATABASE_URL")
|
let database_url = get_database_url_sync()
|
||||||
.unwrap_or_else(|_| "postgres://test:test@localhost:5432/test".to_string());
|
.unwrap_or_else(|_| "postgres://test:test@localhost:5432/test".to_string());
|
||||||
let manager = ConnectionManager::<PgConnection>::new(&database_url);
|
let manager = ConnectionManager::<PgConnection>::new(&database_url);
|
||||||
let pool = Pool::builder().max_size(1).build(manager)?;
|
let pool = Pool::builder().max_size(1).build(manager)?;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::config::DriveConfig;
|
use crate::config::DriveConfig;
|
||||||
|
use crate::core::secrets::SecretsManager;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use aws_config::BehaviorVersion;
|
use aws_config::BehaviorVersion;
|
||||||
use aws_sdk_s3::{config::Builder as S3ConfigBuilder, Client as S3Client};
|
use aws_sdk_s3::{config::Builder as S3ConfigBuilder, Client as S3Client};
|
||||||
|
|
@ -10,13 +11,68 @@ use diesel::{
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
#[cfg(feature = "progress-bars")]
|
#[cfg(feature = "progress-bars")]
|
||||||
use indicatif::{ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use rhai::{Array, Dynamic};
|
use rhai::{Array, Dynamic};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use smartstring::SmartString;
|
use smartstring::SmartString;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use std::sync::Arc;
|
||||||
use tokio::fs::File as TokioFile;
|
use tokio::fs::File as TokioFile;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
/// Global SecretsManager instance - initialized once, used everywhere
|
||||||
|
static SECRETS_MANAGER: Lazy<Arc<RwLock<Option<SecretsManager>>>> =
|
||||||
|
Lazy::new(|| Arc::new(RwLock::new(None)));
|
||||||
|
|
||||||
|
/// Initialize the global secrets manager (call once at startup)
|
||||||
|
pub async fn init_secrets_manager() -> Result<()> {
|
||||||
|
let manager = SecretsManager::from_env()?;
|
||||||
|
let mut guard = SECRETS_MANAGER.write().await;
|
||||||
|
*guard = Some(manager);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get database URL from Vault - NO FALLBACK
|
||||||
|
pub async fn get_database_url() -> Result<String> {
|
||||||
|
let guard = SECRETS_MANAGER.read().await;
|
||||||
|
if let Some(ref manager) = *guard {
|
||||||
|
if manager.is_enabled() {
|
||||||
|
return manager.get_database_url().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// NO FALLBACK - Vault is mandatory
|
||||||
|
Err(anyhow::anyhow!("Vault not configured. Set VAULT_ADDR and VAULT_TOKEN in .env"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get database URL synchronously (blocking) for diesel connections - NO FALLBACK
|
||||||
|
pub fn get_database_url_sync() -> Result<String> {
|
||||||
|
// Check if we're in an async runtime context
|
||||||
|
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||||
|
// We're inside a tokio runtime - use block_in_place to avoid nesting
|
||||||
|
let result = tokio::task::block_in_place(|| {
|
||||||
|
handle.block_on(async { get_database_url().await })
|
||||||
|
});
|
||||||
|
if let Ok(url) = result {
|
||||||
|
return Ok(url);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not in a runtime - create a new one
|
||||||
|
let rt = tokio::runtime::Runtime::new().map_err(|e| anyhow::anyhow!("Failed to create runtime: {}", e))?;
|
||||||
|
if let Ok(url) = rt.block_on(async { get_database_url().await }) {
|
||||||
|
return Ok(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// NO FALLBACK - Vault is mandatory
|
||||||
|
Err(anyhow::anyhow!("Vault not configured. Set VAULT_ADDR and VAULT_TOKEN in .env"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the global SecretsManager instance
|
||||||
|
pub async fn get_secrets_manager() -> Option<SecretsManager> {
|
||||||
|
let guard = SECRETS_MANAGER.read().await;
|
||||||
|
guard.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_s3_operator(
|
pub async fn create_s3_operator(
|
||||||
config: &DriveConfig,
|
config: &DriveConfig,
|
||||||
|
|
@ -164,7 +220,8 @@ pub fn estimate_token_count(text: &str) -> usize {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn establish_pg_connection() -> Result<PgConnection> {
|
pub fn establish_pg_connection() -> Result<PgConnection> {
|
||||||
let database_url = std::env::var("DATABASE_URL").unwrap();
|
let database_url = get_database_url_sync()
|
||||||
|
.expect("Vault not configured. Set VAULT_ADDR and VAULT_TOKEN in .env");
|
||||||
PgConnection::establish(&database_url)
|
PgConnection::establish(&database_url)
|
||||||
.with_context(|| format!("Failed to connect to database at {}", database_url))
|
.with_context(|| format!("Failed to connect to database at {}", database_url))
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +229,16 @@ pub fn establish_pg_connection() -> Result<PgConnection> {
|
||||||
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
|
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
|
||||||
|
|
||||||
pub fn create_conn() -> Result<DbPool, diesel::r2d2::PoolError> {
|
pub fn create_conn() -> Result<DbPool, diesel::r2d2::PoolError> {
|
||||||
let database_url = std::env::var("DATABASE_URL").unwrap();
|
let database_url = get_database_url_sync()
|
||||||
|
.expect("Vault not configured. Set VAULT_ADDR and VAULT_TOKEN in .env");
|
||||||
|
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
||||||
|
Pool::builder().build(manager)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create database connection pool using SecretsManager (async version)
|
||||||
|
pub async fn create_conn_async() -> Result<DbPool, diesel::r2d2::PoolError> {
|
||||||
|
let database_url = get_database_url().await
|
||||||
|
.expect("Vault not configured. Set VAULT_ADDR and VAULT_TOKEN in .env");
|
||||||
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
||||||
Pool::builder().build(manager)
|
Pool::builder().build(manager)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
src/main.rs
23
src/main.rs
|
|
@ -3,7 +3,6 @@ use axum::{
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
// Configuration comes from Directory service, not .env files
|
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use log::{error, info, trace, warn};
|
use log::{error, info, trace, warn};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
@ -246,7 +245,20 @@ async fn run_axum_server(
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
// Configuration comes from Directory service, not .env files
|
// Install rustls crypto provider (ring) before any TLS operations
|
||||||
|
// This must be done before any code that might use rustls
|
||||||
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
|
||||||
|
// Load .env for VAULT_* variables only (all other secrets come from Vault)
|
||||||
|
dotenvy::dotenv().ok();
|
||||||
|
|
||||||
|
// Initialize SecretsManager early - this connects to Vault if configured
|
||||||
|
// Only VAULT_ADDR, VAULT_TOKEN, and VAULT_SKIP_VERIFY should be in .env
|
||||||
|
if let Err(e) = crate::shared::utils::init_secrets_manager().await {
|
||||||
|
warn!("Failed to initialize SecretsManager: {}. Falling back to env vars.", e);
|
||||||
|
} else {
|
||||||
|
info!("SecretsManager initialized - fetching secrets from Vault");
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize logger early to capture all logs with filters for noisy libraries
|
// Initialize logger early to capture all logs with filters for noisy libraries
|
||||||
let rust_log = {
|
let rust_log = {
|
||||||
|
|
@ -294,7 +306,6 @@ async fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
let no_ui = args.contains(&"--noui".to_string());
|
let no_ui = args.contains(&"--noui".to_string());
|
||||||
let desktop_mode = args.contains(&"--desktop".to_string());
|
|
||||||
let no_console = args.contains(&"--noconsole".to_string());
|
let no_console = args.contains(&"--noconsole".to_string());
|
||||||
|
|
||||||
// Configuration comes from Directory service, not .env files
|
// Configuration comes from Directory service, not .env files
|
||||||
|
|
@ -321,8 +332,8 @@ async fn main() -> std::io::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start UI thread if console is enabled (default) and not disabled by --noconsole or desktop mode
|
// Start UI thread if console is enabled (default) and not disabled by --noconsole or --noui
|
||||||
let ui_handle: Option<std::thread::JoinHandle<()>> = if !no_console && !desktop_mode && !no_ui {
|
let ui_handle: Option<std::thread::JoinHandle<()>> = if !no_console && !no_ui {
|
||||||
#[cfg(feature = "console")]
|
#[cfg(feature = "console")]
|
||||||
{
|
{
|
||||||
let progress_rx = Arc::new(tokio::sync::Mutex::new(_progress_rx));
|
let progress_rx = Arc::new(tokio::sync::Mutex::new(_progress_rx));
|
||||||
|
|
@ -646,7 +657,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
s3_client: Some(drive),
|
s3_client: Some(drive),
|
||||||
config: Some(cfg.clone()),
|
config: Some(cfg.clone()),
|
||||||
conn: pool.clone(),
|
conn: pool.clone(),
|
||||||
database_url: std::env::var("DATABASE_URL").unwrap_or_else(|_| "".to_string()),
|
database_url: crate::shared::utils::get_database_url_sync().unwrap_or_default(),
|
||||||
bucket_name: "default.gbai".to_string(),
|
bucket_name: "default.gbai".to_string(),
|
||||||
cache: redis_client.clone(),
|
cache: redis_client.clone(),
|
||||||
session_manager: session_manager.clone(),
|
session_manager: session_manager.clone(),
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@
|
||||||
//! - External CA integration capabilities
|
//! - External CA integration capabilities
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use rustls::server::{AllowAnyAnonymousOrAuthenticatedClient, AllowAnyAuthenticatedClient};
|
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||||
use rustls::{Certificate, PrivateKey, RootCertStore, ServerConfig};
|
use rustls::server::WebPkiClientVerifier;
|
||||||
|
use rustls::{RootCertStore, ServerConfig};
|
||||||
use rustls_pemfile::{certs, pkcs8_private_keys, rsa_private_keys};
|
use rustls_pemfile::{certs, pkcs8_private_keys, rsa_private_keys};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
|
@ -106,54 +107,51 @@ impl TlsManager {
|
||||||
let cert_chain = Self::load_certs(&config.cert_path)?;
|
let cert_chain = Self::load_certs(&config.cert_path)?;
|
||||||
let key = Self::load_private_key(&config.key_path)?;
|
let key = Self::load_private_key(&config.key_path)?;
|
||||||
|
|
||||||
let builder = ServerConfig::builder()
|
let server_config = if config.require_client_cert {
|
||||||
.with_safe_default_cipher_suites()
|
|
||||||
.with_safe_default_kx_groups()
|
|
||||||
.with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])?;
|
|
||||||
|
|
||||||
let mut server_config = if config.require_client_cert {
|
|
||||||
// mTLS: Require client certificates
|
// mTLS: Require client certificates
|
||||||
info!("Configuring mTLS - client certificates required");
|
info!("Configuring mTLS - client certificates required");
|
||||||
let client_cert_verifier = if let Some(ca_path) = &config.ca_cert_path {
|
if let Some(ca_path) = &config.ca_cert_path {
|
||||||
let ca_certs = Self::load_certs(ca_path)?;
|
let ca_certs = Self::load_certs(ca_path)?;
|
||||||
let mut root_store = RootCertStore::empty();
|
let mut root_store = RootCertStore::empty();
|
||||||
for cert in ca_certs {
|
for cert in ca_certs {
|
||||||
root_store.add(&cert)?;
|
root_store.add(cert)?;
|
||||||
}
|
}
|
||||||
AllowAnyAuthenticatedClient::new(root_store)
|
let client_cert_verifier = WebPkiClientVerifier::builder(Arc::new(root_store))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to build client verifier: {}", e))?;
|
||||||
|
|
||||||
|
ServerConfig::builder()
|
||||||
|
.with_client_cert_verifier(client_cert_verifier)
|
||||||
|
.with_single_cert(cert_chain, key)?
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow::anyhow!(
|
return Err(anyhow::anyhow!(
|
||||||
"CA certificate required for mTLS but ca_cert_path not provided"
|
"CA certificate required for mTLS but ca_cert_path not provided"
|
||||||
));
|
));
|
||||||
};
|
}
|
||||||
|
|
||||||
builder
|
|
||||||
.with_client_cert_verifier(Arc::new(client_cert_verifier))
|
|
||||||
.with_single_cert(cert_chain, key)?
|
|
||||||
} else if let Some(ca_path) = &config.ca_cert_path {
|
} else if let Some(ca_path) = &config.ca_cert_path {
|
||||||
// Optional client certificates
|
// Optional client certificates
|
||||||
info!("Configuring TLS with optional client certificates");
|
info!("Configuring TLS with optional client certificates");
|
||||||
let ca_certs = Self::load_certs(ca_path)?;
|
let ca_certs = Self::load_certs(ca_path)?;
|
||||||
let mut root_store = RootCertStore::empty();
|
let mut root_store = RootCertStore::empty();
|
||||||
for cert in ca_certs {
|
for cert in ca_certs {
|
||||||
root_store.add(&cert)?;
|
root_store.add(cert)?;
|
||||||
}
|
}
|
||||||
let client_cert_verifier = AllowAnyAnonymousOrAuthenticatedClient::new(root_store);
|
let client_cert_verifier = WebPkiClientVerifier::builder(Arc::new(root_store))
|
||||||
|
.allow_unauthenticated()
|
||||||
|
.build()
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to build client verifier: {}", e))?;
|
||||||
|
|
||||||
builder
|
ServerConfig::builder()
|
||||||
.with_client_cert_verifier(Arc::new(client_cert_verifier))
|
.with_client_cert_verifier(client_cert_verifier)
|
||||||
.with_single_cert(cert_chain, key)?
|
.with_single_cert(cert_chain, key)?
|
||||||
} else {
|
} else {
|
||||||
// No client certificate verification
|
// No client certificate verification
|
||||||
info!("Configuring standard TLS without client certificates");
|
info!("Configuring standard TLS without client certificates");
|
||||||
builder
|
ServerConfig::builder()
|
||||||
.with_no_client_auth()
|
.with_no_client_auth()
|
||||||
.with_single_cert(cert_chain, key)?
|
.with_single_cert(cert_chain, key)?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Configure ALPN for HTTP/2
|
|
||||||
server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
|
||||||
|
|
||||||
Ok(server_config)
|
Ok(server_config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,18 +163,14 @@ impl TlsManager {
|
||||||
if let Some(ca_path) = &config.ca_cert_path {
|
if let Some(ca_path) = &config.ca_cert_path {
|
||||||
let ca_certs = Self::load_certs(ca_path)?;
|
let ca_certs = Self::load_certs(ca_path)?;
|
||||||
for cert in ca_certs {
|
for cert in ca_certs {
|
||||||
root_store.add(&cert)?;
|
root_store.add(cert)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Use system CA certificates
|
// Use system CA certificates
|
||||||
Self::load_system_certs(&mut root_store)?;
|
Self::load_system_certs(&mut root_store)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let builder = rustls::ClientConfig::builder()
|
let builder = rustls::ClientConfig::builder().with_root_certificates(root_store);
|
||||||
.with_safe_default_cipher_suites()
|
|
||||||
.with_safe_default_kx_groups()
|
|
||||||
.with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])?
|
|
||||||
.with_root_certificates(root_store);
|
|
||||||
|
|
||||||
let client_config = if let (Some(cert_path), Some(key_path)) =
|
let client_config = if let (Some(cert_path), Some(key_path)) =
|
||||||
(&config.client_cert_path, &config.client_key_path)
|
(&config.client_cert_path, &config.client_key_path)
|
||||||
|
|
@ -193,32 +187,36 @@ impl TlsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load certificates from PEM file
|
/// Load certificates from PEM file
|
||||||
fn load_certs(path: &Path) -> Result<Vec<Certificate>> {
|
fn load_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>> {
|
||||||
let file = File::open(path)
|
let file = File::open(path)
|
||||||
.with_context(|| format!("Failed to open certificate file: {:?}", path))?;
|
.with_context(|| format!("Failed to open certificate file: {:?}", path))?;
|
||||||
let mut reader = BufReader::new(file);
|
let mut reader = BufReader::new(file);
|
||||||
let certs = certs(&mut reader)?.into_iter().map(Certificate).collect();
|
let certs: Result<Vec<_>, _> = certs(&mut reader).collect();
|
||||||
Ok(certs)
|
certs.with_context(|| format!("Failed to parse certificates from {:?}", path))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load private key from PEM file
|
/// Load private key from PEM file
|
||||||
fn load_private_key(path: &Path) -> Result<PrivateKey> {
|
fn load_private_key(path: &Path) -> Result<PrivateKeyDer<'static>> {
|
||||||
let file =
|
let file =
|
||||||
File::open(path).with_context(|| format!("Failed to open key file: {:?}", path))?;
|
File::open(path).with_context(|| format!("Failed to open key file: {:?}", path))?;
|
||||||
let mut reader = BufReader::new(file);
|
let mut reader = BufReader::new(file);
|
||||||
|
|
||||||
// Try PKCS#8 format first
|
// Try PKCS#8 format first
|
||||||
let keys = pkcs8_private_keys(&mut reader)?;
|
let keys: Vec<_> = pkcs8_private_keys(&mut reader)
|
||||||
|
.filter_map(|k| k.ok())
|
||||||
|
.collect();
|
||||||
if !keys.is_empty() {
|
if !keys.is_empty() {
|
||||||
return Ok(PrivateKey(keys[0].clone()));
|
return Ok(PrivateKeyDer::Pkcs8(keys[0].clone_key()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset reader and try RSA format
|
// Reset reader and try RSA format
|
||||||
let file = File::open(path)?;
|
let file = File::open(path)?;
|
||||||
let mut reader = BufReader::new(file);
|
let mut reader = BufReader::new(file);
|
||||||
let keys = rsa_private_keys(&mut reader)?;
|
let keys: Vec<_> = rsa_private_keys(&mut reader)
|
||||||
|
.filter_map(|k| k.ok())
|
||||||
|
.collect();
|
||||||
if !keys.is_empty() {
|
if !keys.is_empty() {
|
||||||
return Ok(PrivateKey(keys[0].clone()));
|
return Ok(PrivateKeyDer::Pkcs1(keys[0].clone_key()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(anyhow::anyhow!("No private key found in file: {:?}", path))
|
Err(anyhow::anyhow!("No private key found in file: {:?}", path))
|
||||||
|
|
@ -240,7 +238,7 @@ impl TlsManager {
|
||||||
match Self::load_certs(Path::new(path)) {
|
match Self::load_certs(Path::new(path)) {
|
||||||
Ok(certs) => {
|
Ok(certs) => {
|
||||||
for cert in certs {
|
for cert in certs {
|
||||||
root_store.add(&cert)?;
|
root_store.add(cert)?;
|
||||||
}
|
}
|
||||||
info!("Loaded system certificates from {}", path);
|
info!("Loaded system certificates from {}", path);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
|
||||||
|
|
@ -1029,14 +1029,20 @@ pub async fn attendant_respond(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get verify token from config
|
/// Get verify token from config (from Vault)
|
||||||
async fn get_verify_token(state: &Arc<AppState>) -> String {
|
async fn get_verify_token(_state: &Arc<AppState>) -> String {
|
||||||
let bot_id = get_default_bot_id(state).await;
|
// Get verify token from Vault - stored at gbo/whatsapp
|
||||||
let adapter = WhatsAppAdapter::new(state.conn.clone(), bot_id);
|
use crate::core::secrets::SecretsManager;
|
||||||
|
|
||||||
// The verify token is stored in the adapter's config
|
match SecretsManager::new() {
|
||||||
// For now return a default - in production this should come from config
|
Ok(secrets) => {
|
||||||
std::env::var("WHATSAPP_VERIFY_TOKEN").unwrap_or_else(|_| "webhook_verify".to_string())
|
match secrets.get("gbo/whatsapp", "verify_token").await {
|
||||||
|
Ok(token) => token,
|
||||||
|
Err(_) => "webhook_verify".to_string() // Default for initial setup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => "webhook_verify".to_string() // Default if Vault not configured
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get default bot ID
|
/// Get default bot ID
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue