DigitalOcean Cloud Firewalls: audit every Droplet at once

A Droplet is only as protected as the firewall in front of it, and DigitalOcean does not attach one for you. Put a Cloud Firewall on every Droplet, allow SSH only from places you control, and keep database ports off the public internet.

If you run a handful of Droplets, the firewall question is easy to skip. The Droplet works, the site loads, and nothing complains. The cost shows up later, when something you installed for convenience turns out to be listening on the public internet.

What the documentation says, and what it leaves out

Once a Cloud Firewall is attached, the behaviour is clear. DigitalOcean describes it as a network-based, stateful firewall that blocks all traffic not expressly permitted by a rule. If you configure no outbound rules, no outbound traffic is permitted at all, so a new firewall can break package updates until you add outbound rules.

What the documentation does not do is say what a Droplet with no Cloud Firewall accepts. The Droplet creation page has no firewall setting, and the recommended Droplet setup tells you to create the firewall separately and add your Droplet's tag to it. Its baseline is to restrict all inbound traffic except SSH on port 22.

So plan on the cautious reading. Until you attach a firewall, assume that anything listening on the Droplet's public interface can be reached from the internet. Whatever sits between that and the world is the host firewall, if you configured one, and nothing else.

SSH open to the whole internet

Port 22 is the first thing scanners try. A rule that allows it from 0.0.0.0/0 means any address on the IPv4 internet can attempt a login. Key-only authentication makes that survivable, but it is still noise in your logs and a standing invitation.

Narrow the source to your office or home address, or to a VPN's exit address. Here is a firewall that allows SSH from one address and web traffic from anywhere, with the outbound rules a typical Droplet needs. The rule syntax is the one on the doctl firewall create reference: rules go in one quoted string, separated by spaces.

doctl compute firewall create --name web-fw \
  --inbound-rules "protocol:tcp,ports:22,address:203.0.113.7/32 protocol:tcp,ports:80,address:0.0.0.0/0 protocol:tcp,ports:443,address:0.0.0.0/0" \
  --outbound-rules "protocol:tcp,ports:80,address:0.0.0.0/0 protocol:tcp,ports:443,address:0.0.0.0/0 protocol:udp,ports:53,address:0.0.0.0/0" \
  --tag-names web

That example only covers IPv4. If your Droplets have IPv6 addresses, add rules for ::/0 deliberately instead of leaving the question open.

Database ports that face the internet

PostgreSQL listens on 5432 by default and MySQL on 3306. Redis documentation uses 6379 for a standard instance. None of them belongs on a public address.

PostgreSQL's own default helps you here: its listen_addresses setting defaults to localhost, so a database becomes reachable from outside only after someone changes it. That is usually exactly what happens when an application moves to a second Droplet and someone opens the database to make it work. Redis is blunter. Its security documentation says it is designed to be accessed by trusted clients inside trusted environments, and that a single FLUSHALL from an outsider deletes the whole data set.

For traffic between your own Droplets, use the private network and write rules that name it. DigitalOcean states that firewalls affect both public and VPC traffic, and rules specific to either must name the public or private IP range, so do not assume a rule written for one covers the other.

Managed Databases work differently. Their port is not a fixed number you should guess: DigitalOcean's documentation points you to the port in the cluster's Connection Details. Access is restricted with Trusted Sources instead of a Droplet firewall. The documentation does not say what an empty list allows, so open the Trusted Sources tab for each cluster and put an entry in it rather than assuming. Note also that you cannot add IPv6 rules to a cluster's trusted sources.

Use tags to cover a fleet

A firewall can be applied to Droplets by name, by tag, or both. Tags are the better route. There is no limit on how many Droplets share a tag, while the limits page caps a firewall at 10 Droplets, 5 tags and 50 rules in total. A tag called web on twenty Droplets is one firewall assignment. Twenty Droplets attached by name will not fit.

You can add a tag to an existing firewall from the command line:

doctl compute firewall add-tags <firewall-id> --tag-names "web"

Multiple firewalls on one Droplet are allowed. Their rules combine, and deny rules take precedence across the combined set. That is useful for a shared baseline plus one extra firewall per role, and it is also how a stray permissive rule on a forgotten firewall quietly widens access.

