Back to Blogs

Why and How to Delete Default VPCs Across All AWS Regions

Introduction

Default VPCs in AWS are convenient for getting started quickly, but they pose significant security risks in production environments. Having worked with numerous organizations on cloud security implementations, I've seen how default VPCs can become security vulnerabilities and compliance issues. This guide explains why you should remove them and provides an automated solution to delete default VPCs across all AWS regions.

Why Default VPCs Are a Security Risk

1. Overly Permissive Configuration

Default VPCs come with configurations that prioritize convenience over security:

  • Public subnets by default: All subnets have public IP assignment enabled
  • Internet Gateway attached: Direct internet access without proper controls
  • Default security groups: Often too permissive for production workloads
  • Predictable CIDR blocks: Using standard 172.31.0.0/16 makes them easy targets

2. Compliance and Governance Issues

From my experience implementing CIS compliance and security frameworks:

  • CIS Benchmark violations: Default VPCs fail multiple CIS controls
  • Audit findings: Security audits often flag default VPCs as risks
  • Governance gaps: Default VPCs bypass infrastructure-as-code processes
  • Shadow IT risks: Teams might accidentally deploy resources in default VPCs

3. Operational Challenges

  • Inconsistent networking: Different regions have different default configurations
  • Monitoring gaps: Default VPCs often lack proper logging and monitoring
  • Cost management: Harder to track and allocate costs for default VPC resources

Real-World Impact

In my consulting work, I've encountered several scenarios where default VPCs caused issues:

Healthcare Platform Incident

During a HIPAA compliance audit for a healthcare client, we discovered that a development team had accidentally deployed a database containing PHI in a default VPC. The database was accessible from the internet due to the default security group configuration. This could have resulted in significant compliance violations and fines.

E-commerce Security Breach

An e-commerce startup experienced a security incident when an intern deployed a test application in the default VPC with default security groups. The application had vulnerabilities that were exploited because of the overly permissive network configuration.

The Solution: Automated Default VPC Deletion

Here's a comprehensive script that safely deletes default VPCs across all AWS regions:

#!/bin/bash

# Default VPC Deletion Script
# Author: Ahsan Kazmi
# Purpose: Remove default VPCs from all AWS regions for security compliance

set -e

echo "🚨 AWS Default VPC Cleanup Script"
echo "=================================="
echo "âš ī¸  WARNING: This will delete default VPCs in ALL regions"
echo "âš ī¸  Ensure no resources are running in default VPCs before proceeding"
echo ""

# Confirmation prompt
read -p "Are you sure you want to continue? (yes/no): " confirm
if [[ $confirm != "yes" ]]; then
    echo "❌ Operation cancelled"
    exit 1
fi

# Initialize counters
deleted_count=0
skipped_count=0
error_count=0

