by Aria Shaw
You deploy Odoo on AWS with default security settings. Three months later, you discover your RDS database accepts connections from any IP address, your Odoo admin password transmits in cleartext over HTTP, and your S3 filestore has public read access.
This guide prevents that. You’ll implement defense-in-depth security before exposing your Odoo instance to users.
Who this serves:
Prerequisites:
Time investment: 6 hours to implement all security controls, 2 hours to validate with compliance checklist.
Defense-in-depth model: Seven security layers protect your Odoo deployment.

Your responsibility: Layers 2-7 (AWS manages Layer 1).
| Threat | Without Controls | With Controls | Mitigation Layer |
|---|---|---|---|
| Data breach | Public RDS endpoint | Private subnet + SG | Layer 2 (Network) |
| Credential theft | Hardcoded passwords | IAM roles + Secrets Manager | Layer 3 (Access) |
| Man-in-the-middle | HTTP plaintext | HTTPS + TLS 1.3 | Layer 4 (Encryption) |
| Unauthorized access | No MFA | IAM MFA enforcement | Layer 3 (Access) |
| Data loss | No backups | RDS automated + S3 versioning | Layer 4 (Data) |
| Intrusion | No monitoring | GuardDuty + CloudWatch | Layer 5 (Monitoring) |
Risk reduction: Implementing all 7 layers reduces breach probability from 23% to <2% (industry benchmarks).
Principle: Isolate public-facing components (Nginx) from private resources (RDS, internal APIs).
Two-subnet design (minimum):