The weak spot of tags is that they fail silently. A Droplet created with wbe instead of web is simply not in the policy, and nothing warns you. That is why the audit below matters.

Cloud Firewall versus UFW

They are separate layers. DigitalOcean says the Cloud Firewall is a separate firewall from any software running on a Droplet, such as UFW, and that its rules are not reflected in the Droplet's own firewall. Network-based firewalls stop traffic before it reaches the server. A host tool like UFW runs on the Droplet and is configured separately.

Running both is reasonable, but check that they do not contradict each other, and do not trust UFW alone if you use Docker. Docker's documentation says that traffic to and from a container is diverted before it goes through ufw, so a published container port can be reachable even when a UFW rule says otherwise. A Cloud Firewall sits outside the machine, so Docker's iptables handling does not touch it.

Audit every Droplet at once

This is where DigitalOcean does not give you what you would want. There is no single call that lists Droplets without a firewall. The Droplet object has a tags field and no firewall field, and the firewall list endpoint takes only paging parameters. The firewall object does carry droplet_ids and tags, per the API reference, so you list both and compare them yourself.

First, a quick look. The columns below exist in doctl compute firewall list and doctl compute droplet list.

doctl compute droplet list --format ID,Name,Tags
doctl compute firewall list --format ID,Name,Status,DropletIDs,Tags

For an actual answer, ask which Droplets are covered neither by ID nor by tag. This script prints the unprotected ones. It needs jq, and it uses doctl's JSON output (-o json), which mirrors the API fields above.

droplets=$(doctl compute droplet list -o json)
firewalls=$(doctl compute firewall list -o json)

echo "$droplets" | jq -r --argjson fw "$firewalls" '
  ($fw | map(.droplet_ids // []) | add // []) as $ids
  | ($fw | map(.tags // []) | add // []) as $tags
  | .[]
  | select((.id | IN($ids[])) | not)
  | select(((.tags // []) | any(IN($tags[]))) | not)
  | "\(.id)\t\(.name)"'

Every line it prints is a Droplet with no firewall by either route. Treat that list as urgent.

Then look at what the firewalls allow. This one prints every inbound rule open to the whole internet that is not a deny rule. The documentation says a ports value of 0 means all ports for that protocol, so watch for it.

doctl compute firewall list -o json | jq -r '
  .[] | .name as $n
  | (.inbound_rules // [])[]
  | select((.sources.addresses // []) | any(. == "0.0.0.0/0" or . == "::/0"))
  | select(.action != "deny")
  | "\($n)\t\(.protocol)\t\(.ports)"'

Port 80 and 443 on a web firewall are expected. Port 22 and any database port are not. Run both checks from a scheduled job, and the day a tag typo or an opened port appears you hear about it, not a scanner.

Where this leaves you

The order of work is short. Find the unprotected Droplets with the audit script. Attach a tag-based firewall to them with SSH narrowed to known addresses. Take database ports off public addresses. Then repeat the audit on a schedule, because the failure that matters is the one introduced next month.

FAQ

Does a new Droplet come with a firewall?

No. The Droplet creation page has no firewall setting, and the recommended setup tells you to create a Cloud Firewall separately and apply your Droplet's tag to it.

Can I list Droplets that have no firewall in one command?

Not directly. The Droplet object has no firewall field, so list Droplets and firewalls and compare Droplet IDs and tags yourself, as the script in this article does.

Do I still need UFW if I use a Cloud Firewall?

It is optional. They are separate layers and their rules are not shared. If you run Docker, do not rely on UFW alone, because Docker documents that container traffic can bypass it.

What is the quickest safe change to make today?

Attach a firewall to every Droplet and change any SSH rule from 0.0.0.0/0 to the specific addresses you connect from.

Secorvia connects a DigitalOcean account with a read-scope API token; the DigitalOcean page says what that covers and the connection guide walks through it.

Keep reading
Examples · 7 min read

Four AWS attack paths, and the one change that breaks each

Fundamentals · 5 min read

Toxic combinations: two safe settings that are dangerous together

Fundamentals · 7 min read

What is Cloud Security Posture Management?

Try it against your own cloud account.