diff --git a/.gitignore b/.gitignore index 986ffff..d0dc8cb 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,10 @@ logs/ data_sets vault/agent-out +# RSA Private Keys - DO NOT COMMIT +vault/keys/rsa_private_key.pem +vault/keys/*.pem.old + # Snyk Security Extension - AI Rules (auto-generated) .github/instructions/snyk_rules.instructions.md # Dynamically created Ruuter health endpoint for tests diff --git a/DSL/CronManager/script/delete_secrets_from_vault.sh b/DSL/CronManager/script/delete_secrets_from_vault.sh index 86692e3..056a634 100644 --- a/DSL/CronManager/script/delete_secrets_from_vault.sh +++ b/DSL/CronManager/script/delete_secrets_from_vault.sh @@ -6,8 +6,9 @@ set -e # Exit on any error # Configuration -VAULT_ADDR="${VAULT_ADDR:-http://vault:8200}" -VAULT_TOKEN_FILE="/agent/out/token" +# Use VAULT_AGENT_URL which points to vault-agent-cron proxy +# The agent automatically injects the authentication token +VAULT_ADDR="${VAULT_AGENT_URL:-http://vault-agent-cron:8203}" # Logging function log() { @@ -24,20 +25,9 @@ log " llmModel: $llmModel" log " embeddingModel: $embeddingModel" log " embeddingPlatform: $embeddingPlatform" log " deploymentEnvironment: $deploymentEnvironment" +log " Vault Address: $VAULT_ADDR" -# Read vault token -if [ ! -f "$VAULT_TOKEN_FILE" ]; then - log "ERROR: Vault token file not found at $VAULT_TOKEN_FILE" - exit 1 -fi - -VAULT_TOKEN=$(cat "$VAULT_TOKEN_FILE") -if [ -z "$VAULT_TOKEN" ]; then - log "ERROR: Vault token is empty" - exit 1 -fi - -log "Vault token loaded successfully" +# Note: No token required - vault agent proxy automatically injects authentication # Function to determine platform name get_platform_name() { @@ -65,7 +55,7 @@ build_vault_path() { local platform_name=$2 local model_name=$3 - if [ "$deploymentEnvironment" = "test" ]; then + if [ "$deploymentEnvironment" = "testing" ]; then echo "secret/$secret_type/connections/$platform_name/$deploymentEnvironment/$connectionId" else echo "secret/$secret_type/connections/$platform_name/$deploymentEnvironment/$model_name" @@ -90,9 +80,9 @@ delete_vault_secret() { # Delete secret data log "Deleting secret data..." + # No X-Vault-Token header needed - vault agent proxy auto-injects it local data_response=$(curl -s -w "HTTPSTATUS:%{http_code}" \ -X DELETE \ - -H "X-Vault-Token: $VAULT_TOKEN" \ "$VAULT_ADDR/v1/$data_path") local data_http_code=$(echo "$data_response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2) @@ -108,9 +98,9 @@ delete_vault_secret() { # Delete secret metadata log "Deleting secret metadata..." + # No X-Vault-Token header needed - vault agent proxy auto-injects it local metadata_response=$(curl -s -w "HTTPSTATUS:%{http_code}" \ -X DELETE \ - -H "X-Vault-Token: $VAULT_TOKEN" \ "$VAULT_ADDR/v1/$metadata_path") local metadata_http_code=$(echo "$metadata_response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2) diff --git a/DSL/CronManager/script/store_secrets_in_vault.sh b/DSL/CronManager/script/store_secrets_in_vault.sh index dfc433b..2ec78d6 100644 --- a/DSL/CronManager/script/store_secrets_in_vault.sh +++ b/DSL/CronManager/script/store_secrets_in_vault.sh @@ -6,37 +6,179 @@ set -e # Exit on any error # Configuration -VAULT_ADDR="${VAULT_ADDR:-http://vault:8200}" -VAULT_TOKEN_FILE="/agent/out/token" +# Use VAULT_AGENT_URL which points to vault-agent-cron proxy +# The agent automatically injects the authentication token +VAULT_ADDR="${VAULT_AGENT_URL:-http://vault-agent-cron:8203}" -# Logging function +# Decryption Configuration +PRIVATE_KEY_CACHE="" +PRIVATE_KEY_PATH="secret/encryption/private_key" + +# Python decryption script path +DECRYPT_SCRIPT="/app/src/utils/decrypt_vault_secrets.py" + +# Virtual environment setup +VENV_PATH="/app/python_virtual_env" +UV_BIN="/root/.local/bin/uv" + +# Logging function (writes to stderr to avoid command substitution capture) log() { - echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >&2 } -log "=== Starting Vault Secrets Storage ===" +# Fetch private key from Vault +fetch_private_key() { + if [ -n "$PRIVATE_KEY_CACHE" ]; then + # Key already cached + return 0 + fi + + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Fetching private key from Vault..." >&2 + + # Convert path for KV v2 API + local api_path=$(echo "$PRIVATE_KEY_PATH" | sed 's|^secret/|secret/data/|') + + # Fetch private key from Vault + local response=$(curl -s --max-time 10 -w "HTTPSTATUS:%{http_code}" \ + -X GET \ + "$VAULT_ADDR/v1/$api_path") + + local http_code=$(echo "$response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2) + local body=$(echo "$response" | sed -E 's/HTTPSTATUS:[0-9]*$//') + + if [[ "$http_code" -ne 200 ]]; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Failed to fetch private key from Vault (HTTP $http_code)" >&2 + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Response: $body" >&2 + return 1 + fi + + # Extract private key from JSON response using jq for proper JSON parsing + if command -v jq >/dev/null 2>&1; then + PRIVATE_KEY_CACHE=$(echo "$body" | jq -r '.data.data.key // empty') + else + # Fallback to grep/sed if jq not available + PRIVATE_KEY_CACHE=$(echo "$body" | grep -o '"key":"[^"]*"' | sed 's/"key":"//; s/"$//' | sed 's/\\n/\n/g') + fi + + if [ -z "$PRIVATE_KEY_CACHE" ]; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Private key is empty or could not be extracted" >&2 + return 1 + fi + + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Private key fetched and cached successfully" >&2 +} + +# Decrypt RSA-OAEP encrypted value using Python +decrypt_rsa_oaep() { + local encrypted_base64="$1" + + if [ -z "$encrypted_base64" ]; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: decrypt_rsa_oaep called with empty value" >&2 + return 1 + fi + + # Ensure private key is fetched + if ! fetch_private_key; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Failed to fetch private key" >&2 + return 1 + fi + + # Call Python script for in-memory decryption + # Private key passed as argument + local decrypted_value + local python_stderr + python_stderr=$(mktemp) + decrypted_value=$("$VENV_PATH/bin/python3" "$DECRYPT_SCRIPT" "$encrypted_base64" "$PRIVATE_KEY_CACHE" 2>"$python_stderr") + local exit_code=$? + + # Show Python errors if any + if [ -s "$python_stderr" ]; then + cat "$python_stderr" >&2 + fi + rm -f "$python_stderr" + + if [ $exit_code -ne 0 ]; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Python decryption failed with exit code: $exit_code" >&2 + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Output: $decrypted_value" >&2 + return 1 + fi + + if [ -z "$decrypted_value" ]; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Decrypted value is empty" >&2 + return 1 + fi + + # Output decrypted value to stdout only + echo "$decrypted_value" +} + +# Setup Python environment and install dependencies +setup_python_environment() { + # Check if already setup + if [ -f "$VENV_PATH/.setup_complete" ]; then + return 0 + fi + + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Setting up Python environment..." + + # Install uv if not found + if [ ! -f "$UV_BIN" ]; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Installing uv package manager..." + curl -LsSf https://astral.sh/uv/install.sh | sh || { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Failed to install uv" >&2 + return 1 + } + fi + + # Activate virtual environment + if [ ! -d "$VENV_PATH" ]; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Virtual environment not found at $VENV_PATH" >&2 + return 1 + fi + + source "$VENV_PATH/bin/activate" || { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Failed to activate virtual environment" >&2 + return 1 + } + + # Install cryptography library for RSA-OAEP decryption + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Installing cryptography library..." + "$UV_BIN" pip install --python "$VENV_PATH/bin/python3" "cryptography>=46.0.3" 2>&1 || { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Failed to install cryptography" >&2 + return 1 + } + + # Install loguru for structured logging + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Installing loguru library..." + "$UV_BIN" pip install --python "$VENV_PATH/bin/python3" "loguru>=0.7.3" 2>&1 || { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Failed to install loguru" >&2 + return 1 + } + + # Mark setup as complete + touch "$VENV_PATH/.setup_complete" + + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Python environment setup complete" + return 0 +} -# Debug: Print received parameters +log "=== Starting Vault Secrets Storage ===" log "Received parameters:" log " connectionId: $connectionId" log " llmPlatform: $llmPlatform" log " llmModel: $llmModel" log " deploymentEnvironment: $deploymentEnvironment" +log " Vault Address: $VAULT_ADDR" -# Read vault token -if [ ! -f "$VAULT_TOKEN_FILE" ]; then - log "ERROR: Vault token file not found at $VAULT_TOKEN_FILE" - exit 1 -fi +# Redirect stderr to stdout so cron-manager can capture all logs +exec 2>&1 -VAULT_TOKEN=$(cat "$VAULT_TOKEN_FILE") -if [ -z "$VAULT_TOKEN" ]; then - log "ERROR: Vault token is empty" +# Setup Python environment once at startup (before any decryption) +if ! setup_python_environment; then + log "ERROR: Failed to setup Python environment" exit 1 fi -log "Vault token loaded successfully" - # Function to determine platform name get_platform_name() { case "$llmPlatform" in @@ -115,20 +257,34 @@ store_aws_llm_secrets() { log "Storing AWS LLM secrets..." - # Build JSON payload - local json_payload=$(cat < { + // Encrypt sensitive credentials before sending to vault + const encryptedCredentials = await encryptLLMCredentials({ + // AWS credentials + secretKey: connectionData.secretKey, + accessKey: connectionData.accessKey, + // Azure credentials + apiKey: connectionData.apiKey, + // Embedding AWS credentials + embeddingAccessKey: connectionData.embeddingAccessKey, + embeddingSecretKey: connectionData.embeddingSecretKey, + // Embedding Azure credentials + embeddingAzureApiKey: connectionData.embeddingAzureApiKey, + }); + const payload = { connectionId, llmPlatform: connectionData.llmPlatform, @@ -134,27 +149,27 @@ async function createVaultSecret(connectionId: string, connectionData: LLMConnec embeddingModel: connectionData.embeddingModel, embeddingPlatform: connectionData.embeddingModelPlatform, deploymentEnvironment: connectionData.deploymentEnvironment.toLowerCase(), - // AWS credentials + // AWS credentials (encrypted) ...(connectionData.llmPlatform === 'aws' && { - secretKey: connectionData.secretKey || '', - accessKey: connectionData.accessKey || '', + secretKey: encryptedCredentials.secretKey || '', + accessKey: encryptedCredentials.accessKey || '', }), - // Azure credentials + // Azure credentials (encrypted) ...(connectionData.llmPlatform === 'azure' && { deploymentName: connectionData.deploymentName || '', targetUrl: connectionData.targetUri || '', - apiKey: connectionData.apiKey || '', + apiKey: encryptedCredentials.apiKey || '', }), - // Embedding AWS Bedrock credentials + // Embedding AWS Bedrock credentials (encrypted) ...(connectionData.embeddingModelPlatform === 'aws' && { - embeddingAccessKey: connectionData.embeddingAccessKey || '', - embeddingSecretKey: connectionData.embeddingSecretKey || '', + embeddingAccessKey: encryptedCredentials.embeddingAccessKey || '', + embeddingSecretKey: encryptedCredentials.embeddingSecretKey || '', }), - // Embedding Azure credentials + // Embedding Azure credentials (encrypted) ...(connectionData.embeddingModelPlatform === 'azure' && { embeddingDeploymentName: connectionData.embeddingDeploymentName || '', embeddingTargetUri: connectionData.embeddingTargetUri || '', - embeddingAzureApiKey: connectionData.embeddingAzureApiKey || '', + embeddingAzureApiKey: encryptedCredentials.embeddingAzureApiKey || '', }), }; @@ -248,6 +263,7 @@ export async function createLLMConnection(connectionData: LLMConnectionFormData) }); const connection = data?.response; + console.log('Created LLM Connection:', connection); // After successful database creation, store secrets in vault if (connection && connection.id) { diff --git a/GUI/src/utils/encryption.ts b/GUI/src/utils/encryption.ts new file mode 100644 index 0000000..7909f6c --- /dev/null +++ b/GUI/src/utils/encryption.ts @@ -0,0 +1,204 @@ +/** + * RSA Encryption Utility for LLM Connection Secrets + * + * This module provides RSA encryption functionality using the Web Crypto API + * to encrypt sensitive LLM credentials before storing them in HashiCorp Vault. + */ + +// Cache for the public key +let publicKey: CryptoKey | null = null; + +/** + * Load the RSA public key from Vault via Vite proxy + */ +async function fetchPublicKey() { + // Return cached key if available + if (publicKey) { + return publicKey; + } + + try { + // Use Vite proxy to access vault-agent-gui + // Proxy configured in vite.config.ts: /vault-agent-gui -> http://vault-agent-gui:8202 + const response = await fetch( + '/vault-agent-gui/v1/secret/data/encryption/public_key', + { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + throw new Error(`Failed to fetch public key: ${response.status}`); + } + + const data = await response.json(); + + // Extract PEM string from Vault response + const publicKeyPem = data.data?.data?.key; + + if (!publicKeyPem) { + throw new Error('Public key not found in response'); + } + + // Convert PEM to CryptoKey object for Web Crypto API + const cryptoKey = await importPublicKey(publicKeyPem); + + // Cache the key + publicKey = cryptoKey; + + return cryptoKey; + + } catch (error) { + console.error('Error fetching public key:', error); + throw new Error('Failed to load encryption key'); + } +} + +// Helper: Convert PEM string to CryptoKey +async function importPublicKey(pemString: string) { + // Remove PEM headers and whitespace, keep standard base64 encoding + const pemContents = pemString + .replace('-----BEGIN PUBLIC KEY-----', '') + .replace('-----END PUBLIC KEY-----', '') + .replace(/\s/g, ''); // Remove whitespace and newlines only + + const binaryDer = Uint8Array.from(atob(pemContents), c => c.charCodeAt(0)); + + // Import as CryptoKey for encryption + const cryptoKey = await crypto.subtle.importKey( + 'spki', + binaryDer.buffer, + { + name: 'RSA-OAEP', + hash: 'SHA-256' + }, + false, + ['encrypt'] + ); + + return cryptoKey; +} + +/** + * Encrypt a string value using RSA-OAEP + * @param value - The plaintext value to encrypt + * @returns Base64-encoded encrypted value + */ +export async function encryptValue(value: string): Promise { + if (!value) { + return ''; + } + + try { + const key = await fetchPublicKey(); + + if (!key) { + throw new Error('Failed to load encryption key'); + } + + // Convert string to ArrayBuffer + const encoder = new TextEncoder(); + const data = encoder.encode(value); + + // Encrypt the data + const encryptedData = await window.crypto.subtle.encrypt( + { + name: 'RSA-OAEP', + }, + key, + data + ); + + // Convert to URL-safe base64 (base64url) for transmission + // Replace + with -, / with _, and remove trailing = + const base64 = btoa(String.fromCharCode(...new Uint8Array(encryptedData))) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + return base64; + } catch (error) { + console.error('Error encrypting value:', error); + throw new Error('Failed to encrypt sensitive data'); + } +} + +/** + * Encrypt an object containing sensitive credentials + * @param credentials - Object with potentially sensitive string values + * @returns Object with encrypted values (only encrypts non-empty string values) + */ +export async function encryptCredentials>( + credentials: T +): Promise { + const encrypted: any = { ...credentials }; + + for (const key in credentials) { + const value = credentials[key]; + // Only encrypt non-empty string values + if (typeof value === 'string' && value.length > 0) { + encrypted[key] = await encryptValue(value); + } + } + + return encrypted; +} + +/** + * Encrypt LLM credentials before sending to vault + * @param credentials - The credentials object to encrypt + */ +export async function encryptLLMCredentials(credentials: { + apiKey?: string; + secretKey?: string; + accessKey?: string; + embeddingAccessKey?: string; + embeddingSecretKey?: string; + embeddingAzureApiKey?: string; +}): Promise { + const encrypted: any = { ...credentials }; + + // Encrypt AWS credentials + if (credentials.secretKey) { + encrypted.secretKey = await encryptValue(credentials.secretKey); + } + if (credentials.accessKey) { + encrypted.accessKey = await encryptValue(credentials.accessKey); + } + + // Encrypt Azure credentials + if (credentials.apiKey) { + encrypted.apiKey = await encryptValue(credentials.apiKey); + } + + // Encrypt embedding AWS credentials + if (credentials.embeddingAccessKey) { + encrypted.embeddingAccessKey = await encryptValue(credentials.embeddingAccessKey); + } + if (credentials.embeddingSecretKey) { + encrypted.embeddingSecretKey = await encryptValue(credentials.embeddingSecretKey); + } + + // Encrypt embedding Azure credentials + if (credentials.embeddingAzureApiKey) { + encrypted.embeddingAzureApiKey = await encryptValue(credentials.embeddingAzureApiKey); + } + + return encrypted; +} + +/** + * Check if a value appears to be encrypted (base64 encoded) + * This is a simple heuristic check + */ +export function isEncrypted(value: string): boolean { + if (!value || value.length === 0) { + return false; + } + + // Check if it looks like base64 + const base64Regex = /^[A-Za-z0-9+/]+={0,2}$/; + return base64Regex.test(value) && value.length > 100; // Encrypted values should be reasonably long +} diff --git a/GUI/vite.config.ts b/GUI/vite.config.ts index 268f6d2..32f28eb 100644 --- a/GUI/vite.config.ts +++ b/GUI/vite.config.ts @@ -34,7 +34,13 @@ export default defineConfig({ }), }, allowedHosts: ['est-rag-rtc.rootcode.software', 'localhost', '127.0.0.1'], - + proxy: { + '/vault-agent-gui': { + target: 'http://vault-agent-gui:8202', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/vault-agent-gui/, ''), + }, + }, }, resolve: { alias: { diff --git a/GUI/vite.config.ts.timestamp-1767946562610-7e8d2a8c1f401.mjs b/GUI/vite.config.ts.timestamp-1767946562610-7e8d2a8c1f401.mjs new file mode 100644 index 0000000..b770c4c --- /dev/null +++ b/GUI/vite.config.ts.timestamp-1767946562610-7e8d2a8c1f401.mjs @@ -0,0 +1,70 @@ +// vite.config.ts +import { defineConfig } from "file:///app/node_modules/vite/dist/node/index.js"; +import react from "file:///app/node_modules/@vitejs/plugin-react/dist/index.mjs"; +import tsconfigPaths from "file:///app/node_modules/vite-tsconfig-paths/dist/index.mjs"; +import svgr from "file:///app/node_modules/vite-plugin-svgr/dist/index.mjs"; +import path from "path"; + +// vitePlugin.js +function removeHiddenMenuItems(str) { + var _a, _b; + const badJson = str.replace("export default [", "[").replace("];", "]"); + const correctJson = badJson.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": '); + const isHiddenFeaturesEnabled = ((_a = process.env.REACT_APP_ENABLE_HIDDEN_FEATURES) == null ? void 0 : _a.toLowerCase().trim()) === "true" || ((_b = process.env.REACT_APP_ENABLE_HIDDEN_FEATURES) == null ? void 0 : _b.toLowerCase().trim()) === "1"; + const json = removeHidden(JSON.parse(correctJson), isHiddenFeaturesEnabled); + const updatedJson = JSON.stringify(json); + return "export default " + updatedJson + ";"; +} +function removeHidden(menuItems, isHiddenFeaturesEnabled) { + var _a; + if (!menuItems) + return menuItems; + const arr = (_a = menuItems == null ? void 0 : menuItems.filter((x) => !x.hidden)) == null ? void 0 : _a.filter((x) => isHiddenFeaturesEnabled || x.hiddenMode !== "production"); + for (const a of arr) { + a.children = removeHidden(a.children, isHiddenFeaturesEnabled); + } + return arr; +} + +// vite.config.ts +var __vite_injected_original_dirname = "/app"; +var vite_config_default = defineConfig({ + envPrefix: "REACT_APP_", + plugins: [ + react(), + tsconfigPaths(), + svgr(), + { + name: "removeHiddenMenuItemsPlugin", + transform: (str, id) => { + if (!id.endsWith("/menu-structure.json")) + return str; + return removeHiddenMenuItems(str); + } + } + ], + base: "/rag-search", + build: { + outDir: "./build", + target: "es2015", + emptyOutDir: true + }, + server: { + headers: { + ...process.env.REACT_APP_CSP && { + "Content-Security-Policy": process.env.REACT_APP_CSP + } + }, + allowedHosts: ["est-rag-rtc.rootcode.software", "localhost", "127.0.0.1"] + }, + resolve: { + alias: { + "~@fontsource": path.resolve(__vite_injected_original_dirname, "node_modules/@fontsource"), + "@": `${path.resolve(__vite_injected_original_dirname, "./src")}` + } + } +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiLCAidml0ZVBsdWdpbi5qcyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9hcHBcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9hcHAvdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2FwcC92aXRlLmNvbmZpZy50c1wiO2ltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGUnO1xuaW1wb3J0IHJlYWN0IGZyb20gJ0B2aXRlanMvcGx1Z2luLXJlYWN0JztcbmltcG9ydCB0c2NvbmZpZ1BhdGhzIGZyb20gJ3ZpdGUtdHNjb25maWctcGF0aHMnO1xuaW1wb3J0IHN2Z3IgZnJvbSAndml0ZS1wbHVnaW4tc3Zncic7XG5pbXBvcnQgcGF0aCBmcm9tICdwYXRoJztcbmltcG9ydCB7IHJlbW92ZUhpZGRlbk1lbnVJdGVtcyB9IGZyb20gJy4vdml0ZVBsdWdpbic7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBlbnZQcmVmaXg6ICdSRUFDVF9BUFBfJyxcbiAgcGx1Z2luczogW1xuICAgIHJlYWN0KCksXG4gICAgdHNjb25maWdQYXRocygpLFxuICAgIHN2Z3IoKSxcbiAgICB7XG4gICAgICBuYW1lOiAncmVtb3ZlSGlkZGVuTWVudUl0ZW1zUGx1Z2luJyxcbiAgICAgIHRyYW5zZm9ybTogKHN0ciwgaWQpID0+IHtcbiAgICAgICAgaWYoIWlkLmVuZHNXaXRoKCcvbWVudS1zdHJ1Y3R1cmUuanNvbicpKVxuICAgICAgICAgIHJldHVybiBzdHI7XG4gICAgICAgIHJldHVybiByZW1vdmVIaWRkZW5NZW51SXRlbXMoc3RyKTtcbiAgICAgIH0sXG4gICAgfSxcbiAgXSxcbiAgYmFzZTogJy9yYWctc2VhcmNoJyxcbiAgYnVpbGQ6IHtcbiAgICBvdXREaXI6ICcuL2J1aWxkJyxcbiAgICB0YXJnZXQ6ICdlczIwMTUnLFxuICAgIGVtcHR5T3V0RGlyOiB0cnVlLFxuICB9LFxuICBzZXJ2ZXI6IHtcbiAgICBoZWFkZXJzOiB7XG4gICAgICAuLi4ocHJvY2Vzcy5lbnYuUkVBQ1RfQVBQX0NTUCAmJiB7XG4gICAgICAgICdDb250ZW50LVNlY3VyaXR5LVBvbGljeSc6IHByb2Nlc3MuZW52LlJFQUNUX0FQUF9DU1AsXG4gICAgICB9KSxcbiAgICB9LFxuICAgIGFsbG93ZWRIb3N0czogWydlc3QtcmFnLXJ0Yy5yb290Y29kZS5zb2Z0d2FyZScsICdsb2NhbGhvc3QnLCAnMTI3LjAuMC4xJ10sXG5cbiAgfSxcbiAgcmVzb2x2ZToge1xuICAgIGFsaWFzOiB7XG4gICAgICAnfkBmb250c291cmNlJzogcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgJ25vZGVfbW9kdWxlcy9AZm9udHNvdXJjZScpLFxuICAgICAgJ0AnOiBgJHtwYXRoLnJlc29sdmUoX19kaXJuYW1lLCAnLi9zcmMnKX1gLFxuICAgIH0sXG4gIH0sXG59KTtcbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL2FwcFwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL2FwcC92aXRlUGx1Z2luLmpzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9hcHAvdml0ZVBsdWdpbi5qc1wiO2V4cG9ydCBmdW5jdGlvbiByZW1vdmVIaWRkZW5NZW51SXRlbXMoc3RyKSB7XG4gIGNvbnN0IGJhZEpzb24gPSBzdHIucmVwbGFjZSgnZXhwb3J0IGRlZmF1bHQgWycsICdbJykucmVwbGFjZSgnXTsnLCAnXScpO1xuICBjb25zdCBjb3JyZWN0SnNvbiA9IGJhZEpzb24ucmVwbGFjZSgvKFsnXCJdKT8oW2EtejAtOUEtWl9dKykoWydcIl0pPzovZywgJ1wiJDJcIjogJyk7XG5cbiBjb25zdCBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCA9IFxuICAgIHByb2Nlc3MuZW52LlJFQUNUX0FQUF9FTkFCTEVfSElEREVOX0ZFQVRVUkVTPy50b0xvd2VyQ2FzZSgpLnRyaW0oKSA9PT0gJ3RydWUnIHx8XG4gICAgcHJvY2Vzcy5lbnYuUkVBQ1RfQVBQX0VOQUJMRV9ISURERU5fRkVBVFVSRVM/LnRvTG93ZXJDYXNlKCkudHJpbSgpID09PSAnMSc7XG5cbiAgY29uc3QganNvbiA9IHJlbW92ZUhpZGRlbihKU09OLnBhcnNlKGNvcnJlY3RKc29uKSwgaXNIaWRkZW5GZWF0dXJlc0VuYWJsZWQpO1xuICBcbiAgY29uc3QgdXBkYXRlZEpzb24gPSBKU09OLnN0cmluZ2lmeShqc29uKTtcblxuICByZXR1cm4gJ2V4cG9ydCBkZWZhdWx0ICcgKyB1cGRhdGVkSnNvbiArICc7J1xufVxuXG5mdW5jdGlvbiByZW1vdmVIaWRkZW4obWVudUl0ZW1zLCBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCkge1xuICBpZighbWVudUl0ZW1zKSByZXR1cm4gbWVudUl0ZW1zO1xuICBjb25zdCBhcnIgPSBtZW51SXRlbXNcbiAgICA/LmZpbHRlcih4ID0+ICF4LmhpZGRlbilcbiAgICA/LmZpbHRlcih4ID0+IGlzSGlkZGVuRmVhdHVyZXNFbmFibGVkIHx8IHguaGlkZGVuTW9kZSAhPT0gXCJwcm9kdWN0aW9uXCIpO1xuICBmb3IgKGNvbnN0IGEgb2YgYXJyKSB7XG4gICAgYS5jaGlsZHJlbiA9IHJlbW92ZUhpZGRlbihhLmNoaWxkcmVuLCBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCk7XG4gIH1cbiAgcmV0dXJuIGFycjtcbn1cbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBOEwsU0FBUyxvQkFBb0I7QUFDM04sT0FBTyxXQUFXO0FBQ2xCLE9BQU8sbUJBQW1CO0FBQzFCLE9BQU8sVUFBVTtBQUNqQixPQUFPLFVBQVU7OztBQ0prTCxTQUFTLHNCQUFzQixLQUFLO0FBQXZPO0FBQ0UsUUFBTSxVQUFVLElBQUksUUFBUSxvQkFBb0IsR0FBRyxFQUFFLFFBQVEsTUFBTSxHQUFHO0FBQ3RFLFFBQU0sY0FBYyxRQUFRLFFBQVEsbUNBQW1DLFFBQVE7QUFFaEYsUUFBTSw0QkFDSCxhQUFRLElBQUkscUNBQVosbUJBQThDLGNBQWMsWUFBVyxZQUN2RSxhQUFRLElBQUkscUNBQVosbUJBQThDLGNBQWMsWUFBVztBQUV6RSxRQUFNLE9BQU8sYUFBYSxLQUFLLE1BQU0sV0FBVyxHQUFHLHVCQUF1QjtBQUUxRSxRQUFNLGNBQWMsS0FBSyxVQUFVLElBQUk7QUFFdkMsU0FBTyxvQkFBb0IsY0FBYztBQUMzQztBQUVBLFNBQVMsYUFBYSxXQUFXLHlCQUF5QjtBQWYxRDtBQWdCRSxNQUFHLENBQUM7QUFBVyxXQUFPO0FBQ3RCLFFBQU0sT0FBTSw0Q0FDUixPQUFPLE9BQUssQ0FBQyxFQUFFLFlBRFAsbUJBRVIsT0FBTyxPQUFLLDJCQUEyQixFQUFFLGVBQWU7QUFDNUQsYUFBVyxLQUFLLEtBQUs7QUFDbkIsTUFBRSxXQUFXLGFBQWEsRUFBRSxVQUFVLHVCQUF1QjtBQUFBLEVBQy9EO0FBQ0EsU0FBTztBQUNUOzs7QUR4QkEsSUFBTSxtQ0FBbUM7QUFRekMsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsV0FBVztBQUFBLEVBQ1gsU0FBUztBQUFBLElBQ1AsTUFBTTtBQUFBLElBQ04sY0FBYztBQUFBLElBQ2QsS0FBSztBQUFBLElBQ0w7QUFBQSxNQUNFLE1BQU07QUFBQSxNQUNOLFdBQVcsQ0FBQyxLQUFLLE9BQU87QUFDdEIsWUFBRyxDQUFDLEdBQUcsU0FBUyxzQkFBc0I7QUFDcEMsaUJBQU87QUFDVCxlQUFPLHNCQUFzQixHQUFHO0FBQUEsTUFDbEM7QUFBQSxJQUNGO0FBQUEsRUFDRjtBQUFBLEVBQ0EsTUFBTTtBQUFBLEVBQ04sT0FBTztBQUFBLElBQ0wsUUFBUTtBQUFBLElBQ1IsUUFBUTtBQUFBLElBQ1IsYUFBYTtBQUFBLEVBQ2Y7QUFBQSxFQUNBLFFBQVE7QUFBQSxJQUNOLFNBQVM7QUFBQSxNQUNQLEdBQUksUUFBUSxJQUFJLGlCQUFpQjtBQUFBLFFBQy9CLDJCQUEyQixRQUFRLElBQUk7QUFBQSxNQUN6QztBQUFBLElBQ0Y7QUFBQSxJQUNBLGNBQWMsQ0FBQyxpQ0FBaUMsYUFBYSxXQUFXO0FBQUEsRUFFMUU7QUFBQSxFQUNBLFNBQVM7QUFBQSxJQUNQLE9BQU87QUFBQSxNQUNMLGdCQUFnQixLQUFLLFFBQVEsa0NBQVcsMEJBQTBCO0FBQUEsTUFDbEUsS0FBSyxHQUFHLEtBQUssUUFBUSxrQ0FBVyxPQUFPLENBQUM7QUFBQSxJQUMxQztBQUFBLEVBQ0Y7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo= diff --git a/GUI/vite.config.ts.timestamp-1767946574215-f7ac6ce2fedaa.mjs b/GUI/vite.config.ts.timestamp-1767946574215-f7ac6ce2fedaa.mjs new file mode 100644 index 0000000..b770c4c --- /dev/null +++ b/GUI/vite.config.ts.timestamp-1767946574215-f7ac6ce2fedaa.mjs @@ -0,0 +1,70 @@ +// vite.config.ts +import { defineConfig } from "file:///app/node_modules/vite/dist/node/index.js"; +import react from "file:///app/node_modules/@vitejs/plugin-react/dist/index.mjs"; +import tsconfigPaths from "file:///app/node_modules/vite-tsconfig-paths/dist/index.mjs"; +import svgr from "file:///app/node_modules/vite-plugin-svgr/dist/index.mjs"; +import path from "path"; + +// vitePlugin.js +function removeHiddenMenuItems(str) { + var _a, _b; + const badJson = str.replace("export default [", "[").replace("];", "]"); + const correctJson = badJson.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": '); + const isHiddenFeaturesEnabled = ((_a = process.env.REACT_APP_ENABLE_HIDDEN_FEATURES) == null ? void 0 : _a.toLowerCase().trim()) === "true" || ((_b = process.env.REACT_APP_ENABLE_HIDDEN_FEATURES) == null ? void 0 : _b.toLowerCase().trim()) === "1"; + const json = removeHidden(JSON.parse(correctJson), isHiddenFeaturesEnabled); + const updatedJson = JSON.stringify(json); + return "export default " + updatedJson + ";"; +} +function removeHidden(menuItems, isHiddenFeaturesEnabled) { + var _a; + if (!menuItems) + return menuItems; + const arr = (_a = menuItems == null ? void 0 : menuItems.filter((x) => !x.hidden)) == null ? void 0 : _a.filter((x) => isHiddenFeaturesEnabled || x.hiddenMode !== "production"); + for (const a of arr) { + a.children = removeHidden(a.children, isHiddenFeaturesEnabled); + } + return arr; +} + +// vite.config.ts +var __vite_injected_original_dirname = "/app"; +var vite_config_default = defineConfig({ + envPrefix: "REACT_APP_", + plugins: [ + react(), + tsconfigPaths(), + svgr(), + { + name: "removeHiddenMenuItemsPlugin", + transform: (str, id) => { + if (!id.endsWith("/menu-structure.json")) + return str; + return removeHiddenMenuItems(str); + } + } + ], + base: "/rag-search", + build: { + outDir: "./build", + target: "es2015", + emptyOutDir: true + }, + server: { + headers: { + ...process.env.REACT_APP_CSP && { + "Content-Security-Policy": process.env.REACT_APP_CSP + } + }, + allowedHosts: ["est-rag-rtc.rootcode.software", "localhost", "127.0.0.1"] + }, + resolve: { + alias: { + "~@fontsource": path.resolve(__vite_injected_original_dirname, "node_modules/@fontsource"), + "@": `${path.resolve(__vite_injected_original_dirname, "./src")}` + } + } +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiLCAidml0ZVBsdWdpbi5qcyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9hcHBcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9hcHAvdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2FwcC92aXRlLmNvbmZpZy50c1wiO2ltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGUnO1xuaW1wb3J0IHJlYWN0IGZyb20gJ0B2aXRlanMvcGx1Z2luLXJlYWN0JztcbmltcG9ydCB0c2NvbmZpZ1BhdGhzIGZyb20gJ3ZpdGUtdHNjb25maWctcGF0aHMnO1xuaW1wb3J0IHN2Z3IgZnJvbSAndml0ZS1wbHVnaW4tc3Zncic7XG5pbXBvcnQgcGF0aCBmcm9tICdwYXRoJztcbmltcG9ydCB7IHJlbW92ZUhpZGRlbk1lbnVJdGVtcyB9IGZyb20gJy4vdml0ZVBsdWdpbic7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBlbnZQcmVmaXg6ICdSRUFDVF9BUFBfJyxcbiAgcGx1Z2luczogW1xuICAgIHJlYWN0KCksXG4gICAgdHNjb25maWdQYXRocygpLFxuICAgIHN2Z3IoKSxcbiAgICB7XG4gICAgICBuYW1lOiAncmVtb3ZlSGlkZGVuTWVudUl0ZW1zUGx1Z2luJyxcbiAgICAgIHRyYW5zZm9ybTogKHN0ciwgaWQpID0+IHtcbiAgICAgICAgaWYoIWlkLmVuZHNXaXRoKCcvbWVudS1zdHJ1Y3R1cmUuanNvbicpKVxuICAgICAgICAgIHJldHVybiBzdHI7XG4gICAgICAgIHJldHVybiByZW1vdmVIaWRkZW5NZW51SXRlbXMoc3RyKTtcbiAgICAgIH0sXG4gICAgfSxcbiAgXSxcbiAgYmFzZTogJy9yYWctc2VhcmNoJyxcbiAgYnVpbGQ6IHtcbiAgICBvdXREaXI6ICcuL2J1aWxkJyxcbiAgICB0YXJnZXQ6ICdlczIwMTUnLFxuICAgIGVtcHR5T3V0RGlyOiB0cnVlLFxuICB9LFxuICBzZXJ2ZXI6IHtcbiAgICBoZWFkZXJzOiB7XG4gICAgICAuLi4ocHJvY2Vzcy5lbnYuUkVBQ1RfQVBQX0NTUCAmJiB7XG4gICAgICAgICdDb250ZW50LVNlY3VyaXR5LVBvbGljeSc6IHByb2Nlc3MuZW52LlJFQUNUX0FQUF9DU1AsXG4gICAgICB9KSxcbiAgICB9LFxuICAgIGFsbG93ZWRIb3N0czogWydlc3QtcmFnLXJ0Yy5yb290Y29kZS5zb2Z0d2FyZScsICdsb2NhbGhvc3QnLCAnMTI3LjAuMC4xJ10sXG5cbiAgfSxcbiAgcmVzb2x2ZToge1xuICAgIGFsaWFzOiB7XG4gICAgICAnfkBmb250c291cmNlJzogcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgJ25vZGVfbW9kdWxlcy9AZm9udHNvdXJjZScpLFxuICAgICAgJ0AnOiBgJHtwYXRoLnJlc29sdmUoX19kaXJuYW1lLCAnLi9zcmMnKX1gLFxuICAgIH0sXG4gIH0sXG59KTtcbiIsICJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL2FwcFwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL2FwcC92aXRlUGx1Z2luLmpzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9hcHAvdml0ZVBsdWdpbi5qc1wiO2V4cG9ydCBmdW5jdGlvbiByZW1vdmVIaWRkZW5NZW51SXRlbXMoc3RyKSB7XG4gIGNvbnN0IGJhZEpzb24gPSBzdHIucmVwbGFjZSgnZXhwb3J0IGRlZmF1bHQgWycsICdbJykucmVwbGFjZSgnXTsnLCAnXScpO1xuICBjb25zdCBjb3JyZWN0SnNvbiA9IGJhZEpzb24ucmVwbGFjZSgvKFsnXCJdKT8oW2EtejAtOUEtWl9dKykoWydcIl0pPzovZywgJ1wiJDJcIjogJyk7XG5cbiBjb25zdCBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCA9IFxuICAgIHByb2Nlc3MuZW52LlJFQUNUX0FQUF9FTkFCTEVfSElEREVOX0ZFQVRVUkVTPy50b0xvd2VyQ2FzZSgpLnRyaW0oKSA9PT0gJ3RydWUnIHx8XG4gICAgcHJvY2Vzcy5lbnYuUkVBQ1RfQVBQX0VOQUJMRV9ISURERU5fRkVBVFVSRVM/LnRvTG93ZXJDYXNlKCkudHJpbSgpID09PSAnMSc7XG5cbiAgY29uc3QganNvbiA9IHJlbW92ZUhpZGRlbihKU09OLnBhcnNlKGNvcnJlY3RKc29uKSwgaXNIaWRkZW5GZWF0dXJlc0VuYWJsZWQpO1xuICBcbiAgY29uc3QgdXBkYXRlZEpzb24gPSBKU09OLnN0cmluZ2lmeShqc29uKTtcblxuICByZXR1cm4gJ2V4cG9ydCBkZWZhdWx0ICcgKyB1cGRhdGVkSnNvbiArICc7J1xufVxuXG5mdW5jdGlvbiByZW1vdmVIaWRkZW4obWVudUl0ZW1zLCBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCkge1xuICBpZighbWVudUl0ZW1zKSByZXR1cm4gbWVudUl0ZW1zO1xuICBjb25zdCBhcnIgPSBtZW51SXRlbXNcbiAgICA/LmZpbHRlcih4ID0+ICF4LmhpZGRlbilcbiAgICA/LmZpbHRlcih4ID0+IGlzSGlkZGVuRmVhdHVyZXNFbmFibGVkIHx8IHguaGlkZGVuTW9kZSAhPT0gXCJwcm9kdWN0aW9uXCIpO1xuICBmb3IgKGNvbnN0IGEgb2YgYXJyKSB7XG4gICAgYS5jaGlsZHJlbiA9IHJlbW92ZUhpZGRlbihhLmNoaWxkcmVuLCBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCk7XG4gIH1cbiAgcmV0dXJuIGFycjtcbn1cbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBOEwsU0FBUyxvQkFBb0I7QUFDM04sT0FBTyxXQUFXO0FBQ2xCLE9BQU8sbUJBQW1CO0FBQzFCLE9BQU8sVUFBVTtBQUNqQixPQUFPLFVBQVU7OztBQ0prTCxTQUFTLHNCQUFzQixLQUFLO0FBQXZPO0FBQ0UsUUFBTSxVQUFVLElBQUksUUFBUSxvQkFBb0IsR0FBRyxFQUFFLFFBQVEsTUFBTSxHQUFHO0FBQ3RFLFFBQU0sY0FBYyxRQUFRLFFBQVEsbUNBQW1DLFFBQVE7QUFFaEYsUUFBTSw0QkFDSCxhQUFRLElBQUkscUNBQVosbUJBQThDLGNBQWMsWUFBVyxZQUN2RSxhQUFRLElBQUkscUNBQVosbUJBQThDLGNBQWMsWUFBVztBQUV6RSxRQUFNLE9BQU8sYUFBYSxLQUFLLE1BQU0sV0FBVyxHQUFHLHVCQUF1QjtBQUUxRSxRQUFNLGNBQWMsS0FBSyxVQUFVLElBQUk7QUFFdkMsU0FBTyxvQkFBb0IsY0FBYztBQUMzQztBQUVBLFNBQVMsYUFBYSxXQUFXLHlCQUF5QjtBQWYxRDtBQWdCRSxNQUFHLENBQUM7QUFBVyxXQUFPO0FBQ3RCLFFBQU0sT0FBTSw0Q0FDUixPQUFPLE9BQUssQ0FBQyxFQUFFLFlBRFAsbUJBRVIsT0FBTyxPQUFLLDJCQUEyQixFQUFFLGVBQWU7QUFDNUQsYUFBVyxLQUFLLEtBQUs7QUFDbkIsTUFBRSxXQUFXLGFBQWEsRUFBRSxVQUFVLHVCQUF1QjtBQUFBLEVBQy9EO0FBQ0EsU0FBTztBQUNUOzs7QUR4QkEsSUFBTSxtQ0FBbUM7QUFRekMsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsV0FBVztBQUFBLEVBQ1gsU0FBUztBQUFBLElBQ1AsTUFBTTtBQUFBLElBQ04sY0FBYztBQUFBLElBQ2QsS0FBSztBQUFBLElBQ0w7QUFBQSxNQUNFLE1BQU07QUFBQSxNQUNOLFdBQVcsQ0FBQyxLQUFLLE9BQU87QUFDdEIsWUFBRyxDQUFDLEdBQUcsU0FBUyxzQkFBc0I7QUFDcEMsaUJBQU87QUFDVCxlQUFPLHNCQUFzQixHQUFHO0FBQUEsTUFDbEM7QUFBQSxJQUNGO0FBQUEsRUFDRjtBQUFBLEVBQ0EsTUFBTTtBQUFBLEVBQ04sT0FBTztBQUFBLElBQ0wsUUFBUTtBQUFBLElBQ1IsUUFBUTtBQUFBLElBQ1IsYUFBYTtBQUFBLEVBQ2Y7QUFBQSxFQUNBLFFBQVE7QUFBQSxJQUNOLFNBQVM7QUFBQSxNQUNQLEdBQUksUUFBUSxJQUFJLGlCQUFpQjtBQUFBLFFBQy9CLDJCQUEyQixRQUFRLElBQUk7QUFBQSxNQUN6QztBQUFBLElBQ0Y7QUFBQSxJQUNBLGNBQWMsQ0FBQyxpQ0FBaUMsYUFBYSxXQUFXO0FBQUEsRUFFMUU7QUFBQSxFQUNBLFNBQVM7QUFBQSxJQUNQLE9BQU87QUFBQSxNQUNMLGdCQUFnQixLQUFLLFFBQVEsa0NBQVcsMEJBQTBCO0FBQUEsTUFDbEUsS0FBSyxHQUFHLEtBQUssUUFBUSxrQ0FBVyxPQUFPLENBQUM7QUFBQSxJQUMxQztBQUFBLEVBQ0Y7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo= diff --git a/GUI/vite.config.ts.timestamp-1768278822370-7924bd5f138d9.mjs b/GUI/vite.config.ts.timestamp-1768278822370-7924bd5f138d9.mjs new file mode 100644 index 0000000..3ffe592 --- /dev/null +++ b/GUI/vite.config.ts.timestamp-1768278822370-7924bd5f138d9.mjs @@ -0,0 +1,77 @@ +// vite.config.ts +import { defineConfig } from "file:///app/node_modules/vite/dist/node/index.js"; +import react from "file:///app/node_modules/@vitejs/plugin-react/dist/index.mjs"; +import tsconfigPaths from "file:///app/node_modules/vite-tsconfig-paths/dist/index.mjs"; +import svgr from "file:///app/node_modules/vite-plugin-svgr/dist/index.mjs"; +import path from "path"; + +// vitePlugin.js +function removeHiddenMenuItems(str) { + var _a, _b; + const badJson = str.replace("export default [", "[").replace("];", "]"); + const correctJson = badJson.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": '); + const isHiddenFeaturesEnabled = ((_a = process.env.REACT_APP_ENABLE_HIDDEN_FEATURES) == null ? void 0 : _a.toLowerCase().trim()) === "true" || ((_b = process.env.REACT_APP_ENABLE_HIDDEN_FEATURES) == null ? void 0 : _b.toLowerCase().trim()) === "1"; + const json = removeHidden(JSON.parse(correctJson), isHiddenFeaturesEnabled); + const updatedJson = JSON.stringify(json); + return "export default " + updatedJson + ";"; +} +function removeHidden(menuItems, isHiddenFeaturesEnabled) { + var _a; + if (!menuItems) + return menuItems; + const arr = (_a = menuItems == null ? void 0 : menuItems.filter((x) => !x.hidden)) == null ? void 0 : _a.filter((x) => isHiddenFeaturesEnabled || x.hiddenMode !== "production"); + for (const a of arr) { + a.children = removeHidden(a.children, isHiddenFeaturesEnabled); + } + return arr; +} + +// vite.config.ts +var __vite_injected_original_dirname = "/app"; +var vite_config_default = defineConfig({ + envPrefix: "REACT_APP_", + plugins: [ + react(), + tsconfigPaths(), + svgr(), + { + name: "removeHiddenMenuItemsPlugin", + transform: (str, id) => { + if (!id.endsWith("/menu-structure.json")) + return str; + return removeHiddenMenuItems(str); + } + } + ], + base: "/rag-search", + build: { + outDir: "./build", + target: "es2015", + emptyOutDir: true + }, + server: { + headers: { + ...process.env.REACT_APP_CSP && { + "Content-Security-Policy": process.env.REACT_APP_CSP + } + }, + allowedHosts: ["est-rag-rtc.rootcode.software", "localhost", "127.0.0.1"], + proxy: { + "/vault-agent-gui": { + target: "http://vault-agent-gui:8202", + changeOrigin: true, + rewrite: (path2) => path2.replace(/^\/vault-agent-gui/, "") + } + } + }, + resolve: { + alias: { + "~@fontsource": path.resolve(__vite_injected_original_dirname, "node_modules/@fontsource"), + "@": `${path.resolve(__vite_injected_original_dirname, "./src")}` + } + } +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiLCAidml0ZVBsdWdpbi5qcyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9hcHBcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9hcHAvdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2FwcC92aXRlLmNvbmZpZy50c1wiO2ltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGUnO1xuaW1wb3J0IHJlYWN0IGZyb20gJ0B2aXRlanMvcGx1Z2luLXJlYWN0JztcbmltcG9ydCB0c2NvbmZpZ1BhdGhzIGZyb20gJ3ZpdGUtdHNjb25maWctcGF0aHMnO1xuaW1wb3J0IHN2Z3IgZnJvbSAndml0ZS1wbHVnaW4tc3Zncic7XG5pbXBvcnQgcGF0aCBmcm9tICdwYXRoJztcbmltcG9ydCB7IHJlbW92ZUhpZGRlbk1lbnVJdGVtcyB9IGZyb20gJy4vdml0ZVBsdWdpbic7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBlbnZQcmVmaXg6ICdSRUFDVF9BUFBfJyxcbiAgcGx1Z2luczogW1xuICAgIHJlYWN0KCksXG4gICAgdHNjb25maWdQYXRocygpLFxuICAgIHN2Z3IoKSxcbiAgICB7XG4gICAgICBuYW1lOiAncmVtb3ZlSGlkZGVuTWVudUl0ZW1zUGx1Z2luJyxcbiAgICAgIHRyYW5zZm9ybTogKHN0ciwgaWQpID0+IHtcbiAgICAgICAgaWYoIWlkLmVuZHNXaXRoKCcvbWVudS1zdHJ1Y3R1cmUuanNvbicpKVxuICAgICAgICAgIHJldHVybiBzdHI7XG4gICAgICAgIHJldHVybiByZW1vdmVIaWRkZW5NZW51SXRlbXMoc3RyKTtcbiAgICAgIH0sXG4gICAgfSxcbiAgXSxcbiAgYmFzZTogJy9yYWctc2VhcmNoJyxcbiAgYnVpbGQ6IHtcbiAgICBvdXREaXI6ICcuL2J1aWxkJyxcbiAgICB0YXJnZXQ6ICdlczIwMTUnLFxuICAgIGVtcHR5T3V0RGlyOiB0cnVlLFxuICB9LFxuICBzZXJ2ZXI6IHtcbiAgICBoZWFkZXJzOiB7XG4gICAgICAuLi4ocHJvY2Vzcy5lbnYuUkVBQ1RfQVBQX0NTUCAmJiB7XG4gICAgICAgICdDb250ZW50LVNlY3VyaXR5LVBvbGljeSc6IHByb2Nlc3MuZW52LlJFQUNUX0FQUF9DU1AsXG4gICAgICB9KSxcbiAgICB9LFxuICAgIGFsbG93ZWRIb3N0czogWydlc3QtcmFnLXJ0Yy5yb290Y29kZS5zb2Z0d2FyZScsICdsb2NhbGhvc3QnLCAnMTI3LjAuMC4xJ10sXG4gICAgcHJveHk6IHtcbiAgICAgICcvdmF1bHQtYWdlbnQtZ3VpJzoge1xuICAgICAgICB0YXJnZXQ6ICdodHRwOi8vdmF1bHQtYWdlbnQtZ3VpOjgyMDInLFxuICAgICAgICBjaGFuZ2VPcmlnaW46IHRydWUsXG4gICAgICAgIHJld3JpdGU6IChwYXRoKSA9PiBwYXRoLnJlcGxhY2UoL15cXC92YXVsdC1hZ2VudC1ndWkvLCAnJyksXG4gICAgICB9LFxuICAgIH0sXG4gIH0sXG4gIHJlc29sdmU6IHtcbiAgICBhbGlhczoge1xuICAgICAgJ35AZm9udHNvdXJjZSc6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsICdub2RlX21vZHVsZXMvQGZvbnRzb3VyY2UnKSxcbiAgICAgICdAJzogYCR7cGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgJy4vc3JjJyl9YCxcbiAgICB9LFxuICB9LFxufSk7XG4iLCAiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9hcHBcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9hcHAvdml0ZVBsdWdpbi5qc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vYXBwL3ZpdGVQbHVnaW4uanNcIjtleHBvcnQgZnVuY3Rpb24gcmVtb3ZlSGlkZGVuTWVudUl0ZW1zKHN0cikge1xuICBjb25zdCBiYWRKc29uID0gc3RyLnJlcGxhY2UoJ2V4cG9ydCBkZWZhdWx0IFsnLCAnWycpLnJlcGxhY2UoJ107JywgJ10nKTtcbiAgY29uc3QgY29ycmVjdEpzb24gPSBiYWRKc29uLnJlcGxhY2UoLyhbJ1wiXSk/KFthLXowLTlBLVpfXSspKFsnXCJdKT86L2csICdcIiQyXCI6ICcpO1xuXG4gY29uc3QgaXNIaWRkZW5GZWF0dXJlc0VuYWJsZWQgPSBcbiAgICBwcm9jZXNzLmVudi5SRUFDVF9BUFBfRU5BQkxFX0hJRERFTl9GRUFUVVJFUz8udG9Mb3dlckNhc2UoKS50cmltKCkgPT09ICd0cnVlJyB8fFxuICAgIHByb2Nlc3MuZW52LlJFQUNUX0FQUF9FTkFCTEVfSElEREVOX0ZFQVRVUkVTPy50b0xvd2VyQ2FzZSgpLnRyaW0oKSA9PT0gJzEnO1xuXG4gIGNvbnN0IGpzb24gPSByZW1vdmVIaWRkZW4oSlNPTi5wYXJzZShjb3JyZWN0SnNvbiksIGlzSGlkZGVuRmVhdHVyZXNFbmFibGVkKTtcbiAgXG4gIGNvbnN0IHVwZGF0ZWRKc29uID0gSlNPTi5zdHJpbmdpZnkoanNvbik7XG5cbiAgcmV0dXJuICdleHBvcnQgZGVmYXVsdCAnICsgdXBkYXRlZEpzb24gKyAnOydcbn1cblxuZnVuY3Rpb24gcmVtb3ZlSGlkZGVuKG1lbnVJdGVtcywgaXNIaWRkZW5GZWF0dXJlc0VuYWJsZWQpIHtcbiAgaWYoIW1lbnVJdGVtcykgcmV0dXJuIG1lbnVJdGVtcztcbiAgY29uc3QgYXJyID0gbWVudUl0ZW1zXG4gICAgPy5maWx0ZXIoeCA9PiAheC5oaWRkZW4pXG4gICAgPy5maWx0ZXIoeCA9PiBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCB8fCB4LmhpZGRlbk1vZGUgIT09IFwicHJvZHVjdGlvblwiKTtcbiAgZm9yIChjb25zdCBhIG9mIGFycikge1xuICAgIGEuY2hpbGRyZW4gPSByZW1vdmVIaWRkZW4oYS5jaGlsZHJlbiwgaXNIaWRkZW5GZWF0dXJlc0VuYWJsZWQpO1xuICB9XG4gIHJldHVybiBhcnI7XG59XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQThMLFNBQVMsb0JBQW9CO0FBQzNOLE9BQU8sV0FBVztBQUNsQixPQUFPLG1CQUFtQjtBQUMxQixPQUFPLFVBQVU7QUFDakIsT0FBTyxVQUFVOzs7QUNKa0wsU0FBUyxzQkFBc0IsS0FBSztBQUF2TztBQUNFLFFBQU0sVUFBVSxJQUFJLFFBQVEsb0JBQW9CLEdBQUcsRUFBRSxRQUFRLE1BQU0sR0FBRztBQUN0RSxRQUFNLGNBQWMsUUFBUSxRQUFRLG1DQUFtQyxRQUFRO0FBRWhGLFFBQU0sNEJBQ0gsYUFBUSxJQUFJLHFDQUFaLG1CQUE4QyxjQUFjLFlBQVcsWUFDdkUsYUFBUSxJQUFJLHFDQUFaLG1CQUE4QyxjQUFjLFlBQVc7QUFFekUsUUFBTSxPQUFPLGFBQWEsS0FBSyxNQUFNLFdBQVcsR0FBRyx1QkFBdUI7QUFFMUUsUUFBTSxjQUFjLEtBQUssVUFBVSxJQUFJO0FBRXZDLFNBQU8sb0JBQW9CLGNBQWM7QUFDM0M7QUFFQSxTQUFTLGFBQWEsV0FBVyx5QkFBeUI7QUFmMUQ7QUFnQkUsTUFBRyxDQUFDO0FBQVcsV0FBTztBQUN0QixRQUFNLE9BQU0sNENBQ1IsT0FBTyxPQUFLLENBQUMsRUFBRSxZQURQLG1CQUVSLE9BQU8sT0FBSywyQkFBMkIsRUFBRSxlQUFlO0FBQzVELGFBQVcsS0FBSyxLQUFLO0FBQ25CLE1BQUUsV0FBVyxhQUFhLEVBQUUsVUFBVSx1QkFBdUI7QUFBQSxFQUMvRDtBQUNBLFNBQU87QUFDVDs7O0FEeEJBLElBQU0sbUNBQW1DO0FBUXpDLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQzFCLFdBQVc7QUFBQSxFQUNYLFNBQVM7QUFBQSxJQUNQLE1BQU07QUFBQSxJQUNOLGNBQWM7QUFBQSxJQUNkLEtBQUs7QUFBQSxJQUNMO0FBQUEsTUFDRSxNQUFNO0FBQUEsTUFDTixXQUFXLENBQUMsS0FBSyxPQUFPO0FBQ3RCLFlBQUcsQ0FBQyxHQUFHLFNBQVMsc0JBQXNCO0FBQ3BDLGlCQUFPO0FBQ1QsZUFBTyxzQkFBc0IsR0FBRztBQUFBLE1BQ2xDO0FBQUEsSUFDRjtBQUFBLEVBQ0Y7QUFBQSxFQUNBLE1BQU07QUFBQSxFQUNOLE9BQU87QUFBQSxJQUNMLFFBQVE7QUFBQSxJQUNSLFFBQVE7QUFBQSxJQUNSLGFBQWE7QUFBQSxFQUNmO0FBQUEsRUFDQSxRQUFRO0FBQUEsSUFDTixTQUFTO0FBQUEsTUFDUCxHQUFJLFFBQVEsSUFBSSxpQkFBaUI7QUFBQSxRQUMvQiwyQkFBMkIsUUFBUSxJQUFJO0FBQUEsTUFDekM7QUFBQSxJQUNGO0FBQUEsSUFDQSxjQUFjLENBQUMsaUNBQWlDLGFBQWEsV0FBVztBQUFBLElBQ3hFLE9BQU87QUFBQSxNQUNMLG9CQUFvQjtBQUFBLFFBQ2xCLFFBQVE7QUFBQSxRQUNSLGNBQWM7QUFBQSxRQUNkLFNBQVMsQ0FBQ0EsVUFBU0EsTUFBSyxRQUFRLHNCQUFzQixFQUFFO0FBQUEsTUFDMUQ7QUFBQSxJQUNGO0FBQUEsRUFDRjtBQUFBLEVBQ0EsU0FBUztBQUFBLElBQ1AsT0FBTztBQUFBLE1BQ0wsZ0JBQWdCLEtBQUssUUFBUSxrQ0FBVywwQkFBMEI7QUFBQSxNQUNsRSxLQUFLLEdBQUcsS0FBSyxRQUFRLGtDQUFXLE9BQU8sQ0FBQztBQUFBLElBQzFDO0FBQUEsRUFDRjtBQUNGLENBQUM7IiwKICAibmFtZXMiOiBbInBhdGgiXQp9Cg== diff --git a/GUI/vite.config.ts.timestamp-1768278833602-e10c19bbae925.mjs b/GUI/vite.config.ts.timestamp-1768278833602-e10c19bbae925.mjs new file mode 100644 index 0000000..3ffe592 --- /dev/null +++ b/GUI/vite.config.ts.timestamp-1768278833602-e10c19bbae925.mjs @@ -0,0 +1,77 @@ +// vite.config.ts +import { defineConfig } from "file:///app/node_modules/vite/dist/node/index.js"; +import react from "file:///app/node_modules/@vitejs/plugin-react/dist/index.mjs"; +import tsconfigPaths from "file:///app/node_modules/vite-tsconfig-paths/dist/index.mjs"; +import svgr from "file:///app/node_modules/vite-plugin-svgr/dist/index.mjs"; +import path from "path"; + +// vitePlugin.js +function removeHiddenMenuItems(str) { + var _a, _b; + const badJson = str.replace("export default [", "[").replace("];", "]"); + const correctJson = badJson.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": '); + const isHiddenFeaturesEnabled = ((_a = process.env.REACT_APP_ENABLE_HIDDEN_FEATURES) == null ? void 0 : _a.toLowerCase().trim()) === "true" || ((_b = process.env.REACT_APP_ENABLE_HIDDEN_FEATURES) == null ? void 0 : _b.toLowerCase().trim()) === "1"; + const json = removeHidden(JSON.parse(correctJson), isHiddenFeaturesEnabled); + const updatedJson = JSON.stringify(json); + return "export default " + updatedJson + ";"; +} +function removeHidden(menuItems, isHiddenFeaturesEnabled) { + var _a; + if (!menuItems) + return menuItems; + const arr = (_a = menuItems == null ? void 0 : menuItems.filter((x) => !x.hidden)) == null ? void 0 : _a.filter((x) => isHiddenFeaturesEnabled || x.hiddenMode !== "production"); + for (const a of arr) { + a.children = removeHidden(a.children, isHiddenFeaturesEnabled); + } + return arr; +} + +// vite.config.ts +var __vite_injected_original_dirname = "/app"; +var vite_config_default = defineConfig({ + envPrefix: "REACT_APP_", + plugins: [ + react(), + tsconfigPaths(), + svgr(), + { + name: "removeHiddenMenuItemsPlugin", + transform: (str, id) => { + if (!id.endsWith("/menu-structure.json")) + return str; + return removeHiddenMenuItems(str); + } + } + ], + base: "/rag-search", + build: { + outDir: "./build", + target: "es2015", + emptyOutDir: true + }, + server: { + headers: { + ...process.env.REACT_APP_CSP && { + "Content-Security-Policy": process.env.REACT_APP_CSP + } + }, + allowedHosts: ["est-rag-rtc.rootcode.software", "localhost", "127.0.0.1"], + proxy: { + "/vault-agent-gui": { + target: "http://vault-agent-gui:8202", + changeOrigin: true, + rewrite: (path2) => path2.replace(/^\/vault-agent-gui/, "") + } + } + }, + resolve: { + alias: { + "~@fontsource": path.resolve(__vite_injected_original_dirname, "node_modules/@fontsource"), + "@": `${path.resolve(__vite_injected_original_dirname, "./src")}` + } + } +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiLCAidml0ZVBsdWdpbi5qcyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9hcHBcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9hcHAvdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2FwcC92aXRlLmNvbmZpZy50c1wiO2ltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGUnO1xuaW1wb3J0IHJlYWN0IGZyb20gJ0B2aXRlanMvcGx1Z2luLXJlYWN0JztcbmltcG9ydCB0c2NvbmZpZ1BhdGhzIGZyb20gJ3ZpdGUtdHNjb25maWctcGF0aHMnO1xuaW1wb3J0IHN2Z3IgZnJvbSAndml0ZS1wbHVnaW4tc3Zncic7XG5pbXBvcnQgcGF0aCBmcm9tICdwYXRoJztcbmltcG9ydCB7IHJlbW92ZUhpZGRlbk1lbnVJdGVtcyB9IGZyb20gJy4vdml0ZVBsdWdpbic7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBlbnZQcmVmaXg6ICdSRUFDVF9BUFBfJyxcbiAgcGx1Z2luczogW1xuICAgIHJlYWN0KCksXG4gICAgdHNjb25maWdQYXRocygpLFxuICAgIHN2Z3IoKSxcbiAgICB7XG4gICAgICBuYW1lOiAncmVtb3ZlSGlkZGVuTWVudUl0ZW1zUGx1Z2luJyxcbiAgICAgIHRyYW5zZm9ybTogKHN0ciwgaWQpID0+IHtcbiAgICAgICAgaWYoIWlkLmVuZHNXaXRoKCcvbWVudS1zdHJ1Y3R1cmUuanNvbicpKVxuICAgICAgICAgIHJldHVybiBzdHI7XG4gICAgICAgIHJldHVybiByZW1vdmVIaWRkZW5NZW51SXRlbXMoc3RyKTtcbiAgICAgIH0sXG4gICAgfSxcbiAgXSxcbiAgYmFzZTogJy9yYWctc2VhcmNoJyxcbiAgYnVpbGQ6IHtcbiAgICBvdXREaXI6ICcuL2J1aWxkJyxcbiAgICB0YXJnZXQ6ICdlczIwMTUnLFxuICAgIGVtcHR5T3V0RGlyOiB0cnVlLFxuICB9LFxuICBzZXJ2ZXI6IHtcbiAgICBoZWFkZXJzOiB7XG4gICAgICAuLi4ocHJvY2Vzcy5lbnYuUkVBQ1RfQVBQX0NTUCAmJiB7XG4gICAgICAgICdDb250ZW50LVNlY3VyaXR5LVBvbGljeSc6IHByb2Nlc3MuZW52LlJFQUNUX0FQUF9DU1AsXG4gICAgICB9KSxcbiAgICB9LFxuICAgIGFsbG93ZWRIb3N0czogWydlc3QtcmFnLXJ0Yy5yb290Y29kZS5zb2Z0d2FyZScsICdsb2NhbGhvc3QnLCAnMTI3LjAuMC4xJ10sXG4gICAgcHJveHk6IHtcbiAgICAgICcvdmF1bHQtYWdlbnQtZ3VpJzoge1xuICAgICAgICB0YXJnZXQ6ICdodHRwOi8vdmF1bHQtYWdlbnQtZ3VpOjgyMDInLFxuICAgICAgICBjaGFuZ2VPcmlnaW46IHRydWUsXG4gICAgICAgIHJld3JpdGU6IChwYXRoKSA9PiBwYXRoLnJlcGxhY2UoL15cXC92YXVsdC1hZ2VudC1ndWkvLCAnJyksXG4gICAgICB9LFxuICAgIH0sXG4gIH0sXG4gIHJlc29sdmU6IHtcbiAgICBhbGlhczoge1xuICAgICAgJ35AZm9udHNvdXJjZSc6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsICdub2RlX21vZHVsZXMvQGZvbnRzb3VyY2UnKSxcbiAgICAgICdAJzogYCR7cGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgJy4vc3JjJyl9YCxcbiAgICB9LFxuICB9LFxufSk7XG4iLCAiY29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2Rpcm5hbWUgPSBcIi9hcHBcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9hcHAvdml0ZVBsdWdpbi5qc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vYXBwL3ZpdGVQbHVnaW4uanNcIjtleHBvcnQgZnVuY3Rpb24gcmVtb3ZlSGlkZGVuTWVudUl0ZW1zKHN0cikge1xuICBjb25zdCBiYWRKc29uID0gc3RyLnJlcGxhY2UoJ2V4cG9ydCBkZWZhdWx0IFsnLCAnWycpLnJlcGxhY2UoJ107JywgJ10nKTtcbiAgY29uc3QgY29ycmVjdEpzb24gPSBiYWRKc29uLnJlcGxhY2UoLyhbJ1wiXSk/KFthLXowLTlBLVpfXSspKFsnXCJdKT86L2csICdcIiQyXCI6ICcpO1xuXG4gY29uc3QgaXNIaWRkZW5GZWF0dXJlc0VuYWJsZWQgPSBcbiAgICBwcm9jZXNzLmVudi5SRUFDVF9BUFBfRU5BQkxFX0hJRERFTl9GRUFUVVJFUz8udG9Mb3dlckNhc2UoKS50cmltKCkgPT09ICd0cnVlJyB8fFxuICAgIHByb2Nlc3MuZW52LlJFQUNUX0FQUF9FTkFCTEVfSElEREVOX0ZFQVRVUkVTPy50b0xvd2VyQ2FzZSgpLnRyaW0oKSA9PT0gJzEnO1xuXG4gIGNvbnN0IGpzb24gPSByZW1vdmVIaWRkZW4oSlNPTi5wYXJzZShjb3JyZWN0SnNvbiksIGlzSGlkZGVuRmVhdHVyZXNFbmFibGVkKTtcbiAgXG4gIGNvbnN0IHVwZGF0ZWRKc29uID0gSlNPTi5zdHJpbmdpZnkoanNvbik7XG5cbiAgcmV0dXJuICdleHBvcnQgZGVmYXVsdCAnICsgdXBkYXRlZEpzb24gKyAnOydcbn1cblxuZnVuY3Rpb24gcmVtb3ZlSGlkZGVuKG1lbnVJdGVtcywgaXNIaWRkZW5GZWF0dXJlc0VuYWJsZWQpIHtcbiAgaWYoIW1lbnVJdGVtcykgcmV0dXJuIG1lbnVJdGVtcztcbiAgY29uc3QgYXJyID0gbWVudUl0ZW1zXG4gICAgPy5maWx0ZXIoeCA9PiAheC5oaWRkZW4pXG4gICAgPy5maWx0ZXIoeCA9PiBpc0hpZGRlbkZlYXR1cmVzRW5hYmxlZCB8fCB4LmhpZGRlbk1vZGUgIT09IFwicHJvZHVjdGlvblwiKTtcbiAgZm9yIChjb25zdCBhIG9mIGFycikge1xuICAgIGEuY2hpbGRyZW4gPSByZW1vdmVIaWRkZW4oYS5jaGlsZHJlbiwgaXNIaWRkZW5GZWF0dXJlc0VuYWJsZWQpO1xuICB9XG4gIHJldHVybiBhcnI7XG59XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQThMLFNBQVMsb0JBQW9CO0FBQzNOLE9BQU8sV0FBVztBQUNsQixPQUFPLG1CQUFtQjtBQUMxQixPQUFPLFVBQVU7QUFDakIsT0FBTyxVQUFVOzs7QUNKa0wsU0FBUyxzQkFBc0IsS0FBSztBQUF2TztBQUNFLFFBQU0sVUFBVSxJQUFJLFFBQVEsb0JBQW9CLEdBQUcsRUFBRSxRQUFRLE1BQU0sR0FBRztBQUN0RSxRQUFNLGNBQWMsUUFBUSxRQUFRLG1DQUFtQyxRQUFRO0FBRWhGLFFBQU0sNEJBQ0gsYUFBUSxJQUFJLHFDQUFaLG1CQUE4QyxjQUFjLFlBQVcsWUFDdkUsYUFBUSxJQUFJLHFDQUFaLG1CQUE4QyxjQUFjLFlBQVc7QUFFekUsUUFBTSxPQUFPLGFBQWEsS0FBSyxNQUFNLFdBQVcsR0FBRyx1QkFBdUI7QUFFMUUsUUFBTSxjQUFjLEtBQUssVUFBVSxJQUFJO0FBRXZDLFNBQU8sb0JBQW9CLGNBQWM7QUFDM0M7QUFFQSxTQUFTLGFBQWEsV0FBVyx5QkFBeUI7QUFmMUQ7QUFnQkUsTUFBRyxDQUFDO0FBQVcsV0FBTztBQUN0QixRQUFNLE9BQU0sNENBQ1IsT0FBTyxPQUFLLENBQUMsRUFBRSxZQURQLG1CQUVSLE9BQU8sT0FBSywyQkFBMkIsRUFBRSxlQUFlO0FBQzVELGFBQVcsS0FBSyxLQUFLO0FBQ25CLE1BQUUsV0FBVyxhQUFhLEVBQUUsVUFBVSx1QkFBdUI7QUFBQSxFQUMvRDtBQUNBLFNBQU87QUFDVDs7O0FEeEJBLElBQU0sbUNBQW1DO0FBUXpDLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQzFCLFdBQVc7QUFBQSxFQUNYLFNBQVM7QUFBQSxJQUNQLE1BQU07QUFBQSxJQUNOLGNBQWM7QUFBQSxJQUNkLEtBQUs7QUFBQSxJQUNMO0FBQUEsTUFDRSxNQUFNO0FBQUEsTUFDTixXQUFXLENBQUMsS0FBSyxPQUFPO0FBQ3RCLFlBQUcsQ0FBQyxHQUFHLFNBQVMsc0JBQXNCO0FBQ3BDLGlCQUFPO0FBQ1QsZUFBTyxzQkFBc0IsR0FBRztBQUFBLE1BQ2xDO0FBQUEsSUFDRjtBQUFBLEVBQ0Y7QUFBQSxFQUNBLE1BQU07QUFBQSxFQUNOLE9BQU87QUFBQSxJQUNMLFFBQVE7QUFBQSxJQUNSLFFBQVE7QUFBQSxJQUNSLGFBQWE7QUFBQSxFQUNmO0FBQUEsRUFDQSxRQUFRO0FBQUEsSUFDTixTQUFTO0FBQUEsTUFDUCxHQUFJLFFBQVEsSUFBSSxpQkFBaUI7QUFBQSxRQUMvQiwyQkFBMkIsUUFBUSxJQUFJO0FBQUEsTUFDekM7QUFBQSxJQUNGO0FBQUEsSUFDQSxjQUFjLENBQUMsaUNBQWlDLGFBQWEsV0FBVztBQUFBLElBQ3hFLE9BQU87QUFBQSxNQUNMLG9CQUFvQjtBQUFBLFFBQ2xCLFFBQVE7QUFBQSxRQUNSLGNBQWM7QUFBQSxRQUNkLFNBQVMsQ0FBQ0EsVUFBU0EsTUFBSyxRQUFRLHNCQUFzQixFQUFFO0FBQUEsTUFDMUQ7QUFBQSxJQUNGO0FBQUEsRUFDRjtBQUFBLEVBQ0EsU0FBUztBQUFBLElBQ1AsT0FBTztBQUFBLE1BQ0wsZ0JBQWdCLEtBQUssUUFBUSxrQ0FBVywwQkFBMEI7QUFBQSxNQUNsRSxLQUFLLEdBQUcsS0FBSyxRQUFRLGtDQUFXLE9BQU8sQ0FBQztBQUFBLElBQzFDO0FBQUEsRUFDRjtBQUNGLENBQUM7IiwKICAibmFtZXMiOiBbInBhdGgiXQp9Cg== diff --git a/README.md b/README.md index d8e33a0..a577de1 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,16 @@ The **BYK-RAG Module** is part of the Burokratt ecosystem, designed to provide * - Admins can create "connections" and switch providers/models without downtime. - Models searchable via dropdown with cache-enabled indicators. +- **Enhanced Security with RSA Encryption** + - LLM credentials encrypted with RSA-4096 asymmetric encryption before storage. + - GUI encrypts using public key; CronManager decrypts with private key. + - Additional security layer beyond HashiCorp Vault's encryption. + - **Knowledge Base Integration** - Continuous sync with central knowledge base (CKB). - Last sync timestamp displayed in UI. - LLMs restricted to answering only from CKB content. - - “I don’t know” payload returned when confidence is low. + - "I don't know" payload returned when confidence is low. - **Citations & Transparency** - All responses are accompanied with **clear citations**. @@ -23,7 +28,7 @@ The **BYK-RAG Module** is part of the Burokratt ecosystem, designed to provide * - **Analytics & Monitoring** - External **Langfuse dashboard** for API usage, inference trends, cost analysis, and performance logs. - Agencies can configure cost alerts and view alerts via LLM Alerts UI. - - Logs integrated with **Grafana Loki**. + - Logs integrated with **Grafana Loki**. ### Storing Langfuse Secrets diff --git a/docker-compose.yml b/docker-compose.yml index f4a43b0..8a9d119 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -117,7 +117,6 @@ services: volumes: - ./DSL/Resql:/DSL - ./shared:/shared - - ./DSL/DatasetGenerator/output_datasets:/app/output_datasets networks: - bykstack @@ -128,7 +127,7 @@ services: - REACT_APP_RUUTER_API_URL=http://localhost:8086 - REACT_APP_RUUTER_PRIVATE_API_URL=http://localhost:8088 - REACT_APP_CUSTOMER_SERVICE_LOGIN=http://localhost:3004/et/dev-auth - - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:3004 http://localhost:3005 ws://localhost https://est-rag-rtc.rootcode.software; + - REACT_APP_CSP=upgrade-insecure-requests; default-src 'self'; font-src 'self' data:; img-src 'self' data:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; object-src 'none'; connect-src 'self' http://localhost:8086 http://localhost:8088 http://localhost:3004 http://localhost:3005 https://vault-agent-gui:8202 ws://localhost https://est-rag-rtc.rootcode.software; - DEBUG_ENABLED=true - CHOKIDAR_USEPOLLING=true - PORT=3001 @@ -174,25 +173,25 @@ services: cron-manager: container_name: cron-manager image: cron-manager-python:latest - user: "root" + user: root volumes: - ./DSL/CronManager/DSL:/DSL - ./DSL/CronManager/script:/app/scripts - ./src/vector_indexer:/app/src/vector_indexer + - ./src/utils/decrypt_vault_secrets.py:/app/src/utils/decrypt_vault_secrets.py:ro # Decryption utility (read-only) - cron_data:/app/data - shared-volume:/app/shared # Access to shared resources for cross-container coordination - ./datasets:/app/datasets # Direct access to datasets folder for diff identifier operations - ./grafana-configs/loki_logger.py:/app/src/vector_indexer/loki_logger.py - ./.env:/app/.env:ro - - vault-agent-token:/agent/out:ro # Mount vault token for accessing vault secrets environment: - server.port=9010 - PYTHONPATH=/app:/app/src/vector_indexer - - VAULT_ADDR=http://vault:8200 + - VAULT_AGENT_URL=http://vault-agent-cron:8203 ports: - 9010:8080 depends_on: - - vault-agent-llm + - vault-agent-cron networks: - bykstack @@ -444,10 +443,8 @@ services: - vault-data:/vault/file - ./vault/config:/vault/config:ro - ./vault/logs:/vault/logs - expose: - - "8200" networks: - - bykstack + - vault-network # Only on vault-network for security restart: unless-stopped healthcheck: test: ["CMD", "sh", "-c", "wget -q -O- http://127.0.0.1:8200/v1/sys/health || exit 0"] @@ -468,14 +465,74 @@ services: volumes: - vault-data:/vault/data - vault-agent-creds:/agent/credentials - - vault-agent-token:/agent/out + - vault-agent-gui-token:/agent/gui-token + - vault-agent-cron-token:/agent/cron-token + - vault-agent-llm-token:/agent/llm-token - ./vault-init.sh:/vault-init.sh:ro networks: - - bykstack + - vault-network # Access vault + - bykstack # Access to write agent tokens entrypoint: ["/bin/sh"] - command: ["-c", "apk add --no-cache curl jq && chmod -R 755 /agent/credentials && chmod -R 770 /agent/out && chown -R vault:vault /agent/credentials /agent/out && su vault -s /bin/sh /vault-init.sh"] + command: + - -c + - | + apk add --no-cache curl jq uuidgen openssl + # Create and set permissions for all agent directories + mkdir -p /agent/credentials /agent/gui-token /agent/cron-token /agent/llm-token /agent/out + chown -R vault:vault /agent/credentials /agent/gui-token /agent/cron-token /agent/llm-token /agent/out + chmod 755 /agent/credentials /agent/gui-token /agent/cron-token /agent/llm-token /agent/out + # Run vault initialization as vault user + su vault -s /bin/sh /vault-init.sh restart: "no" + vault-agent-gui: + image: hashicorp/vault:1.20.3 + container_name: vault-agent-gui + command: ["vault", "agent", "-config=/agent/config/gui-agent.hcl", "-log-level=info"] + depends_on: + vault-init: + condition: service_completed_successfully + cap_add: + - IPC_LOCK + volumes: + - ./vault/agents/gui/gui-agent.hcl:/agent/config/gui-agent.hcl:ro + - vault-agent-creds:/agent/credentials:ro + - vault-agent-gui-token:/agent/gui-token + networks: + - vault-network # Access vault + - bykstack # Accessible by GUI service + restart: unless-stopped + healthcheck: + test: ["CMD", "sh", "-c", "test -f /agent/gui-token/token && test -s /agent/gui-token/token"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 5s + + vault-agent-cron: + image: hashicorp/vault:1.20.3 + container_name: vault-agent-cron + command: ["vault", "agent", "-config=/agent/config/cron-agent.hcl", "-log-level=info"] + depends_on: + vault-init: + condition: service_completed_successfully + cap_add: + - IPC_LOCK + volumes: + - ./vault/agents/cron/cron-agent.hcl:/agent/config/cron-agent.hcl:ro + - vault-agent-creds:/agent/credentials:ro + - vault-agent-cron-token:/agent/cron-token + networks: + - vault-network # Access vault + - bykstack # Accessible by CronManager service + restart: unless-stopped + healthcheck: + test: ["CMD", "sh", "-c", "test -f /agent/cron-token/token && test -s /agent/cron-token/token"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 5s + vault-agent-llm: image: hashicorp/vault:1.20.3 container_name: vault-agent-llm @@ -488,10 +545,17 @@ services: volumes: - ./vault/agents/llm/agent.hcl:/agent/config/agent.hcl:ro - vault-agent-creds:/agent/credentials:ro - - vault-agent-token:/agent/out + - vault-agent-llm-token:/agent/llm-token networks: - - bykstack + - vault-network # Access vault + - bykstack # Accessible by LLM service restart: unless-stopped + healthcheck: + test: ["CMD", "sh", "-c", "test -f /agent/llm-token/token && test -s /agent/llm-token/token"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 5s # LLM Orchestration Service llm-orchestration-service: @@ -506,17 +570,15 @@ services: - .env environment: - ENVIRONMENT=production - - VAULT_ADDR=http://vault:8200 - - VAULT_TOKEN=/agent/out/token + - VAULT_ADDR=http://vault-agent-llm:8201 + # VAULT_TOKEN not set - vault-agent-llm proxy handles authentication volumes: - ./src/llm_config_module/config:/app/src/llm_config_module/config:ro - ./src/optimization/optimized_modules:/app/src/optimization/optimized_modules - llm_orchestration_logs:/app/logs - - vault-agent-token:/agent/out:ro networks: - bykstack depends_on: - - vault - vault-agent-llm healthcheck: test: ["CMD", "curl", "-f", "http://llm-orchestration-service:8100/health"] @@ -602,12 +664,20 @@ volumes: name: cron_data vault-agent-creds: name: vault-agent-creds - vault-agent-token: - name: vault-agent-token + vault-agent-gui-token: + name: vault-agent-gui-token + vault-agent-cron-token: + name: vault-agent-cron-token + vault-agent-llm-token: + name: vault-agent-llm-token opensearch-data: name: opensearch-data networks: bykstack: name: bykstack - driver: bridge \ No newline at end of file + driver: bridge + vault-network: + name: vault-network + driver: bridge + internal: true # No external access - isolated network \ No newline at end of file diff --git a/docs/VAULT_SECURITY_ARCHITECTURE.md b/docs/VAULT_SECURITY_ARCHITECTURE.md new file mode 100644 index 0000000..fe6fd74 --- /dev/null +++ b/docs/VAULT_SECURITY_ARCHITECTURE.md @@ -0,0 +1,1320 @@ +# Vault Security Architecture + +## Overview + +This document provides a technical deep dive into the HashiCorp Vault security architecture implemented in the RAG-Module. The design follows a defense-in-depth strategy with multiple security layers to protect sensitive credentials used by LLM providers (AWS Bedrock, Azure OpenAI) and embedding services. + +### Security Principles + +1. **Zero Trust Network**: No service has direct access to Vault server +2. **Least Privilege**: Each service gets only the minimum required permissions +3. **Defense in Depth**: Multiple security layers (network, authentication, authorization) +4. **Secure by Default**: Deny-all policies with explicit allow rules +5. **Credential Isolation**: Secrets never exposed in environment variables or logs + +--- + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Docker Network: bykstack │ +│ (Application Layer - No Direct Vault Access) │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ GUI │ │ CronManager │ │ LLM Service │ │ +│ │ (Frontend) │ │ (Worker) │ │ Orchestrator │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ +│ │ :8202 │ :8203 │ :8201 │ +│ ▼ ▼ ▼ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ vault-agent │ │ vault-agent │ │ vault-agent │ │ +│ │ -gui │ │ -cron │ │ -llm │ │ +│ │ (Proxy) │ │ (Proxy) │ │ (Proxy) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ +│ └───────────────────┴────────────────────┘ │ +│ │ │ +└─────────────────────────────┼─────────────────────────────────────────┘ + │ + │ Secured Connection + │ +┌─────────────────────────────▼─────────────────────────────────────────┐ +│ Docker Network: vault-network │ +│ (Internal Only - No External Access) │ +│ │ +│ ┌──────────────────┐ │ +│ │ Vault Server │ │ +│ │ (Core Vault) │ │ +│ │ Port: 8200 │ │ +│ └──────────────────┘ │ +│ │ +│ - KV v2 Secrets Engine │ +│ - AppRole Auth Method │ +│ - Policy Enforcement │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Network Security & Isolation + +### Dual-Network Architecture + +The system uses two isolated Docker networks to create a security boundary: + +#### 1. **vault-network** (Internal Network) +- **Purpose**: Vault core server isolation +- **Access**: Only Vault server and Vault agents +- **Configuration**: `internal: true` (no external routing) +- **Security Benefit**: Vault server is completely unreachable from outside containers + +#### 2. **bykstack** (Application Network) +- **Purpose**: Application services communication +- **Access**: All application containers and Vault agents +- **Security Benefit**: Applications can only reach Vault agents, never Vault directly + +### Why This Matters + +``` + Without Network Isolation: + App → Vault (direct access with token) + Risk: Token compromise = full Vault access + + With Network Isolation: + App → Vault Agent → Vault + Benefit: Agent handles auth, app never sees token +``` + +### Port Exposure Strategy + +| Service | Port | Network | Exposed to Host | Purpose | +|---------|------|---------|-----------------|---------| +| Vault Server | 8200 | vault-network | No | Core secrets storage | +| vault-agent-gui | 8202 | bykstack | No | GUI proxy | +| vault-agent-cron | 8203 | bykstack | No | CronManager proxy | +| vault-agent-llm | 8201 | bykstack | No | LLM service proxy | + +**Security Principle**: No Vault-related ports are exposed to the host machine, preventing external attacks. + +--- + +## Authentication Layer + +### AppRole Authentication Method + +Vault uses **AppRole** authentication - a machine-oriented authentication method designed for automated workflows. + +#### How AppRole Works + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ AppRole Authentication Flow │ +└─────────────────────────────────────────────────────────────────┘ + +1. Initialization Phase (vault-init container): + + vault-init + │ + ├─► Creates AppRole: "gui-service" + ├─► Creates AppRole: "cron-manager-service" + ├─► Creates AppRole: "llm-orchestration-service" + │ + └─► Generates credentials: + - role_id (static identifier) + - secret_id (secret credential, renewable) + +2. Credential Storage: + + Credentials written to shared Docker volumes: + /agent/credentials/gui_role_id + /agent/credentials/gui_secret_id + /agent/credentials/cron_role_id + /agent/credentials/cron_secret_id + /agent/credentials/llm_role_id + /agent/credentials/llm_secret_id + +3. Vault Agent Authentication: + + vault-agent-gui + │ + ├─► Reads: role_id + secret_id + ├─► Authenticates with Vault + └─► Receives: Vault token (automatically renewed) + +4. Token Management: + + vault-agent caches token and handles: + - Automatic renewal before expiration + - Token rotation on renewal failure + - Transparent injection into API requests +``` + +### Role-Based Identity Management + +Each service gets its own isolated identity: + +1. **gui-service AppRole** + - Identity: Frontend application + - Policy: gui-policy + - Permissions: Read encryption public key only + +2. **cron-manager-service AppRole** + - Identity: Background worker/scheduler + - Policy: cron-manager-policy + - Permissions: Full CRUD on secrets + encryption key access + +3. **llm-orchestration-service AppRole** + - Identity: LLM request handler + - Policy: llm-orchestration-policy + - Permissions: Read-only access to connection credentials + +### Credential Lifecycle + +``` +Timeline: Credential Generation and Rotation + +Day 0 (Initial Setup): + vault-init: Generate role_id + secret_id + ↓ + Write to: /agent/credentials/ + ↓ + vault-agent: Authenticate with Vault + ↓ + Receive: Token (TTL: 1 hour, renewable) + +Day 0+: Automatic Token Renewal: + vault-agent: Monitor token expiration + ↓ + Before expiry: Request token renewal + ↓ + Vault: Extend token lifetime (1 hour) + ↓ + Repeat: Continuous renewal cycle + +Container Restart: + vault-init: Check if Vault is sealed + ↓ + If unsealed: Regenerate secret_id only + ↓ + vault-agent: Re-authenticate with new secret_id + ↓ + New token issued and cached +``` + +**Security Benefit**: Short-lived tokens with automatic rotation limit the damage from token compromise. + +--- + +## Authorization & Policy Model + +### Policy-Based Access Control (PBAC) + +Vault enforces authorization through **policies** - declarative rules that define what each identity can access. + +#### Policy Architecture + +``` +┌────────────────────────────────────────────────────────────────┐ +│ Vault Policy Layer │ +└────────────────────────────────────────────────────────────────┘ + +Token (issued to vault-agent-gui) + │ + ├─► Associated Policy: "gui-policy" + │ + └─► Allowed Paths: + secret/data/encryption/public_key (read, list) + secret/data/encryption/private_key (denied) + secret/data/llm/connections/* (denied) + secret/data/embeddings/connections/* (denied) + + +Token (issued to vault-agent-cron) + │ + ├─► Associated Policy: "cron-manager-policy" + │ + └─► Allowed Paths: + secret/data/llm/connections/* (create, read, update, delete, list) + secret/data/embeddings/connections/* (create, read, update, delete, list) + secret/data/encryption/public_key (read, list) + secret/data/encryption/private_key (read, list) + + +Token (issued to vault-agent-llm) + │ + ├─► Associated Policy: "llm-orchestration-policy" + │ + └─► Allowed Paths: + secret/data/llm/connections/* (read, list) + secret/data/embeddings/connections/* (read, list) + secret/data/encryption/* (explicitly denied) +``` + +### Three-Tier Policy Structure + +#### Tier 1: GUI Policy (Minimal Permissions) +**Purpose**: Allow frontend to encrypt user input + +**Permissions**: +- Read: `secret/data/encryption/public_key` +- List: `secret/metadata/encryption/public_key` + +**Denied**: +- All other paths (deny-by-default) + +**Use Case**: Frontend fetches public key to encrypt sensitive credentials (API keys, access keys) before sending to backend. + +--- + +#### Tier 2: CronManager Policy (Secret Management) +**Purpose**: Write secrets to Vault and decrypt sensitive data + +**Permissions**: +- Create/Read/Update/Delete: `secret/data/llm/connections/*` +- Create/Read/Update/Delete: `secret/data/embeddings/connections/*` +- List: `secret/metadata/llm/connections/*` +- List: `secret/metadata/embeddings/connections/*` +- Read: `secret/data/encryption/public_key` (for verification) +- Read: `secret/data/encryption/private_key` (for decryption) + +**Use Case**: Receives encrypted credentials from GUI, decrypts them using private key, stores plaintext in Vault. + +--- + +#### Tier 3: LLM Orchestration Policy (Read-Only) +**Purpose**: Retrieve credentials to make LLM API calls + +**Permissions**: +- Read: `secret/data/llm/connections/*` +- Read: `secret/data/embeddings/connections/*` +- List: `secret/metadata/llm/connections/*` +- List: `secret/metadata/embeddings/connections/*` + +**Explicitly Denied**: +- Deny: `secret/data/encryption/*` (no access to encryption keys) + +**Use Case**: Fetch AWS/Azure credentials to authenticate with LLM providers. + +--- + +### Wildcard Path Patterns + +Policies use **glob-style wildcards** (`*`) to match nested paths: + +``` +Pattern: secret/data/llm/connections/* + +Matches: + secret/data/llm/connections/azure_openai/testing/14 + secret/data/llm/connections/azure_openai/production/gpt-4o + secret/data/llm/connections/aws_bedrock/testing/42 + secret/data/llm/connections/aws_bedrock/production/claude-3-sonnet + +Does NOT match: + secret/data/embeddings/connections/... + secret/data/encryption/... +``` + +**Security Benefit**: One policy rule covers all current and future connection secrets without requiring policy updates. + +### Deny-by-Default Security + +``` +Request Flow with Policy Enforcement: + +vault-agent-llm sends request: + GET /v1/secret/data/encryption/private_key + +Vault Policy Engine: + 1. Check token's associated policies: "llm-orchestration-policy" + 2. Evaluate rules in policy: + - Path: secret/data/encryption/* + - Capability: deny + 3. Decision: DENIED + +Response: HTTP 403 Forbidden + + +vault-agent-cron sends request: + GET /v1/secret/data/encryption/private_key + +Vault Policy Engine: + 1. Check token's associated policies: "cron-manager-policy" + 2. Evaluate rules in policy: + - Path: secret/data/encryption/* + - Capability: read + 3. Decision: ALLOWED + +Response: HTTP 200 OK + secret data +``` + +--- + +## Vault Agent Architecture + +### Why Vault Agents? + +Traditional approach has security risks: + +``` + Direct Vault Access (Insecure): + +App Container + │ env: VAULT_TOKEN=test_token_hvs.abc123xyz... + │ env: VAULT_ADDR=http://vault:8200 + │ + └─► Direct HTTP call to Vault + - Token in environment (logged, visible) + - Token in memory dumps + - Token in application code + - No automatic renewal +``` + +Vault Agent proxy pattern solves these issues: + +``` + Vault Agent Proxy (Secure): + +App Container + │ env: VAULT_ADDR=http://vault-agent-llm:8201 + │ (NO VAULT_TOKEN variable) + │ + └─► HTTP call to Vault Agent (no token) + │ + └─► Vault Agent injects token automatically + │ + └─► Vault validates token + │ + └─► Returns secret +``` + +### Proxy Pattern Benefits + +1. **Token Isolation**: Application never sees or handles tokens +2. **Automatic Renewal**: Agent manages token lifecycle transparently +3. **Simplified Code**: No token management logic in application +4. **Audit Trail**: All requests logged through agent layer +5. **Single Point of Auth**: Centralized authentication handling + +### Three-Agent Deployment + +#### Agent 1: vault-agent-gui +``` +Configuration: + - Listens: 0.0.0.0:8202 (within bykstack network) + - Auth: AppRole (gui-service) + - Policy: gui-policy + - Token Cache: /agent/gui-token/token + +Connected Services: + - GUI (React Frontend) + +Token Lifecycle: + - Default Lease: 768h (32 days) + - Auto-renewal: Before expiration +``` + +#### Agent 2: vault-agent-cron +``` +Configuration: + - Listens: 0.0.0.0:8203 (within bykstack network) + - Auth: AppRole (cron-manager-service) + - Policy: cron-manager-policy + - Token Cache: /agent/cron-token/token + +Connected Services: + - CronManager (Python worker) + +Token Lifecycle: + - Default Lease: 768h (32 days) + - Auto-renewal: Before expiration +``` + +#### Agent 3: vault-agent-llm +``` +Configuration: + - Listens: 0.0.0.0:8201 (within bykstack network) + - Auth: AppRole (llm-orchestration-service) + - Policy: llm-orchestration-policy + - Token Cache: /agent/llm-token/token + +Connected Services: + - LLM Orchestration Service (FastAPI) + +Token Lifecycle: + - Default Lease: 1h (shorter for higher security) + - Auto-renewal: Every ~45 minutes +``` + +### Token Caching and Auto-Renewal + +``` +Vault Agent Token Management Cycle: + +┌─────────────────────────────────────────────────────────────┐ +│ Token Lifecycle │ +└─────────────────────────────────────────────────────────────┘ + +T=0: Initial Authentication + vault-agent reads credentials + │ + ├─► POST /v1/auth/approle/login + │ Body: { role_id, secret_id } + │ + └─► Receives: { token, ttl: 3600s, renewable: true } + │ + └─► Cache token in: /agent/llm-token/token + + +T=45min: Proactive Renewal (75% of TTL) + vault-agent monitors expiration + │ + ├─► POST /v1/auth/token/renew-self + │ Header: X-Vault-Token: + │ + └─► Receives: { token, ttl: 3600s } (same token, extended) + │ + └─► Update cache: /agent/llm-token/token + + +T=59min: Renewal Failed (fallback) + If renewal fails: + │ + ├─► Re-authenticate from scratch + │ POST /v1/auth/approle/login + │ + └─► New token issued and cached + + +Application Request (anytime): + App sends: GET http://vault-agent-llm:8201/v1/secret/data/llm/... + │ + ├─► vault-agent intercepts request + ├─► Injects header: X-Vault-Token: + ├─► Forwards to: http://vault:8200/v1/secret/data/llm/... + │ + └─► Returns response to application +``` + +### Transparent Authentication Injection + +When an application makes a request: + +``` +Step 1: Application Code (No Token) +-------- +import requests +response = requests.get( + "http://vault-agent-llm:8201/v1/secret/data/llm/connections/azure_openai/testing/14" +) +# Note: No X-Vault-Token header sent! + + +Step 2: Vault Agent Intercepts +-------- +vault-agent-llm receives request: + - Request headers: { User-Agent: python-requests/2.31.0 } + - No token present + + +Step 3: Agent Adds Authentication +-------- +vault-agent-llm modifies request: + - Add header: X-Vault-Token: vault_token_test + - Forward to: http://vault:8200/v1/secret/data/llm/connections/azure_openai/testing/14 + + +Step 4: Vault Validates +-------- +Vault server: + - Checks token validity + - Looks up associated policy: llm-orchestration-policy + - Evaluates path permission: secret/data/llm/connections/* → ALLOWED + - Returns secret data + + +Step 5: Agent Forwards Response +-------- +vault-agent-llm returns to application: + - Status: 200 OK + - Body: { "data": { "data": { "endpoint": "...", "api_key": "..." } } } + + +Application receives response: + - Thinks it talked directly to Vault + - Never handled or saw the token +``` + +--- + +## Secret Storage Strategy + +### KV v2 Secrets Engine + +Vault uses the **Key-Value version 2** (KV v2) secrets engine, which provides: + +1. **Versioning**: Every secret write creates a new version +2. **Audit Trail**: Track who changed what and when +3. **Rollback**: Restore previous versions if needed +4. **Soft Delete**: Deleted secrets can be recovered +5. **Metadata**: Store additional context (tags, timestamps) + +### Path Hierarchy and Organization + +``` +vault (root) +└── secret/ (KV v2 mount point) + ├── llm/ + │ └── connections/ + │ ├── azure_openai/ + │ │ ├── testing/ + │ │ │ ├── 14 → { connection_id, endpoint, api_key, deployment_name } + │ │ │ ├── 15 → { ... } + │ │ │ └── 16 → { ... } + │ │ └── production/ + │ │ ├── gpt-4o → { connection_id, endpoint, api_key, ... } + │ │ └── gpt-4o-mini → { ... } + │ └── aws_bedrock/ + │ ├── testing/ + │ │ └── 14 → { connection_id, access_key, secret_key } + │ └── production/ + │ ├── claude-3-sonnet → { ... } + │ └── claude-3-opus → { ... } + │ + ├── embeddings/ + │ └── connections/ + │ ├── azure_openai/ + │ │ └── testing/ + │ │ └── 14 → { connection_id, endpoint, api_key, model } + │ └── aws_bedrock/ + │ └── testing/ + │ └── 14 → { connection_id, access_key, secret_key, model } + │ + └── encryption/ + ├── public_key → { key: "-----BEGIN PUBLIC KEY-----...", algorithm: "RSA-OAEP", ... } + └── private_key → { key: "-----BEGIN PRIVATE KEY-----...", algorithm: "RSA-OAEP", ... } +``` + +### Path Structure Logic + +**Testing Environment:** +``` +Pattern: secret/llm/connections/{platform}/{environment}/{connection_id} +Example: secret/llm/connections/azure_openai/testing/14 + +Why connection_id? + - Multiple test configurations per platform + - Easy to create/delete during development + - Unique identifier for each test setup +``` + +**Production Environment:** +``` +Pattern: secret/llm/connections/{platform}/{environment}/{model} +Example: secret/llm/connections/azure_openai/production/gpt-4o + +Why model name? + - One canonical credential per model + - Predictable path for application lookups + - Clear naming convention +``` + +### Version Control and Audit Trail + +KV v2 automatically versions every write: + +``` +Example: Updating Azure API Key + +Version 1 (Initial): + Path: secret/data/llm/connections/azure_openai/testing/14 + Data: { + "connection_id": "14", + "endpoint": "https://xxx.openai.azure.com/", + "api_key": "old-key-abc123", + "deployment_name": "gpt-4o-deployment" + } + Metadata: { + "version": 1, + "created_time": "2026-01-08T10:30:00Z", + "created_by": "cron-manager-service" + } + + +Version 2 (After Key Rotation): + Path: secret/data/llm/connections/azure_openai/testing/14 + Data: { + "connection_id": "14", + "endpoint": "https://xxx.openai.azure.com/", + "api_key": "new-key-xyz789", ← Updated + "deployment_name": "gpt-4o-deployment" + } + Metadata: { + "version": 2, + "created_time": "2026-01-09T14:20:00Z", + "created_by": "cron-manager-service" + } + + +Accessing Versions: + - Latest: GET /v1/secret/data/llm/connections/azure_openai/testing/14 + - Version 1: GET /v1/secret/data/llm/connections/azure_openai/testing/14?version=1 + - Version 2: GET /v1/secret/data/llm/connections/azure_openai/testing/14?version=2 +``` + +**Security Benefit**: Full audit trail of credential changes with rollback capability. + +--- + +## Access Control Matrix + +### Service-to-Secret Mapping + +| Service | LLM Connections | Embedding Connections | Public Key | Private Key | Token Lookup | +|---------|-----------------|----------------------|------------|-------------|--------------| +| **GUI (Frontend)** | Denied | Denied | Read | Denied | Read | +| **CronManager** | Full CRUD | Full CRUD | Read | Read | Read | +| **LLM Service** | Read | Read | Denied | Denied | Read | + +### Permission Boundaries + +#### GUI Service +``` + Allowed Actions: + - GET secret/data/encryption/public_key + - LIST secret/metadata/encryption/public_key + - GET auth/token/lookup-self (verify own token) + + Denied Actions: + - Any operation on secret/data/llm/* + - Any operation on secret/data/embeddings/* + - GET secret/data/encryption/private_key + - Any write operations + +Use Case Flow: + 1. User enters API key in frontend + 2. Frontend fetches public key from Vault + 3. Frontend encrypts API key with public key + 4. Sends encrypted data to backend +``` + +#### CronManager Service +``` + Allowed Actions: + - CREATE/READ/UPDATE/DELETE secret/data/llm/connections/* + - CREATE/READ/UPDATE/DELETE secret/data/embeddings/connections/* + - LIST secret/metadata/llm/connections/* + - LIST secret/metadata/embeddings/connections/* + - GET secret/data/encryption/public_key + - GET secret/data/encryption/private_key + - GET auth/token/lookup-self + + Denied Actions: + - Modify encryption keys (read-only) + - Access secrets outside defined paths + +Use Case Flow: + 1. Receives encrypted credentials from frontend + 2. Fetches private key from Vault + 3. Decrypts credentials locally + 4. Stores plaintext credentials in Vault + 5. Returns success/failure to frontend +``` + +#### LLM Orchestration Service +``` + Allowed Actions: + - GET secret/data/llm/connections/* + - GET secret/data/embeddings/connections/* + - LIST secret/metadata/llm/connections/* + - LIST secret/metadata/embeddings/connections/* + - GET auth/token/lookup-self + + Denied Actions (Explicit Deny): + - Any operation on secret/data/encryption/* (cannot access keys) + - Any write operations on secrets + - Any access to secrets outside defined paths + +Use Case Flow: + 1. Receives LLM request from user + 2. Determines required LLM provider (AWS/Azure) + 3. Fetches connection credentials from Vault + 4. Makes authenticated API call to LLM provider + 5. Returns LLM response to user +``` + +### Least Privilege Enforcement + +``` +Scenario: LLM Service Attempts Unauthorized Access + +Request: + GET http://vault-agent-llm:8201/v1/secret/data/encryption/private_key + +Vault Decision Chain: + 1. Token extracted from request: test_token_hvs.CAESIFS1ZfMfAtwYd9LJ27A1nzg... + 2. Token lookup: Associated policy = "llm-orchestration-policy" + 3. Path evaluation: secret/data/encryption/private_key + 4. Policy rule match: + path "secret/data/encryption/*" { + capabilities = ["deny"] + } + 5. Decision: DENY + +Response: + Status: 403 Forbidden + Body: { "errors": ["permission denied"] } + +Audit Log: + [2026-01-09T10:30:00Z] path=secret/data/encryption/private_key + action=read identity=llm-orchestration-service result=denied +``` + +**Security Principle**: Even if LLM service is compromised, attacker cannot access encryption keys. + +--- + +## Initialization & Bootstrapping + +### vault-init Container Workflow + +The `vault-init` container is a one-time initialization container that sets up Vault on first deployment: + +``` +┌────────────────────────────────────────────────────────────────┐ +│ vault-init Startup Sequence │ +└────────────────────────────────────────────────────────────────┘ + +Step 1: Wait for Vault Health + └─► Retry loop: Check http://vault:8200/v1/sys/health + └─► Wait until Vault server is responsive + +Step 2: Check Vault Status + └─► Is Vault initialized? + ├─► NO → First Time Setup (Steps 3-10) + └─► YES → Subsequent Deployment (Steps 11-12) + +═══════════════════════════════════════════════════════════════════ +FIRST TIME DEPLOYMENT +═══════════════════════════════════════════════════════════════════ + +Step 3: Initialize Vault + └─► POST /v1/sys/init + └─► Receives: + - Unseal keys (5 keys, threshold 3) + - Root token + +Step 4: Unseal Vault + └─► POST /v1/sys/unseal (3 times with different keys) + └─► Vault becomes operational + +Step 5: Enable KV v2 Secrets Engine + └─► POST /v1/sys/mounts/secret + Body: { type: "kv-v2" } + +Step 6: Enable AppRole Authentication + └─► POST /v1/sys/auth/approle + Body: { type: "approle" } + +Step 7: Create Policies + └─► POST /v1/sys/policies/acl/gui-policy + └─► POST /v1/sys/policies/acl/cron-manager-policy + └─► POST /v1/sys/policies/acl/llm-orchestration-policy + +Step 8: Create AppRoles + └─► POST /v1/auth/approle/role/gui-service + └─► POST /v1/auth/approle/role/cron-manager-service + └─► POST /v1/auth/approle/role/llm-orchestration-service + +Step 9: Generate Credentials + └─► GET /v1/auth/approle/role/gui-service/role-id + └─► POST /v1/auth/approle/role/gui-service/secret-id + └─► Repeat for cron-manager and llm-orchestration + +Step 10: Generate RSA Keypair + └─► openssl genrsa -out private_key.pem 2048 + └─► openssl rsa -pubout -in private_key.pem -out public_key.pem + └─► POST /v1/secret/data/encryption/public_key + └─► POST /v1/secret/data/encryption/private_key + +Step 11: Write Credentials to Shared Volumes + └─► /agent/credentials/gui_role_id + └─► /agent/credentials/gui_secret_id + └─► /agent/credentials/cron_role_id + └─► /agent/credentials/cron_secret_id + └─► /agent/credentials/llm_role_id + └─► /agent/credentials/llm_secret_id + +═══════════════════════════════════════════════════════════════════ +SUBSEQUENT DEPLOYMENT (Container Restart) +═══════════════════════════════════════════════════════════════════ + +Step 12: Check Vault Seal Status + └─► GET /v1/sys/seal-status + └─► If unsealed: Skip unseal steps + +Step 13: Regenerate Secret IDs Only + └─► POST /v1/auth/approle/role/gui-service/secret-id + └─► POST /v1/auth/approle/role/cron-manager-service/secret-id + └─► POST /v1/auth/approle/role/llm-orchestration-service/secret-id + └─► Write new secret_ids to /agent/credentials/ + +Note: role_ids remain unchanged (static identifiers) +Note: Existing secrets and policies preserved +Note: RSA keypair NOT regenerated (preserved) + +═══════════════════════════════════════════════════════════════════ +COMPLETION +═══════════════════════════════════════════════════════════════════ + +Step 14: Set File Permissions + └─► chown vault:vault /agent/credentials/* + └─► chmod 644 /agent/credentials/* + +Step 15: Exit Successfully + └─► Container stops with exit code 0 + └─► vault-agent containers start (depends_on: service_completed_successfully) +``` + +### Unseal Key Management + +Vault starts in a **sealed** state and must be unsealed before use: + +``` +Sealed State: + - Vault knows where data is stored + - Vault cannot decrypt data (encryption key unknown) + - All API operations return "Vault is sealed" + +Unsealing Process: + - Vault initialized with Shamir's Secret Sharing + - Master key split into 5 parts (configurable) + - Threshold: Any 3 of 5 keys can unseal + - Each unseal key submitted separately + +Security Model: + No single person can unseal Vault alone + Compromise of 2 keys is insufficient + Distributed trust across operators + +Current Implementation: + - All 5 unseal keys stored in vault-data volume + - Suitable for development/testing + - Production: Use Vault Auto-Unseal with AWS KMS, Azure Key Vault, etc. +``` + +**Security Trade-off**: Current setup prioritizes ease of deployment over maximum security. For production, implement auto-unseal with cloud HSM. + +### Root Token Handling + +The root token has unlimited access to Vault: + +``` +Root Token Lifecycle: + +1. Generation: + - Created during vault init + - Full superuser permissions + - Never expires + +2. Current Usage: + - Used by vault-init script only + - Performs initial setup tasks + - Not stored in application containers + +3. Security Best Practice: + - Revoke root token after initialization + - Use admin policies with limited scope instead + - Regenerate root token only for emergency recovery + +4. Production Recommendation: + - Revoke: vault token revoke + - Recreate if needed: vault operator generate-root +``` + +### Credential File Security + +``` +File Permissions on Shared Volume: + +/agent/credentials/ +├── gui_role_id (644 - readable by all agents) +├── gui_secret_id (644 - readable by all agents) +├── cron_role_id (644) +├── cron_secret_id (644) +├── llm_role_id (644) +└── llm_secret_id (644) + +Owner: vault:vault (UID 100, GID 1000) +Container Access: Read-only mounts in agent containers + +Security Considerations: + Files isolated within Docker volumes (not host filesystem) + Agent containers mount as read-only + Only vault-init has write access + All agents can read all credentials (volume-level isolation only) + +``` + +--- + +## Security Best Practices Implemented + +### 1. No Direct Vault Access from Applications + +``` + Implemented Pattern: + +Application Container + └─► Vault Agent Proxy (same network) + └─► Vault Server (isolated network) + +Benefits: + - Application never handles tokens + - Token rotation transparent to app + - Reduced attack surface + - Centralized authentication audit +``` + +### 2. Token Environment Variable Isolation + +``` + Implemented Pattern: + +docker-compose.yml (LLM Service): + environment: + - VAULT_ADDR=http://vault-agent-llm:8201 + # NO VAULT_TOKEN variable + +``` + +### 3. Credential File Permissions + +``` + Implemented Pattern: + +vault-init sets permissions: + chown vault:vault /agent/credentials/* + chmod 644 /agent/credentials/* + +Vault agents run as: + user: vault (UID 100) + +Why This Matters: + - Non-root user execution + - Principle of least privilege + - Reduced container escape impact +``` + +### 4. Container User Restrictions + +``` + Implemented Pattern: + +cron-manager: + user: "1000:1000" # Non-root user + +vault containers: + user: vault (implicit) + +Benefits: + - Limits filesystem access + - Prevents privilege escalation + - Reduces blast radius of exploits +``` + +### 5. Health Check Strategies + +``` + Implemented Pattern: + +Vault Server Health Check: + test: wget -q -O- http://127.0.0.1:8200/v1/sys/health + interval: 5s + retries: 20 + start_period: 10s + +Vault Agent Health Check: + test: test -f /agent/llm-token/token && test -s /agent/llm-token/token + interval: 10s + retries: 3 + start_period: 5s + +Benefits: + - Automated service recovery + - Dependency ordering (agents wait for Vault) + - Token presence validation + - Monitoring integration ready +``` + +--- + +## Operational Security + +### Container Restart Scenarios + +#### Scenario 1: Vault Server Restart + +``` +Event: docker-compose restart vault + +Impact: + Vault data persists (vault-data volume) + Vault automatically unseals (unseal keys in volume) + Policies and secrets intact + AppRole configurations intact + +Agent Behavior: + - Existing tokens remain valid + - Agents reconnect automatically + - No re-authentication needed (unless token expired) + +Downtime: + - ~5-10 seconds (health check dependent) +``` + +#### Scenario 2: Vault Agent Restart + +``` +Event: docker-compose restart vault-agent-llm + +Impact: + Credentials still available (vault-agent-creds volume) + Agent re-authenticates automatically + New token issued and cached + +Application Behavior: + - Brief connection failure during restart + - Retry logic handles transient errors + - No manual intervention required + +Downtime: + - ~3-5 seconds (agent startup time) +``` + +#### Scenario 3: Application Container Restart + +``` +Event: docker-compose restart llm-orchestration-service + +Impact: + No Vault changes needed + Vault agent still running + Tokens still valid + +Application Behavior: + - Reconnects to vault-agent-llm:8201 + - No authentication logic in app + - Immediate secret access + +Downtime: + - Application startup time only +``` + +#### Scenario 4: Full System Restart + +``` +Event: docker-compose down && docker-compose up + +Startup Order: + 1. vault (health check: vault ready) + 2. vault-init (runs setup, exits) + 3. vault-agent-* (wait for init completion) + 4. Applications (wait for agents) + +vault-init Behavior: + - Detects Vault already initialized + - Skips initialization steps + - Regenerates secret_ids only + - Updates credential files + +Result: + All services start with fresh credentials + Existing secrets preserved + No manual intervention needed +``` + +### Token Regeneration Strategy + +``` +Current Implementation: + +1. On Every Container Restart: + └─► vault-init regenerates secret_ids + └─► Vault agents get new tokens + └─► Old tokens remain valid until expiration + +2. Token Lifecycle: + └─► Issue: vault-agent authenticates + └─► Use: Application makes requests + └─► Renew: vault-agent extends TTL + └─► Expire: Automatic renewal failed + └─► Re-issue: vault-agent re-authenticates + +3. Security Benefits: + Short-lived tokens (1 hour for LLM, 32 days for others) + Automatic rotation on agent restart + No manual token management + Compromised tokens have limited lifetime +``` + +### Audit Logging Capabilities + +Vault provides comprehensive audit logging (not currently enabled in configuration): + +``` +Enabling Audit Logs: + +vault audit enable file file_path=/vault/logs/audit.log + +Logged Information: + - Timestamp of request + - Client identity (AppRole, token) + - Request path and method + - Success or failure + - Policy evaluation result + - Response data (hashed for secrets) + +Example Audit Entry: +{ + "time": "2026-01-09T10:30:00.123Z", + "type": "response", + "auth": { + "entity_id": "llm-orchestration-service", + "policies": ["llm-orchestration-policy"] + }, + "request": { + "path": "secret/data/llm/connections/azure_openai/testing/14", + "operation": "read" + }, + "response": { + "status": 200 + } +} + +Use Cases: + - Security incident investigation + - Compliance reporting + - Anomaly detection + - Access pattern analysis +``` + +### Monitoring Integration Points + +``` +Recommended Monitoring Metrics: + +1. Vault Health: + - Endpoint: http://vault:8200/v1/sys/health + - Metrics: sealed status, initialized status + +2. Token Usage: + - Endpoint: http://vault:8200/v1/auth/token/lookup-self + - Metrics: TTL remaining, creation time, policies + +3. Secret Access Patterns: + - Source: Audit logs + - Metrics: Access frequency, denied requests, unique clients + +4. Agent Health: + - Endpoint: Container health checks + - Metrics: Token file presence, agent uptime + +5. Application Errors: + - Source: Application logs + - Metrics: Vault connection failures, 403 errors, timeouts + +Integration with Grafana: + - Loki for log aggregation + - Prometheus for metrics (add vault exporter) + - Alertmanager for notifications +``` + +--- + +## Architecture Summary + +### Security Layers + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Layer 1: Network Isolation │ +│ - Vault on internal network only │ +│ - No external routing │ +└──────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ Layer 2: Authentication (AppRole) │ +│ - Machine identities │ +│ - Role-based credentials │ +│ - Short-lived tokens │ +└──────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ Layer 3: Authorization (Policies) │ +│ - Path-based access control │ +│ - Least privilege enforcement │ +│ - Explicit deny rules │ +└──────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ Layer 4: Proxy Abstraction (Vault Agents) │ +│ - Token isolation │ +│ - Automatic renewal │ +│ - Transparent authentication │ +└──────────────────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────┐ +│ Layer 5: Application Layer │ +│ - No token handling │ +│ - Simple HTTP calls │ +│ - Automatic retry logic │ +└──────────────────────────────────────────────────────────────┘ +``` + +### Defense in Depth + +``` +Attack Scenario: Compromised LLM Service Container + +Attacker Capabilities: + Cannot access Vault directly (network isolation) + Cannot read other service tokens (volume isolation) + Cannot access encryption keys (policy denial) + Can read LLM connection credentials (authorized) + +Blast Radius: + - Limited to LLM/embedding connection secrets + - Cannot modify secrets (read-only policy) + - Cannot access private encryption key + - Cannot pivot to other services + +Mitigation: + - Rotate compromised credentials immediately + - Revoke vault-agent-llm token + - Regenerate secret_id for llm-orchestration-service + - Restart llm-orchestration-service container +``` + +--- + +## Conclusion + +The Vault security architecture implements industry best practices for secrets management: + + **Network Segmentation**: Vault isolated on internal network + **Strong Authentication**: AppRole with renewable tokens + **Granular Authorization**: Path-based policies with least privilege + **Proxy Pattern**: Applications never handle tokens directly + **Automated Operations**: Self-healing agents and token renewal + **Audit Capability**: Full request logging available + **Defense in Depth**: Multiple security layers prevent single point of failure + +This architecture provides a secure foundation for managing sensitive credentials in the RAG-Module while maintaining operational simplicity and developer productivity. diff --git a/src/llm_orchestration_service.py b/src/llm_orchestration_service.py index a6cc98e..006835a 100644 --- a/src/llm_orchestration_service.py +++ b/src/llm_orchestration_service.py @@ -535,21 +535,12 @@ async def bot_response_generator() -> AsyncIterator[str]: logger.info( f"[{request.chatId}] [{stream_ctx.stream_id}] Sending {len(doc_references)} document references before END" ) - references_data = [ - ref.model_dump() for ref in doc_references - ] - references_message = { - "chatId": request.chatId, - "payload": { - "type": "references", - "references": references_data, - }, - "timestamp": str( - int(datetime.now().timestamp() * 1000) - ), - "sentTo": [], - } - yield f"data: {json_module.dumps(references_message)}\n\n" + # Format references as markdown text + refs_text = "\n\n**References:**\n" + "\n".join( + f"{i + 1}. [{ref.document_url}]({ref.document_url})" + for i, ref in enumerate(doc_references) + ) + yield self._format_sse(request.chatId, refs_text) yield self._format_sse(request.chatId, "END") @@ -594,21 +585,12 @@ async def bot_response_generator() -> AsyncIterator[str]: logger.info( f"[{request.chatId}] [{stream_ctx.stream_id}] Sending {len(doc_references)} document references before END" ) - references_data = [ - ref.model_dump() for ref in doc_references - ] - references_message = { - "chatId": request.chatId, - "payload": { - "type": "references", - "references": references_data, - }, - "timestamp": str( - int(datetime.now().timestamp() * 1000) - ), - "sentTo": [], - } - yield f"data: {json_module.dumps(references_message)}\n\n" + # Format references as markdown text + refs_text = "\n\n**References:**\n" + "\n".join( + f"{i + 1}. [{ref.document_url}]({ref.document_url})" + for i, ref in enumerate(doc_references) + ) + yield self._format_sse(request.chatId, refs_text) yield self._format_sse(request.chatId, "END") diff --git a/src/llm_orchestrator_config/vault/vault_client.py b/src/llm_orchestrator_config/vault/vault_client.py index 3616940..b0c3a3d 100644 --- a/src/llm_orchestrator_config/vault/vault_client.py +++ b/src/llm_orchestrator_config/vault/vault_client.py @@ -6,6 +6,7 @@ from typing import Optional, Dict, Any, cast from loguru import logger import hvac +from hvac.exceptions import InvalidPath, Forbidden from llm_orchestrator_config.vault.exceptions import ( VaultConnectionError, @@ -20,9 +21,10 @@ def get_vault_client( vault_url: Optional[str] = None, - token_path: str = "/agent/out/token", + token_path: str = "/agent/llm-token/token", mount_point: str = "secret", timeout: int = 10, + use_token_file: bool = True, ) -> "VaultAgentClient": """Get or create singleton VaultAgentClient instance. @@ -31,9 +33,10 @@ def get_vault_client( Args: vault_url: Vault server URL (defaults to VAULT_ADDR env var) - token_path: Path to Vault Agent token file + token_path: Path to Vault Agent token file (only used if use_token_file=True) mount_point: KV v2 mount point timeout: Request timeout in seconds + use_token_file: Whether to read token from file (False when using vault agent proxy) Returns: Singleton VaultAgentClient instance @@ -48,6 +51,7 @@ def get_vault_client( token_path=token_path, mount_point=mount_point, timeout=timeout, + use_token_file=use_token_file, ) logger.info("Created singleton VaultAgentClient instance") @@ -60,22 +64,32 @@ class VaultAgentClient: def __init__( self, vault_url: Optional[str] = None, - token_path: str = "/agent/out/token", + token_path: str = "/agent/llm-token/token", mount_point: str = "secret", timeout: int = 10, - ): + use_token_file: bool = True, + ) -> None: """Initialize Vault Agent client. Args: vault_url: Vault server URL (defaults to VAULT_ADDR env var) - token_path: Path to Vault Agent token file + token_path: Path to Vault Agent token file (only used if use_token_file=True) mount_point: KV v2 mount point timeout: Request timeout in seconds + use_token_file: Whether to read token from file (False when using vault agent proxy) """ self.vault_url = vault_url or os.getenv("VAULT_ADDR", "http://vault:8200") self.token_path = Path(token_path) self.mount_point = mount_point self.timeout = timeout + self.use_token_file = use_token_file + + # Auto-detect proxy mode: if URL points to vault-agent, don't use token file + if "vault-agent" in self.vault_url.lower(): + self.use_token_file = False + logger.info( + "Detected vault agent proxy in URL, token authentication will be handled by proxy" + ) # Initialize hvac client self.client = hvac.Client( @@ -83,8 +97,11 @@ def __init__( timeout=timeout, ) - # Load token from Vault Agent - self._load_token() + # Load token from file only if not using proxy + if self.use_token_file: + self._load_token() + else: + logger.info("Using vault agent proxy - no token file required") logger.info(f"Vault Agent client initialized: {self.vault_url}") @@ -106,7 +123,7 @@ def _load_token(self) -> None: if not token: raise VaultTokenError("Vault Agent token file is empty") - # Log token info for debugging (first and last 4 chars only for security) + # Log token info for debugging (first and last 4 chars only) token_preview = f"{token[:4]}...{token[-4:]}" if len(token) > 8 else "****" logger.debug(f"Loaded token: {token_preview} (length: {len(token)})") @@ -114,7 +131,7 @@ def _load_token(self) -> None: logger.debug("Vault Agent token loaded successfully") except (OSError, IOError) as e: - raise VaultTokenError(f"Failed to read Vault Agent token: {e}") + raise VaultTokenError(f"Failed to read Vault Agent token: {e}") from e def is_authenticated(self) -> bool: """Check if client is authenticated with Vault. @@ -123,7 +140,15 @@ def is_authenticated(self) -> bool: True if authenticated, False otherwise """ try: - # Check if we have a token + # If using proxy mode, skip token checks + if not self.use_token_file: + logger.debug( + "Using vault agent proxy - skipping token authentication check" + ) + # Just verify vault is accessible + return self.is_vault_available() + + # Check token is available if not hasattr(self.client, "token") or not self.client.token: logger.debug("No token set on client") return False @@ -169,9 +194,7 @@ def is_vault_available(self) -> bool: # Try to get additional details from response body if available try: - if hasattr(response, "json") and callable( - getattr(response, "json") - ): + if hasattr(response, "json") and callable(response.json): health_data = response.json() logger.debug(f"Vault health details: {health_data}") except Exception as e: @@ -233,14 +256,14 @@ def get_secret(self, path: str) -> Optional[Dict[str, Any]]: logger.debug(f"Secret not found at path: {path}") return None - except hvac.exceptions.InvalidPath: + except InvalidPath: logger.debug(f"Secret not found at path: {path}") return None - except hvac.exceptions.Forbidden as e: - raise VaultSecretError(f"Access denied to secret path {path}: {e}") + except Forbidden as e: + raise VaultSecretError(f"Access denied to secret path {path}: {e}") from e except Exception as e: logger.error(f"Error retrieving secret from path {path}: {e}") - raise VaultSecretError(f"Failed to retrieve secret: {e}") + raise VaultSecretError(f"Failed to retrieve secret: {e}") from e def list_secrets(self, path: str) -> Optional[list[str]]: """List secrets at the given path. @@ -278,12 +301,12 @@ def list_secrets(self, path: str) -> Optional[list[str]]: logger.debug(f"No secrets found at path: {path}") return None - except hvac.exceptions.InvalidPath: + except InvalidPath: logger.debug(f"Path not found: {path}") return None except Exception as e: logger.error(f"Error listing secrets at path {path}: {e}") - raise VaultSecretError(f"Failed to list secrets: {e}") + raise VaultSecretError(f"Failed to list secrets: {e}") from e def refresh_token(self) -> bool: """Refresh token from Vault Agent. diff --git a/src/utils/decrypt_vault_secrets.py b/src/utils/decrypt_vault_secrets.py new file mode 100644 index 0000000..e6c8bf5 --- /dev/null +++ b/src/utils/decrypt_vault_secrets.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +""" +RSA-OAEP Decryption Utility for Vault Secrets + +This utility provides in-memory decryption of RSA-OAEP encrypted values +without writing sensitive data to disk. +""" + +import sys +import base64 +from typing import NoReturn +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.asymmetric import padding, rsa +from cryptography.hazmat.backends import default_backend +from loguru import logger + +# Configure logger to write ONLY to stderr (stdout is reserved for decrypted value) +logger.remove() # Remove default handler +logger.add( + sys.stderr, + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name} - {message}", + level="INFO", +) + + +def base64url_to_bytes(base64url_string: str) -> bytes: + """ + Convert Base64URL encoded string to bytes. + + Args: + base64url_string: Base64URL encoded string (with - and _ instead of + and /) + + Returns: + Decoded bytes + """ + # Replace URL-safe characters with standard base64 characters + base64_string = base64url_string.replace("-", "+").replace("_", "/") + + # Add padding if needed (base64 requires length to be multiple of 4) + padding_needed = 4 - (len(base64_string) % 4) + if padding_needed != 4: + base64_string += "=" * padding_needed + + # Decode base64 to bytes + return base64.b64decode(base64_string) + + +def decrypt_rsa_oaep(encrypted_base64url: str, private_key_pem: str) -> str: + """ + Decrypt RSA-OAEP encrypted value using private key. + + All operations are performed in memory - no disk I/O. + + Args: + encrypted_base64url: Base64URL encoded encrypted data + private_key_pem: RSA private key in PEM format + + Returns: + Decrypted plaintext string + + Raises: + ValueError: If decryption fails (invalid key, padding, or corrupted data) + Exception: For other errors + """ + try: + # Load private key from PEM string (in memory, no file) + loaded_key = serialization.load_pem_private_key( + private_key_pem.encode("utf-8"), password=None, backend=default_backend() + ) + + # Type check and cast to RSA private key + if not isinstance(loaded_key, rsa.RSAPrivateKey): + raise TypeError("Private key must be an RSA key") + + private_key: rsa.RSAPrivateKey = loaded_key + + # Decode Base64URL to bytes (in memory) + encrypted_bytes = base64url_to_bytes(encrypted_base64url) + + # Decrypt using RSA-OAEP with SHA-256 (in memory) + plaintext_bytes = private_key.decrypt( + encrypted_bytes, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None, + ), + ) + + # Convert bytes to string + plaintext = plaintext_bytes.decode("utf-8") + + logger.debug( + f"Successfully decrypted {len(encrypted_base64url)} bytes of encrypted data" + ) + + return plaintext + + except ValueError as e: + # Invalid padding or decryption failure + raise ValueError(f"Decryption failed: {e}") from e + except TypeError as e: + # Wrong key type + raise TypeError(f"Invalid key type: {e}") from e + except Exception as e: + # Other errors (invalid key format, corrupted data, etc.) + raise RuntimeError(f"Decryption error: {e}") from e + + +def main() -> NoReturn: + """ + CLI interface for decryption. + + Usage: + python decrypt_vault_secrets.py + + The private key PEM should be a single string with \n for newlines. + """ + if len(sys.argv) != 3: + logger.error( + "Usage: python decrypt_vault_secrets.py " + ) + logger.info( + "Example: python decrypt_vault_secrets.py 'A8xF2nQ7...' '-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----'" + ) + sys.exit(1) + + encrypted_value = sys.argv[1] + private_key_pem = sys.argv[2] + + logger.debug(f"Received encrypted value of length: {len(encrypted_value)}") + logger.debug("Private key PEM received") + + # Handle literal \n in shell arguments + private_key_pem = private_key_pem.replace("\\n", "\n") + + try: + plaintext = decrypt_rsa_oaep(encrypted_value, private_key_pem) + + logger.debug("Decryption successful, outputting plaintext to stdout") + + # Output to stdout only (for shell script capture) + print(plaintext) + + sys.exit(0) + + except ValueError as e: + logger.error(f"Decryption failed: {e}") + sys.exit(1) + except Exception as e: + logger.error(f"Unexpected error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/integration_tests/test_indexing.py b/tests/integration_tests/test_indexing.py index a792d2a..b134e5b 100644 --- a/tests/integration_tests/test_indexing.py +++ b/tests/integration_tests/test_indexing.py @@ -129,7 +129,7 @@ async def test_indexing_pipeline_e2e( break except requests.exceptions.RequestException: logger.debug( - f"LLM orchestration health check attempt {i+1}/{max_retries} failed" + f"LLM orchestration health check attempt {i + 1}/{max_retries} failed" ) time.sleep(2) else: diff --git a/vault-init.sh b/vault-init.sh index cd36e2d..63db07a 100644 --- a/vault-init.sh +++ b/vault-init.sh @@ -67,23 +67,70 @@ if [ ! -f "$INIT_FLAG" ]; then --header='Content-Type: application/json' \ "$VAULT_ADDR/v1/sys/auth/approle" >/dev/null 2>&1 || echo "AppRole already enabled" - # Create policy - echo "Creating llm-orchestration policy..." - POLICY='path "secret/metadata/llm/*" { capabilities = ["list", "delete"] } -path "secret/data/llm/*" { capabilities = ["create", "read", "update", "delete"] } -path "auth/token/lookup-self" { capabilities = ["read"] } -path "secret/metadata/embeddings/*" { capabilities = ["list", "delete"] } -path "secret/data/embeddings/*" { capabilities = ["create", "read", "update", "delete"] }' + # Create GUI policy - Read public encryption key only + echo "Creating gui-policy..." + GUI_POLICY='path "secret/data/encryption/public_key" { capabilities = ["read"] } +path "secret/metadata/encryption/public_key" { capabilities = ["read"] } +path "secret/data/encryption/private_key" { capabilities = ["deny"] } +path "secret/data/llm/*" { capabilities = ["deny"] } +path "secret/data/embeddings/*" { capabilities = ["deny"] }' - POLICY_JSON=$(echo "$POLICY" | jq -Rs '{"policy":.}') - wget -q -O- --post-data="$POLICY_JSON" \ + GUI_POLICY_JSON=$(echo "$GUI_POLICY" | jq -Rs '{"policy":.}') + wget -q -O- --post-data="$GUI_POLICY_JSON" \ --header="X-Vault-Token: $ROOT_TOKEN" \ --header='Content-Type: application/json' \ - "$VAULT_ADDR/v1/sys/policies/acl/llm-orchestration" >/dev/null + "$VAULT_ADDR/v1/sys/policies/acl/gui-policy" >/dev/null - # Create AppRole + # Create CronManager policy - Read encryption keys + Write LLM/Embedding secrets + echo "Creating cron-manager-policy..." + CRON_POLICY='path "secret/data/encryption/public_key" { capabilities = ["read"] } +path "secret/metadata/encryption/public_key" { capabilities = ["read"] } +path "secret/data/encryption/private_key" { capabilities = ["read"] } +path "secret/metadata/encryption/private_key" { capabilities = ["read"] } +path "secret/data/llm/connections/*" { capabilities = ["create", "read", "update", "delete"] } +path "secret/metadata/llm/connections/*" { capabilities = ["read", "list", "delete"] } +path "secret/data/embeddings/connections/*" { capabilities = ["create", "read", "update", "delete"] } +path "secret/metadata/embeddings/connections/*" { capabilities = ["read", "list", "delete"] } +path "auth/token/lookup-self" { capabilities = ["read"] }' + + CRON_POLICY_JSON=$(echo "$CRON_POLICY" | jq -Rs '{"policy":.}') + wget -q -O- --post-data="$CRON_POLICY_JSON" \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + --header='Content-Type: application/json' \ + "$VAULT_ADDR/v1/sys/policies/acl/cron-manager-policy" >/dev/null + + # Create LLM Orchestration policy - Read LLM/Embedding secrets only + echo "Creating llm-orchestration-policy..." + LLM_POLICY='path "secret/data/llm/connections/*" { capabilities = ["read", "list"] } +path "secret/metadata/llm/connections/*" { capabilities = ["read", "list"] } +path "secret/data/embeddings/connections/*" { capabilities = ["read", "list"] } +path "secret/metadata/embeddings/connections/*" { capabilities = ["read", "list"] } +path "secret/data/encryption/*" { capabilities = ["deny"] } +path "auth/token/lookup-self" { capabilities = ["read"] }' + + LLM_POLICY_JSON=$(echo "$LLM_POLICY" | jq -Rs '{"policy":.}') + wget -q -O- --post-data="$LLM_POLICY_JSON" \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + --header='Content-Type: application/json' \ + "$VAULT_ADDR/v1/sys/policies/acl/llm-orchestration-policy" >/dev/null + + # Create GUI AppRole + echo "Creating gui-service AppRole..." + wget -q -O- --post-data='{"token_policies":["gui-policy"],"token_no_default_policy":true,"token_ttl":"15m","token_max_ttl":"1h","secret_id_ttl":"24h","secret_id_num_uses":0,"bind_secret_id":true}' \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + --header='Content-Type: application/json' \ + "$VAULT_ADDR/v1/auth/approle/role/gui-service" >/dev/null + + # Create CronManager AppRole + echo "Creating cron-manager-service AppRole..." + wget -q -O- --post-data='{"token_policies":["cron-manager-policy"],"token_no_default_policy":true,"token_ttl":"30m","token_max_ttl":"8h","secret_id_ttl":"24h","secret_id_num_uses":0,"bind_secret_id":true}' \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + --header='Content-Type: application/json' \ + "$VAULT_ADDR/v1/auth/approle/role/cron-manager-service" >/dev/null + + # Create LLM Orchestration AppRole echo "Creating llm-orchestration-service AppRole..." - wget -q -O- --post-data='{"token_policies":["llm-orchestration"],"token_no_default_policy":true,"token_ttl":"1h","token_max_ttl":"24h","secret_id_ttl":"24h","secret_id_num_uses":0,"bind_secret_id":true}' \ + wget -q -O- --post-data='{"token_policies":["llm-orchestration-policy"],"token_no_default_policy":true,"token_ttl":"1h","token_max_ttl":"24h","secret_id_ttl":"24h","secret_id_num_uses":0,"bind_secret_id":true}' \ --header="X-Vault-Token: $ROOT_TOKEN" \ --header='Content-Type: application/json' \ "$VAULT_ADDR/v1/auth/approle/role/llm-orchestration-service" >/dev/null @@ -91,27 +138,108 @@ path "secret/data/embeddings/*" { capabilities = ["create", "read", "update", "d # Ensure credentials directory exists mkdir -p /agent/credentials - # Get role_id - echo "Getting role_id..." - ROLE_ID=$(wget -q -O- \ + # Get GUI credentials + echo "Getting GUI credentials..." + GUI_ROLE_ID=$(wget -q -O- \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + "$VAULT_ADDR/v1/auth/approle/role/gui-service/role-id" | \ + grep -o '"role_id":"[^"]*"' | cut -d':' -f2 | tr -d '"') + echo "$GUI_ROLE_ID" > /agent/credentials/gui_role_id + + GUI_SECRET_ID=$(wget -q -O- --post-data='' \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + "$VAULT_ADDR/v1/auth/approle/role/gui-service/secret-id" | \ + grep -o '"secret_id":"[^"]*"' | cut -d':' -f2 | tr -d '"') + echo "$GUI_SECRET_ID" > /agent/credentials/gui_secret_id + + # Get CronManager credentials + echo "Getting CronManager credentials..." + CRON_ROLE_ID=$(wget -q -O- \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + "$VAULT_ADDR/v1/auth/approle/role/cron-manager-service/role-id" | \ + grep -o '"role_id":"[^"]*"' | cut -d':' -f2 | tr -d '"') + echo "$CRON_ROLE_ID" > /agent/credentials/cron_role_id + + CRON_SECRET_ID=$(wget -q -O- --post-data='' \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + "$VAULT_ADDR/v1/auth/approle/role/cron-manager-service/secret-id" | \ + grep -o '"secret_id":"[^"]*"' | cut -d':' -f2 | tr -d '"') + echo "$CRON_SECRET_ID" > /agent/credentials/cron_secret_id + + # Get LLM Orchestration credentials + echo "Getting LLM Orchestration credentials..." + LLM_ROLE_ID=$(wget -q -O- \ --header="X-Vault-Token: $ROOT_TOKEN" \ "$VAULT_ADDR/v1/auth/approle/role/llm-orchestration-service/role-id" | \ grep -o '"role_id":"[^"]*"' | cut -d':' -f2 | tr -d '"') - echo "$ROLE_ID" > /agent/credentials/role_id + echo "$LLM_ROLE_ID" > /agent/credentials/llm_role_id - # Generate secret_id - echo "Generating secret_id..." - SECRET_ID=$(wget -q -O- --post-data='' \ + LLM_SECRET_ID=$(wget -q -O- --post-data='' \ --header="X-Vault-Token: $ROOT_TOKEN" \ "$VAULT_ADDR/v1/auth/approle/role/llm-orchestration-service/secret-id" | \ grep -o '"secret_id":"[^"]*"' | cut -d':' -f2 | tr -d '"') - echo "$SECRET_ID" > /agent/credentials/secret_id + echo "$LLM_SECRET_ID" > /agent/credentials/llm_secret_id + + # Set secure permissions + chmod 640 /agent/credentials/*_role_id + chmod 640 /agent/credentials/*_secret_id + + # Generate RSA keypair for credential encryption/decryption + echo "Generating RSA keypair for encryption..." + + # Create temp directory for key generation + TEMP_KEY_DIR="/tmp/vault-keys-$$" + mkdir -p "$TEMP_KEY_DIR" + + # Generate private key (RSA-2048) + if ! openssl genrsa -out "$TEMP_KEY_DIR/private.pem" 2048 2>/dev/null; then + echo "ERROR: Failed to generate private key" + exit 1 + fi + + # Extract public key from private key + if ! openssl rsa -in "$TEMP_KEY_DIR/private.pem" -pubout -out "$TEMP_KEY_DIR/public.pem" 2>/dev/null; then + echo "ERROR: Failed to extract public key" + exit 1 + fi + + echo "Keys generated successfully" + + # Read keys and escape for JSON + PRIVATE_KEY=$(cat "$TEMP_KEY_DIR/private.pem" | sed ':a;N;$!ba;s/\n/\\n/g') + PUBLIC_KEY=$(cat "$TEMP_KEY_DIR/public.pem" | sed ':a;N;$!ba;s/\n/\\n/g') + CREATED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ) + KEY_ID="rsa-keypair-$(date +%s)" + + # Store public key in Vault + echo "Storing public key in Vault..." + wget -q -O- --post-data='{"data":{"key":"'"$PUBLIC_KEY"'","algorithm":"RSA-OAEP","key_size":2048,"key_id":"'"$KEY_ID"'","created_at":"'"$CREATED_AT"'"}}' \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + --header='Content-Type: application/json' \ + "$VAULT_ADDR/v1/secret/data/encryption/public_key" >/dev/null - chmod 644 /agent/credentials/role_id /agent/credentials/secret_id + # Store private key in Vault + echo "Storing private key in Vault..." + wget -q -O- --post-data='{"data":{"key":"'"$PRIVATE_KEY"'","algorithm":"RSA-OAEP","key_size":2048,"key_id":"'"$KEY_ID"'","created_at":"'"$CREATED_AT"'"}}' \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + --header='Content-Type: application/json' \ + "$VAULT_ADDR/v1/secret/data/encryption/private_key" >/dev/null + + # Clean up temporary files + rm -rf "$TEMP_KEY_DIR" + echo "RSA keypair generated and stored successfully" + + # Store test LLM credentials for testing + echo "Creating test LLM credentials..." + wget -q -O- --post-data='{"data":{"access_key":"TEST_AWS_ACCESS_KEY","secret_key":"TEST_AWS_SECRET_KEY","environment":"production","model":"claude-3"}}' \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + --header='Content-Type: application/json' \ + "$VAULT_ADDR/v1/secret/data/llm/connections/aws_bedrock/production/claude-3" >/dev/null # Mark as initialized touch "$INIT_FLAG" echo "=== First time setup complete ===" + else echo "=== SUBSEQUENT DEPLOYMENT ===" @@ -141,36 +269,71 @@ else sleep 2 echo "Vault unsealed" - - # Get root token - ROOT_TOKEN=$(grep -o '"root_token":"[^"]*"' "$UNSEAL_KEYS_FILE" | cut -d':' -f2 | tr -d '"') - export VAULT_TOKEN="$ROOT_TOKEN" - - # Ensure credentials directory exists - mkdir -p /agent/credentials - - # Regenerate secret_id after unseal - echo "Regenerating secret_id..." - SECRET_ID=$(wget -q -O- --post-data='' \ - --header="X-Vault-Token: $ROOT_TOKEN" \ - "$VAULT_ADDR/v1/auth/approle/role/llm-orchestration-service/secret-id" | \ - grep -o '"secret_id":"[^"]*"' | cut -d':' -f2 | tr -d '"') - echo "$SECRET_ID" > /agent/credentials/secret_id - chmod 644 /agent/credentials/secret_id - - # Ensure role_id exists - if [ ! -f /agent/credentials/role_id ]; then - echo "Copying role_id..." - mkdir -p /agent/credentials - ROLE_ID=$(wget -q -O- \ - --header="X-Vault-Token: $ROOT_TOKEN" \ - "$VAULT_ADDR/v1/auth/approle/role/llm-orchestration-service/role-id" | \ - grep -o '"role_id":"[^"]*"' | cut -d':' -f2 | tr -d '"') - echo "$ROLE_ID" > /agent/credentials/role_id - chmod 644 /agent/credentials/role_id - fi else - echo "Vault is unsealed. No action needed." + echo "Vault is already unsealed" + fi + + # Get root token + ROOT_TOKEN=$(grep -o '"root_token":"[^"]*"' "$UNSEAL_KEYS_FILE" | cut -d':' -f2 | tr -d '"') + export VAULT_TOKEN="$ROOT_TOKEN" + + # Ensure credentials directory exists + mkdir -p /agent/credentials + + # Always regenerate all secret_ids on restart + echo "Regenerating GUI secret_id..." + GUI_SECRET_ID=$(wget -q -O- --post-data='' \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + "$VAULT_ADDR/v1/auth/approle/role/gui-service/secret-id" | \ + grep -o '"secret_id":"[^"]*"' | cut -d':' -f2 | tr -d '"') + echo "$GUI_SECRET_ID" > /agent/credentials/gui_secret_id + + echo "Regenerating CronManager secret_id..." + CRON_SECRET_ID=$(wget -q -O- --post-data='' \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + "$VAULT_ADDR/v1/auth/approle/role/cron-manager-service/secret-id" | \ + grep -o '"secret_id":"[^"]*"' | cut -d':' -f2 | tr -d '"') + echo "$CRON_SECRET_ID" > /agent/credentials/cron_secret_id + + echo "Regenerating LLM secret_id..." + LLM_SECRET_ID=$(wget -q -O- --post-data='' \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + "$VAULT_ADDR/v1/auth/approle/role/llm-orchestration-service/secret-id" | \ + grep -o '"secret_id":"[^"]*"' | cut -d':' -f2 | tr -d '"') + echo "$LLM_SECRET_ID" > /agent/credentials/llm_secret_id + + # Set permissions + chmod 640 /agent/credentials/*_secret_id + + # Ensure role_ids exist + if [ ! -f /agent/credentials/gui_role_id ]; then + echo "Copying GUI role_id..." + GUI_ROLE_ID=$(wget -q -O- \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + "$VAULT_ADDR/v1/auth/approle/role/gui-service/role-id" | \ + grep -o '"role_id":"[^"]*"' | cut -d':' -f2 | tr -d '"') + echo "$GUI_ROLE_ID" > /agent/credentials/gui_role_id + chmod 640 /agent/credentials/gui_role_id + fi + + if [ ! -f /agent/credentials/cron_role_id ]; then + echo "Copying CronManager role_id..." + CRON_ROLE_ID=$(wget -q -O- \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + "$VAULT_ADDR/v1/auth/approle/role/cron-manager-service/role-id" | \ + grep -o '"role_id":"[^"]*"' | cut -d':' -f2 | tr -d '"') + echo "$CRON_ROLE_ID" > /agent/credentials/cron_role_id + chmod 640 /agent/credentials/cron_role_id + fi + + if [ ! -f /agent/credentials/llm_role_id ]; then + echo "Copying LLM role_id..." + LLM_ROLE_ID=$(wget -q -O- \ + --header="X-Vault-Token: $ROOT_TOKEN" \ + "$VAULT_ADDR/v1/auth/approle/role/llm-orchestration-service/role-id" | \ + grep -o '"role_id":"[^"]*"' | cut -d':' -f2 | tr -d '"') + echo "$LLM_ROLE_ID" > /agent/credentials/llm_role_id + chmod 640 /agent/credentials/llm_role_id fi fi diff --git a/vault/agents/cron/cron-agent.hcl b/vault/agents/cron/cron-agent.hcl new file mode 100644 index 0000000..f2db227 --- /dev/null +++ b/vault/agents/cron/cron-agent.hcl @@ -0,0 +1,47 @@ +# Vault Agent Configuration for CronManager Service +# This agent provides CronManager with access to encryption keys and write access to secrets + +vault { + address = "http://vault:8200" + retry { + num_retries = 5 + } +} + +# Auto-authentication using AppRole +auto_auth { + method "approle" { + mount_path = "auth/approle" + config = { + role_id_file_path = "/agent/credentials/cron_role_id" + secret_id_file_path = "/agent/credentials/cron_secret_id" + remove_secret_id_file_after_reading = false + } + } + + # Write token to file for CronManager service to use + sink "file" { + config = { + path = "/agent/cron-token/token" + mode = 0640 + } + } +} + +# Caching configuration +cache { + default_lease_duration = "30m" # Medium TTL for CronManager +} + +# API proxy listener for CronManager service +listener "tcp" { + address = "0.0.0.0:8203" + tls_disable = true +} + +# API proxy configuration +api_proxy { + use_auto_auth_token = true + enforce_consistency = "always" + when_inconsistent = "forward" +} diff --git a/vault/agents/gui/gui-agent.hcl b/vault/agents/gui/gui-agent.hcl new file mode 100644 index 0000000..a28db87 --- /dev/null +++ b/vault/agents/gui/gui-agent.hcl @@ -0,0 +1,47 @@ +# Vault Agent Configuration for GUI Service +# This agent provides GUI with access to public encryption key only + +vault { + address = "http://vault:8200" + retry { + num_retries = 5 + } +} + +# Auto-authentication using AppRole +auto_auth { + method "approle" { + mount_path = "auth/approle" + config = { + role_id_file_path = "/agent/credentials/gui_role_id" + secret_id_file_path = "/agent/credentials/gui_secret_id" + remove_secret_id_file_after_reading = false + } + } + + # Write token to file for GUI service to use + sink "file" { + config = { + path = "/agent/gui-token/token" + mode = 0640 + } + } +} + +# Caching configuration +cache { + default_lease_duration = "15m" # Short-lived tokens for GUI +} + +# API proxy listener for GUI service +listener "tcp" { + address = "0.0.0.0:8202" + tls_disable = true +} + +# API proxy configuration +api_proxy { + use_auto_auth_token = true + enforce_consistency = "always" + when_inconsistent = "forward" +} diff --git a/vault/agents/llm/agent.hcl b/vault/agents/llm/agent.hcl index 4a0b410..d7237be 100644 --- a/vault/agents/llm/agent.hcl +++ b/vault/agents/llm/agent.hcl @@ -1,32 +1,34 @@ vault { address = "http://vault:8200" + retry { + num_retries = 5 + } } -pid_file = "/agent/out/pidfile" - auto_auth { method "approle" { mount_path = "auth/approle" config = { - role_id_file_path = "/agent/credentials/role_id" - secret_id_file_path = "/agent/credentials/secret_id" + role_id_file_path = "/agent/credentials/llm_role_id" + secret_id_file_path = "/agent/credentials/llm_secret_id" remove_secret_id_file_after_reading = false } } sink "file" { config = { - path = "/agent/out/token" + path = "/agent/llm-token/token" + mode = 0640 } } } cache { - default_lease_duration = "1h" + default_lease_duration = "1h" # Longer TTL for LLM service } listener "tcp" { - address = "127.0.0.1:8201" + address = "0.0.0.0:8201" # Listen on all interfaces tls_disable = true }