# Loop through all AWS regions
for region in $(aws ec2 describe-regions \
    --query "Regions[*].RegionName" \
    --output text); do
    
    echo ""
    echo "🌍 Processing Region: $region"
    echo "----------------------------------------"
    
    # Find default VPC in current region
    vpc_id=$(aws ec2 describe-vpcs \
        --region "$region" \
        --filters Name=isDefault,Values=true \
        --query "Vpcs[0].VpcId" \
        --output text 2>/dev/null)
    
    if [[ "$vpc_id" == "None" || "$vpc_id" == "" ]]; then
        echo "  â„šī¸  No default VPC found in $region"
        ((skipped_count++))
        continue
    fi
    
    echo "  đŸŽ¯ Found default VPC: $vpc_id"
    
    # Check for running instances
    instances=$(aws ec2 describe-instances \
        --region "$region" \
        --filters Name=vpc-id,Values=$vpc_id Name=instance-state-name,Values=running \
        --query "Reservations[*].Instances[*].InstanceId" \
        --output text 2>/dev/null)
    
    if [[ -n "$instances" && "$instances" != "" ]]; then
        echo "  âš ī¸  WARNING: Running instances found in default VPC: $instances"
        echo "  ❌ Skipping deletion to prevent service disruption"
        ((error_count++))
        continue
    fi
    
    # Start deletion process
    echo "  đŸ—‘ī¸  Starting deletion process..."
    
    # 1. Detach and delete Internet Gateways
    echo "    â€ĸ Removing Internet Gateways..."
    for igw in $(aws ec2 describe-internet-gateways \
        --region "$region" \
        --filters Name=attachment.vpc-id,Values=$vpc_id \
        --query "InternetGateways[*].InternetGatewayId" \
        --output text 2>/dev/null); do
        
        if [[ -n "$igw" && "$igw" != "" ]]; then
            echo "      - Detaching IGW: $igw"
            aws ec2 detach-internet-gateway \
                --internet-gateway-id "$igw" \
                --vpc-id "$vpc_id" \
                --region "$region" 2>/dev/null || true
            
            echo "      - Deleting IGW: $igw"
            aws ec2 delete-internet-gateway \
                --internet-gateway-id "$igw" \
                --region "$region" 2>/dev/null || true
        fi
    done
    
    # 2. Delete Subnets
    echo "    â€ĸ Removing Subnets..."
    for subnet in $(aws ec2 describe-subnets \
        --region "$region" \
        --filters Name=vpc-id,Values=$vpc_id \
        --query "Subnets[*].SubnetId" \
        --output text 2>/dev/null); do
        
        if [[ -n "$subnet" && "$subnet" != "" ]]; then
            echo "      - Deleting Subnet: $subnet"
            aws ec2 delete-subnet \
                --subnet-id "$subnet" \
                --region "$region" 2>/dev/null || true
        fi
    done
    
    # 3. Delete Route Tables (non-main)
    echo "    â€ĸ Removing Route Tables..."
    for rt in $(aws ec2 describe-route-tables \
        --region "$region" \
        --filters Name=vpc-id,Values=$vpc_id \
        --query "RouteTables[?Associations[?Main!=\`true\`] || length(Associations)==\`0\`].RouteTableId" \
        --output text 2>/dev/null); do
        
        if [[ -n "$rt" && "$rt" != "" ]]; then
            echo "      - Deleting Route Table: $rt"
            aws ec2 delete-route-table \
                --route-table-id "$rt" \
                --region "$region" 2>/dev/null || true
        fi
    done
    
    # 4. Delete Security Groups (non-default)
    echo "    â€ĸ Removing Security Groups..."
    for sg in $(aws ec2 describe-security-groups \
        --region "$region" \
        --filters Name=vpc-id,Values=$vpc_id \
        --query "SecurityGroups[?GroupName!='default'].GroupId" \
        --output text 2>/dev/null); do
        
        if [[ -n "$sg" && "$sg" != "" ]]; then
            echo "      - Deleting Security Group: $sg"
            aws ec2 delete-security-group \
                --group-id "$sg" \
                --region "$region" 2>/dev/null || true
        fi
    done
    
    # 5. Delete Network ACLs (non-default)
    echo "    â€ĸ Removing Network ACLs..."
    for acl in $(aws ec2 describe-network-acls \
        --region "$region" \
        --filters Name=vpc-id,Values=$vpc_id \
        --query "NetworkAcls[?IsDefault==\`false\`].NetworkAclId" \
        --output text 2>/dev/null); do
        
        if [[ -n "$acl" && "$acl" != "" ]]; then
            echo "      - Deleting Network ACL: $acl"
            aws ec2 delete-network-acl \
                --network-acl-id "$acl" \
                --region "$region" 2>/dev/null || true
        fi
    done
    
    # 6. Finally, delete the VPC
    echo "    â€ĸ Deleting VPC: $vpc_id"
    if aws ec2 delete-vpc \
        --vpc-id "$vpc_id" \
        --region "$region" 2>/dev/null; then
        echo "  ✅ Successfully deleted VPC: $vpc_id"
        ((deleted_count++))
    else
        echo "  ❌ Failed to delete VPC: $vpc_id"
        ((error_count++))
    fi
done

# Summary
echo ""
echo "📊 DELETION SUMMARY"
echo "==================="
echo "✅ VPCs deleted: $deleted_count"
echo "â­ī¸  Regions skipped: $skipped_count"
echo "❌ Errors encountered: $error_count"
echo ""

if [[ $deleted_count -gt 0 ]]; then
    echo "🎉 Default VPC cleanup completed successfully!"
    echo "🔒 Your AWS account is now more secure"
