Skip to main content

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:

  1. Go to VPC > Subnets
  2. Select a subnet
  3. Click the Route table tab
  4. Look for a route with destination 0.0.0.0/0 pointing to igw-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:

  1. Create a snapshot of your existing cluster
  2. Create a new DB subnet group using private subnets
  3. Restore the snapshot to a new cluster using the private subnet group
  4. Update your applications to use the new cluster endpoint
  5. Delete the old cluster once migration is verified

AWS Console Method

Step 1: Create a Snapshot of Your Existing Cluster

  1. Open the Amazon Neptune console
  2. Make sure you are in the us-east-1 region (or your cluster's region)
  3. Click Databases in the left menu
  4. Select your Neptune cluster
  5. Click Actions > Take snapshot
  6. Enter a snapshot name (e.g., my-cluster-migration-snapshot)
  7. Click Take snapshot
  8. Wait for the snapshot status to show Available

Step 2: Create a Private Subnet Group

  1. In the Neptune console, click Subnet groups in the left menu
  2. Click Create DB subnet group
  3. Enter:
    • Name: my-cluster-private-subnet-group
    • Description: Private subnets for Neptune cluster
    • VPC: Select your VPC
  4. Under Add subnets, select at least two private subnets in different Availability Zones
  5. Click Create

Step 3: Restore to a New Cluster

  1. Click Snapshots in the left menu
  2. Select the snapshot you created
  3. Click Actions > Restore snapshot
  4. 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
  5. Configure other settings as needed (instance class, encryption, etc.)
  6. Click Restore DB cluster

Step 4: Update Applications and Clean Up

  1. Wait for the new cluster status to show Available
  2. Note the new cluster endpoint (shown in the cluster details)
  3. Update your applications to use the new endpoint
  4. Test thoroughly to ensure connectivity
  5. 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:

  1. Open the Amazon Neptune console
  2. Click Databases and select your new cluster
  3. In the Connectivity & security section, verify:
    • The Subnet group shows your private subnet group
    • The subnets listed are your private subnets
  4. 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

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.