Contents

Odoo AWS Security: Lock Down Production in 6 Hours

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.


Security Architecture Overview

Defense-in-depth model: Seven security layers protect your Odoo deployment.

Security Stack

Defense-in-depth security model showing 7 layers from physical security to compliance controls

Your responsibility: Layers 2-7 (AWS manages Layer 1).

Threat Model

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).


Network Layer: VPC Design

Principle: Isolate public-facing components (Nginx) from private resources (RDS, internal APIs).

VPC Architecture

Two-subnet design (minimum):

VPC architecture with public subnet for EC2 and private subnet for RDS, showing traffic flow through Internet Gateway and NAT Gateway

Traffic flow:

  1. User → Internet → Internet Gateway → Public Subnet EC2 (Nginx)
  2. EC2 Odoo → Private Subnet RDS (internal VPC routing)
  3. RDS → NAT Gateway → Internet (for security patches only)

Create VPC

# 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

Create Subnets

# 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"

Configure Internet Gateway

# 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

Complete VPC & Security Groups Setup

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

Network ACLs (Optional Defense Layer)

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:


Access Control: IAM & Secrets

Principle: Eliminate hardcoded credentials. Use temporary credentials with least-privilege permissions.

IAM Role for EC2

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:

Attach to EC2 instance:

aws ec2 associate-iam-instance-profile \
  --instance-id i-0your-instance-id \
  --iam-instance-profile Name=OdooEC2InstanceProfile

Secrets Manager for Database Password

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.

MFA Enforcement

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

Data Protection: Encryption

Principle: Encrypt data at rest (storage) and in transit (network).

RDS Encryption at Rest

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

S3 Encryption

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

EBS Encryption

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

SSL/TLS Configuration

Objective: Achieve A+ rating on SSL Labs test.

Nginx SSL Configuration

📋 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:

Let’s Encrypt SSL Certificate

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

SSL Labs Validation

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

Monitoring & Alerting

Objective: Detect anomalies before they impact users.

CloudWatch Agent Installation

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:

CloudWatch Alarms

Database 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

GuardDuty Threat Detection

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:


Compliance Validation

GDPR Requirements

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):

SOC2 Type II

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)

HIPAA Compliance

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();

Security Incident Response

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:

  1. Compromised AWS Credentials
    • Indicators: UnauthorizedAccess GuardDuty alert, unusual API calls
    • Actions: Disable IAM user, assess resource creation, remediate
  2. RDS Data Breach
    • Indicators: Security group modification, unusual connections
    • Actions: Block public access, forensics, PITR restore
  3. EC2 Instance Compromise
    • Indicators: Malware detection, unauthorized processes
    • Actions: Create forensic snapshot, isolate instance, rebuild
  4. S3 Bucket Public Access
    • Indicators: Public ACL detected, unusual access patterns
    • Actions: Block public access, review logs, implement policies

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:


Next Steps

Validate Your Security Posture

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:

Get Production Security Tools

Replace $20K+ security consultants with battle-tested automation and compliance frameworks:

Continue Learning

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.