else
    echo "â„šī¸  No default VPCs were found or deleted"
fi

echo ""
echo "📝 Next Steps:"
echo "1. Verify no resources were accidentally deleted"
echo "2. Update your infrastructure-as-code templates"
echo "3. Implement VPC creation policies to prevent default VPC usage"
echo "4. Set up monitoring for new default VPC creation"

Enhanced Security Script with Logging

For production environments, here's an enhanced version with comprehensive logging:

#!/bin/bash

# Enhanced Default VPC Deletion Script with Logging
# Author: Ahsan Kazmi

set -e

# Configuration
LOG_FILE="/tmp/vpc-deletion-$(date +%Y%m%d-%H%M%S).log"
SNS_TOPIC_ARN="${SNS_TOPIC_ARN:-}"  # Set this environment variable
DRY_RUN="${DRY_RUN:-false}"

# Logging function
log() {
    local level=$1
    shift
    local message="$@"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}

# Send notification
send_notification() {
    local subject="$1"
    local message="$2"
    
    if [[ -n "$SNS_TOPIC_ARN" ]]; then
        aws sns publish \
            --topic-arn "$SNS_TOPIC_ARN" \
            --subject "$subject" \
            --message "$message" 2>/dev/null || true
    fi
}

log "INFO" "Starting Default VPC Deletion Process"
log "INFO" "Log file: $LOG_FILE"
log "INFO" "Dry run mode: $DRY_RUN"

# Pre-flight checks
log "INFO" "Performing pre-flight checks..."

# Check AWS CLI configuration
if ! aws sts get-caller-identity &>/dev/null; then
    log "ERROR" "AWS CLI not configured or credentials invalid"
    exit 1
fi

# Get account information
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
log "INFO" "Operating on AWS Account: $ACCOUNT_ID"

# Initialize tracking
declare -A region_results
total_regions=0
successful_deletions=0
failed_deletions=0

# Process each region
for region in $(aws ec2 describe-regions --query "Regions[*].RegionName" --output text); do
    ((total_regions++))
    log "INFO" "Processing region: $region"
    
    # Find default VPC
    vpc_id=$(aws ec2 describe-vpcs \
        --region "$region" \
        --filters Name=isDefault,Values=true \
        --query "Vpcs[0].VpcId" \
        --output text 2>/dev/null)
    
    if [[ "$vpc_id" == "None" || -z "$vpc_id" ]]; then
        log "INFO" "No default VPC in region $region"
        region_results[$region]="NO_DEFAULT_VPC"
        continue
    fi
    
    log "INFO" "Found default VPC $vpc_id in region $region"
    
    # Safety check: Look for resources
    resource_check=$(aws ec2 describe-instances \
        --region "$region" \
        --filters Name=vpc-id,Values=$vpc_id \
        --query "length(Reservations[*].Instances[*])" \
        --output text 2>/dev/null)
    
    if [[ "$resource_check" != "0" ]]; then
        log "WARN" "Found $resource_check instances in default VPC $vpc_id in region $region"
        region_results[$region]="HAS_RESOURCES"
        ((failed_deletions++))
        continue
    fi
    
    if [[ "$DRY_RUN" == "true" ]]; then
        log "INFO" "DRY RUN: Would delete VPC $vpc_id in region $region"
        region_results[$region]="DRY_RUN"
        continue
    fi
    
    # Perform deletion
    if delete_default_vpc "$region" "$vpc_id"; then
        log "INFO" "Successfully deleted VPC $vpc_id in region $region"
        region_results[$region]="DELETED"
        ((successful_deletions++))
    else
        log "ERROR" "Failed to delete VPC $vpc_id in region $region"
        region_results[$region]="FAILED"
        ((failed_deletions++))
    fi
done

