357 lines
8.3 KiB
Markdown
357 lines
8.3 KiB
Markdown
# CORS and Vault Client Implementation
|
|
|
|
## ❌ Why Your Changes Won't Work
|
|
|
|
You tried to fix CORS by adding these changes:
|
|
|
|
```typescript
|
|
// ❌ 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:
|
|
1. The header is sent to the server
|
|
2. The server ignores it (only the server's response headers matter)
|
|
3. 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:**
|
|
```typescript
|
|
// 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:
|
|
|
|
```hcl
|
|
# 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:
|
|
|
|
```yaml
|
|
# 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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```typescript
|
|
// ✅ 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)
|
|
```typescript
|
|
// ❌ 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)
|
|
```typescript
|
|
// ✅ Clean, maintainable, type-safe
|
|
const client = new VaultClient(options);
|
|
const data = await client.read(path); // Simple!
|
|
```
|
|
|
|
### Benefits of VaultClient
|
|
|
|
1. **Error Handling**
|
|
```typescript
|
|
try {
|
|
await client.read('secret/data/test');
|
|
} catch (error) {
|
|
if (error instanceof VaultError) {
|
|
console.log(error.statusCode); // 403
|
|
console.log(error.errors); // ["permission denied"]
|
|
}
|
|
}
|
|
```
|
|
|
|
2. **Automatic Retries**
|
|
- Retries on network errors
|
|
- Exponential backoff
|
|
- Configurable retry count
|
|
|
|
3. **Timeout Protection**
|
|
```typescript
|
|
const client = new VaultClient({
|
|
...options,
|
|
timeout: 5000 // 5 seconds
|
|
});
|
|
// Request will abort after 5 seconds
|
|
```
|
|
|
|
4. **Path Normalization**
|
|
```typescript
|
|
// 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
|
|
```
|
|
|
|
5. **Authentication Methods**
|
|
```typescript
|
|
// 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');
|
|
```
|
|
|
|
6. **Type Safety**
|
|
```typescript
|
|
// 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)
|
|
```bash
|
|
curl -X GET \
|
|
-H "X-Vault-Token: your-token" \
|
|
http://localhost:8200/v1/secret/data/test
|
|
```
|
|
|
|
### 2. Test from browser console
|
|
```javascript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
1. **CORS must be configured on the Vault SERVER, not the client**
|
|
2. **`mode: 'no-cors'` prevents you from reading responses**
|
|
3. **Use the VaultClient class for clean, maintainable code**
|
|
4. **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!
|
|
|