AWS RDS Instance Deletion Protection Disabled

High Risk Infrastructure Security
awsrdsdatabasedeletion-protectionavailabilitydata-lossbusiness-continuityterraformcloudformation

What it is

A critical availability vulnerability where Amazon RDS database instances are configured without deletion protection, making them vulnerable to accidental or malicious deletion. This can result in permanent data loss, business disruption, and costly recovery efforts. Without deletion protection, RDS instances can be deleted through the console, CLI, or API calls without additional safeguards, potentially causing irreversible damage to production databases containing critical business data.

# VULNERABLE: RDS instance without deletion protection
resource "aws_db_instance" "production_db" {
  identifier     = "production-database"
  engine         = "postgres"
  engine_version = "14.9"
  instance_class = "db.r6g.large"
  
  allocated_storage     = 100
  max_allocated_storage = 1000
  storage_type         = "gp3"
  storage_encrypted    = true
  
  db_name  = "production"
  username = "dbadmin"
  password = var.db_password
  
  # Missing: deletion_protection = true
  # Database can be accidentally deleted
  
  backup_retention_period = 7
  backup_window          = "03:00-04:00"
  maintenance_window     = "sun:04:00-sun:05:00"
  
  vpc_security_group_ids = [aws_security_group.db_sg.id]
  db_subnet_group_name   = aws_db_subnet_group.db_subnet_group.name
  
  skip_final_snapshot = true  # ALSO VULNERABLE
  
  tags = {
    Environment = "production"
    Application = "web-app"
  }
}

# VULNERABLE: CloudFormation without deletion protection
Resources:
  ProductionDatabase:
    Type: AWS::RDS::DBInstance
    Properties:
      DBInstanceIdentifier: production-database
      Engine: postgres
      EngineVersion: 14.9
      DBInstanceClass: db.r6g.large
      AllocatedStorage: 100
      MaxAllocatedStorage: 1000
      StorageType: gp3
      StorageEncrypted: true
      DBName: production
      MasterUsername: dbadmin
      MasterUserPassword: !Ref DBPassword
      # Missing: DeletionProtection: true
      BackupRetentionPeriod: 7
      PreferredBackupWindow: "03:00-04:00"
      PreferredMaintenanceWindow: "sun:04:00-sun:05:00"
      VPCSecurityGroups:
        - !Ref DatabaseSecurityGroup
      DBSubnetGroupName: !Ref DatabaseSubnetGroup
      DeleteAutomatedBackups: false
      Tags:
        - Key: Environment
          Value: production
        - Key: Application
          Value: web-app

# VULNERABLE: AWS CLI database creation
aws rds create-db-instance \
  --db-instance-identifier production-database \
  --engine postgres \
  --engine-version 14.9 \
  --db-instance-class db.r6g.large \
  --allocated-storage 100 \
  --storage-encrypted \
  --db-name production \
  --master-username dbadmin \
  --master-user-password mypassword \
  --backup-retention-period 7
  # Missing: --deletion-protection
# SECURE: RDS instance with deletion protection and comprehensive security
resource "aws_db_instance" "production_db" {
  identifier     = "production-database"
  engine         = "postgres"
  engine_version = "14.9"
  instance_class = "db.r6g.large"
  
  allocated_storage     = 100
  max_allocated_storage = 1000
  storage_type         = "gp3"
  storage_encrypted    = true
  kms_key_id          = aws_kms_key.rds_encryption.arn
  
  db_name  = "production"
  username = "dbadmin"
  password = random_password.db_password.result
  
  # Enable deletion protection
  deletion_protection = true
  
  # Comprehensive backup configuration
  backup_retention_period   = 30
  backup_window            = "03:00-04:00"
  copy_tags_to_snapshot    = true
  delete_automated_backups = false
  skip_final_snapshot      = false
  final_snapshot_identifier = "production-database-final-snapshot-${formatdate("YYYY-MM-DD-hhmm", timestamp())}"
  
  # Point-in-time recovery
  enabled_cloudwatch_logs_exports = ["postgresql"]
  
  maintenance_window = "sun:04:00-sun:05:00"
  auto_minor_version_upgrade = true
  
  # Network security
  vpc_security_group_ids = [aws_security_group.db_sg.id]
  db_subnet_group_name   = aws_db_subnet_group.db_subnet_group.name
  publicly_accessible    = false
  
  # Performance insights
  performance_insights_enabled = true
  performance_insights_kms_key_id = aws_kms_key.rds_encryption.arn
  performance_insights_retention_period = 7
  
  # Monitoring
  monitoring_interval = 60
  monitoring_role_arn = aws_iam_role.rds_monitoring_role.arn
  
  tags = {
    Environment = "production"
    Application = "web-app"
    Protected   = "true"
    Encrypted   = "true"
  }
  
  lifecycle {
    prevent_destroy = true
    ignore_changes  = [password]
  }
}

