The API Key Breach Scenario
Your D2C integrates with:
Stripe - Payment processing
Twilio - SMS notifications
Shopify - Inventory sync
Mailchimp - Email marketing
FedEx - Shipping
Each requires API keys to function. One developer commits code to GitHub with an API key hardcoded:
stripe_api = "sk_live_51234567890abcdef" # EXPOSED!
Within 1 Hour
Bot discovers key on GitHub
Uses it to steal $50,000 in fraudulent charges
Your customers' payment data is accessed
You discover breach 72 hours later
GDPR fines: $4M (4% of revenue or $20M, whichever is greater)
Stripe suspends your account
With Proper Security
stripe_api = os.environ.get('STRIPE_API_KEY') # Secured!
Key never exposed in code
Stolen? Rotate immediately without code changes
Access audited and logged
Compliance automatic
The Difference: $50,000 loss + $4M fines + reputation destroyed
vs. bulletproof security with zero overhead.
We've implemented 150+ Odoo systems. The ones with proper secret management? Zero breaches, compliant, trusted. The ones without? Data breaches, compliance failures, regulatory fines, customer loss. That's $5M-$20M in accumulated risk and lost trust.
The Secret Management Problem
Common Mistakes (WRONG)
# ❌ Hardcoded in code
stripe_key = "sk_live_abc123"
# ❌ Stored in git repository
# git log shows entire history of secrets
# ❌ Logged in error messages
logger.error(f"API call failed with key {api_key}")
# ❌ Stored in database without encryption
config['stripe_key'] = "sk_live_abc123" # Plain text!
# ❌ Sent in emails or Slack
"Here's the API key: sk_live_abc123"
Impact of Exposed Keys
| Impact | Cost |
|---|---|
| Fraudulent charges | $50k-$500k |
| GDPR fines | $4M-$20M |
| Account suspension | Business halt |
| Reputation damage | Permanent customer loss |
Storing Secrets Securely (Three-Layer Approach)
Layer 1: Environment Variables
Why: Not in code, easy to rotate, different per environment.
STRIPE_API_KEY=sk_live_abc123
TWILIO_API_KEY=twilio_secret_xyz
MAILCHIMP_API_KEY=mailchimp_token_789
DATABASE_PASSWORD=super_secret_123
.env
.env.local
.env.*.local
import os
stripe_key = os.environ.get('STRIPE_API_KEY')
if not stripe_key:
raise Exception("STRIPE_API_KEY environment variable not set")
# Use stripe_key for all API calls
# Option 1: Set before running Odoo
export STRIPE_API_KEY="sk_live_abc123"
./odoo-bin -c /etc/odoo/odoo.conf -d database_name
# Option 2: Docker (recommended)
# docker-compose.yml
environment:
- STRIPE_API_KEY=sk_live_abc123
- TWILIO_API_KEY=twilio_secret_xyz
Layer 2: ir.config_parameter (UI-Configurable)
When to use: Settings users configure in Odoo UI.
from odoo import models, fields, api
class StripeSetting(models.TransientModel):
_name = 'stripe.settings'
_inherit = 'res.config.settings'
stripe_api_key = fields.Char(
string='Stripe API Key',
help='Get from https://dashboard.stripe.com/apikeys'
)
@api.model
def get_values(self):
res = super().get_values()
config = self.env['ir.config_parameter'].sudo()
res.update(
stripe_api_key=config.get_param('stripe.api_key', ''),
)
return res
def set_values(self):
res = super().set_values()
config = self.env['ir.config_parameter'].sudo()
config.set_param('stripe.api_key', self.stripe_api_key)
return res
class PaymentProcessor(models.Model):
_name = 'payment.processor'
def process_payment(self):
api_key = self.env['ir.config_parameter'].sudo().get_param(
'stripe.api_key'
)
# Use api_key
Layer 3: Encrypted Field (Maximum Security)
When to use: Highly sensitive data (passwords, API keys for financial systems).
from odoo import models, fields, api
from cryptography.fernet import Fernet
import os
class SecurePaymentConfig(models.Model):
_name = 'secure.payment.config'
name = fields.Char(string='Config Name', required=True)
# Encrypted field (stored encrypted in database)
api_key = fields.Char(
string='API Key',
groups='base.group_system', # Only system admin can read
)
# Store encryption key in environment (NOT database)
_encryption_key = os.environ.get('ENCRYPTION_KEY')
def _encrypt_api_key(self, api_key):
"""Encrypt before storing."""
if not self._encryption_key:
raise Exception("ENCRYPTION_KEY not set")
cipher = Fernet(self._encryption_key.encode())
return cipher.encrypt(api_key.encode()).decode()
def _decrypt_api_key(self, encrypted_key):
"""Decrypt when retrieving."""
if not self._encryption_key:
raise Exception("ENCRYPTION_KEY not set")
cipher = Fernet(self._encryption_key.encode())
return cipher.decrypt(encrypted_key.encode()).decode()
@api.model_create_multi
def create(self, vals_list):
"""Encrypt API key before storing."""
for vals in vals_list:
if 'api_key' in vals:
vals['api_key'] = self._encrypt_api_key(vals['api_key'])
return super().create(vals_list)
def get_decrypted_api_key(self):
"""Retrieve decrypted API key."""
return self._decrypt_api_key(self.api_key)
Real D2C Example: Complete Secret Management
Directory Structure
odoo_project/
├── .env # Local secrets (never commit)
├── .env.production # Production secrets
├── .gitignore # Includes .env
├── docker-compose.yml # Load .env
├── odoo/
│ ├── models/
│ │ └── payment_integration.py
│ └── views/
│ └── payment_settings_views.xml
└── .github/
└── workflows/
└── deploy.yml # Secure secret injection
Payment Gateway Configuration
from odoo import models, fields, api
from odoo.exceptions import ValidationError
import os
import requests
class PaymentGateway(models.Model):
_name = 'payment.gateway'
name = fields.Char(string='Gateway Name', required=True)
provider = fields.Selection([
('stripe', 'Stripe'),
('paypal', 'PayPal'),
('square', 'Square'),
])
is_active = fields.Boolean(default=True)
def action_process_payment(self, amount, customer):
"""Process payment using configured gateway."""
# Retrieve API key from environment or config
api_key = self._get_api_key()
if not api_key:
raise ValidationError("Payment gateway API key not configured")
# Log transaction (without exposing API key!)
self.env['payment.log'].create({
'gateway': self.name,
'amount': amount,
'customer': customer.name,
'masked_key': api_key[:10] + '***', # Log masked key
'timestamp': fields.Datetime.now(),
})
# Call payment API
response = requests.post(
f'https://api.{self.provider}.com/payments',
headers={'Authorization': f'Bearer {api_key}'},
json={'amount': amount, 'customer': customer.id}
)
return response.json()
def _get_api_key(self):
"""Get API key from secure source."""
# Priority 1: Environment variable
env_key = os.environ.get(f'{self.provider.upper()}_API_KEY')
if env_key:
return env_key
# Priority 2: ir.config_parameter
config = self.env['ir.config_parameter'].sudo()
key = config.get_param(f'payment.{self.provider}.api_key')
if key:
return key
# Priority 3: Company-specific config
company = self.env.company
if hasattr(company, f'{self.provider}_api_key'):
encrypted_key = getattr(company, f'{self.provider}_api_key')
return self._decrypt(encrypted_key)
return False
Docker Setup
version: '3'
services:
odoo:
image: odoo:18
ports:
- "8069:8069"
environment:
- HOST=db
- USER=odoo
- PASSWORD=odoo
- STRIPE_API_KEY=${STRIPE_API_KEY}
- TWILIO_API_KEY=${TWILIO_API_KEY}
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
depends_on:
- db
db:
image: postgres:15
environment:
- POSTGRES_DB=odoo
- POSTGRES_USER=odoo
- POSTGRES_PASSWORD=${DB_PASSWORD}
# Create .env file
cat > .env << EOF
STRIPE_API_KEY=sk_live_abc123
TWILIO_API_KEY=twilio_secret_xyz
ENCRYPTION_KEY=your_encryption_key_32_chars
DB_PASSWORD=super_secret_db_password
EOF
# Run (loads .env automatically)
docker-compose up -d
GDPR Compliance & Audit Trails
Log All Sensitive Operations
class SensitiveActionLog(models.Model):
_name = 'sensitive.action.log'
action = fields.Char(required=True) # What happened
user_id = fields.Many2one('res.users', required=True) # Who did it
timestamp = fields.Datetime(auto_now_add=True) # When
ip_address = fields.Char() # From where
result = fields.Selection([
('success', 'Success'),
('failed', 'Failed'),
])
details = fields.Text() # What was the operation
@staticmethod
def log_api_access(user, action, result='success', details=''):
"""Log access to sensitive operations."""
request = request.environ.get('werkzeug.request')
ip = request.remote_addr if request else 'unknown'
return CurrentRecordEnv['sensitive.action.log'].create({
'action': action,
'user_id': user.id,
'ip_address': ip,
'result': result,
'details': details,
})
class PaymentProcessor(models.Model):
_inherit = 'payment.processor'
def process_payment(self):
try:
# Process payment
result = self._call_stripe_api()
# Log success
SensitiveActionLog.log_api_access(
user=self.env.user,
action='Payment processed via Stripe',
result='success',
details=f'Amount: $' + '{result["amount"]}'
)
except Exception as e:
# Log failure
SensitiveActionLog.log_api_access(
user=self.env.user,
action='Payment processing failed',
result='failed',
details=str(e)
)
Your Action Items
Immediate (1 hour)
❏ Create .env file with all secrets
❏ Add .env to .gitignore
❏ Replace hardcoded API keys with environment variables
❏ Remove secrets from git history
Short-term (4 hours)
❏ Store UI-configurable secrets in ir.config_parameter
❏ Add access controls (groups='base.group_system')
❏ Implement audit logging for sensitive operations
❏ Test secret rotation
Ongoing
❏ Rotate API keys quarterly
❏ Monitor audit logs for suspicious access
❏ Keep encryption keys secure
❏ Review compliance (GDPR, PCI-DSS if needed)
Free Secret Management Security Audit
Stop risking data breaches. Most D2C brands have secrets exposed in code or logs. Finding and fixing this prevents $5M-$20M in compliance fines and breach costs. We'll audit current secret storage practices, identify exposed keys in git history, remove secrets from codebase, implement environment variable management, set up audit trails, and test key rotation procedures.
