Neptune Cluster Uses Public Subnet
Overview
This check identifies Amazon Neptune database clusters that are deployed in public subnets. Neptune is a graph database service designed for storing and querying highly connected data. When placed in a public subnet, the cluster becomes more exposed to potential internet-based threats.
A public subnet is one that has a route to an internet gateway, making resources potentially accessible from the internet. A private subnet has no direct internet route, keeping resources isolated.
Risk
If this check fails, your Neptune cluster may be at risk:
- Data exposure: Attackers could attempt to access or steal your graph data
- Data tampering: Malicious actors might try to modify or inject false data into your graph database
- Service disruption: Public exposure increases the risk of denial-of-service attacks or exploitation attempts
Even with security groups configured, placing databases in public subnets violates the defense-in-depth principle and increases your attack surface.
Remediation Steps
Prerequisites
You need:
- Access to the AWS Console with permissions to manage Neptune and VPC resources
- At least two private subnets in different Availability Zones within your VPC
How to identify private vs public subnets
A subnet is public if its route table has a route to an internet gateway (igw-xxxxx). A subnet is private if it has no such route.
To check in the AWS Console:
- Go to VPC > Subnets
- Select a subnet
- Click the Route table tab
- Look for a route with destination
0.0.0.0/0pointing toigw-xxxxx- If present: public subnet
- If absent (or routes to NAT gateway): private subnet
Important: Migration Required
Neptune cluster subnet groups cannot be changed after creation. To move a Neptune cluster from public to private subnets, you must:
- Create a snapshot of your existing cluster
- Create a new DB subnet group using private subnets
- Restore the snapshot to a new cluster using the private subnet group
- Update your applications to use the new cluster endpoint
- Delete the old cluster once migration is verified
AWS Console Method
Step 1: Create a Snapshot of Your Existing Cluster
- Open the Amazon Neptune console
- Make sure you are in the us-east-1 region (or your cluster's region)
- Click Databases in the left menu
- Select your Neptune cluster
- Click Actions > Take snapshot
- Enter a snapshot name (e.g.,
my-cluster-migration-snapshot) - Click Take snapshot
- Wait for the snapshot status to show Available
Step 2: Create a Private Subnet Group
- In the Neptune console, click Subnet groups in the left menu
- Click Create DB subnet group
- Enter:
- Name:
my-cluster-private-subnet-group - Description:
Private subnets for Neptune cluster - VPC: Select your VPC
- Name:
- Under Add subnets, select at least two private subnets in different Availability Zones
- Click Create
Step 3: Restore to a New Cluster
- Click Snapshots in the left menu
- Select the snapshot you created
- Click Actions > Restore snapshot
- Configure the new cluster:
- DB cluster identifier: Enter a new name (e.g.,
my-cluster-private) - DB subnet group: Select your new private subnet group
- VPC security group: Select or create a security group that only allows access from your application subnets
- DB cluster identifier: Enter a new name (e.g.,
- Configure other settings as needed (instance class, encryption, etc.)
- Click Restore DB cluster
Step 4: Update Applications and Clean Up
- Wait for the new cluster status to show Available
- Note the new cluster endpoint (shown in the cluster details)
- Update your applications to use the new endpoint
- Test thoroughly to ensure connectivity
- Once verified, delete the old cluster:
- Select the old cluster
- Click Actions > Delete
- Follow the prompts (consider taking a final snapshot)
AWS CLI (optional)
Step 1: Create a snapshot
aws neptune create-db-cluster-snapshot \
--db-cluster-identifier my-neptune-cluster \
--db-cluster-snapshot-identifier my-cluster-migration-snapshot \
--region us-east-1
Step 2: Wait for snapshot to be available
aws neptune wait db-cluster-snapshot-available \
--db-cluster-snapshot-identifier my-cluster-migration-snapshot \
--region us-east-1
Step 3: Create a private subnet group
aws neptune create-db-subnet-group \
--db-subnet-group-name my-cluster-private-subnet-group \
--db-subnet-group-description "Private subnets for Neptune cluster" \
--subnet-ids subnet-aaaaaaaa subnet-bbbbbbbb \
--region us-east-1
Replace subnet-aaaaaaaa and subnet-bbbbbbbb with your private subnet IDs.
Step 4: Restore to a new cluster
aws neptune restore-db-cluster-from-snapshot \
--db-cluster-identifier my-cluster-private \
--snapshot-identifier my-cluster-migration-snapshot \
--db-subnet-group-name my-cluster-private-subnet-group \
--vpc-security-group-ids sg-xxxxxxxx \
--region us-east-1
Step 5: Create an instance in the new cluster
aws neptune create-db-instance \
--db-instance-identifier my-cluster-private-instance-1 \
--db-instance-class db.r5.large \
--db-cluster-identifier my-cluster-private \
--engine neptune \
--region us-east-1
Step 6: Get the new endpoint
aws neptune describe-db-clusters \
--db-cluster-identifier my-cluster-private \
--query 'DBClusters[0].Endpoint' \
--output text \
--region us-east-1
Step 7: Delete the old cluster (after verification)
# Delete instances first
aws neptune delete-db-instance \
--db-instance-identifier my-neptune-cluster-instance-1 \
--region us-east-1
# Wait for instance deletion
aws neptune wait db-instance-deleted \
--db-instance-identifier my-neptune-cluster-instance-1 \
--region us-east-1
# Delete the cluster
aws neptune delete-db-cluster \
--db-cluster-identifier my-neptune-cluster \
--skip-final-snapshot \
--region us-east-1
CloudFormation (optional)
Use this template to deploy a new Neptune cluster in private subnets. You will still need to migrate data from your existing cluster using snapshots.
AWSTemplateFormatVersion: '2010-09-09'
Description: Neptune cluster deployed in private subnets
Parameters:
VpcId:
Type: AWS::EC2::VPC::Id
Description: VPC where Neptune will be deployed
PrivateSubnet1Id:
Type: AWS::EC2::Subnet::Id
Description: First private subnet ID
PrivateSubnet2Id:
Type: AWS::EC2::Subnet::Id
Description: Second private subnet ID
ClusterIdentifier:
Type: String
Description: Neptune cluster identifier
Default: my-neptune-cluster
Resources:
NeptuneSubnetGroup:
Type: AWS::Neptune::DBSubnetGroup
Properties:
DBSubnetGroupDescription: Subnet group for Neptune using private subnets only
DBSubnetGroupName: !Sub '${ClusterIdentifier}-private-subnet-group'
SubnetIds:
- !Ref PrivateSubnet1Id
- !Ref PrivateSubnet2Id
Tags:
- Key: Name
Value: !Sub '${ClusterIdentifier}-subnet-group'
NeptuneSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Neptune cluster
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 8182
ToPort: 8182
SourceSecurityGroupId: !Ref NeptuneClientSecurityGroup
Tags:
- Key: Name
Value: !Sub '${ClusterIdentifier}-sg'
NeptuneClientSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Neptune clients
VpcId: !Ref VpcId
Tags:
- Key: Name
Value: !Sub '${ClusterIdentifier}-client-sg'
NeptuneCluster:
Type: AWS::Neptune::DBCluster
Properties:
DBClusterIdentifier: !Ref ClusterIdentifier
DBSubnetGroupName: !Ref NeptuneSubnetGroup
VpcSecurityGroupIds:
- !Ref NeptuneSecurityGroup
IamAuthEnabled: true
StorageEncrypted: true
DeletionProtection: true
Tags:
- Key: Name
Value: !Ref ClusterIdentifier
NeptuneInstance:
Type: AWS::Neptune::DBInstance
Properties:
DBClusterIdentifier: !Ref NeptuneCluster
DBInstanceClass: db.r5.large
DBInstanceIdentifier: !Sub '${ClusterIdentifier}-instance-1'
Tags:
- Key: Name
Value: !Sub '${ClusterIdentifier}-instance-1'
Outputs:
ClusterEndpoint:
Description: Neptune cluster endpoint
Value: !GetAtt NeptuneCluster.Endpoint
ClusterReadEndpoint:
Description: Neptune cluster read endpoint
Value: !GetAtt NeptuneCluster.ReadEndpoint
ClusterPort:
Description: Neptune cluster port
Value: !GetAtt NeptuneCluster.Port
Deploy the stack:
aws cloudformation create-stack \
--stack-name neptune-private-cluster \
--template-body file://template.yaml \
--parameters \
ParameterKey=VpcId,ParameterValue=vpc-xxxxxxxx \
ParameterKey=PrivateSubnet1Id,ParameterValue=subnet-aaaaaaaa \
ParameterKey=PrivateSubnet2Id,ParameterValue=subnet-bbbbbbbb \
ParameterKey=ClusterIdentifier,ParameterValue=my-neptune-cluster \
--region us-east-1
Terraform (optional)
Use this configuration to deploy a new Neptune cluster in private subnets.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
variable "cluster_identifier" {
description = "Neptune cluster identifier"
type = string
default = "my-neptune-cluster"
}
variable "vpc_id" {
description = "VPC ID where Neptune will be deployed"
type = string
}
variable "private_subnet_ids" {
description = "List of private subnet IDs (minimum 2)"
type = list(string)
}
resource "aws_neptune_subnet_group" "private" {
name = "${var.cluster_identifier}-private-subnet-group"
description = "Subnet group for Neptune using private subnets only"
subnet_ids = var.private_subnet_ids
tags = {
Name = "${var.cluster_identifier}-subnet-group"
}
}
resource "aws_security_group" "neptune" {
name = "${var.cluster_identifier}-sg"
description = "Security group for Neptune cluster"
vpc_id = var.vpc_id
tags = {
Name = "${var.cluster_identifier}-sg"
}
}
resource "aws_security_group" "neptune_client" {
name = "${var.cluster_identifier}-client-sg"
description = "Security group for Neptune clients"
vpc_id = var.vpc_id
tags = {
Name = "${var.cluster_identifier}-client-sg"
}
}
resource "aws_security_group_rule" "neptune_ingress" {
type = "ingress"
from_port = 8182
to_port = 8182
protocol = "tcp"
security_group_id = aws_security_group.neptune.id
source_security_group_id = aws_security_group.neptune_client.id
}
resource "aws_neptune_cluster" "main" {
cluster_identifier = var.cluster_identifier
neptune_subnet_group_name = aws_neptune_subnet_group.private.name
vpc_security_group_ids = [aws_security_group.neptune.id]
iam_database_authentication_enabled = true
storage_encrypted = true
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "${var.cluster_identifier}-final-snapshot"
tags = {
Name = var.cluster_identifier
}
}
resource "aws_neptune_cluster_instance" "main" {
cluster_identifier = aws_neptune_cluster.main.id
instance_class = "db.r5.large"
identifier = "${var.cluster_identifier}-instance-1"
tags = {
Name = "${var.cluster_identifier}-instance-1"
}
}
output "cluster_endpoint" {
description = "Neptune cluster endpoint"
value = aws_neptune_cluster.main.endpoint
}
output "cluster_reader_endpoint" {
description = "Neptune cluster reader endpoint"
value = aws_neptune_cluster.main.reader_endpoint
}
output "cluster_port" {
description = "Neptune cluster port"
value = aws_neptune_cluster.main.port
}
Deploy with Terraform:
terraform init
terraform plan -var="vpc_id=vpc-xxxxxxxx" -var='private_subnet_ids=["subnet-aaaaaaaa","subnet-bbbbbbbb"]'
terraform apply -var="vpc_id=vpc-xxxxxxxx" -var='private_subnet_ids=["subnet-aaaaaaaa","subnet-bbbbbbbb"]'
Verification
After completing the migration:
- Open the Amazon Neptune console
- Click Databases and select your new cluster
- In the Connectivity & security section, verify:
- The Subnet group shows your private subnet group
- The subnets listed are your private subnets
- Run Prowler again to confirm the check passes:
prowler aws --checks neptune_cluster_uses_public_subnet
Advanced verification with AWS CLI
Get the subnet group details:
aws neptune describe-db-clusters \
--db-cluster-identifier my-cluster-private \
--query 'DBClusters[0].DBSubnetGroup' \
--output text \
--region us-east-1
Verify the subnets are private (no internet gateway route):
# Get subnet IDs from the subnet group
aws neptune describe-db-subnet-groups \
--db-subnet-group-name my-cluster-private-subnet-group \
--query 'DBSubnetGroups[0].Subnets[*].SubnetIdentifier' \
--output text \
--region us-east-1
# For each subnet, check its route table
aws ec2 describe-route-tables \
--filters "Name=association.subnet-id,Values=subnet-aaaaaaaa" \
--query 'RouteTables[0].Routes[?GatewayId!=`local`]' \
--output table \
--region us-east-1
A private subnet should NOT have a route to an internet gateway (igw-xxxxx).
Additional Resources
- Amazon Neptune User Guide
- Amazon Neptune Security
- VPC Subnet Basics
- Restoring from a Neptune Snapshot
Notes
-
Downtime consideration: This migration requires creating a new cluster and updating application endpoints. Plan for a maintenance window and coordinate with your team.
-
Data freshness: The snapshot captures data at a point in time. Consider the time gap between taking the snapshot and completing the migration.
-
Cost: During migration, you will have two clusters running temporarily. Delete the old cluster promptly after successful migration to avoid extra costs.
-
Security groups: Ensure your new security group only allows access from trusted sources (application servers, bastion hosts) and not from
0.0.0.0/0. -
IAM authentication: Consider enabling IAM database authentication for additional security beyond network isolation.
-
Encryption: If your original cluster was not encrypted, you can enable encryption when restoring to the new cluster. Encryption cannot be added to an existing cluster.