# KMS key for RDS encryption
resource "aws_kms_key" "rds_encryption" {
  description             = "KMS key for RDS encryption"
  deletion_window_in_days = 7
  enable_key_rotation     = true
  
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "Enable IAM User Permissions"
        Effect = "Allow"
        Principal = {
          AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
        }
        Action   = "kms:*"
        Resource = "*"
      },
      {
        Sid    = "Allow RDS Service"
        Effect = "Allow"
        Principal = {
          Service = "rds.amazonaws.com"
        }
        Action = [
          "kms:Decrypt",
          "kms:GenerateDataKey",
          "kms:CreateGrant",
          "kms:DescribeKey"
        ]
        Resource = "*"
      }
    ]
  })
  
  tags = {
    Name = "RDS Encryption Key"
    Purpose = "rds-encryption"
  }
}

resource "aws_kms_alias" "rds_encryption" {
  name          = "alias/rds-encryption-key"
  target_key_id = aws_kms_key.rds_encryption.key_id
}

# Secure password generation
resource "random_password" "db_password" {
  length  = 32
  special = true
  
  lifecycle {
    ignore_changes = [result]
  }
}

resource "aws_secretsmanager_secret" "db_password" {
  name                    = "rds-master-password"
  description             = "Master password for RDS instance"
  recovery_window_in_days = 7
  
  tags = {
    Environment = "production"
    Purpose     = "rds-credentials"
  }
}

resource "aws_secretsmanager_secret_version" "db_password" {
  secret_id     = aws_secretsmanager_secret.db_password.id
  secret_string = jsonencode({
    username = aws_db_instance.production_db.username
    password = random_password.db_password.result
    host     = aws_db_instance.production_db.endpoint
    port     = aws_db_instance.production_db.port
    dbname   = aws_db_instance.production_db.db_name
  })
}

# IAM role for RDS monitoring
resource "aws_iam_role" "rds_monitoring_role" {
  name = "rds-monitoring-role"
  
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "monitoring.rds.amazonaws.com"
        }
      }
    ]
  })
  
  tags = {
    Name = "RDS Monitoring Role"
    Purpose = "rds-monitoring"
  }
}

resource "aws_iam_role_policy_attachment" "rds_monitoring" {
  role       = aws_iam_role.rds_monitoring_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole"
}

# CloudWatch alarms for database monitoring
resource "aws_cloudwatch_metric_alarm" "database_cpu" {
  alarm_name          = "rds-high-cpu"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/RDS"
  period              = "300"
  statistic           = "Average"
  threshold           = "80"
  alarm_description   = "This metric monitors RDS CPU utilization"
  alarm_actions       = [aws_sns_topic.alerts.arn]
  
  dimensions = {
    DBInstanceIdentifier = aws_db_instance.production_db.id
  }
  
  tags = {
    Environment = "production"
    Purpose     = "database-monitoring"
  }
}

resource "aws_cloudwatch_metric_alarm" "database_connections" {
  alarm_name          = "rds-high-connections"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "DatabaseConnections"
  namespace           = "AWS/RDS"
  period              = "300"
  statistic           = "Average"
  threshold           = "80"
  alarm_description   = "This metric monitors RDS connection count"
  alarm_actions       = [aws_sns_topic.alerts.arn]
  
  dimensions = {
    DBInstanceIdentifier = aws_db_instance.production_db.id
  }
  
  tags = {
    Environment = "production"
    Purpose     = "database-monitoring"
  }
}

