June 12, 2025

How to get rekt using AWS Neptune

Written by

Daniel Grzelak

I recently got asked by my mean boss to do some work. Specifically he wanted me to understand how a Neptune database may get exposed to the internet and pwned. It was awful. Not the pwnage, the having to do work.

Anyway, this post is about some of what I found.

Apologies to my AWS friends who suggested an alternative title for this movie, “Best practices for using AWS Neptune”. That just didn’t resonate with the focus groups. 🤷

What is Neptune?

Well its a big blue planet but also it’s Amazon’s managed graph database. You use it to store relationships for stuff like social networks, fraud detection, and recommendation engines. I’ve seen it be used for certain patient data too. Maybe you’ve heard of Neo4j? It’s like that.

How do things end badly on Neptune?

Time and time again, whether its S3 buckets or RDS instances, databases get ransacked when they are exposed directly to the internet with little or no authentication. So the obvious thing to do is figure out how that happens for Neptune.

By default Neptune doesn’t require any authentication. That’s right. You just send it your queries - read, write, whatever - and it executes them.

Seems bad until you try to make a Neptune instance public. It appears easy to just set the --publicly-accessible flag when calling create-db-instance. Except that:

“This flag should no longer be used” is Amazonian for, ‘if you try to supply it in the cli or CloudFormation we will not let you’, and throw one of these errors:

Property validation failure: [Encountered unsupported properties in {/}: [PubliclyAccessible]

Given engine version x.x.x.x does not support public endpoints.

I thought that since some version in the distant past (I checked the oldest version I could start, 1.3.2.1 2024-06-20), Neptune instances could only be started inside a VPC with a private IP address. AWS let me know that in fact, that was never possible and the error messages are artefacts of “shared control plane infrastructure”.

Furthermore, it’s not easy to just stick a Neptune instance behind an ALB or ELB because Neptune requires a persistent TCP connection with low latency and doesn’t support the HTTP connection reuse / load balancing behavior that ALBs and ELBs typically introduce.

If you try really hard you can still do it. You can still get rekt. I got an instance up and running behind a network load balancer (NLB) and it worked a treat but by that time I was exhausted from fighting both the API and CloudFormation.

OR you can just be old enough to have setup Neptune when the --publicly-accessible flag still worked, I think. Again, I’ve been told by AWS that this flag has never been supported, but I’m keeping this gag since I found a bunch of public instances (more on this further down).

So it’s all good if it’s not public?

Security nerds don’t like insecure defaults for a reason.

If a Neptune instance is running without authentication inside a VPC, anyone or anything in that VPC can still access it. That doesn’t sound bad until you learn about server-side request forgery (SSRF) vulnerabilities, which allow an attacker to trick systems into making web requests on their behalf. There are hundreds of these SSRF vulnerabilities on Hackerone and that’s just the tip of the ice berg. What if you are all modern and running a bunch of containers in Kubernetes inside the same VPC? Suddenly it doesn’t seem that unlikely that one of those containers could be hacked or tricked into making a request.

What can an attacker do on Neptune?

Neptune the database (not the AWS service) has a rich API to do pretty much everything you’d expect and a bit more:

  • Query data
  • Bulk load data
  • Export data
  • Create, update, and delete data
  • Stream data
  • Manage instances
  • Monitor instances
  • … and more

Most (all?) of the actions are described in the data API documentation. Neptune supports 3 graph query languages:

  1. Gremlin
  2. openCypher
  3. SPARQL

These are all nice facts but it takes a little playing with a Neptune instance to truely internalise the power here. Remember, by default Neptune is completely unauthenticated and access is restricted to the VPC. So by default, who ever has access to the VPC, can do everything an engineer building an app can do.

Below are some examples to help you internalize what this means. The point is, once you have access, you REALLY have access, unless IAM authentication is enabled.

Get instance status and configuration

Documented here.

curl -k  https://[IP]:8182/status | jq .

{
  "status": "healthy",
  "startTime": "Tue May 06 03:08:14 UTC 2025",
  "dbEngineVersion": "1.4.5.0.R3",
  "role": "writer",
  "dfeQueryEngine": "viaQueryHint",
  "gremlin": {
    "version": "tinkerpop-3.7.1"
  },
  "sparql": {
    "version": "sparql-1.1"
  },
  "opencypher": {
    "version": "Neptune-9.0.20190305-1.0"
  },
  "labMode": {
    "ObjectIndex": "disabled",
    "ReadWriteConflictDetection": "enabled"
  },
  "features": {
    "SlowQueryLogs": "info",
    "InlineServerGeneratedEdgeId": "disabled",
    "ResultCache": {
      "status": "disabled"
    },
    "IAMAuthentication": "disabled",
    "Streams": "disabled",
    "AuditLog": "enabled"
  },
  "settings": {
    "StrictTimeoutValidation": "true",
    "clusterQueryTimeoutInMs": "120000",
    "SlowQueryLogsThreshold": "1"
  }
}

Get a node count

curl -k -X POST https://[IP]:8182/gremlin \
  -H 'Content-Type: application/json' \
  -d '{"gremlin": "g.V().count()"}' \
  | jq -r '.result.data["@value"][0]["@value"]'

1788

Get all the nodes

Documented here.

Be careful with this one. You probably want to use a limit if the database is big.

curl -k -X POST https://[IP]:8182/gremlin \
  -H 'Content-Type: application/json' \
  -d '{"gremlin": "g.V().elementMap()"}' \
  | jq -r '.result.data["@value"]'
  
[
  {
    "@type": "g:Map",
    "@value": [
      {
        "@type": "g:T",
        "@value": "id"
      },
      "c566715e-a1d4-4096-8f93-6232ea6eca05",
      {
        "@type": "g:T",
        "@value": "label"
      },
      "Person",
      "name",
      "Alice",
      "id",
      {
        "@type": "g:Int64",
        "@value": 1
      }
    ]
  }
]

Add a node

curl -k -X POST https://[IP]:8182/opencypher \
  -H "Content-Type: application/json" \
  -d '{"query": "CREATE (n:Person {name: \"Alice\", id: 1})"}
{"results":[]}

Detele all nodes

curl -k -X POST https://[IP]:8182/gremlin \
  -H 'Content-Type: application/json' \
  -d '{"gremlin": "g.V().drop()"}' \
  | jq -r '.result.data'

{
  "@type": "g:List",
  "@value": []
}

How does an attacker find Neptune?

Just imagine a middle aged bald white man pointing at the night sky. That’s me.

If you’re keen to be the rek-er not the rek-ee, the obvious thing to do is to scan for Neptune databases exposed on the interwebs. By default Neptune runs on port 8182, so step 1 is to find every IP in the Amazon ranges responding on TCP port 8182. Amazon publishes it’s IP ranges in a pretty JSON file and masscan can do the rest in a few minutes.

masscan -p8182 --open --rate=100000 -iL aws_prefixes.txt 

Neptune speaks HTTPS, so once an open port has been found it’s simple to check if it’s Neptune with a web request like:

curl -k https://[IP]:8182/            
{
   "detailedMessage":"no gremlin script supplied",
   "requestId":"dccb5469-189d-3797-354c-3fd3ab9b52b6",
   "code":"MissingParameterException",
   "message":"no gremlin script supplied"
}

You can also ask nicely for the number of nodes/vertices stored in a given instance. Nodes and verticies are interchangeable nouns for ‘record’ but in fancy graph language.

I asked nicely myself so you don’t have to, and got these results:

What can you do to protect Neptune?

I’m going to go out on a limb and say that Neptune was built for running in trusted, closed environments. It wasn’t built to be on the internet. Most of the security features appear to be a little tacked on. However, that doesn’t mean it can’t be secured. AWS has extensively documented security best practices for Neptune.

My top recommendations:

  1. Never expose Neptune to the internet
    Neptune runs in a VPC by default. Avoid finding clever ways to expose it with a load balancer or other device. Trust that AWS made it hard for a reason.
  2. Restrict network access with security groups
    By default Neptune runs without authentication and even with authentication it is network accessible to the entire VPC it is in. User security groups to restrict network access to only the systems that require it.
  3. Enable IAM authentication
    Even if it’s Neptune is running inside a VPC, without authentication any entity inside that VPC can perform all actions on the instance. Enabling IAM authentication effectively enables AWS IAM policy models which allows you to restrict access to known good users and roles.
  4. Restrict principal policies to least privilege
    Once IAM authentication is enabled, it’s possible restrict the operations each principal can perform. Do this and ensure systems and users only have the permissions they need to perform their functions. This will limit the blast radius in the event another part of your cloud is compromised. Note that administrative actions are controlled by rds: IAM actions, and other actions are controlled by either neptune-db: or neptune-graph: IAM actions. I’m not smart enough to decipher how and when to use which, but the good news is it can take up to 10 minutes to figure out if you did it right.
  1. Enable audit logging
    Audit logging doesn’t log all operations but it does log most data operations. In the event of a breach, you want to be able to understand what data was affected and how. Below is the kind of data you get - you can see useful fields like source and destination IPs, the full HTTP request line including query parameters, a number of HTTP headers including the host heder, and the actual Neptune queries.
1745987911902, 192.168.1.12:64998, 192.168.1.99:8182, HTTP_POST, [unknown], [unknown], "HttpObjectAggregator$AggregatedFullHttpRequest(decodeResult: success, version: HTTP/1.1, content: CompositeByteBuf(ridx: 0, widx: 28, cap: 28, components=1)) POST /gremlin HTTP/1.1 Host: dg-test-neptune-nlb-1d7140b499b33902.elb.ap-southeast-2.amazonaws.com:8182 User-Agent: curl/8.7.1 Accept: */* Content-Type: application/json Content-Length: 28", {'gremlin': 'g.V().count()'}
1745988923913, 192.168.1.12:29353, 192.168.1.99:8182, HTTP_POST, [unknown], [unknown], [unknown], {'query': 'MATCH (n) RETURN n LIMIT 5'}
1745989128219, 192.168.1.12:14223, 192.168.1.99:8182, HTTP_POST, [unknown], [unknown], "HttpObjectAggregator$AggregatedFullHttpRequest(decodeResult: success, version: HTTP/1.1, content: CompositeByteBuf(ridx: 0, widx: 52, cap: 52, components=1)) POST /sparql HTTP/1.1 Host: dg-test-neptune-nlb-1d7140b499b33902.elb.ap-southeast-2.amazonaws.com:8182 User-Agent: curl/8.7.1 Accept: */* Content-Type: application/x-www-form-urlencoded Content-Length: 52", query=SELECT * WHERE {?s ?p ?o} LIMIT 10

There are a few controls that are enabled by default like automated backups, encryption at rest (at the disk level), and encryption in transit with TLS.

And then?

I reported the exposed instances to AWS and I believe they are doing their best to get in touch with the owners of those instances to ensure they don’t get rekt. It’s technically not in AWS section of the the shared responsibility model, so I’m really thankful for all the work they put in to help customers do better. AWS provided the following note for inclusion in this post:

AWS investigated the reported concern. AWS can confirm that there are no issues with AWS services and are operating as designed. The issues described in this blog are the result of actions taken by AWS customers. As a security best practice, an Amazon Neptune Database cluster can only be created in a VPC, and its endpoints are only accessible within that VPC, usually from an Amazon Elastic Compute Cloud (Amazon EC2) instance running in that VPC. A customer must take actions to change their VPC security controls, or add external network access capability, such as Network Load Balancers, to make an Amazon Neptune endpoint publicly accessible. Furthermore, AWS recommends that customers use of AWS Identity and Access Management (IAM) to authenticate to their Neptune Database instance or Database cluster. When IAM database authentication is enabled, each request must be signed using AWS Signature Version 4.

I expect most cloud security products (including Plerion) have a basic check to see if Neptune is public. However, if you want smart risk assessment of running Neptune instances beyond just knowing if they are public, that’s coming very soon to the Plerion product. You can start a free trial anytime without talking to a salesperson.

Blog

Learn cloud security with our research blog

X
Stay ahead in cloud security
Sign up for the Plerion newsletter and get:
🔸Expert strategies for securing your cloud
🔸Invitations to exclusive events and workshops
🔸Updates on Plerion’s latest features
🔸Early access to cloud security research
Check - Elements Webflow Library - BRIX Templates
Thanks for joining our newsletter.
Oops! Something went wrong while submitting the form.