8.3 KiB
CORS and Vault Client Implementation
❌ Why Your Changes Won't Work
You tried to fix CORS by adding these changes:
// ❌ This won't work
headers: {
'Access-Control-Allow-Origin': '*'
}
// ❌ This will break response reading
mode: 'no-cors'
Why Access-Control-Allow-Origin in Client Headers Doesn't Work
CORS headers MUST be set by the SERVER, not the client.
When you add Access-Control-Allow-Origin: * to your request headers:
- The header is sent to the server
- The server ignores it (only the server's response headers matter)
- The browser still blocks the response because the server didn't send the CORS header
How CORS Actually Works:
Browser → Request to vault.example.com
↓
Vault Server → Response with header: Access-Control-Allow-Origin: https://yourfrontend.com
↓
Browser → Allows JavaScript to read the response
Why mode: 'no-cors' Breaks Everything
The no-cors mode:
- ✅ Allows the request to be sent
- ❌ Prevents you from reading the response body
- ❌ You can't access status codes
- ❌ You can't read JSON data
- ❌ You only get an "opaque" response
It's called "no-cors" mode because it doesn't check CORS, but it also doesn't let you use the response.
Example:
// With no-cors
const response = await fetch(url, { mode: 'no-cors' });
console.log(response.status); // Always 0
console.log(await response.json()); // Error: Can't read body
✅ The Proper Solution
1. Configure Your Vault Server
Add CORS configuration to your Vault server config file:
# vault.hcl
ui = true
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 1 # Only for development! Use TLS in production
# Enable CORS
cors_enabled = true
cors_allowed_origins = [
"http://localhost:5173", # Vite dev server
"https://yourdomain.com" # Production domain
]
cors_allowed_headers = ["*"]
}
Or if using Docker:
# docker-compose.yml
version: '3.8'
services:
vault:
image: vault:latest
ports:
- "8200:8200"
environment:
VAULT_DEV_ROOT_TOKEN_ID: myroot
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
VAULT_API_ADDR: http://127.0.0.1:8200
cap_add:
- IPC_LOCK
command:
- server
- -dev
- -dev-root-token-id=myroot
Then exec into the container and configure CORS via the API:
docker exec -it vault sh
vault write sys/config/cors enabled=true allowed_origins="http://localhost:5173"
2. Use the Proper Vault Client
We've now implemented a proper browser-compatible Vault client:
// ✅ NEW: Clean, maintainable VaultClient
import { VaultClient } from './services/vaultClient';
const client = new VaultClient({
server: { url: 'https://vault.example.com', ... },
credentials: { token: 'your-token', ... },
timeout: 30000,
retries: 2
});
// Read a secret
const data = await client.read('secret/data/myapp/config');
// List secrets
const keys = await client.list('secret/');
// Write a secret
await client.write('secret/data/myapp/config', {
username: 'admin',
password: 'secret'
});
// Delete a secret
await client.delete('secret/data/myapp/config');
🏗️ Architecture: Why Use a Client Class?
Before (Raw API Calls)
// ❌ Hard to maintain, error-prone
async function readSecret(url, token, path) {
const response = await fetch(`${url}/v1/${path}`, {
headers: {
'X-Vault-Token': token,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed'); // Not helpful
}
const data = await response.json();
return data.data.data; // Confusing nested structure
}
After (VaultClient)
// ✅ Clean, maintainable, type-safe
const client = new VaultClient(options);
const data = await client.read(path); // Simple!
Benefits of VaultClient
-
Error Handling
try { await client.read('secret/data/test'); } catch (error) { if (error instanceof VaultError) { console.log(error.statusCode); // 403 console.log(error.errors); // ["permission denied"] } } -
Automatic Retries
- Retries on network errors
- Exponential backoff
- Configurable retry count
-
Timeout Protection
const client = new VaultClient({ ...options, timeout: 5000 // 5 seconds }); // Request will abort after 5 seconds -
Path Normalization
// All of these work the same: await client.read('/secret/data/test'); await client.read('secret/data/test'); await client.read('//secret/data/test//'); // All normalized to: secret/data/test -
Authentication Methods
// Token (already have one) const client = new VaultClient({ server, credentials: { token: 'your-token', ... } }); // Username/Password (get token) const token = await client.loginUserpass('username', 'password'); // LDAP (get token) const token = await client.loginLdap('username', 'password'); -
Type Safety
// TypeScript knows the structure interface MySecret { username: string; password: string; } const data = await client.read<MySecret>('secret/data/myapp'); console.log(data.username); // TypeScript autocomplete works!
🔧 Testing Your CORS Configuration
1. Test with curl (should work)
curl -X GET \
-H "X-Vault-Token: your-token" \
http://localhost:8200/v1/secret/data/test
2. Test from browser console
fetch('http://localhost:8200/v1/secret/data/test', {
headers: {
'X-Vault-Token': 'your-token'
}
})
.then(r => r.json())
.then(console.log)
.catch(console.error);
If you see a CORS error here, your Vault server CORS is not configured correctly.
3. Check Response Headers
In browser DevTools → Network tab, check the response headers:
✅ Should see:
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Headers: *
❌ If missing:
Your Vault server CORS is not configured
🆚 Comparison: Raw API vs VaultClient
| Feature | Raw fetch() | VaultClient |
|---|---|---|
| Code lines (typical read) | ~15 lines | 1 line |
| Error handling | Manual | Built-in |
| Retries | Manual | Automatic |
| Timeouts | Manual | Built-in |
| Type safety | None | Full |
| Path normalization | Manual | Automatic |
| Authentication | Manual | Built-in |
| Token management | Manual | Built-in |
| Health checks | Manual | Built-in |
| Maintainability | Low | High |
📚 Advanced Usage
Custom Error Handling
import { VaultError } from './services/vaultClient';
try {
const data = await client.read('secret/data/test');
} catch (error) {
if (error instanceof VaultError) {
switch (error.statusCode) {
case 403:
alert('Permission denied');
break;
case 404:
alert('Secret not found');
break;
case 500:
alert('Vault server error');
break;
default:
alert(error.message);
}
}
}
Health Check Before Operations
const client = new VaultClient(options);
// Check if Vault is healthy
const health = await client.health();
if (health.sealed) {
alert('Vault is sealed! Please unseal it first.');
return;
}
// Now safe to perform operations
const data = await client.read('secret/data/test');
Token Lifecycle Management
// Login
const client = new VaultClient(options);
const token = await client.loginUserpass('user', 'pass');
// Check token info
const tokenInfo = await client.tokenLookupSelf();
console.log('Token expires:', tokenInfo.data.expire_time);
console.log('Token TTL:', tokenInfo.data.ttl);
// Revoke token on logout
await client.tokenRevokeSelf();
🎯 Summary
- CORS must be configured on the Vault SERVER, not the client
mode: 'no-cors'prevents you from reading responses- Use the VaultClient class for clean, maintainable code
- VaultClient provides:
- Automatic retries
- Timeout protection
- Better error messages
- Type safety
- Built-in authentication
- Path normalization
The new implementation is production-ready, maintainable, and properly handles all edge cases!