# SECURE: CloudFormation with deletion protection
Resources:
  RDSEncryptionKey:
    Type: AWS::KMS::Key
    Properties:
      Description: KMS key for RDS encryption
      EnableKeyRotation: true
      KeyPolicy:
        Version: '2012-10-17'
        Statement:
          - Sid: Enable IAM User Permissions
            Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
            Action: 'kms:*'
            Resource: '*'
          - Sid: Allow RDS Service
            Effect: Allow
            Principal:
              Service: rds.amazonaws.com
            Action:
              - kms:Decrypt
              - kms:GenerateDataKey
              - kms:CreateGrant
              - kms:DescribeKey
            Resource: '*'
      Tags:
        - Key: Name
          Value: RDS Encryption Key
        - Key: Purpose
          Value: rds-encryption

  RDSEncryptionKeyAlias:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: alias/rds-encryption-key
      TargetKeyId: !Ref RDSEncryptionKey

  DBPassword:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: rds-master-password
      Description: Master password for RDS instance
      GenerateSecretString:
        SecretStringTemplate: '{"username": "dbadmin"}'
        GenerateStringKey: 'password'
        PasswordLength: 32
        ExcludeCharacters: '"@/\'
      Tags:
        - Key: Environment
          Value: production
        - Key: Purpose
          Value: rds-credentials

  ProductionDatabase:
    Type: AWS::RDS::DBInstance
    Properties:
      DBInstanceIdentifier: production-database
      Engine: postgres
      EngineVersion: 14.9
      DBInstanceClass: db.r6g.large
      AllocatedStorage: 100
      MaxAllocatedStorage: 1000
      StorageType: gp3
      StorageEncrypted: true
      KmsKeyId: !Ref RDSEncryptionKey
      DBName: production
      MasterUsername: dbadmin
      MasterUserPassword: !Sub '{{resolve:secretsmanager:${DBPassword}:SecretString:password}}'
      # Enable deletion protection
      DeletionProtection: true
      # Comprehensive backup configuration
      BackupRetentionPeriod: 30
      PreferredBackupWindow: "03:00-04:00"
      CopyTagsToSnapshot: true
      DeleteAutomatedBackups: false
      DBSnapshotIdentifier: !Sub "production-database-final-snapshot-${AWS::StackName}"
      EnableCloudwatchLogsExports:
        - postgresql
      PreferredMaintenanceWindow: "sun:04:00-sun:05:00"
      AutoMinorVersionUpgrade: true
      VPCSecurityGroups:
        - !Ref DatabaseSecurityGroup
      DBSubnetGroupName: !Ref DatabaseSubnetGroup
      PubliclyAccessible: false
      EnablePerformanceInsights: true
      PerformanceInsightsKMSKeyId: !Ref RDSEncryptionKey
      PerformanceInsightsRetentionPeriod: 7
      MonitoringInterval: 60
      MonitoringRoleArn: !GetAtt RDSMonitoringRole.Arn
      Tags:
        - Key: Environment
          Value: production
        - Key: Application
          Value: web-app
        - Key: Protected
          Value: 'true'
        - Key: Encrypted
          Value: 'true'
    DeletionPolicy: Snapshot
    UpdateReplacePolicy: Snapshot

# SECURE: AWS CLI with deletion protection
aws rds create-db-instance \
  --db-instance-identifier production-database \
  --engine postgres \
  --engine-version 14.9 \
  --db-instance-class db.r6g.large \
  --allocated-storage 100 \
  --storage-encrypted \
  --kms-key-id "arn:aws:kms:region:account:key/key-id" \
  --db-name production \
  --master-username dbadmin \
  --master-user-password mypassword \
  --backup-retention-period 30 \
  --copy-tags-to-snapshot \
  --no-delete-automated-backups \
  --deletion-protection \
  --enable-performance-insights \
  --monitoring-interval 60 \
  --vpc-security-group-ids sg-12345678 \
  --db-subnet-group-name production-subnet-group \
  --no-publicly-accessible

💡 Why This Fix Works

The vulnerable examples show RDS instances created without deletion protection, making them susceptible to accidental deletion. The secure alternatives demonstrate comprehensive protection including deletion protection, encryption, automated backups, monitoring, and lifecycle policies to prevent data loss and ensure database availability.

Why it happens

Database administrators create RDS instances without enabling deletion protection, often prioritizing quick deployment over safety measures. Deletion protection is disabled by default and must be explicitly enabled, leaving many production databases vulnerable to accidental deletion through console errors, script mistakes, or malicious actions.

Root causes

RDS Instance Without Deletion Protection Flag

Database administrators create RDS instances without enabling deletion protection, often prioritizing quick deployment over safety measures. Deletion protection is disabled by default and must be explicitly enabled, leaving many production databases vulnerable to accidental deletion through console errors, script mistakes, or malicious actions.

Infrastructure Code Missing Protection Settings

Terraform and CloudFormation templates that define RDS instances without the deletion_protection parameter or DeletionProtection property. This oversight commonly occurs when using basic database configurations, copying examples without security considerations, or when teams are unaware of this safety feature's importance for production databases.

Fixes

1

Enable Deletion Protection on RDS Instances

Set deletion_protection = true in Terraform or DeletionProtection: true in CloudFormation for all production RDS instances. This prevents accidental deletion and requires the protection to be explicitly disabled before deletion can proceed, providing an additional safety layer for critical databases.

2

Implement Database Backup and Recovery Strategy

Combine deletion protection with automated backups, point-in-time recovery, and cross-region snapshot replication. Configure appropriate backup retention periods and test recovery procedures regularly. Use automated backup monitoring and alerting to ensure backup systems are functioning properly.

3

Establish Database Lifecycle Policies

Create organizational policies requiring deletion protection for all production databases. Implement infrastructure scanning and compliance monitoring to detect unprotected instances. Establish change management processes that require approval for disabling deletion protection or deleting databases.

Detect This Vulnerability in Your Code

Sourcery automatically identifies aws rds instance deletion protection disabled and many other security issues in your codebase.