# Generate summary report
generate_summary_report() {
    local report_file="/tmp/vpc-deletion-report-$(date +%Y%m%d-%H%M%S).json"
    
    cat > "$report_file" << EOF
{
    "account_id": "$ACCOUNT_ID",
    "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
    "total_regions": $total_regions,
    "successful_deletions": $successful_deletions,
    "failed_deletions": $failed_deletions,
    "dry_run": $DRY_RUN,
    "results": {
EOF

    local first=true
    for region in "${!region_results[@]}"; do
        if [[ "$first" == "true" ]]; then
            first=false
        else
            echo "," >> "$report_file"
        fi
        echo "        \"$region\": \"${region_results[$region]}\"" >> "$report_file"
    done

    cat >> "$report_file" << EOF
    }
}
EOF

    log "INFO" "Summary report generated: $report_file"
    echo "$report_file"
}

# Function to delete default VPC
delete_default_vpc() {
    local region=$1
    local vpc_id=$2
    
    log "INFO" "Starting deletion of VPC $vpc_id in region $region"
    
    # Delete in correct order to avoid dependency issues
    
    # 1. Internet Gateways
    for igw in $(aws ec2 describe-internet-gateways \
        --region "$region" \
        --filters Name=attachment.vpc-id,Values=$vpc_id \
        --query "InternetGateways[*].InternetGatewayId" \
        --output text 2>/dev/null); do
        
        if [[ -n "$igw" ]]; then
            log "INFO" "Detaching and deleting IGW $igw"
            aws ec2 detach-internet-gateway --internet-gateway-id "$igw" --vpc-id "$vpc_id" --region "$region" 2>/dev/null || true
            aws ec2 delete-internet-gateway --internet-gateway-id "$igw" --region "$region" 2>/dev/null || true
        fi
    done
    
    # 2. Subnets
    for subnet in $(aws ec2 describe-subnets \
        --region "$region" \
        --filters Name=vpc-id,Values=$vpc_id \
        --query "Subnets[*].SubnetId" \
        --output text 2>/dev/null); do
        
        if [[ -n "$subnet" ]]; then
            log "INFO" "Deleting subnet $subnet"
            aws ec2 delete-subnet --subnet-id "$subnet" --region "$region" 2>/dev/null || true
        fi
    done
    
    # 3. Route Tables (non-main)
    for rt in $(aws ec2 describe-route-tables \
        --region "$region" \
        --filters Name=vpc-id,Values=$vpc_id \
        --query "RouteTables[?Associations[?Main!=\`true\`] || length(Associations)==\`0\`].RouteTableId" \
        --output text 2>/dev/null); do
        
        if [[ -n "$rt" ]]; then
            log "INFO" "Deleting route table $rt"
            aws ec2 delete-route-table --route-table-id "$rt" --region "$region" 2>/dev/null || true
        fi
    done
    
    # 4. Security Groups (non-default)
    for sg in $(aws ec2 describe-security-groups \
        --region "$region" \
        --filters Name=vpc-id,Values=$vpc_id \
        --query "SecurityGroups[?GroupName!='default'].GroupId" \
        --output text 2>/dev/null); do
        
        if [[ -n "$sg" ]]; then
            log "INFO" "Deleting security group $sg"
            aws ec2 delete-security-group --group-id "$sg" --region "$region" 2>/dev/null || true
        fi
    done
    
    # 5. Network ACLs (non-default)
    for acl in $(aws ec2 describe-network-acls \
        --region "$region" \
        --filters Name=vpc-id,Values=$vpc_id \
        --query "NetworkAcls[?IsDefault==\`false\`].NetworkAclId" \
        --output text 2>/dev/null); do
        
        if [[ -n "$acl" ]]; then
            log "INFO" "Deleting network ACL $acl"
            aws ec2 delete-network-acl --network-acl-id "$acl" --region "$region" 2>/dev/null || true
        fi
    done
    
    # 6. Finally delete the VPC
    log "INFO" "Deleting VPC $vpc_id"
    if aws ec2 delete-vpc --vpc-id "$vpc_id" --region "$region" 2>/dev/null; then
        return 0
    else
        return 1
    fi
}

# Generate and send final report
report_file=$(generate_summary_report)
log "INFO" "Process completed. Successful: $successful_deletions, Failed: $failed_deletions"

# Send notification if configured
if [[ -n "$SNS_TOPIC_ARN" ]]; then
    send_notification "Default VPC Deletion Complete" "$(cat "$report_file")"
fi

# Exit with appropriate code
if [[ $failed_deletions -gt 0 ]]; then
    exit 1
else
    exit 0
fi