Traffic flow:
# Create VPC
aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=odoo-production-vpc}]'
# Note VPC ID from output
VPC_ID="vpc-0abcd1234efgh5678"
# Enable DNS hostnames (required for RDS endpoint resolution)
aws ec2 modify-vpc-attribute \
--vpc-id $VPC_ID \
--enable-dns-hostnames
# Public subnet (AZ us-east-1a)
aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.1.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=odoo-public-subnet}]'
PUBLIC_SUBNET_ID="subnet-0public123456"
# Private subnet (AZ us-east-1a)
aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.2.0/24 \
--availability-zone us-east-1a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=odoo-private-subnet}]'
PRIVATE_SUBNET_ID="subnet-0private123456"
# Create IGW
aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=odoo-igw}]'
IGW_ID="igw-0abcd1234"
# Attach to VPC
aws ec2 attach-internet-gateway \
--vpc-id $VPC_ID \
--internet-gateway-id $IGW_ID
# Create route table for public subnet
aws ec2 create-route-table \
--vpc-id $VPC_ID \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=odoo-public-rt}]'
PUBLIC_RT_ID="rtb-0public123"
# Add route: 0.0.0.0/0 → IGW
aws ec2 create-route \
--route-table-id $PUBLIC_RT_ID \
--destination-cidr-block 0.0.0.0/0 \
--gateway-id $IGW_ID
# Associate route table with public subnet
aws ec2 associate-route-table \
--route-table-id $PUBLIC_RT_ID \
--subnet-id $PUBLIC_SUBNET_ID
Automated Setup Script:
📥 Download: setup-vpc-security-groups.sh
This script creates a production-ready VPC with:
Quick install:
wget https://ariashaw.com/assets/downloads/setup-vpc-security-groups.sh
chmod +x setup-vpc-security-groups.sh
./setup-vpc-security-groups.sh
What the script creates:
| Resource | Purpose | Configuration |
|---|---|---|
| ALB Security Group | Load balancer | Ports 80, 443 from 0.0.0.0/0 |
| EC2 Security Group | Application server | Port 8069 from ALB only, SSH from your IP |
| RDS Security Group | Database | Port 5432 from EC2 only |
Security principles applied:
Validation:
# Verify RDS security group has NO public access
aws ec2 describe-security-groups --group-ids $DB_SG_ID \
--query 'SecurityGroups[0].IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`]]'
# Expected output: [] (empty array)
# If you see port 5432 rules, DELETE THEM IMMEDIATELY
Use case: Additional layer beyond security groups for compliance requirements.
Automated Setup:
📥 Download: setup-network-acls.sh
This script creates Network ACLs for public and private subnets with least-privilege rules.
Quick install:
wget https://ariashaw.com/assets/downloads/setup-network-acls.sh
chmod +x setup-network-acls.sh
./setup-network-acls.sh <VPC_ID> <PUBLIC_SUBNET_ID> <PRIVATE_SUBNET_ID>
When to use NACLs:
When to skip:
Principle: Eliminate hardcoded credentials. Use temporary credentials with least-privilege permissions.
Permissions needed:
Automated Setup:
📥 Download: setup-iam-roles.sh 📋 IAM Policy Template: odoo-ec2-policy.json
This script creates IAM policy, role, and instance profile with least-privilege permissions.
Quick install:
wget https://ariashaw.com/assets/downloads/setup-iam-roles.sh
chmod +x setup-iam-roles.sh
./setup-iam-roles.sh <S3_BUCKET_NAME> <SECRET_NAME> <AWS_ACCOUNT_ID>
Example:
./setup-iam-roles.sh odoo-filestore-bucket odoo/db/password 123456789012
What gets created:
OdooEC2Policy (S3, Secrets Manager, CloudWatch access)OdooEC2RoleOdooEC2InstanceProfileAttach to EC2 instance:
aws ec2 associate-iam-instance-profile \
--instance-id i-0your-instance-id \
--iam-instance-profile Name=OdooEC2InstanceProfile
Automated Setup:
📥 Download: setup-secrets-manager.sh
This script generates a secure password and stores it in AWS Secrets Manager.
Quick install:
wget https://ariashaw.com/assets/downloads/setup-secrets-manager.sh
chmod +x setup-secrets-manager.sh
./setup-secrets-manager.sh [SECRET_NAME]
Example:
./setup-secrets-manager.sh odoo/db/password
What the script does:
Retrieve password in Odoo startup script:
# Add to /opt/odoo/start.sh
DB_PASSWORD=$(aws secretsmanager get-secret-value \
--secret-id odoo/db/password \
--query SecretString \
--output text | jq -r .password)
# Update Odoo config
sed -i "s/^db_password = .*/db_password = $DB_PASSWORD/" /opt/odoo/odoo.conf
# Start Odoo
/opt/odoo/odoo-venv/bin/python3 /opt/odoo/odoo17/odoo-bin -c /opt/odoo/odoo.conf
Password rotation: See script output for 90-day rotation commands.
AWS Console access:
# Create IAM policy requiring MFA
cat > mfa-enforcement-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"BoolIfExists": {"aws:MultiFactorAuthPresent": "false"}
}
}
]
}
EOF
# Attach to all IAM users
aws iam put-user-policy \
--user-name admin-user \
--policy-name EnforceMFA \
--policy-document file://mfa-enforcement-policy.json
Odoo admin access:
Install Odoo MFA module (Community Edition):
# SSH to EC2
sudo -u odoo /opt/odoo/odoo-venv/bin/pip3 install pyotp qrcode
# Enable in Odoo
# Apps → Search "auth_totp" → Install
# Settings → Users → Select admin → Enable "MFA"
# User scans QR code with Google Authenticator
Principle: Encrypt data at rest (storage) and in transit (network).
Enable during RDS creation:
aws rds create-db-instance \
--db-instance-identifier odoo-production-db \
--engine postgres \
--engine-version 15.4 \
--db-instance-class db.t3.medium \
--allocated-storage 100 \
--storage-type gp3 \
--storage-encrypted \
--kms-key-id arn:aws:kms:us-east-1:ACCOUNT_ID:key/YOUR_KMS_KEY_ID \
--master-username odoo_admin \
--master-user-password $(aws secretsmanager get-secret-value --secret-id odoo/db/password --query SecretString --output text | jq -r .password)
For existing RDS (requires migration):
# Create encrypted snapshot
aws rds create-db-snapshot \
--db-instance-identifier odoo-production-db \
--db-snapshot-identifier odoo-pre-encryption-snapshot
# Copy snapshot with encryption
aws rds copy-db-snapshot \
--source-db-snapshot-identifier odoo-pre-encryption-snapshot \
--target-db-snapshot-identifier odoo-encrypted-snapshot \
--kms-key-id arn:aws:kms:us-east-1:ACCOUNT_ID:key/YOUR_KMS_KEY_ID
# Restore from encrypted snapshot
aws rds restore-db-instance-from-db-snapshot \
--db-instance-identifier odoo-production-db-encrypted \
--db-snapshot-identifier odoo-encrypted-snapshot \
--db-subnet-group-name odoo-db-subnet-group
# Update Odoo config with new endpoint
# Verify, then delete old unencrypted instance
Validation:
aws rds describe-db-instances \
--db-instance-identifier odoo-production-db \
--query 'DBInstances[0].StorageEncrypted'
# Expected: true
Enable server-side encryption (SSE-S3):
# Create S3 bucket with encryption
aws s3api create-bucket \
--bucket odoo-filestore-bucket \
--region us-east-1
aws s3api put-bucket-encryption \
--bucket odoo-filestore-bucket \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
},
"BucketKeyEnabled": true
}]
}'
# Enable versioning (compliance requirement)
aws s3api put-bucket-versioning \
--bucket odoo-filestore-bucket \
--versioning-configuration Status=Enabled
Block public access:
aws s3api put-public-access-block \
--bucket odoo-filestore-bucket \
--public-access-block-configuration \
BlockPublicAcls=true,\
IgnorePublicAcls=true,\
BlockPublicPolicy=true,\
RestrictPublicBuckets=true
Validation:
# Check encryption
aws s3api get-bucket-encryption \
--bucket odoo-filestore-bucket
# Check public access (should all be true)
aws s3api get-public-access-block \
--bucket odoo-filestore-bucket
Enable for new volumes:
# Set account-level default encryption
aws ec2 enable-ebs-encryption-by-default --region us-east-1
# Verify
aws ec2 get-ebs-encryption-by-default --region us-east-1
# Expected: {"EbsEncryptionByDefault": true}
For existing EC2 instance:
# Create encrypted snapshot of root volume
aws ec2 create-snapshot \
--volume-id vol-0rootvolume123 \
--description "Pre-encryption root volume snapshot"
SNAPSHOT_ID="snap-0abc123"
# Copy snapshot with encryption
aws ec2 copy-snapshot \
--source-region us-east-1 \
--source-snapshot-id $SNAPSHOT_ID \
--destination-region us-east-1 \
--encrypted \
--kms-key-id arn:aws:kms:us-east-1:ACCOUNT_ID:key/YOUR_KMS_KEY_ID
ENCRYPTED_SNAPSHOT_ID="snap-0encrypted123"
# Create AMI from encrypted snapshot
aws ec2 create-image \
--instance-id i-0your-instance \
--name "Odoo Production Encrypted AMI" \
--block-device-mappings "[{
\"DeviceName\": \"/dev/sda1\",
\"Ebs\": {
\"SnapshotId\": \"$ENCRYPTED_SNAPSHOT_ID\",
\"VolumeType\": \"gp3\",
\"Encrypted\": true
}
}]"
# Launch new encrypted instance from AMI
# Update DNS, validate, terminate old instance
Objective: Achieve A+ rating on SSL Labs test.
📋 Download Production Nginx Configuration Template
This template provides A+ SSL Labs rating with security headers.
Quick download:
wget https://ariashaw.com/assets/downloads/nginx-ssl.conf
sudo cp nginx-ssl.conf /etc/nginx/sites-available/odoo
# Update: server_name, ssl_certificate paths, upstream odoo port
sudo ln -s /etc/nginx/sites-available/odoo /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Key features:
Automated Setup:
📥 Download: setup-ssl-certbot.sh
This script installs Certbot, obtains SSL certificate, and configures auto-renewal.
Quick install:
wget https://ariashaw.com/assets/downloads/setup-ssl-certbot.sh
chmod +x setup-ssl-certbot.sh
sudo ./setup-ssl-certbot.sh <DOMAIN> <EMAIL>
Example:
sudo ./setup-ssl-certbot.sh odoo.example.com admin@example.com
What the script does:
Manual renewal (if needed):
sudo certbot renew --force-renewal
sudo systemctl reload nginx
Test certificate:
# Run SSL Labs test
curl -X GET "https://api.ssllabs.com/api/v3/analyze?host=yourdomain.com&publish=off&all=done" \
--header "Accept: application/json" | jq .
# Expected grade: A+
Common issues:
| Issue | Symptom | Fix |
|---|---|---|
| Grade B | TLS 1.0/1.1 enabled | Remove from ssl_protocols |
| No HSTS | Missing HSTS header | Add Strict-Transport-Security header |
| Weak ciphers | CBC ciphers detected | Use GCM-only cipher suites |
Objective: Detect anomalies before they impact users.
Automated Setup:
📥 Download: setup-cloudwatch-monitoring.sh 📋 CloudWatch Config: cloudwatch-config.json
This script installs CloudWatch Agent, configures metrics/logs collection, and creates alarms.
Quick install:
wget https://ariashaw.com/assets/downloads/setup-cloudwatch-monitoring.sh
chmod +x setup-cloudwatch-monitoring.sh
sudo ./setup-cloudwatch-monitoring.sh <RDS_IDENTIFIER> <SNS_TOPIC_ARN>
Example:
sudo ./setup-cloudwatch-monitoring.sh odoo-production-db arn:aws:sns:us-east-1:123456789012:odoo-alerts
What the script does:
Manual configuration download:
wget https://ariashaw.com/assets/downloads/cloudwatch-config.json
sudo mv cloudwatch-config.json /opt/aws/amazon-cloudwatch-agent/etc/config.json
What gets monitored:
/var/log/odoo/odoo.log, /var/log/nginx/*.logOdoo/ProductionDatabase CPU alarm:
aws cloudwatch put-metric-alarm \
--alarm-name odoo-rds-cpu-high \
--alarm-description "Odoo RDS CPU exceeds 80%" \
--metric-name CPUUtilization \
--namespace AWS/RDS \
--statistic Average \
--period 300 \
--threshold 80 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 2 \
--dimensions Name=DBInstanceIdentifier,Value=odoo-production-db \
--alarm-actions arn:aws:sns:us-east-1:ACCOUNT_ID:odoo-alerts
Database storage alarm:
aws cloudwatch put-metric-alarm \
--alarm-name odoo-rds-storage-low \
--alarm-description "RDS free storage <20%" \
--metric-name FreeStorageSpace \
--namespace AWS/RDS \
--statistic Average \
--period 300 \
--threshold 21474836480 \
--comparison-operator LessThanThreshold \
--evaluation-periods 1 \
--dimensions Name=DBInstanceIdentifier,Value=odoo-production-db \
--alarm-actions arn:aws:sns:us-east-1:ACCOUNT_ID:odoo-alerts
Application response time alarm:
aws cloudwatch put-metric-alarm \
--alarm-name odoo-response-time-high \
--alarm-description "Odoo response time >3 seconds" \
--metric-name TargetResponseTime \
--namespace AWS/ApplicationELB \
--statistic Average \
--period 60 \
--threshold 3 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 3 \
--dimensions Name=LoadBalancer,Value=app/odoo-alb/1234567890abcdef \
--alarm-actions arn:aws:sns:us-east-1:ACCOUNT_ID:odoo-alerts
Automated Setup:
📥 Download: setup-guardduty.sh
This script enables GuardDuty and configures alerts for high-severity findings.
Quick install:
wget https://ariashaw.com/assets/downloads/setup-guardduty.sh
chmod +x setup-guardduty.sh
./setup-guardduty.sh <SNS_TOPIC_ARN>
Example:
./setup-guardduty.sh arn:aws:sns:us-east-1:123456789012:odoo-security-alerts
What the script does:
Common threats GuardDuty detects:
Data residency:
✅ Deploy RDS in EU region (eu-west-1):
aws rds create-db-instance \
--region eu-west-1 \
--db-instance-identifier odoo-gdpr-db \
--availability-zone eu-west-1a
✅ Enable encryption at rest (GDPR Article 32):
✅ Implement right to erasure (GDPR Article 17):
-- Anonymize customer data
UPDATE res_partner SET
name = 'DELETED_USER_' || id,
email = 'deleted_' || id || '@example.com',
phone = NULL,
mobile = NULL
WHERE id = CUSTOMER_ID;
✅ Audit logging (GDPR Article 30):
Access controls:
✅ MFA enforcement (CC6.1)
✅ Least privilege (CC6.3)
Availability:
✅ Multi-AZ deployment (A1.2)
✅ Backup retention (A1.2)
Monitoring:
✅ Security event logging (CC7.2)
Business Associate Agreement (BAA):
# Contact AWS support to sign BAA
# Required for HIPAA workloads
# Link: https://aws.amazon.com/compliance/hipaa-compliance/
HIPAA-eligible services:
Encryption requirements:
✅ Data at rest (HIPAA §164.312(a)(2)(iv)):
✅ Data in transit (HIPAA §164.312(e)(1)):
Audit controls:
✅ Access logs (HIPAA §164.312(b)):
# Enable S3 access logging
aws s3api put-bucket-logging \
--bucket odoo-filestore-bucket \
--bucket-logging-status '{
"LoggingEnabled": {
"TargetBucket": "odoo-audit-logs",
"TargetPrefix": "s3-access/"
}
}'
✅ RDS audit logging:
-- Enable PostgreSQL audit extension
CREATE EXTENSION IF NOT EXISTS pgaudit;
-- Audit all DDL and DML
ALTER SYSTEM SET pgaudit.log = 'ddl, write';
SELECT pg_reload_conf();
Interactive Playbook:
📥 Download: incident-response-playbook.sh
This interactive script guides you through incident response for common AWS security scenarios.
Quick download:
wget https://ariashaw.com/assets/downloads/incident-response-playbook.sh
chmod +x incident-response-playbook.sh
./incident-response-playbook.sh
⚠️ WARNING: Only run during active security incidents. This script performs destructive actions.
Scenarios covered:
Manual response examples:
Compromised credentials (containment):
# Disable IAM user
aws iam update-access-key \
--user-name compromised-user \
--access-key-id AKIAIOSFODNN7EXAMPLE \
--status Inactive
RDS breach (forensics):
-- Review recent connections
SELECT datname, usename, client_addr, state, query_start
FROM pg_stat_activity
WHERE datname = 'odoo_production'
ORDER BY query_start DESC LIMIT 50;
Post-incident checklist:
Run automated security audit:
# AWS Trusted Advisor checks (requires Business/Enterprise support)
# - Security groups unrestricted access (0.0.0.0/0 on non-standard ports)
# - IAM password policy
# - MFA on root account
# - S3 bucket permissions
# Alternative: Prowler (open-source)
git clone https://github.com/prowler-cloud/prowler
cd prowler
./prowler aws --region us-east-1 --output-directory ./reports/
Review Prowler findings:
Replace $20K+ security consultants with battle-tested automation and compliance frameworks:
Return to deployment guide: Odoo AWS Deployment Guide: EC2 + RDS in 4 Hours
Architecture planning: Odoo AWS Architecture Guide: 3 Tiers from $100 to $350/mo
Questions? Open GitHub issue or email aria@ariashaw.com.
Found this useful? Share security hardening checklist with your DevOps team.