Financial Info MCP Server - Secrets Manager
This document describes the local secrets manager implementation for the Financial Info MCP Server.
Overview
The secrets manager allows different clients to use their own Polygon API keys by including an x-client-id header in their HTTP requests. This enables:
- Multi-tenant API key management
- Client-specific rate limiting and billing
- Secure key storage and rotation
- Fallback mechanisms for backward compatibility
Setup
1. Docker Configuration
The docker-compose.yml has been updated to map the secrets file:
volumes:
- ${HOME}/mcp-gateway/secrets/.keys.yml:/app/fininfo/.keys.yml
2. Create Secrets File
Create the secrets file on your host system:
mkdir -p ${HOME}/mcp-gateway/secrets
touch ${HOME}/mcp-gateway/secrets/.keys.yml
chmod 600 ${HOME}/mcp-gateway/secrets/.keys.yml
3. Configure Client API Keys
Edit the secrets file with your client configurations:
# Default fallback key
default: your_default_polygon_api_key
# Client-specific keys
client_demo: demo_polygon_api_key
client_prod: production_polygon_api_key
client_test: test_polygon_api_key
Usage
Client Requests
Clients should include the x-client-id header in their HTTP requests:
curl -X POST "http://localhost:8001/sse" \
-H "Content-Type: application/json" \
-H "x-client-id: client_demo" \
-d '{
"method": "tools/call",
"params": {
"name": "get_stock_aggregates",
"arguments": {
"stock_ticker": "AAPL",
"multiplier": 1,
"timespan": "day",
"from_date": "2023-01-01",
"to_date": "2023-01-31"
}
}
}'
Fallback Behavior
If no x-client-id header is provided or the client ID is not found:
- Uses the
POLYGON_API_KEYenvironment variable (backward compatibility) - Falls back to the
defaultkey from the secrets file - Throws an error if no API key is available
Security Features
Current Implementation
- YAML file-based storage with secure file permissions
- Client ID validation and logging
- API key masking in logs
- Graceful fallback mechanisms
Future Enhancements
The secrets manager is designed to be extensible:
# Encryption support using SECRET_KEY
def _decrypt_file_content(self, encrypted_content: bytes) -> str:
encryption_key = self._get_encryption_key()
fernet = Fernet(encryption_key)
return fernet.decrypt(encrypted_content).decode('utf-8')
# External secrets manager integration
def _fetch_from_vault(self, client_id: str) -> str:
# Connect to HashiCorp Vault, AWS Secrets Manager, etc.
pass
API Key Management
Reloading Secrets
The secrets manager supports runtime reloading:
# Programmatically reload secrets
secrets_manager.reload_secrets()
Monitoring
The server logs all API key access attempts with redacted keys for security:
INFO: 🔑 Client ID found in header: client_demo
INFO: API key found for client_id: client_demo (key: oN7d...EKTp)
INFO: ✅ Using client-specific API key for client: client_demo
API keys are automatically redacted in logs showing only the first 4 and last 4 characters.
File Encryption (Supported)
The secrets manager now supports encrypted secrets files using the existing SECRET_KEY.
Encrypting a Secrets File
Use the built-in encryption method:
# Encrypt the current secrets file
success = secrets_manager.encrypt_secrets_file()
# Encrypt a specific file
success = secrets_manager.encrypt_secrets_file('plain.yml', 'encrypted.yml')
Or manually encrypt using the SECRET_KEY:
from cryptography.fernet import Fernet
import base64
import hashlib
# Generate encryption key from SECRET_KEY
secret_key = os.environ.get("SECRET_KEY")
key_bytes = hashlib.sha256(secret_key.encode()).digest()
encryption_key = base64.urlsafe_b64encode(key_bytes)
# Encrypt secrets file
fernet = Fernet(encryption_key)
with open('.keys.yml', 'r') as f:
plain_content = f.read()
encrypted_data = fernet.encrypt(plain_content.encode('utf-8'))
encoded_data = base64.b64encode(encrypted_data).decode('utf-8')
with open('.keys.yml.encrypted', 'w') as f:
f.write(encoded_data)
Using Encrypted Files
The secrets manager automatically detects and decrypts encrypted files:
- Filename-based Detection: Files ending with
.encryptedare recognized as encrypted - Transparent Decryption: Encrypted files are automatically decrypted using the SECRET_KEY
- Error Handling: Clear error messages if decryption fails
INFO: Encrypted secrets file detected (filename ends with .encrypted), attempting to decrypt...
INFO: Successfully decrypted secrets file
Example usage:
- Plain text:
.keys.yml→ loaded directly - Encrypted:
.keys.yml.encrypted→ automatically decrypted
Encryption Format
Encrypted files are stored as base64-encoded Fernet tokens:
- Detection: Files with
.encryptedextension are treated as encrypted - Encoding: Base64 encoded for text file storage
- Key Derivation: SHA256 hash of SECRET_KEY for consistent key generation
- Content: Fernet-encrypted YAML data encoded as base64 text
Encryption Utility Script
A utility script encrypt_secrets.py is provided for easy encryption/decryption:
# Encrypt the default secrets file
python encrypt_secrets.py
# Encrypt a specific file
python encrypt_secrets.py plain.yml encrypted.yml
# Test decryption of an encrypted file
python encrypt_secrets.py --test encrypted.yml
# Decrypt an encrypted file back to plain text
python encrypt_secrets.py --decrypt encrypted.yml decrypted.yml
The script requires the SECRET_KEY environment variable to be set.
Troubleshooting
Common Issues
- File not found: Ensure the secrets file exists at the mapped path
- Permission denied: Check file permissions (should be 600)
- YAML parsing error: Validate YAML syntax
- No API key found: Check client ID spelling and file contents
Debug Logging
Enable debug logging to see detailed information:
logging.basicConfig(level=logging.DEBUG)
Health Check
Check secrets manager status:
stats = secrets_manager.get_stats()
print(f"Loaded {stats['client_count']} clients")
print(f"File exists: {stats['file_exists']}")
Production Considerations
- Backup: Regularly backup the secrets file
- Rotation: Implement API key rotation procedures
- Monitoring: Monitor API usage per client
- Encryption: Consider encrypting the secrets file
- Access Control: Restrict file system access
- Auditing: Log all key access attempts
Integration Examples
AWS Secrets Manager
import boto3
class AWSSecretsManager(SecretsManager):
def __init__(self):
self.client = boto3.client('secretsmanager')
def get_api_key(self, client_id: str) -> str:
response = self.client.get_secret_value(
SecretId=f'fininfo/clients/{client_id}/api-key'
)
return response['SecretString']
HashiCorp Vault
import hvac
class VaultSecretsManager(SecretsManager):
def __init__(self):
self.client = hvac.Client(url='https://vault.example.com')
def get_api_key(self, client_id: str) -> str:
response = self.client.secrets.kv.v2.read_secret_version(
path=f'fininfo/clients/{client_id}'
)
return response['data']['data']['api_key']