Terraform Prevention Policy

To prevent default VPCs from being created in the future, implement this Service Control Policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PreventDefaultVPCCreation",
      "Effect": "Deny",
      "Action": [
        "ec2:CreateDefaultVpc"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:PrincipalServiceName": [
            "ec2.amazonaws.com"
          ]
        }
      }
    },
    {
      "Sid": "PreventDefaultVPCModification",
      "Effect": "Deny",
      "Action": [
        "ec2:ModifyVpcAttribute"
      ],
      "Resource": "*",
      "Condition": {
        "Bool": {
          "ec2:IsDefaultVpc": "true"
        }
      }
    }
  ]
}

Monitoring and Alerting

Set up CloudWatch Events to monitor for default VPC creation:

resource "aws_cloudwatch_event_rule" "default_vpc_created" {
  name        = "default-vpc-creation-alert"
  description = "Alert when default VPC is created"

  event_pattern = jsonencode({
    source      = ["aws.ec2"]
    detail-type = ["AWS API Call via CloudTrail"]
    detail = {
      eventSource = ["ec2.amazonaws.com"]
      eventName   = ["CreateDefaultVpc"]
    }
  })
}

resource "aws_cloudwatch_event_target" "sns" {
  rule      = aws_cloudwatch_event_rule.default_vpc_created.name
  target_id = "SendToSNS"
  arn       = aws_sns_topic.security_alerts.arn
}

resource "aws_lambda_function" "auto_delete_default_vpc" {
  filename         = "auto_delete_default_vpc.zip"
  function_name    = "auto-delete-default-vpc"
  role            = aws_iam_role.lambda_role.arn
  handler         = "index.handler"
  runtime         = "python3.9"
  timeout         = 300

  environment {
    variables = {
      SNS_TOPIC = aws_sns_topic.security_alerts.arn
    }
  }
}

Best Practices After Deletion

1. Infrastructure as Code

Always create VPCs using Infrastructure as Code tools:

  • Use Terraform, CloudFormation, or CDK
  • Define explicit CIDR blocks and security configurations
  • Implement proper tagging and naming conventions
  • Version control all infrastructure definitions

2. Security by Design

  • Create private subnets by default
  • Use NAT Gateways for outbound internet access
  • Implement least-privilege security groups
  • Enable VPC Flow Logs for monitoring

3. Governance and Compliance

  • Implement Service Control Policies to prevent default VPC creation
  • Set up automated compliance checking
  • Regular security audits and reviews
  • Document network architecture and security controls

Troubleshooting Common Issues

VPC Deletion Fails

If the script fails to delete a VPC, common causes include:

  • Hidden resources: ENIs, Lambda functions, or RDS instances
  • VPC Peering connections: Must be deleted first
  • VPN connections: Disconnect before deletion
  • Elastic IPs: Release associated Elastic IPs

Manual Cleanup Commands

# Find all ENIs in a VPC
aws ec2 describe-network-interfaces \
    --filters Name=vpc-id,Values=vpc-12345678 \
    --query 'NetworkInterfaces[*].[NetworkInterfaceId,Description,Status]' \
    --output table

# Find VPC Peering connections
aws ec2 describe-vpc-peering-connections \
    --filters Name=requester-vpc-info.vpc-id,Values=vpc-12345678 \
    --query 'VpcPeeringConnections[*].[VpcPeeringConnectionId,Status.Code]' \
    --output table

# Find NAT Gateways
aws ec2 describe-nat-gateways \
    --filter Name=vpc-id,Values=vpc-12345678 \
    --query 'NatGateways[*].[NatGatewayId,State]' \
    --output table

Conclusion

Removing default VPCs is a critical security hardening step that should be part of every AWS account setup process. The automated script provided here safely removes default VPCs while providing comprehensive logging and safety checks.

Key takeaways:

  • Security First: Default VPCs pose unnecessary security risks
  • Automate Safely: Use scripts with proper error handling and logging
  • Prevent Recreation: Implement policies to prevent future default VPC creation
  • Monitor Continuously: Set up alerts for any new default VPC creation

Remember, security is not a one-time task but an ongoing process. Regular audits, automated compliance checking, and proactive monitoring are essential for maintaining a secure AWS environment.