A Docker footgun led to a vandal deleting NewsBlur's MongoDB database (2021)
blog.newsblur.com
blog.newsblur.com
And with the chances of your average script kiddie/hacker/idiot getting caught being lower than a typical bike thief in Amsterdam this likely will not stop. It would be great if we could get a secure internet. But what we get instead is an internet that can only be kept running by large companies because they supposedly have the budget and the knowledge to do this at a scale that makes security affordable. Which means that a ton of innovation will simply never happen because for a large number of fledgling companies the decision between working on their business or working on the security of their business is a moot one, if they don't do the one the other will kill it and doing both at the same time is too costly.
NewsBlur is useful, destroying it serves no purpose at all. And make no mistake: the hacker clearly aimed to simply destroy it and pretend they have copied the data, so they were more than willing to do just that, wanton destruction for a miserly amount of money. Whoever did this may think they're l33t and cool but I personally think they are utter trash.
The problem is that your mental model of a firewall tends to be that it sits 'in front of' all applications on your machine. Docker breaks this assumption by implicitly inserting rules into iptables.
Worse still, if you then inspect the state of the firewall (using UFW), it does not show you any of the rules that Docker inserted, because those rules are on a separate iptables chain.
The problem, then, is the interaction between Docker and UFW. UFW is designed as a 'simple' frontend for iptables, for situations where the advanced configurability of iptables is unneccessary. If you manage the firewall using UFW, you are not really supposed to also inject 'raw' iptables rules, especially not on non-standard chains. However, this is exactly what Docker does.
Therefore, we end up in the hairy situation where Docker adjusts your firewall, but UFW does not provide any indication that anything has changed. Neither project considers this a bug, and they just point to each other for a solution.
Personally, I think it should be on Docker to, at the very least, throw up some warnings when it is about to change firewall rules, as this is clearly unexpected behavior for many users (despite Dockers claims to the contrary).
As nice as it would be for Docker to be more configurable in terms of where rules get inserted, that would not have helped here because the user already misconfigured things (should be using docker networks for db access). Technically docker even has a chain that you can throw rules into that will get hit before docker's rules... so it's there just very manual (instead of, say, having a ufw mode).
Yes, which runs counter to how every other application works. If you ask, say, sshd to listen on port 22, your firewall will still sit in front of it, unless you explicitly poke a hole in it.
It should also have option (I only found "complete on/complete off" one) to "just" create its DOCKER* chains and leave the jumping to them to the user
Docker has a `DOCKER-USER` chain where the user can inject their own rules before docker's rules are run.
But even then, the user flat out should not be using `-p` unless they want to expose the service outside of the machine. That is the well documented networking model of docker. Docker also includes a network abstraction that should have been used here to give access to other services that need it and isolate it from the things that don't.
> I personally think they are utter trash.
In this case, even criminal, given the extortion attempt.
I think that this is true for the parties that have the budget for a proper security team. But your average programmer is not able to keep up with both the very rapid evolution of the security scene, a bunch of tooling to build their application with (backend, frontend) and then the burden of writing the application itself. Even with an interest, and without the urge for an MVP or the subscription to 'break-fast' you are essentially a sitting duck given enough time. After all there is an army of attackers out there and you are just you.
I have a ton of ideas of how I could make my current project much better by adding a service. But I'm not going down that route because I know I will inevitably end up in an arms race that will stop me from being able to focus on the application and that will shift more and more resources to the security domain. So instead the app is limited in functionality and only works using local storage in the browser, everything else is utterly static. It's a dumb and very much brute force solution but it works. It is also severely limiting.
I doubt this was even specifically targeted at NewsBlur. Reading the article it sounds like it's just an automated attack that scans for any open mongo servers on the internet and does the same thing to any of them. Not that that's any better, really.
> When I containerized MongoDB, Docker helpfully inserted an allow rule into iptables, opening up MongoDB to the world
But the blog post doesn't mention how Docker "helpfully inserted an allow rule". Is this because NewsBlur ran the container using the -p 27017:27017 flag without reading the docs around what publishing a port does?
You don't need to publish a port for (2) containers to talk to each other, they can do that over a private Docker network that both containers belong to (something Docker Compose does for you by default). You can also choose to -p 127.0.0.1:27017:27017 which will only publish the port so that only localhost can access it, something like this is handy if you publish a web port to localhost so nginx not running in Docker can connect to it.
So I agree with you, the author is being very dishonest here.
Yep, and ended up shifting blame to Docker by calling it a "footgun."
But in 2021 they:
- Ran their main DB in Docker without knowing the fundamentals of Docker
- Shipped it to production without anyone catching it in a review process
I mean, the incident isn't fun and I wish no one harm on their production data but this is like saying you never used Linux before, stumbled upon a prompt onto your production server, ran `rm -rf /var/lib/mongodb` and now you wrote a blog about how you're upset at Ken Thompson and Dennis Ritchie[0] because an `rm` footgun allowed you to delete your production database.Now, to be constructive about this. Can or should Docker do anything to help prevent this? Would adding a warning to the terminal output that you're opening a port to the outside world be worth it? That sort of feels like overkill. Maybe a Docker plugin could be created to add more text or extra protection through y/n prompts when running anything dangerous, such as `docker compose down -v`. But this feels like a scenario where someone would do the bad thing without protection, get bitten by it and then discover the plugin afterwards where it's too late.
[0]: The creators of the `rm` tool.
Yet another reminder that the most important ability in systems engineering is good judgement.
Not to negate the fact that many are surprised by how docker bypasses ufw rules. This is painful and I would love to see a way for people to safely use port forwarding without surprises like this.
This is not a consequence of Docker's action but a fact of how Linux networking works. Docker could have bigger warnings, or default to only accepting connections from localhost (I think it should), but it could not easily change the way Linux networking works to extend the host's firewall to the containers.
This seems to me like a combination of multiple foot-guns, first being the Docker one - followed by the fact Mongo was not configured to authenticate the connection.
Heroku by default run PostgreSQL open to the world (which is problematic for other reasons) but they get away with it by relying on PG's decent authentication.
My default is to prefer to build systems with multiple layers of security, such that there is no reliance on a single issue like this.
Security should be the default stance, and any port exposed through docker should initially be restricted to local only. Going global should be explicit, imo.
The most important bit of information the above chart shows us is what a full database transfer looks like in terms of bandwidth. From 6p to 9:30p, the amount of data was the expected amount from a working primary server with multiple secondaries syncing to it. At 3a, you’ll see an enormous amount of data transfered.
This tells us that the hacker was an automated digital vandal rather than a concerted hacking attempt. And if we were to pay the ransom, it wouldn’t do anything because the vandals don’t have the data and have nothing to release.
Since that level of data transfer only occurred once (when they did it), they know the attacker did not actually transfer any data.
Whatever ways AWS marketing might encourage people to do things via their "Well Architected" series of publications, the reality is that the path of least resistance with VPC networking is what the linked article describes: everything on public subnets (with maybe security groups or network ACLs to restrict it somewhat).
Splitting infrastructure into public/private subnets is very tedious, and can at times be difficult to debug. In some of my most recent work it took some time to understand why and how the components needed to be linked to make a combination of an Internet Gateway, Network Firewall, and NAT Gateway work together. Combined with the use of multiple private subnets residing in different AZs meant testing permutations and fussing with the "reachability" analyzer. It is this kind of thing that makes people go with the first thing that works so that they can move on to getting productive work done.
Typically one wouldn't serve traffic via a NAT Gateway, and that's true for our case as well. The problem we ran into was not properly configuring VPC endpoints for a workload that heavily interacts with S3 and running up a tiny, but still not ideal, bill for network utilization over the NAT Gateway instead. It's not obvious to may people that interacting with AWS APIs happens over the public Internet even from within AWS infrastructure. The cost of ongoing cost NAT Gateways versus the free per-VPC Internet Gateway feels like it disincentivizes starting with a more secure setup.
Ultimately what I'm trying to say is that it's somewhat negligent of AWS to not make an almost universal requirement of modern web and SaaS cloud networking be simple and cheap to implement.
> We had been relying on the firewall to provide protection against threats, but when the firewall silently failed, we were left exposed.
> a change needs to be made as to which database users have permission to drop the database
I think these two definitely highlight the importance of the always using the "layered onion" model of security.
The combination of a firewall, a password, and a nuanced permissions model would have been sufficient to mitigate the attack, even if one or both of the others failed simultaneously.
I could self-host an RSS reader, but Newsblur works well enough that I haven't bothered.
Neither do other databases like PG, curiously enough. The recommendation seems to be to link to LDAP or use authentication hooks.
Or perhaps use client and server certificates for increased security - https://www.postgresql.org/docs/current/ssl-tcp.html#SSL-SER...
I don't think there's a way to avoid a DOS vector even if you're careful. If someone can access your database directly, they can make enough attempts to lock a user. The only way to be careful is to avoid public access to the db. But if you do that effectively, you don't have the issue of accounts getting locked.
It's a dubious argument to ever lock an account as a safety measure. Arguably, downtime is not a good tradeoff, especially if you're following best practices and using a very long, truly random password. An extremely secure db setup shouldn't require you to disable a "safety" feature that creates a new DoS vector because you've followed best practices.
OOF. Those can be nasty. Many of the tools "helpfully" inject jumps to their chains at the very front of the table, libvirt example:
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
9105K 4151M LIBVIRT_INP all -- * * 0.0.0.0/0 0.0.0.0/0
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 LIBVIRT_FWX all -- * * 0.0.0.0/0 0.0.0.0/0
0 0 LIBVIRT_FWI all -- * * 0.0.0.0/0 0.0.0.0/0
0 0 LIBVIRT_FWO all -- * * 0.0.0.0/0 0.0.0.0/0
Chain OUTPUT (policy ACCEPT 229K packets, 39M bytes)
pkts bytes target prot opt in out source destination
6893K 1628M LIBVIRT_OUT all -- * * 0.0.0.0/0 0.0.0.0/0
So if you relied on your firewall blocking access and didn't test it it's easy to be bamboozled.The common approach is to allow other tools only manage their own chains and call those chains from your <firewall tool> but not that many tools allow for that and want to take over your firewall.
I don’t understand this. Was the snapshot made of the compromised database or of the old not in use one? Why would the backup (snapshot?) delete itself on reconnection?
This appears to me like another case where we could have built a simple, 1-process vertical on a single box and called it a day.
https://twitter.com/joaoqalves/status/1332638533572550656?s=...
if it’s unavoidable, then monitoring your db isn’t reachable from the public internet is important.
docker listens on port X. Accessible on machine. Also accessible from outside regardless of ufw.
No amount of time and experience will make you think that configuring a software to listen on a port will automagically poke a hole in the firewall.
It starts with the simple truth: `docker` doesn't `listen` on any port.
Or maybe a simple question: How can I run `docker run -p 8080 nginx` over and over without port conflict?
Or - lets expand scope even more. How is docker supposed to know about your choice of firewall? What about upstream firewalls? What about multiple versions of firewalls on a host (ufw vs. fern vs.)?
Can go on and on..
> It starts with the simple truth: `docker` doesn't `listen` on any port.
If I run the command below, `docker-proxy` starts listening on an incrementing port.
> Or maybe a simple question: How can I run `docker run -p 8080 nginx` over and over without port conflict?
Because you're not specifying a port on the host; you're specifying a port on the container. I've never used the single port form of `-p`; I would've guessed it was the same as `-p 8080:8080`.
May be a simple question: How are you able run nginx and open http:// localhost without it making any changes to the firewall. Can go on and on.
What's wrong with "users should be careful" and "software shouldn't contain footguns"?
Everyone knows what the correct solution is. That's not what the discussion is about.
> Not taking time to think about how the software works
You're blaming the effect of poor design on alleged incompetence of people you know nothing about.
Clearly not.
Why not utilize the ingress/egress firewall rules offered by nearly all cloud/vps providers instead of relying on iptables of an instance?
Because its advantageous to use all the facilities your OS provides. The fact that docker bypasses the highlevel firewall on the system and pokes holes through via iptables is very unfortunate, and something I myself have learned as recently as 2019.
Related to your question about docker knowing which interface to bind to, it generally sets up it's own docker network interface. With a highlevel firewall it's trivial to attach that interface to any firewall zone created (eg public zone with only specific ports exposed).
There are a bunch of tutorials about how to tame Docker, but this one is the solution that I use and it was the simplest that I've found https://unrouted.io/2017/08/15/docker-firewall/
This way, even if you mistakenly expose your container ports to 0.0.0.0, you won't be able to access them until exposing them with iptables
This is pure ignorance and slandering the Docker team for it seems weird.
Try doing the same with Apache2. Multiple instances listening on Port 80.
This is the difference, and the source of ignorance.
Also, AIUI podman manages to not ignore the firewall, so it's not like it's some inherent issue.
maybe its fair to say that we shouldn't attempt to find an individual to blame. but that doesn't mean docker as an organization didnt screw up here
And if I don't want to use UFW or I am on a distro with another firewall, let alone an upstream firewall or other solution, tough luck?
My 2c:
- NEVER expose a database to the public. Use at least a micro-service BFEs that do exactly what the app needs and nothing more.
- Use a load balancer/gateway between your servers and the outside world (only port 80 and 443 should be open, 80 should redirect to 443).
- Use Docker-(Compose) as runtime/orchestrator only for development. For production use hardened cloud runtimes (e.g. Google Cloud Run, k3s/k8s, ...) with zero trust as default setting. Adhere to best practices!
- Before migrating production data, make sure everything works and nothing is exposed to the webs.
Tbh. I think your blog post is ridiculous, because IMHO the whole thing is your fault.
Even in case we accidently violate some security rules (like open ports,...) the application is still safe ?
They didn't intentionally expose the database; they thought that it was safe and docker bypassed their security.
> because IMHO the whole thing is your fault
How so? They had a working firewall, and then docker bypassed it, which they had no way of knowing to expect.
> It would make for a much more dramatic read if I was hit through a vulnerability in Docker instead of a footgun. By having Docker silently override the firewall, Docker has made it easier for developers who want to open up ports on their containers at the expense of security.
When I want to run my own web server on an arbitrary Linux distro inside of a container, I want to bind to 0.0.0.0:80 and 0.0.0.0:443. That's it.
When I want to run my own database on an arbitrary Linux distro inside of a container, without making it accessible externally, I want to just omit exposing ports. That's it.
I don't want to think about the firewall on the system (even what kind of a firewall it is running, if any) or configure it separately, otherwise it'd be as bind mount target directories on host system not being automatically created, making me use "mkdir -p PATH", which already adds unnecessary friction (if I want the files to be available in the host OS, as opposed to just in abstracted away Docker volumes). After all, if there were more steps, then an awkward dance would inevitably start: "Hey, we launched the container on SOME_DISTRO but we can't access the UI/API/whatever." and you'd have to write platform-specific instructions, as opposed to being able to just give a Docker Compose file or an equivalent and be done with it.
Thus, my original thoughts were along the lines of: "It's not at the expense of security, as much as it is to the benefit of your convenience without any changes to security, if you are aware of what you are doing." though that isn't charitable enough. Something like this would be more helpful instead:
# Suppose you want to let people know that your container wants to listen on the port 2000
# You can specify it in the Dockerfile, though this will be more like documentation for the container, as opposed to publishing anything
# Docs: https://docs.docker.com/engine/reference/builder/#expose
# File: Dockerfile
EXPOSE 2000/tcp
...
# If you want your port to be available locally, then Docker gives you that ability as well
# You can even change the port, for example, to 2005 on the host
# Docs: https://docs.docker.com/compose/compose-file/compose-file-v3/#ports
# File: docker-compose.yml
ports:
- "127.0.0.1:2005:2000/tcp"
...
# If you want your port to be available remotely, then you can either omit the IP address or use 0.0.0.0
# Let's take the above example and use 2005 as the exposed port again
# File docker-compose.yml
ports:
- "2005:2000/tcp"
...
# For examples of Docker CLI without Docker Compose/Swarm, have a look at the docs: https://docs.docker.com/config/containers/container-networking/
# There's also good information there about additional networking options, such as creating custom networks for limiting what can talk to what.
Actually, here's an example that anyone can run, with Docker CLI (using the httpd web server as example, same idea applies to DBs): # Launch everything (detached, so we can use the same terminal session, remove containers once stopped)
docker run -d --rm --name "net_test_not_exposed" httpd:alpine3.17
docker run -d --rm --name "net_test_local_access" -p "127.0.0.1:2005:80/tcp" httpd:alpine3.17
docker run -d --rm --name "net_test_public_access" -p "0.0.0.0:2006:80/tcp" httpd:alpine3.17
docker run -d --rm --name "net_test_public_access_shorthand" -p "2007:80" httpd:alpine3.17
# Check that everything is running
docker ps
# Or different formatting
docker ps --format "{{.Names}} is {{.Status}} with ports {{.Ports}}"
# Then you can use a browser to check them, or do it manually, either locally or from another node (with actual IP address)
# 2005 will be available locally, whereas 2006 and 2007 will be available remotely
curl http://127.0.0.1:2005
curl http://127.0.0.1:2006
curl http://127.0.0.1:2007
# Finally, the cleanup (the --rm will get rid of the containers)
docker kill net_test_not_exposed net_test_local_access net_test_public_access net_test_public_access_shorthand
I will say that this is a good suggestion:> Better would be for Docker to issue a warning when it detects that the most popular firewall on Linux is active and filtering traffic to a port that Docker is about to open.
However, the thing with warnings is that they're easy to miss. Either way, I've been there - when I was learning Docker a number of years back, I left its socket open and by the morning the tiny VPS was mining crypto. Good docs help, maybe exposing things publicly by default with a port binding or even using the shorthand in the first place isn't such a good idea either.
Edit: note I said Docker specifically, nothing about containerization.
I mean... if they weren't using docker it would have been fine, but because they used docker it wasn't fine. That reads like docker is the problem. That further layers could have mitigated it doesn't make docker not the problem.
They shouldn't have needed to, for this.
> But because they didn't and used a software firewall as their only line of defense
Okay? That should have been safe. Like, sure, there are ways to add more layers, but that layer shouldn't have failed them.
> didn't bother with authentication for the DB server, it wasn't fine.
They shouldn't have needed to. Like yes, running a DB without authentication isn't a great idea, but they had good reason to believe that it wouldn't be exposed.
They didn't secure their database with auth/access control and they misconfigured docker.
They have a few things at their disposal:
- Using the docker-user chain to set firewall rules
- Running docker such that the default bind address for the port directive is 127.0.0.1 instead of 0.0.0.0. This puts a safety on the footgun.
- Explicitly setting the bind address of the port directive when bringing up the container.
Docker didn't come along, install itself, and open up the port. The sysadmin did. It's unfortunate they didn't know how it interacted with the firewall or how to properly configure it, but given that, should they really have been rolling it out to production systems?
They tested on prod and learned in trial by fire.
True, although they had reason to believe that that was safe.
> they misconfigured docker.
Ah, no, that's where we disagree. They didn't configure it, docker shipped an insane default that bypasses existing security measures. There is absolutely no reason to expect that running a program in docker magically makes it ignore the system firewall.
By the same token having an unsecured database is a bad default, so we can keep passing the buck around.
That's like the antithesis of layered security. There would be no point in layers if you assume a single layer will protect you.
the problem is that it was possible to accidentally expose their credential-less db to the internet _at all_ and that they had no monitoring or tools in place to detect the misconfiguration. that’s a design flaw, and again, not a docker-specific problem.
Docker could not extend the host's firewall to containers without changing how iptables work.
That's exactly what happens. Docker sticking its rules before the normal filters is exactly what I mean when I say it bypasses the firewall rules. Like... you're literally describing the implementation details of what I said it does.
> The host's firewall just doesn't apply to containers and VMs, because they are not listening on a port on the host.
They clearly are? If a docker container was listening but not on a port on the host's internet-facing interface (indirected though it may be), none of this would be a problem. The problem is precisely that if you have a host firewall rule that says "this port is blocked", docker will "helpfully" preempt that and connect that port to a container, which is a massive and unreasonable footgun.
> Docker could not extend the host's firewall to containers without changing how iptables work.
podman seems to manage fine, so I have trouble believing that.
VirtualBox works the same way, if you use the bridge networking. So does kvm. And so does Podman, I am not sure why you thought otherwise (maybe there is a mode where it uses a userspace proxy rather than iptables? When running rootless for example? Does not happen on my machine, ufw rules are ignored).
(genuinly curious - I only have experience with Docker and never felt the need to look elsewhere, even with footguns, but I'm still curious)
Though personally I would miss Docker Swarm which comes with Docker by default and, in my opinion, is a nice bridge between something like Docker Compose (easy single node deployments) and Nomad/Kubernetes (both more complex in comparison, need separate setup and lots of admin work).
Personally I also think that Docker is sufficient in most cases and is pretty boring and stable. Docker Desktop might irk some, but seems like they wanted that revenue.
However, for desktop GUI, Podman has Podman Desktop: https://podman-desktop.io/
Or one can also consider Rancher Desktop as another GUI tool, if desired: https://rancherdesktop.io/
Also, I do need to play with nftables.
Also, disabling UFW by accident can happen quite easily, if you e.g. remove your systems' Python version (to install a new one) it will uninstall UFW as well, opening up all ports & chains in the process. On a desktop system that might be OK, but it's just not acceptable for a server system IMHO. Hence the first thing I do in any Ansible base role is to remove UFW and install Ferm.
Ferm isn't perfect either but it has much better support for most iptables functionality and the way it builds up and applies rules makes it way more suited to a production environment.