From 4b51adb6918af2a948488b53c8a855bf4f415e15 Mon Sep 17 00:00:00 2001 From: Samuel Clay Date: Sun, 27 Jun 2021 21:04:07 -0400 Subject: [PATCH] Blog post: How a Docker footgun led to a vandal deleting NewsBlur's MongoDB database --- blog/_posts/2021-06-25-story-of-a-hacking.md | 148 +++++++++++------- blog/_sass/minima/_base.scss | 2 + .../2021/06/25/story-of-a-hacking/index.html | 111 ++++++++----- blog/_site/assets/main.css | 2 +- blog/_site/assets/main.css.map | 4 +- blog/_site/assets/ornament-pill.png | Bin 0 -> 1715 bytes blog/_site/feed.xml | 99 ++++++++---- blog/_site/index.html | 99 ++++++++---- blog/assets/ornament-pill.png | Bin 0 -> 1715 bytes 9 files changed, 312 insertions(+), 153 deletions(-) create mode 100644 blog/_site/assets/ornament-pill.png create mode 100644 blog/assets/ornament-pill.png diff --git a/blog/_posts/2021-06-25-story-of-a-hacking.md b/blog/_posts/2021-06-25-story-of-a-hacking.md index c32432774..cb22159ad 100644 --- a/blog/_posts/2021-06-25-story-of-a-hacking.md +++ b/blog/_posts/2021-06-25-story-of-a-hacking.md @@ -1,89 +1,129 @@ --- layout: post -title: Story of a Hacking +title: How a Docker footgun led to a vandal deleting NewsBlur's MongoDB database tags: ['backend'] --- -I'd like to answer a few questions about what happened here. +I'm in the process of moving everything on NewsBlur over to Docker containers in prep for a [big redesign launching next week](https://beta.newsblur.com). It's been a great year of maintenance and I've enjoyed the fruits of Ansible + Docker for NewsBlur's 5 database servers (PostgreSQL, MongoDB, Redis, Elasticsearch, and soon ML models). The day was wrapping up and I settled into [a new book on how to tame the machines once they're smarter than us](https://en.wikipedia.org/wiki/Human_Compatible) when I received a strange NewsBlur error on my phone. + + "query killed during yield: renamed collection 'newsblur.feed_icons' to 'newsblur.system.drop.1624498448i220t-1.feed_icons'" + +There are honestly no sets of words in that error message that I ever want to see again. What is the word `drop` doing in that error message? Better go find out. + +Logging into the MongoDB machine to check out what state the DB is in and I come across the following... + +{% highlight javascript %} +nbset:PRIMARY> show dbs +READ__ME_TO_RECOVER_YOUR_DATA 0.000GB +newsblur 0.718GB + +nbset:PRIMARY> use READ__ME_TO_RECOVER_YOUR_DATA +switched to db READ__ME_TO_RECOVER_YOUR_DATA + +nbset:PRIMARY> db.README.find() +{ + "_id" : ObjectId("60d3e112ac48d82047aab95d"), + "content" : "All your data is a backed up. You must pay 0.03 BTC to XXXXXXFTHISGUYXXXXXXX 48 hours for recover it. After 48 hours expiration we will leaked and exposed all your data. In case of refusal to pay, we will contact the General Data Protection Regulation, GDPR and notify them that you store user data in an open form and is not safe. Under the rules of the law, you face a heavy fine or arrest and your base dump will be dropped from our server! You can buy bitcoin here, does not take much time to buy https://localbitcoins.com or https://buy.moonpay.io/ After paying write to me in the mail with your DB IP: FTHISGUY@recoverme.one and you will receive a link to download your database dump." +} +{% endhighlight %} + +Two thoughts immediately occured: + + 1. Thank goodness I have some recently checked backups on hand + 2. No way they have that data without me noticing + +Three and a half hours before this happened, I switched the MongoDB cluster over to the new servers. When I did that, I shut down the original primary in order to delete it in a few days when all was well. And thank goodness I did that as it came in handy a few hours later. Knowing this, I realized that the hacker could not have taken all that data in so little time. + +With that in mind, I'd like to answer a few questions about what happened here. 1. Was any data leaked during the hack? How do you know? 2. How did NewsBlur's MongoDB server get hacked? 3. What will happen to ensure this doesn't happen again? -Let's start by talking about the importance of your data. As you may know, NewsBlur is open source and enjoys the added protection of having thousands of people looking at the codebase and dozens contributing back to it. +Let's start by talking about the most important question of all which is what happened to your data. ### 1. Was any data leaked during the hack? How do you know? I can definitively write that no data was leaked during the hack. I know this because of two different sets of logs showing that the automated attacker only issued deletion commands and did not transfer any data off of the MongoDB server. -This is what the day looks like. This 24 graph of bandwidth shows: +Below is a snapshot of the bandwidth of the db-mongo1 machine over 24 hours: -You can imagine the stress I experienced in the forty minutes between 9:35p, when the hack began, and 10:15p, when the fresh backup snapshot was identified and put into gear. +You can imagine the stress I experienced in the forty minutes between 9:35p, when the hack began, and 10:15p, when the fresh backup snapshot was identified and put into gear. Let's breakdown each moment: - 1. **6:10p**: - 2. **9:35p**: - 3. **10:15p**: - 4. **3:00a**: - 5. **4:30a**: + 1. **6:10p**: The new db-mongo1 server was put into rotation as the new MongoDB primary server. This machine was the first of the new, soon-to-be private cloud. + 2. **9:35p**: Three hours later an automated hacking attempt opened a connection to the db-mongo1 server and immediately dropped the database. Downtime ensued. + 3. **10:15p**: Before the former primary server could be placed into rotation, a snapshot of the server was made to ensure the backup would not delete itself upon reconnection. This cost a few hours of downtime, but saved nearly 18 hours of a day's data by not forcing me to go into the daily backup archive. + 4. **3:00a**: Snapshot completes, replication from original primary server to new db-mongo1 begins. What you see in the next hour and a half is what the transfer of the DB looks like in terms of bandwidth. + 5. **4:30a**: Replication, which is inbound from the old primary server, completes, and now replication begins outbound on the new secondaries. NewsBlur is now back up. + +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. + +While the server was being snapshot, I used that time to figure out how the hacker got in. ### 2. How did NewsBlur's MongoDB server get hacked? -It would make for a much more dramatic read if I was hit through a vulnerability in Docker instead of a footgun. +Turns out the ufw firewall I enabled and diligently kept on a strict allowlist with only my internal servers didn't work on a new server because of Docker. When I containerized MongoDB, Docker helpfully inserted an allow rule into iptables, opening up MongoDB to the world. So while my firewall was "active", doing a `sudo iptables -L | grep 27017` showed that MongoDB was open the world. - nbset:PRIMARY> show dbs - READ__ME_TO_RECOVER_YOUR_DATA 0.000GB - admin 0.000GB - local 16.471GB - newsblur 0.718GB - - nbset:PRIMARY> use READ__ME_TO_RECOVER_YOUR_DATA - switched to db READ__ME_TO_RECOVER_YOUR_DATA - - nbset:PRIMARY> show collections - README - system.profile - - nbset:PRIMARY> db.README.find() - { "_id" : ObjectId("60d3e112ac48d82047aab95d"), "content" : "All your data is a backed up. You must pay 0.03 BTC to XXXXXXFTHISGUYXXXXXXX 48 hours for recover it. After 48 hours expiration we will leaked and exposed all your data. In case of refusal to pay, we will contact the General Data Protection Regulation, GDPR and notify them that you store user data in an open form and is not safe. Under the rules of the law, you face a heavy fine or arrest and your base dump will be dropped from our server! You can buy bitcoin here, does not take much time to buy https://localbitcoins.com or https://buy.moonpay.io/ After paying write to me in the mail with your DB IP: FTHISGUY@recoverme.one and you will receive a link to download your database dump." } +To be honest, I'm a bit surprised it took over 3 hours from when I flipped the switch to when a hacker/vandal dropped NewsBlur's MongoDB collections and pretended to ransom about 250GB of data. This is the work of an automated hack and one that I was prepared for. NewsBlur was back online a few hours later once the backups were restored. And the Docker-made hole was immediately patched. -Looking at the MongoDB access logs, we can invoke a pretty neat command to find everybody who is not one of the 100 known NewsBlur machines that has accessed MongoDB. +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. 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. - $ cat /var/log/mongodb/mongod.log | egrep -v "159.65.XX.XX|161.89.XX.XX|<< SNIP: A hundred more servers >>" + - 2021-06-24T01:33:45.531+0000 I NETWORK [listener] connection accepted from 171.25.193.78:26003 #63455699 (1189 connections now open) - 2021-06-24T01:33:45.635+0000 I NETWORK [conn63455699] received client metadata from 171.25.193.78:26003 conn63455699: { driver: { name: "PyMongo", version: "3.11.4" }, os: { type: "Linux", name: "Linux", architecture: "x86_64", version: "5.4.0-74-generic" }, platform: "CPython 3.8.5.final.0" } - 2021-06-24T01:33:46.010+0000 I NETWORK [listener] connection accepted from 171.25.193.78:26557 #63455724 (1189 connections now open) - 2021-06-24T01:33:46.092+0000 I NETWORK [conn63455724] received client metadata from 171.25.193.78:26557 conn63455724: { driver: { name: "PyMongo", version: "3.11.4" }, os: { type: "Linux", name: "Linux", architecture: "x86_64", version: "5.4.0-74-generic" }, platform: "CPython 3.8.5.final.0" } - 2021-06-24T01:33:46.500+0000 I NETWORK [conn63455724] end connection 171.25.193.78:26557 (1198 connections now open) - 2021-06-24T01:33:46.533+0000 I NETWORK [conn63455699] end connection 171.25.193.78:26003 (1200 connections now open) - 2021-06-24T01:34:06.533+0000 I NETWORK [listener] connection accepted from 185.220.101.6:10056 #63456621 (1266 connections now open) - 2021-06-24T01:34:06.627+0000 I NETWORK [conn63456621] received client metadata from 185.220.101.6:10056 conn63456621: { driver: { name: "PyMongo", version: "3.11.4" }, os: { type: "Linux", name: "Linux", architecture: "x86_64", version: "5.4.0-74-generic" }, platform: "CPython 3.8.5.final.0" } - 2021-06-24T01:34:06.890+0000 I NETWORK [listener] connection accepted from 185.220.101.6:21642 #63456637 (1264 connections now open) - 2021-06-24T01:34:06.962+0000 I NETWORK [conn63456637] received client metadata from 185.220.101.6:21642 conn63456637: { driver: { name: "PyMongo", version: "3.11.4" }, os: { type: "Linux", name: "Linux", architecture: "x86_64", version: "5.4.0-74-generic" }, platform: "CPython 3.8.5.final.0" } - 2021-06-24T01:34:08.018+0000 I COMMAND [conn63456637] dropDatabase config - starting - 2021-06-24T01:34:08.018+0000 I COMMAND [conn63456637] dropDatabase config - dropping 1 collections - 2021-06-24T01:34:08.018+0000 I COMMAND [conn63456637] dropDatabase config - dropping collection: config.transactions - 2021-06-24T01:34:08.020+0000 I STORAGE [conn63456637] dropCollection: config.transactions (no UUID) - renaming to drop-pending collection: config.system.drop.1624498448i1t-1.transactions with drop optime { ts: Timestamp(1624498448, 1), t: -1 } - 2021-06-24T01:34:08.029+0000 I REPL [replication-14545] Completing collection drop for config.system.drop.1624498448i1t-1.transactions with drop optime { ts: Timestamp(1624498448, 1), t: -1 } (notification optime: { ts: Timestamp(1624498448, 1), t: -1 }) - 2021-06-24T01:34:08.030+0000 I STORAGE [replication-14545] Finishing collection drop for config.system.drop.1624498448i1t-1.transactions (no UUID). - 2021-06-24T01:34:08.030+0000 I COMMAND [conn63456637] dropDatabase config - successfully dropped 1 collections (most recent drop optime: { ts: Timestamp(1624498448, 1), t: -1 }) after 7ms. dropping database - 2021-06-24T01:34:08.032+0000 I REPL [replication-14546] Completing collection drop for config.system.drop.1624498448i1t-1.transactions with drop optime { ts: Timestamp(1624498448, 1), t: -1 } (notification optime: { ts: Timestamp(1624498448, 5), t: -1 }) - 2021-06-24T01:34:08.041+0000 I COMMAND [conn63456637] dropDatabase config - finished - 2021-06-24T01:34:08.398+0000 I COMMAND [conn63456637] dropDatabase newsblur - starting - 2021-06-24T01:34:08.398+0000 I COMMAND [conn63456637] dropDatabase newsblur - dropping 37 collections +The second reason we know that no data was taken comes from looking through the MongoDB access logs. With these rich and verbose logging sources we can invoke a pretty neat command to find everybody who is not one of the 100 known NewsBlur machines that has accessed MongoDB. - << SNIP: It goes on for a while... >> +

+$ cat /var/log/mongodb/mongod.log | egrep -v "159.65.XX.XX|161.89.XX.XX|<< SNIP: A hundred more servers >>"
 
-    2021-06-24T01:35:18.840+0000 I COMMAND  [conn63456637] dropDatabase newsblur - finished
+2021-06-24T01:33:45.531+0000 I NETWORK  [listener] connection accepted from 171.25.193.78:26003 #63455699 (1189 connections now open)
+2021-06-24T01:33:45.635+0000 I NETWORK  [conn63455699] received client metadata from 171.25.193.78:26003 conn63455699: { driver: { name: "PyMongo", version: "3.11.4" }, os: { type: "Linux", name: "Linux", architecture: "x86_64", version: "5.4.0-74-generic" }, platform: "CPython 3.8.5.final.0" }
+2021-06-24T01:33:46.010+0000 I NETWORK  [listener] connection accepted from 171.25.193.78:26557 #63455724 (1189 connections now open)
+2021-06-24T01:33:46.092+0000 I NETWORK  [conn63455724] received client metadata from 171.25.193.78:26557 conn63455724: { driver: { name: "PyMongo", version: "3.11.4" }, os: { type: "Linux", name: "Linux", architecture: "x86_64", version: "5.4.0-74-generic" }, platform: "CPython 3.8.5.final.0" }
+2021-06-24T01:33:46.500+0000 I NETWORK  [conn63455724] end connection 171.25.193.78:26557 (1198 connections now open)
+2021-06-24T01:33:46.533+0000 I NETWORK  [conn63455699] end connection 171.25.193.78:26003 (1200 connections now open)
+2021-06-24T01:34:06.533+0000 I NETWORK  [listener] connection accepted from 185.220.101.6:10056 #63456621 (1266 connections now open)
+2021-06-24T01:34:06.627+0000 I NETWORK  [conn63456621] received client metadata from 185.220.101.6:10056 conn63456621: { driver: { name: "PyMongo", version: "3.11.4" }, os: { type: "Linux", name: "Linux", architecture: "x86_64", version: "5.4.0-74-generic" }, platform: "CPython 3.8.5.final.0" }
+2021-06-24T01:34:06.890+0000 I NETWORK  [listener] connection accepted from 185.220.101.6:21642 #63456637 (1264 connections now open)
+2021-06-24T01:34:06.962+0000 I NETWORK  [conn63456637] received client metadata from 185.220.101.6:21642 conn63456637: { driver: { name: "PyMongo", version: "3.11.4" }, os: { type: "Linux", name: "Linux", architecture: "x86_64", version: "5.4.0-74-generic" }, platform: "CPython 3.8.5.final.0" }
+2021-06-24T01:34:08.018+0000 I COMMAND  [conn63456637] dropDatabase config - starting
+2021-06-24T01:34:08.018+0000 I COMMAND  [conn63456637] dropDatabase config - dropping 1 collections
+2021-06-24T01:34:08.018+0000 I COMMAND  [conn63456637] dropDatabase config - dropping collection: config.transactions
+2021-06-24T01:34:08.020+0000 I STORAGE  [conn63456637] dropCollection: config.transactions (no UUID) - renaming to drop-pending collection: config.system.drop.1624498448i1t-1.transactions with drop optime { ts: Timestamp(1624498448, 1), t: -1 }
+2021-06-24T01:34:08.029+0000 I REPL     [replication-14545] Completing collection drop for config.system.drop.1624498448i1t-1.transactions with drop optime { ts: Timestamp(1624498448, 1), t: -1 } (notification optime: { ts: Timestamp(1624498448, 1), t: -1 })
+2021-06-24T01:34:08.030+0000 I STORAGE  [replication-14545] Finishing collection drop for config.system.drop.1624498448i1t-1.transactions (no UUID).
+2021-06-24T01:34:08.030+0000 I COMMAND  [conn63456637] dropDatabase config - successfully dropped 1 collections (most recent drop optime: { ts: Timestamp(1624498448, 1), t: -1 }) after 7ms. dropping database
+2021-06-24T01:34:08.032+0000 I REPL     [replication-14546] Completing collection drop for config.system.drop.1624498448i1t-1.transactions with drop optime { ts: Timestamp(1624498448, 1), t: -1 } (notification optime: { ts: Timestamp(1624498448, 5), t: -1 })
+2021-06-24T01:34:08.041+0000 I COMMAND  [conn63456637] dropDatabase config - finished
+2021-06-24T01:34:08.398+0000 I COMMAND  [conn63456637] dropDatabase newsblur - starting
+2021-06-24T01:34:08.398+0000 I COMMAND  [conn63456637] dropDatabase newsblur - dropping 37 collections
 
-What you see above...
+<< SNIP: It goes on for a while... >>
 
-When I visited the IP address of the [two](http://185.220.101.6/) [connections](http://171.25.193.78/) above, I found a Tor exit router:
+2021-06-24T01:35:18.840+0000 I COMMAND  [conn63456637] dropDatabase newsblur - finished
+
+ +The above is a lot, but the important bit of information to take from it is that by using a reductive filter, capturing everything that doesn't match a known IP, I was able to find the two connections that were made a few seconds apart. Both connections from these unknown IPs occured only moments before the database-wide deletion. By following the connection ID, it became easy to see the hacker come into the server only to delete it seconds later. + +Interestingly, when I visited the IP address of the [two](http://185.220.101.6/) [connections](http://171.25.193.78/) above, I found a Tor exit router: +This means that it is virtually impossible to track down who is responsible due to the anonymity preserving quality of Tor exit routers. [Tor exit nodes have poor reputations](https://blog.cloudflare.com/the-trouble-with-tor/) due to the havoc they wreak. Site owners are split on whether to block Tor entirely, but some see the value of allowing anonymous traffic to hit their servers. In NewsBlur's case, because NewsBlur is a home of free speech, allowing users in countries with censored news outlets to bypass restrictions and get access to the world at wide, the continuing risk of supporting anonymous Internet traffic is worth the cost. + ### 3. What will happen to ensure this doesn't happen again? -VPC all the way. +Of course, being in support of free speech and providing enhanced ways to access speech comes at a cost. So for NewsBlur to continue serving traffic to all of its worldwide readers, several changes have to be made. + +The first change is the one that, ironically, we were in the process of moving to. A VPC, a virtual private cloud, keeps critical servers only accessible from others servers in a private network. But in moving to a private network, I need to migrate all of the data off of the publicly accessible machines. And this was the first step in that process. + +The second change is to use database user authentication on all of the databases. We had been relying on the firewall to provide protection against threats, but when the firewall silently failed, we were left exposed. Now who's to say that this would have been caught if the firewall failed but authentication was in place. I suspect the password needs to be long enough to not be brute-forced, because eventually, knowing that an open but password protected DB is there, it could very possibly end up on a list. + +Lastly, a change needs to be made as to which database users have permission to drop the database. Most database users only need read and write privileges. The ideal would be a localhost-only user being allowed to perform potentially destructive actions. If a rogue database user starts deleting stories, it would get noticed a whole lot faster than a database being dropped all at once. + +But each of these is only one piece of a defense strategy. [As this well-attended Hacker News thread from the day of the hack made clear](https://news.ycombinator.com/item?id=27613217), a proper defense strategy can never rely on only one move. And for NewsBlur that move was a allowlist-only firewall that worked perfectly up until it didn't. + +As usually, the real heros are backups. Regularly well tested backups are a necessary component to any web service. And with that, I'll prepare to [launch the big NewsBlur redesign later this week](https://beta.newsblur.com). diff --git a/blog/_sass/minima/_base.scss b/blog/_sass/minima/_base.scss index e61b0be3f..ea092b114 100644 --- a/blog/_sass/minima/_base.scss +++ b/blog/_sass/minima/_base.scss @@ -161,6 +161,8 @@ code { pre { padding: 8px 12px; overflow-x: auto; + white-space: pre-wrap; + word-wrap: break-word; > code { border: 0; diff --git a/blog/_site/2021/06/25/story-of-a-hacking/index.html b/blog/_site/2021/06/25/story-of-a-hacking/index.html index d40981755..3a2541fcb 100644 --- a/blog/_site/2021/06/25/story-of-a-hacking/index.html +++ b/blog/_site/2021/06/25/story-of-a-hacking/index.html @@ -6,21 +6,21 @@ -Story of a Hacking | The NewsBlur Blog +How a Docker footgun led to a vandal deleting NewsBlur’s MongoDB database | The NewsBlur Blog - + - - + + - + +{"description":"I’m in the process of moving everything on NewsBlur over to Docker containers in prep for a big redesign launching next week. It’s been a great year of maintenance and I’ve enjoyed the fruits of Ansible + Docker for NewsBlur’s 5 database servers (PostgreSQL, MongoDB, Redis, Elasticsearch, and soon ML models). The day was wrapping up and I settled into a new book on how to tame the machines once they’re smarter than us when I received a strange NewsBlur error on my phone.","mainEntityOfPage":{"@type":"WebPage","@id":"https://blog2.newsblur.com/2021/06/25/story-of-a-hacking/"},"publisher":{"@type":"Organization","logo":{"@type":"ImageObject","url":"https://blog2.newsblur.com/assets/newsblur_logo_512.png"}},"url":"https://blog2.newsblur.com/2021/06/25/story-of-a-hacking/","headline":"How a Docker footgun led to a vandal deleting NewsBlur’s MongoDB database","dateModified":"2021-06-25T00:00:00-04:00","datePublished":"2021-06-25T00:00:00-04:00","@type":"BlogPosting","@context":"https://schema.org"} @@ -63,14 +63,45 @@
-

Story of a Hacking

+

How a Docker footgun led to a vandal deleting NewsBlur's MongoDB database

-

I’d like to answer a few questions about what happened here.

+

I’m in the process of moving everything on NewsBlur over to Docker containers in prep for a big redesign launching next week. It’s been a great year of maintenance and I’ve enjoyed the fruits of Ansible + Docker for NewsBlur’s 5 database servers (PostgreSQL, MongoDB, Redis, Elasticsearch, and soon ML models). The day was wrapping up and I settled into a new book on how to tame the machines once they’re smarter than us when I received a strange NewsBlur error on my phone.

+ +
"query killed during yield: renamed collection 'newsblur.feed_icons' to 'newsblur.system.drop.1624498448i220t-1.feed_icons'"
+
+ +

There are honestly no sets of words in that error message that I ever want to see again. What is the word drop doing in that error message? Better go find out.

+ +

Logging into the MongoDB machine to check out what state the DB is in and I come across the following…

+ +
nbset:PRIMARY> show dbs
+READ__ME_TO_RECOVER_YOUR_DATA   0.000GB
+newsblur                        0.718GB
+
+nbset:PRIMARY> use READ__ME_TO_RECOVER_YOUR_DATA
+switched to db READ__ME_TO_RECOVER_YOUR_DATA
+    
+nbset:PRIMARY> db.README.find()
+{ 
+    "_id" : ObjectId("60d3e112ac48d82047aab95d"), 
+    "content" : "All your data is a backed up. You must pay 0.03 BTC to XXXXXXFTHISGUYXXXXXXX 48 hours for recover it. After 48 hours expiration we will leaked and exposed all your data. In case of refusal to pay, we will contact the General Data Protection Regulation, GDPR and notify them that you store user data in an open form and is not safe. Under the rules of the law, you face a heavy fine or arrest and your base dump will be dropped from our server! You can buy bitcoin here, does not take much time to buy https://localbitcoins.com or https://buy.moonpay.io/ After paying write to me in the mail with your DB IP: FTHISGUY@recoverme.one and you will receive a link to download your database dump." 
+}
+ +

Two thoughts immediately occured:

+ +
    +
  1. Thank goodness I have some recently checked backups on hand
  2. +
  3. No way they have that data without me noticing
  4. +
+ +

Three and a half hours before this happened, I switched the MongoDB cluster over to the new servers. When I did that, I shut down the original primary in order to delete it in a few days when all was well. And thank goodness I did that as it came in handy a few hours later. Knowing this, I realized that the hacker could not have taken all that data in so little time.

+ +

With that in mind, I’d like to answer a few questions about what happened here.

  1. Was any data leaked during the hack? How do you know?
  2. @@ -78,50 +109,46 @@
  3. What will happen to ensure this doesn’t happen again?
-

Let’s start by talking about the importance of your data. As you may know, NewsBlur is open source and enjoys the added protection of having thousands of people looking at the codebase and dozens contributing back to it.

+

Let’s start by talking about the most important question of all which is what happened to your data.

1. Was any data leaked during the hack? How do you know?

I can definitively write that no data was leaked during the hack. I know this because of two different sets of logs showing that the automated attacker only issued deletion commands and did not transfer any data off of the MongoDB server.

-

This is what the day looks like. This 24 graph of bandwidth shows:

+

Below is a snapshot of the bandwidth of the db-mongo1 machine over 24 hours:

-

You can imagine the stress I experienced in the forty minutes between 9:35p, when the hack began, and 10:15p, when the fresh backup snapshot was identified and put into gear.

+

You can imagine the stress I experienced in the forty minutes between 9:35p, when the hack began, and 10:15p, when the fresh backup snapshot was identified and put into gear. Let’s breakdown each moment:

    -
  1. 6:10p:
  2. -
  3. 9:35p:
  4. -
  5. 10:15p:
  6. -
  7. 3:00a:
  8. -
  9. 4:30a:
  10. +
  11. 6:10p: The new db-mongo1 server was put into rotation as the new MongoDB primary server. This machine was the first of the new, soon-to-be private cloud.
  12. +
  13. 9:35p: Three hours later an automated hacking attempt opened a connection to the db-mongo1 server and immediately dropped the database. Downtime ensued.
  14. +
  15. 10:15p: Before the former primary server could be placed into rotation, a snapshot of the server was made to ensure the backup would not delete itself upon reconnection. This cost a few hours of downtime, but saved nearly 18 hours of a day’s data by not forcing me to go into the daily backup archive.
  16. +
  17. 3:00a: Snapshot completes, replication from original primary server to new db-mongo1 begins. What you see in the next hour and a half is what the transfer of the DB looks like in terms of bandwidth.
  18. +
  19. 4:30a: Replication, which is inbound from the old primary server, completes, and now replication begins outbound on the new secondaries. NewsBlur is now back up.
+

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.

+ +

While the server was being snapshot, I used that time to figure out how the hacker got in.

+

2. How did NewsBlur’s MongoDB server get hacked?

-

It would make for a much more dramatic read if I was hit through a vulnerability in Docker instead of a footgun.

+

Turns out the ufw firewall I enabled and diligently kept on a strict allowlist with only my internal servers didn’t work on a new server because of Docker. When I containerized MongoDB, Docker helpfully inserted an allow rule into iptables, opening up MongoDB to the world. So while my firewall was “active”, doing a sudo iptables -L | grep 27017 showed that MongoDB was open the world.

-
nbset:PRIMARY> show dbs
-READ__ME_TO_RECOVER_YOUR_DATA   0.000GB
-admin                           0.000GB
-local                          16.471GB
-newsblur                        0.718GB
+

To be honest, I’m a bit surprised it took over 3 hours from when I flipped the switch to when a hacker/vandal dropped NewsBlur’s MongoDB collections and pretended to ransom about 250GB of data. This is the work of an automated hack and one that I was prepared for. NewsBlur was back online a few hours later once the backups were restored. And the Docker-made hole was immediately patched.

-nbset:PRIMARY> use READ__ME_TO_RECOVER_YOUR_DATA -switched to db READ__ME_TO_RECOVER_YOUR_DATA +

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. 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.

-nbset:PRIMARY> show collections -README -system.profile +

-nbset:PRIMARY> db.README.find() -{ "_id" : ObjectId("60d3e112ac48d82047aab95d"), "content" : "All your data is a backed up. You must pay 0.03 BTC to XXXXXXFTHISGUYXXXXXXX 48 hours for recover it. After 48 hours expiration we will leaked and exposed all your data. In case of refusal to pay, we will contact the General Data Protection Regulation, GDPR and notify them that you store user data in an open form and is not safe. Under the rules of the law, you face a heavy fine or arrest and your base dump will be dropped from our server! You can buy bitcoin here, does not take much time to buy https://localbitcoins.com or https://buy.moonpay.io/ After paying write to me in the mail with your DB IP: FTHISGUY@recoverme.one and you will receive a link to download your database dump." } -
+

The second reason we know that no data was taken comes from looking through the MongoDB access logs. With these rich and verbose logging sources we can invoke a pretty neat command to find everybody who is not one of the 100 known NewsBlur machines that has accessed MongoDB.

-

Looking at the MongoDB access logs, we can invoke a pretty neat command to find everybody who is not one of the 100 known NewsBlur machines that has accessed MongoDB.

- -
$ cat /var/log/mongodb/mongod.log | egrep -v "159.65.XX.XX|161.89.XX.XX|<< SNIP: A hundred more servers >>"
+

+$ cat /var/log/mongodb/mongod.log | egrep -v "159.65.XX.XX|161.89.XX.XX|<< SNIP: A hundred more servers >>"
 
 2021-06-24T01:33:45.531+0000 I NETWORK  [listener] connection accepted from 171.25.193.78:26003 #63455699 (1189 connections now open)
 2021-06-24T01:33:45.635+0000 I NETWORK  [conn63455699] received client metadata from 171.25.193.78:26003 conn63455699: { driver: { name: "PyMongo", version: "3.11.4" }, os: { type: "Linux", name: "Linux", architecture: "x86_64", version: "5.4.0-74-generic" }, platform: "CPython 3.8.5.final.0" }
@@ -150,15 +177,27 @@ nbset:PRIMARY> db.README.find()
 2021-06-24T01:35:18.840+0000 I COMMAND  [conn63456637] dropDatabase newsblur - finished
 
-

What you see above…

+

The above is a lot, but the important bit of information to take from it is that by using a reductive filter, capturing everything that doesn’t match a known IP, I was able to find the two connections that were made a few seconds apart. Both connections from these unknown IPs occured only moments before the database-wide deletion. By following the connection ID, it became easy to see the hacker come into the server only to delete it seconds later.

-

When I visited the IP address of the two connections above, I found a Tor exit router:

+

Interestingly, when I visited the IP address of the two connections above, I found a Tor exit router:

+

This means that it is virtually impossible to track down who is responsible due to the anonymity preserving quality of Tor exit routers. Tor exit nodes have poor reputations due to the havoc they wreak. Site owners are split on whether to block Tor entirely, but some see the value of allowing anonymous traffic to hit their servers. In NewsBlur’s case, because NewsBlur is a home of free speech, allowing users in countries with censored news outlets to bypass restrictions and get access to the world at wide, the continuing risk of supporting anonymous Internet traffic is worth the cost.

+

3. What will happen to ensure this doesn’t happen again?

-

VPC all the way.

+

Of course, being in support of free speech and providing enhanced ways to access speech comes at a cost. So for NewsBlur to continue serving traffic to all of its worldwide readers, several changes have to be made.

+ +

The first change is the one that, ironically, we were in the process of moving to. A VPC, a virtual private cloud, keeps critical servers only accessible from others servers in a private network. But in moving to a private network, I need to migrate all of the data off of the publicly accessible machines. And this was the first step in that process.

+ +

The second change is to use database user authentication on all of the databases. We had been relying on the firewall to provide protection against threats, but when the firewall silently failed, we were left exposed. Now who’s to say that this would have been caught if the firewall failed but authentication was in place. I suspect the password needs to be long enough to not be brute-forced, because eventually, knowing that an open but password protected DB is there, it could very possibly end up on a list.

+ +

Lastly, a change needs to be made as to which database users have permission to drop the database. Most database users only need read and write privileges. The ideal would be a localhost-only user being allowed to perform potentially destructive actions. If a rogue database user starts deleting stories, it would get noticed a whole lot faster than a database being dropped all at once.

+ +

But each of these is only one piece of a defense strategy. As this well-attended Hacker News thread from the day of the hack made clear, a proper defense strategy can never rely on only one move. And for NewsBlur that move was a allowlist-only firewall that worked perfectly up until it didn’t.

+ +

As usually, the real heros are backups. Regularly well tested backups are a necessary component to any web service. And with that, I’ll prepare to launch the big NewsBlur redesign later this week.

diff --git a/blog/_site/assets/main.css b/blog/_site/assets/main.css index 18a222172..a965e3fc4 100644 --- a/blog/_site/assets/main.css +++ b/blog/_site/assets/main.css @@ -47,7 +47,7 @@ pre, code { font-size: 15px; border: 1px solid #e5e8e4; border-radius: 3px; back code { padding: 1px 5px; } -pre { padding: 8px 12px; overflow-x: auto; } +pre { padding: 8px 12px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word; } pre > code { border: 0; padding-right: 0; padding-left: 0; } diff --git a/blog/_site/assets/main.css.map b/blog/_site/assets/main.css.map index 4f411cb43..c6817b3ef 100644 --- a/blog/_site/assets/main.css.map +++ b/blog/_site/assets/main.css.map @@ -11,10 +11,10 @@ "sourcesContent": [ "@import \"minima\";\n", "@charset \"utf-8\";\n\n// Define defaults for each variable.\n\n$base-font-family: \"Whitney A\", \"Whitney B\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\" !default;\n$base-font-size: 16px !default;\n$base-font-weight: 400 !default;\n$small-font-size: $base-font-size * 0.875 !default;\n$base-line-height: 1.5 !default;\n\n$spacing-unit: 30px !default;\n\n$text-color: #111 !default;\n$background-color: #FDFCF3 !default;\n$brand-color: #C66426 !default;\n$title-color: #1b3e63 !default;\n\n$grey-color: #7a8878 !default;\n$grey-color-light: lighten($grey-color, 40%) !default;\n$grey-color-dark: darken($grey-color, 25%) !default;\n\n$table-text-align: left !default;\n\n// Width of the content area\n$content-width: 800px !default;\n\n$on-palm: 600px !default;\n$on-laptop: 800px !default;\n\n// Use media queries like this:\n// @include media-query($on-palm) {\n// .wrapper {\n// padding-right: $spacing-unit / 2;\n// padding-left: $spacing-unit / 2;\n// }\n// }\n@mixin media-query($device) {\n @media screen and (max-width: $device) {\n @content;\n }\n}\n\n@mixin relative-font-size($ratio) {\n font-size: $base-font-size * $ratio;\n}\n\n// Import partials.\n@import\n \"minima/base\",\n \"minima/layout\",\n \"minima/syntax-highlighting\"\n;\n", - "/**\n * Reset some basic elements\n */\nbody, h1, h2, h3, h4, h5, h6,\np, blockquote, pre, hr,\ndl, dd, ol, ul, figure {\n margin: 0;\n padding: 0;\n}\n\n\n\n/**\n * Basic styling\n */\nbody {\n font: $base-font-weight #{$base-font-size}/#{$base-line-height} $base-font-family;\n color: $text-color;\n background-color: $background-color;\n -webkit-text-size-adjust: 100%;\n -webkit-font-feature-settings: \"kern\" 1;\n -moz-font-feature-settings: \"kern\" 1;\n -o-font-feature-settings: \"kern\" 1;\n font-feature-settings: \"kern\" 1;\n font-kerning: normal;\n display: flex;\n min-height: 100vh;\n flex-direction: column;\n}\n\n\n\n/**\n * Set `margin-bottom` to maintain vertical rhythm\n */\nh1, h2, h3, h4, h5, h6,\np, blockquote, pre,\nul, ol, dl, figure,\n%vertical-rhythm {\n margin-bottom: $spacing-unit / 2;\n}\n\n\n\n/**\n * `main` element\n */\nmain {\n display: block; /* Default value of `display` of `main` element is 'inline' in IE 11. */\n}\n\n\n\n/**\n * Images\n */\nimg {\n max-width: 100%;\n vertical-align: middle;\n}\n\n\n\n/**\n * Figures\n */\nfigure > img {\n display: block;\n}\n\nfigcaption {\n font-size: $small-font-size;\n}\n\n\n\n/**\n * Lists\n */\nul, ol {\n margin-left: $spacing-unit;\n}\n\nli {\n > ul,\n > ol {\n margin-bottom: 0;\n }\n}\n\n\n\n/**\n * Headings\n */\nh1, h2, h3, h4, h5, h6 {\n font-weight: $base-font-weight;\n}\n\n\n\n/**\n * Links\n */\na {\n color: $brand-color;\n text-decoration: none;\n\n &:visited {\n color: darken($brand-color, 15%);\n }\n\n &:hover {\n color: $text-color;\n text-decoration: underline;\n }\n\n .social-media-list &:hover {\n text-decoration: none;\n\n .username {\n text-decoration: underline;\n }\n }\n}\n\n\n/**\n * Blockquotes\n */\nblockquote {\n color: $grey-color;\n border-left: 4px solid $grey-color-light;\n padding-left: $spacing-unit / 2;\n @include relative-font-size(1.125);\n letter-spacing: -1px;\n font-style: italic;\n\n > :last-child {\n margin-bottom: 0;\n }\n}\n\n\n\n/**\n * Code formatting\n */\npre,\ncode {\n @include relative-font-size(0.9375);\n border: 1px solid $grey-color-light;\n border-radius: 3px;\n background-color: #eef;\n}\n\ncode {\n padding: 1px 5px;\n}\n\npre {\n padding: 8px 12px;\n overflow-x: auto;\n\n > code {\n border: 0;\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n\n\n/**\n * Wrapper\n */\n.wrapper {\n max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit} * 2));\n max-width: calc(#{$content-width} - (#{$spacing-unit} * 2));\n margin-right: auto;\n margin-left: auto;\n padding-right: $spacing-unit;\n padding-left: $spacing-unit;\n @extend %clearfix;\n\n @include media-query($on-laptop) {\n max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit}));\n max-width: calc(#{$content-width} - (#{$spacing-unit}));\n padding-right: $spacing-unit / 2;\n padding-left: $spacing-unit / 2;\n }\n}\n\n\n\n/**\n * Clearfix\n */\n%clearfix:after {\n content: \"\";\n display: table;\n clear: both;\n}\n\n\n\n/**\n * Icons\n */\n\n.svg-icon {\n width: 16px;\n height: 16px;\n display: inline-block;\n fill: #{$grey-color};\n padding-right: 5px;\n vertical-align: text-top;\n}\n\n.social-media-list {\n li + li {\n padding-top: 5px;\n }\n}\n\n\n\n/**\n * Tables\n */\ntable {\n margin-bottom: $spacing-unit;\n width: 100%;\n text-align: $table-text-align;\n color: lighten($text-color, 18%);\n border-collapse: collapse;\n border: 1px solid $grey-color-light;\n tr {\n &:nth-child(even) {\n background-color: lighten($grey-color-light, 6%);\n }\n }\n th, td {\n padding: ($spacing-unit / 3) ($spacing-unit / 2);\n }\n th {\n background-color: lighten($grey-color-light, 3%);\n border: 1px solid darken($grey-color-light, 4%);\n border-bottom-color: darken($grey-color-light, 12%);\n }\n td {\n border: 1px solid $grey-color-light;\n }\n}\n", + "/**\n * Reset some basic elements\n */\nbody, h1, h2, h3, h4, h5, h6,\np, blockquote, pre, hr,\ndl, dd, ol, ul, figure {\n margin: 0;\n padding: 0;\n}\n\n\n\n/**\n * Basic styling\n */\nbody {\n font: $base-font-weight #{$base-font-size}/#{$base-line-height} $base-font-family;\n color: $text-color;\n background-color: $background-color;\n -webkit-text-size-adjust: 100%;\n -webkit-font-feature-settings: \"kern\" 1;\n -moz-font-feature-settings: \"kern\" 1;\n -o-font-feature-settings: \"kern\" 1;\n font-feature-settings: \"kern\" 1;\n font-kerning: normal;\n display: flex;\n min-height: 100vh;\n flex-direction: column;\n}\n\n\n\n/**\n * Set `margin-bottom` to maintain vertical rhythm\n */\nh1, h2, h3, h4, h5, h6,\np, blockquote, pre,\nul, ol, dl, figure,\n%vertical-rhythm {\n margin-bottom: $spacing-unit / 2;\n}\n\n\n\n/**\n * `main` element\n */\nmain {\n display: block; /* Default value of `display` of `main` element is 'inline' in IE 11. */\n}\n\n\n\n/**\n * Images\n */\nimg {\n max-width: 100%;\n vertical-align: middle;\n}\n\n\n\n/**\n * Figures\n */\nfigure > img {\n display: block;\n}\n\nfigcaption {\n font-size: $small-font-size;\n}\n\n\n\n/**\n * Lists\n */\nul, ol {\n margin-left: $spacing-unit;\n}\n\nli {\n > ul,\n > ol {\n margin-bottom: 0;\n }\n}\n\n\n\n/**\n * Headings\n */\nh1, h2, h3, h4, h5, h6 {\n font-weight: $base-font-weight;\n}\n\n\n\n/**\n * Links\n */\na {\n color: $brand-color;\n text-decoration: none;\n\n &:visited {\n color: darken($brand-color, 15%);\n }\n\n &:hover {\n color: $text-color;\n text-decoration: underline;\n }\n\n .social-media-list &:hover {\n text-decoration: none;\n\n .username {\n text-decoration: underline;\n }\n }\n}\n\n\n/**\n * Blockquotes\n */\nblockquote {\n color: $grey-color;\n border-left: 4px solid $grey-color-light;\n padding-left: $spacing-unit / 2;\n @include relative-font-size(1.125);\n letter-spacing: -1px;\n font-style: italic;\n\n > :last-child {\n margin-bottom: 0;\n }\n}\n\n\n\n/**\n * Code formatting\n */\npre,\ncode {\n @include relative-font-size(0.9375);\n border: 1px solid $grey-color-light;\n border-radius: 3px;\n background-color: #eef;\n}\n\ncode {\n padding: 1px 5px;\n}\n\npre {\n padding: 8px 12px;\n overflow-x: auto;\n white-space: pre-wrap;\n word-wrap: break-word;\n\n > code {\n border: 0;\n padding-right: 0;\n padding-left: 0;\n }\n}\n\n\n\n/**\n * Wrapper\n */\n.wrapper {\n max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit} * 2));\n max-width: calc(#{$content-width} - (#{$spacing-unit} * 2));\n margin-right: auto;\n margin-left: auto;\n padding-right: $spacing-unit;\n padding-left: $spacing-unit;\n @extend %clearfix;\n\n @include media-query($on-laptop) {\n max-width: -webkit-calc(#{$content-width} - (#{$spacing-unit}));\n max-width: calc(#{$content-width} - (#{$spacing-unit}));\n padding-right: $spacing-unit / 2;\n padding-left: $spacing-unit / 2;\n }\n}\n\n\n\n/**\n * Clearfix\n */\n%clearfix:after {\n content: \"\";\n display: table;\n clear: both;\n}\n\n\n\n/**\n * Icons\n */\n\n.svg-icon {\n width: 16px;\n height: 16px;\n display: inline-block;\n fill: #{$grey-color};\n padding-right: 5px;\n vertical-align: text-top;\n}\n\n.social-media-list {\n li + li {\n padding-top: 5px;\n }\n}\n\n\n\n/**\n * Tables\n */\ntable {\n margin-bottom: $spacing-unit;\n width: 100%;\n text-align: $table-text-align;\n color: lighten($text-color, 18%);\n border-collapse: collapse;\n border: 1px solid $grey-color-light;\n tr {\n &:nth-child(even) {\n background-color: lighten($grey-color-light, 6%);\n }\n }\n th, td {\n padding: ($spacing-unit / 3) ($spacing-unit / 2);\n }\n th {\n background-color: lighten($grey-color-light, 3%);\n border: 1px solid darken($grey-color-light, 4%);\n border-bottom-color: darken($grey-color-light, 12%);\n }\n td {\n border: 1px solid $grey-color-light;\n }\n}\n", "/**\n * Site header\n */\n.site-header {\n border-top: 5px solid $title-color;\n border-bottom: 1px solid $grey-color-light;\n min-height: $spacing-unit * 1.865;\n position: sticky;\n top: 0;\n background-color: $background-color;\n\n .wrapper {\n display: flex;\n align-items: center;\n }\n}\n\n.site-subheader {\n padding: 12px 0;\n border-bottom: 1px solid $grey-color-light;\n background-color: rgb(245, 243, 220);\n .top {\n color: #574621;\n }\n .bottom {\n color: #887541;\n }\n}\n\n.site-title {\n @include relative-font-size(2.125);\n font-weight: 600;\n line-height: $base-line-height * $base-font-size * 2.25;\n letter-spacing: -1px;\n margin-bottom: 0;\n float: left;\n display: flex;\n align-items: center;\n\n &,\n &:visited {\n color: $title-color;\n }\n\n @include media-query($on-palm) {\n @include relative-font-size(1.625);\n }\n\n .site-title-image {\n float: left;\n margin: 8px 18px 8px 0;\n\n @include media-query($on-palm) {\n margin-top: 4px;\n }\n\n img {\n width: 72px;\n height: 72px;\n\n @include media-query($on-palm) {\n width: 54px;\n height: 54px;\n }\n }\n\n }\n .site-title-text {\n white-space: nowrap;\n }\n}\n\n.site-nav {\n margin-left: auto;\n line-height: $base-line-height * $base-font-size * 2.25;\n\n .nav-trigger {\n display: none;\n }\n\n .menu-icon {\n display: none;\n }\n\n .page-link {\n color: $title-color;\n line-height: $base-line-height;\n\n // Gaps between nav items, but not on the last one\n &:not(:last-child) {\n margin-right: 20px;\n }\n }\n\n @include media-query($on-palm) {\n position: absolute;\n top: $spacing-unit / 2;\n right: $spacing-unit / 2;\n background-color: $background-color;\n border: 1px solid $grey-color-light;\n border-radius: 5px;\n text-align: right;\n\n label[for=\"nav-trigger\"] {\n display: block;\n float: right;\n width: 36px;\n height: 36px;\n z-index: 2;\n cursor: pointer;\n }\n\n .menu-icon {\n display: block;\n float: right;\n width: 36px;\n height: 26px;\n line-height: 0;\n padding-top: 10px;\n text-align: center;\n\n > svg {\n fill: $grey-color-dark;\n }\n }\n\n input ~ .trigger {\n clear: both;\n display: none;\n }\n\n input:checked ~ .trigger {\n display: block;\n padding-bottom: 5px;\n }\n\n .page-link {\n display: block;\n padding: 5px 10px;\n\n &:not(:last-child) {\n margin-right: 0;\n }\n margin-left: 20px;\n }\n }\n}\n\n\n\n/**\n * Site footer\n */\n.site-footer {\n border-top: 1px solid $grey-color-light;\n padding: $spacing-unit 0;\n}\n\n.footer-heading {\n @include relative-font-size(1.125);\n margin-bottom: $spacing-unit / 2;\n}\n\n.contact-list,\n.social-media-list {\n list-style: none;\n margin-left: 0;\n}\n\n.footer-col-wrapper {\n @include relative-font-size(0.9375);\n color: $grey-color;\n margin-left: -$spacing-unit / 2;\n @extend %clearfix;\n}\n\n.footer-col {\n float: left;\n margin-bottom: $spacing-unit / 2;\n padding-left: $spacing-unit / 2;\n}\n\n.footer-col-1 {\n width: -webkit-calc(35% - (#{$spacing-unit} / 2));\n width: calc(35% - (#{$spacing-unit} / 2));\n}\n\n.footer-col-2 {\n width: -webkit-calc(20% - (#{$spacing-unit} / 2));\n width: calc(20% - (#{$spacing-unit} / 2));\n}\n\n.footer-col-3 {\n width: -webkit-calc(45% - (#{$spacing-unit} / 2));\n width: calc(45% - (#{$spacing-unit} / 2));\n}\n\n@include media-query($on-laptop) {\n .footer-col-1,\n .footer-col-2 {\n width: -webkit-calc(50% - (#{$spacing-unit} / 2));\n width: calc(50% - (#{$spacing-unit} / 2));\n }\n\n .footer-col-3 {\n width: -webkit-calc(100% - (#{$spacing-unit} / 2));\n width: calc(100% - (#{$spacing-unit} / 2));\n }\n}\n\n@include media-query($on-palm) {\n .footer-col {\n float: none;\n width: -webkit-calc(100% - (#{$spacing-unit} / 2));\n width: calc(100% - (#{$spacing-unit} / 2));\n }\n}\n\n\n\n/**\n * Page content\n */\n.page-content {\n padding: $spacing-unit 0;\n flex: 1;\n}\n\n.page-heading {\n @include relative-font-size(2);\n}\n\n.post-list-heading {\n @include relative-font-size(1.75);\n}\n\n.post-list {\n margin-left: 0;\n list-style: none;\n\n > li {\n margin-bottom: $spacing-unit;\n }\n}\n\n.post-meta {\n font-size: $small-font-size;\n color: $grey-color;\n}\n\n.post-link {\n display: block;\n @include relative-font-size(2.0);\n}\n\n\n\n/**\n * Posts\n */\n.post-header {\n margin-bottom: $spacing-unit;\n}\n\n.post-title {\n @include relative-font-size(2.625);\n letter-spacing: -1px;\n line-height: 1;\n\n @include media-query($on-laptop) {\n @include relative-font-size(2.25);\n }\n}\n\n.post-content {\n margin-bottom: $spacing-unit;\n\n h2 {\n @include relative-font-size(2);\n\n @include media-query($on-laptop) {\n @include relative-font-size(1.75);\n }\n }\n\n h3 {\n @include relative-font-size(1.625);\n\n @include media-query($on-laptop) {\n @include relative-font-size(1.375);\n }\n }\n\n h4 {\n @include relative-font-size(1.25);\n\n @include media-query($on-laptop) {\n @include relative-font-size(1.125);\n }\n }\n}\n", "/**\n * Syntax highlighting styles\n */\n.highlight {\n background: #fff;\n @extend %vertical-rhythm;\n\n .highlighter-rouge & {\n background: #eef;\n }\n\n .c { color: #998; font-style: italic } // Comment\n .err { color: #a61717; background-color: #e3d2d2 } // Error\n .k { font-weight: bold } // Keyword\n .o { font-weight: bold } // Operator\n .cm { color: #998; font-style: italic } // Comment.Multiline\n .cp { color: #999; font-weight: bold } // Comment.Preproc\n .c1 { color: #998; font-style: italic } // Comment.Single\n .cs { color: #999; font-weight: bold; font-style: italic } // Comment.Special\n .gd { color: #000; background-color: #fdd } // Generic.Deleted\n .gd .x { color: #000; background-color: #faa } // Generic.Deleted.Specific\n .ge { font-style: italic } // Generic.Emph\n .gr { color: #a00 } // Generic.Error\n .gh { color: #999 } // Generic.Heading\n .gi { color: #000; background-color: #dfd } // Generic.Inserted\n .gi .x { color: #000; background-color: #afa } // Generic.Inserted.Specific\n .go { color: #888 } // Generic.Output\n .gp { color: #555 } // Generic.Prompt\n .gs { font-weight: bold } // Generic.Strong\n .gu { color: #aaa } // Generic.Subheading\n .gt { color: #a00 } // Generic.Traceback\n .kc { font-weight: bold } // Keyword.Constant\n .kd { font-weight: bold } // Keyword.Declaration\n .kp { font-weight: bold } // Keyword.Pseudo\n .kr { font-weight: bold } // Keyword.Reserved\n .kt { color: #458; font-weight: bold } // Keyword.Type\n .m { color: #099 } // Literal.Number\n .s { color: #d14 } // Literal.String\n .na { color: #008080 } // Name.Attribute\n .nb { color: #0086B3 } // Name.Builtin\n .nc { color: #458; font-weight: bold } // Name.Class\n .no { color: #008080 } // Name.Constant\n .ni { color: #800080 } // Name.Entity\n .ne { color: #900; font-weight: bold } // Name.Exception\n .nf { color: #900; font-weight: bold } // Name.Function\n .nn { color: #555 } // Name.Namespace\n .nt { color: #000080 } // Name.Tag\n .nv { color: #008080 } // Name.Variable\n .ow { font-weight: bold } // Operator.Word\n .w { color: #bbb } // Text.Whitespace\n .mf { color: #099 } // Literal.Number.Float\n .mh { color: #099 } // Literal.Number.Hex\n .mi { color: #099 } // Literal.Number.Integer\n .mo { color: #099 } // Literal.Number.Oct\n .sb { color: #d14 } // Literal.String.Backtick\n .sc { color: #d14 } // Literal.String.Char\n .sd { color: #d14 } // Literal.String.Doc\n .s2 { color: #d14 } // Literal.String.Double\n .se { color: #d14 } // Literal.String.Escape\n .sh { color: #d14 } // Literal.String.Heredoc\n .si { color: #d14 } // Literal.String.Interpol\n .sx { color: #d14 } // Literal.String.Other\n .sr { color: #009926 } // Literal.String.Regex\n .s1 { color: #d14 } // Literal.String.Single\n .ss { color: #990073 } // Literal.String.Symbol\n .bp { color: #999 } // Name.Builtin.Pseudo\n .vc { color: #008080 } // Name.Variable.Class\n .vg { color: #008080 } // Name.Variable.Global\n .vi { color: #008080 } // Name.Variable.Instance\n .il { color: #099 } // Literal.Number.Integer.Long\n}\n" ], "names": [], - "mappings": "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" + "mappings": "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" } \ No newline at end of file diff --git a/blog/_site/assets/ornament-pill.png b/blog/_site/assets/ornament-pill.png new file mode 100644 index 0000000000000000000000000000000000000000..28c247c4fa778f3bc9784c7e1460c0c376016029 GIT binary patch literal 1715 zcmY*a2~<*97=9=fk##|9|)U?tjm{?-hh>@;61= zAOQd{Wzcvixa4%XRcKXb{4WU?c!>FS007>^ z@bwOr!(RO0x@m;Eg!)41qmD1S8DjU1$jCplVoVhz^7gsB1|icd@<(J@7UULIY%NPF z2um2*EZEmJQq@Mzh@GjktA0Oq;c5D$9#sa1c_65O5h@daV*uD3q!v@HyoRa!si`hR z3mm_Jm}`6_j2(rI}ov9gKLoB>Cv@*cr! zM`34e5TTKz&r=XLn^Ks`SWPq`zlde_wto0 z^sZiqi#CFV)Q3uQEwkPXS4H@_dBD1O_vCnt^v{*k#Bqf*<#}QQPvWWn5YEjS9!<_G z_E^_}ymbFZ*|3rszS8AoGU3)*&Af2E{*K3l zeVH5G`D$&Wy+VJBY*oFwE+yS1GO+bV`pu4J&O(XsZE)(TOSg+&{C2Hy3;y!oZ?C*A zXHaVSv__8WsmbxhxngG;8QjlpKKEWfImi?i)l{IuHHX11{&|)&PN1eaq5bMDusnyZ z%VD|2?a+6mzdLv5!_dq3n00CB;9Uk2YssJ}khns?uxO1UFIR+#Q%@!qcLqEz(FA1P zRW8vUKjdfMl8xTS_Rcirm39|;*h-d0=WCD`x4&cQ!=5h#B%}H~O>%bJV1|))Vgqu* zD_>{BL1CKFzeJP;0E)WU{>4~4u0{?F6iz)pwjQSE-#|$?E}C5y)4iJU{-;haX^yS+8`$>?>T`QyVw=tzznvbk@jC9ARdN6OnTV3jl3U&VU8k;u zh2wXub)r&JJHjwKg_T(RcWf+ZOwZX*0|ZquzPyA(@5Bn9O*x~EnVP;qJG_*w)nzX| zk^)y&+Je3OD~lAINh~iI5InO^FQxJh7g(XEJPZO>W>l`<0dwzs=d9ixfTBu*-A0Dh zc2t*$=1ZO-_spt7?8jHY{r@P zrKh6a+?QCi@p5N=-#g-1{+y?%3faakF415KLp`sA(?^#LExXXcO`Ezp+AiO$%Ql>p zhOs!(+PCZgxicav@z;sv2jjK6T=uy*VcpXHu@xr4`&nlx+W7Ro^1V}>@OHv5_jFc} zt$)(Jlix%$vqWx;c$LYG&Q->o16T~OoiUw(hcV@RW$C&UNhBNvEHFC zvIad+rbf4BbqJekyll2021-06-25T18:27:17-04:00https://blog2.newsblur.com/feed.xmlThe NewsBlur BlogNewsBlur is a personal news reader that brings people together to talk about the world. +Jekyll2021-06-27T21:03:53-04:00https://blog2.newsblur.com/feed.xmlThe NewsBlur BlogNewsBlur is a personal news reader that brings people together to talk about the world. A new sound of an old instrument. -Story of a Hacking2021-06-25T00:00:00-04:002021-06-25T00:00:00-04:00https://blog2.newsblur.com/2021/06/25/story-of-a-hacking<p>I’d like to answer a few questions about what happened here.</p> +How a Docker footgun led to a vandal deleting NewsBlur’s MongoDB database2021-06-25T00:00:00-04:002021-06-25T00:00:00-04:00https://blog2.newsblur.com/2021/06/25/story-of-a-hacking<p>I’m in the process of moving everything on NewsBlur over to Docker containers in prep for a <a href="https://beta.newsblur.com">big redesign launching next week</a>. It’s been a great year of maintenance and I’ve enjoyed the fruits of Ansible + Docker for NewsBlur’s 5 database servers (PostgreSQL, MongoDB, Redis, Elasticsearch, and soon ML models). The day was wrapping up and I settled into <a href="https://en.wikipedia.org/wiki/Human_Compatible">a new book on how to tame the machines once they’re smarter than us</a> when I received a strange NewsBlur error on my phone.</p> + +<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"query killed during yield: renamed collection 'newsblur.feed_icons' to 'newsblur.system.drop.1624498448i220t-1.feed_icons'" +</code></pre></div></div> + +<p>There are honestly no sets of words in that error message that I ever want to see again. What is the word <code class="language-plaintext highlighter-rouge">drop</code> doing in that error message? Better go find out.</p> + +<p>Logging into the MongoDB machine to check out what state the DB is in and I come across the following…</p> + +<figure class="highlight"><pre><code class="language-javascript" data-lang="javascript"><span class="nx">nbset</span><span class="p">:</span><span class="nx">PRIMARY</span><span class="o">&gt;</span> <span class="nx">show</span> <span class="nx">dbs</span> +<span class="nx">READ__ME_TO_RECOVER_YOUR_DATA</span> <span class="mf">0.000</span><span class="nx">GB</span> +<span class="nx">newsblur</span> <span class="mf">0.718</span><span class="nx">GB</span> + +<span class="nx">nbset</span><span class="p">:</span><span class="nx">PRIMARY</span><span class="o">&gt;</span> <span class="nx">use</span> <span class="nx">READ__ME_TO_RECOVER_YOUR_DATA</span> +<span class="nx">switched</span> <span class="nx">to</span> <span class="nx">db</span> <span class="nx">READ__ME_TO_RECOVER_YOUR_DATA</span> + +<span class="nx">nbset</span><span class="p">:</span><span class="nx">PRIMARY</span><span class="o">&gt;</span> <span class="nx">db</span><span class="p">.</span><span class="nx">README</span><span class="p">.</span><span class="nx">find</span><span class="p">()</span> +<span class="p">{</span> + <span class="dl">"</span><span class="s2">_id</span><span class="dl">"</span> <span class="p">:</span> <span class="nx">ObjectId</span><span class="p">(</span><span class="dl">"</span><span class="s2">60d3e112ac48d82047aab95d</span><span class="dl">"</span><span class="p">),</span> + <span class="dl">"</span><span class="s2">content</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">All your data is a backed up. You must pay 0.03 BTC to XXXXXXFTHISGUYXXXXXXX 48 hours for recover it. After 48 hours expiration we will leaked and exposed all your data. In case of refusal to pay, we will contact the General Data Protection Regulation, GDPR and notify them that you store user data in an open form and is not safe. Under the rules of the law, you face a heavy fine or arrest and your base dump will be dropped from our server! You can buy bitcoin here, does not take much time to buy https://localbitcoins.com or https://buy.moonpay.io/ After paying write to me in the mail with your DB IP: FTHISGUY@recoverme.one and you will receive a link to download your database dump.</span><span class="dl">"</span> +<span class="p">}</span></code></pre></figure> + +<p>Two thoughts immediately occured:</p> + +<ol> + <li>Thank goodness I have some recently checked backups on hand</li> + <li>No way they have that data without me noticing</li> +</ol> + +<p>Three and a half hours before this happened, I switched the MongoDB cluster over to the new servers. When I did that, I shut down the original primary in order to delete it in a few days when all was well. And thank goodness I did that as it came in handy a few hours later. Knowing this, I realized that the hacker could not have taken all that data in so little time.</p> + +<p>With that in mind, I’d like to answer a few questions about what happened here.</p> <ol> <li>Was any data leaked during the hack? How do you know?</li> @@ -8,50 +39,46 @@ A new sound of an old instrument. <li>What will happen to ensure this doesn’t happen again?</li> </ol> -<p>Let’s start by talking about the importance of your data. As you may know, NewsBlur is open source and enjoys the added protection of having thousands of people looking at the codebase and dozens contributing back to it.</p> +<p>Let’s start by talking about the most important question of all which is what happened to your data.</p> <h3 id="1-was-any-data-leaked-during-the-hack-how-do-you-know">1. Was any data leaked during the hack? How do you know?</h3> <p>I can definitively write that no data was leaked during the hack. I know this because of two different sets of logs showing that the automated attacker only issued deletion commands and did not transfer any data off of the MongoDB server.</p> -<p>This is what the day looks like. This 24 graph of bandwidth shows:</p> +<p>Below is a snapshot of the bandwidth of the db-mongo1 machine over 24 hours:</p> <p><img src="/assets/hack-timeline.png" style="border: 1px solid rgba(0,0,0,0.1);" /></p> -<p>You can imagine the stress I experienced in the forty minutes between 9:35p, when the hack began, and 10:15p, when the fresh backup snapshot was identified and put into gear.</p> +<p>You can imagine the stress I experienced in the forty minutes between 9:35p, when the hack began, and 10:15p, when the fresh backup snapshot was identified and put into gear. Let’s breakdown each moment:</p> <ol> - <li><strong>6:10p</strong>:</li> - <li><strong>9:35p</strong>:</li> - <li><strong>10:15p</strong>:</li> - <li><strong>3:00a</strong>:</li> - <li><strong>4:30a</strong>:</li> + <li><strong>6:10p</strong>: The new db-mongo1 server was put into rotation as the new MongoDB primary server. This machine was the first of the new, soon-to-be private cloud.</li> + <li><strong>9:35p</strong>: Three hours later an automated hacking attempt opened a connection to the db-mongo1 server and immediately dropped the database. Downtime ensued.</li> + <li><strong>10:15p</strong>: Before the former primary server could be placed into rotation, a snapshot of the server was made to ensure the backup would not delete itself upon reconnection. This cost a few hours of downtime, but saved nearly 18 hours of a day’s data by not forcing me to go into the daily backup archive.</li> + <li><strong>3:00a</strong>: Snapshot completes, replication from original primary server to new db-mongo1 begins. What you see in the next hour and a half is what the transfer of the DB looks like in terms of bandwidth.</li> + <li><strong>4:30a</strong>: Replication, which is inbound from the old primary server, completes, and now replication begins outbound on the new secondaries. NewsBlur is now back up.</li> </ol> +<p>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.</p> + +<p>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.</p> + +<p>While the server was being snapshot, I used that time to figure out how the hacker got in.</p> + <h3 id="2-how-did-newsblurs-mongodb-server-get-hacked">2. How did NewsBlur’s MongoDB server get hacked?</h3> -<p>It would make for a much more dramatic read if I was hit through a vulnerability in Docker instead of a footgun.</p> +<p>Turns out the ufw firewall I enabled and diligently kept on a strict allowlist with only my internal servers didn’t work on a new server because of Docker. When I containerized MongoDB, Docker helpfully inserted an allow rule into iptables, opening up MongoDB to the world. So while my firewall was “active”, doing a <code class="language-plaintext highlighter-rouge">sudo iptables -L | grep 27017</code> showed that MongoDB was open the world.</p> -<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nbset:PRIMARY&gt; show dbs -READ__ME_TO_RECOVER_YOUR_DATA 0.000GB -admin 0.000GB -local 16.471GB -newsblur 0.718GB +<p>To be honest, I’m a bit surprised it took over 3 hours from when I flipped the switch to when a hacker/vandal dropped NewsBlur’s MongoDB collections and pretended to ransom about 250GB of data. This is the work of an automated hack and one that I was prepared for. NewsBlur was back online a few hours later once the backups were restored. And the Docker-made hole was immediately patched.</p> -nbset:PRIMARY&gt; use READ__ME_TO_RECOVER_YOUR_DATA -switched to db READ__ME_TO_RECOVER_YOUR_DATA +<p>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. 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.</p> -nbset:PRIMARY&gt; show collections -README -system.profile +<p><img src="/assets/ornament-pill.png" style="display: block; margin: 0 auto;width: 100px;" /></p> -nbset:PRIMARY&gt; db.README.find() -{ "_id" : ObjectId("60d3e112ac48d82047aab95d"), "content" : "All your data is a backed up. You must pay 0.03 BTC to XXXXXXFTHISGUYXXXXXXX 48 hours for recover it. After 48 hours expiration we will leaked and exposed all your data. In case of refusal to pay, we will contact the General Data Protection Regulation, GDPR and notify them that you store user data in an open form and is not safe. Under the rules of the law, you face a heavy fine or arrest and your base dump will be dropped from our server! You can buy bitcoin here, does not take much time to buy https://localbitcoins.com or https://buy.moonpay.io/ After paying write to me in the mail with your DB IP: FTHISGUY@recoverme.one and you will receive a link to download your database dump." } -</code></pre></div></div> +<p>The second reason we know that no data was taken comes from looking through the MongoDB access logs. With these rich and verbose logging sources we can invoke a pretty neat command to find everybody who is not one of the 100 known NewsBlur machines that has accessed MongoDB.</p> -<p>Looking at the MongoDB access logs, we can invoke a pretty neat command to find everybody who is not one of the 100 known NewsBlur machines that has accessed MongoDB.</p> - -<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ cat /var/log/mongodb/mongod.log | egrep -v "159.65.XX.XX|161.89.XX.XX|&lt;&lt; SNIP: A hundred more servers &gt;&gt;" +<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight" style="max-height: 200px;"><code> +$ cat /var/log/mongodb/mongod.log | egrep -v "159.65.XX.XX|161.89.XX.XX|&lt;&lt; SNIP: A hundred more servers &gt;&gt;" 2021-06-24T01:33:45.531+0000 I NETWORK [listener] connection accepted from 171.25.193.78:26003 #63455699 (1189 connections now open) 2021-06-24T01:33:45.635+0000 I NETWORK [conn63455699] received client metadata from 171.25.193.78:26003 conn63455699: { driver: { name: "PyMongo", version: "3.11.4" }, os: { type: "Linux", name: "Linux", architecture: "x86_64", version: "5.4.0-74-generic" }, platform: "CPython 3.8.5.final.0" } @@ -80,15 +107,27 @@ nbset:PRIMARY&gt; db.README.find() 2021-06-24T01:35:18.840+0000 I COMMAND [conn63456637] dropDatabase newsblur - finished </code></pre></div></div> -<p>What you see above…</p> +<p>The above is a lot, but the important bit of information to take from it is that by using a reductive filter, capturing everything that doesn’t match a known IP, I was able to find the two connections that were made a few seconds apart. Both connections from these unknown IPs occured only moments before the database-wide deletion. By following the connection ID, it became easy to see the hacker come into the server only to delete it seconds later.</p> -<p>When I visited the IP address of the <a href="http://185.220.101.6/">two</a> <a href="http://171.25.193.78/">connections</a> above, I found a Tor exit router:</p> +<p>Interestingly, when I visited the IP address of the <a href="http://185.220.101.6/">two</a> <a href="http://171.25.193.78/">connections</a> above, I found a Tor exit router:</p> <p><img src="/assets/hack-tor.png" /></p> +<p>This means that it is virtually impossible to track down who is responsible due to the anonymity preserving quality of Tor exit routers. <a href="https://blog.cloudflare.com/the-trouble-with-tor/">Tor exit nodes have poor reputations</a> due to the havoc they wreak. Site owners are split on whether to block Tor entirely, but some see the value of allowing anonymous traffic to hit their servers. In NewsBlur’s case, because NewsBlur is a home of free speech, allowing users in countries with censored news outlets to bypass restrictions and get access to the world at wide, the continuing risk of supporting anonymous Internet traffic is worth the cost.</p> + <h3 id="3-what-will-happen-to-ensure-this-doesnt-happen-again">3. What will happen to ensure this doesn’t happen again?</h3> -<p>VPC all the way.</p>I’d like to answer a few questions about what happened here.Android app update: premium subscriptions, saved searches, in-app browser, auto-dark mode2020-11-03T07:41:03-05:002020-11-03T07:41:03-05:00https://blog2.newsblur.com/2020/11/03/android-app-update-premium-subscriptions-saved<p>For a point release this one sure is big. The Android app has been upgraded to include a bunch of features found on the web.</p> +<p>Of course, being in support of free speech and providing enhanced ways to access speech comes at a cost. So for NewsBlur to continue serving traffic to all of its worldwide readers, several changes have to be made.</p> + +<p>The first change is the one that, ironically, we were in the process of moving to. A VPC, a virtual private cloud, keeps critical servers only accessible from others servers in a private network. But in moving to a private network, I need to migrate all of the data off of the publicly accessible machines. And this was the first step in that process.</p> + +<p>The second change is to use database user authentication on all of the databases. We had been relying on the firewall to provide protection against threats, but when the firewall silently failed, we were left exposed. Now who’s to say that this would have been caught if the firewall failed but authentication was in place. I suspect the password needs to be long enough to not be brute-forced, because eventually, knowing that an open but password protected DB is there, it could very possibly end up on a list.</p> + +<p>Lastly, a change needs to be made as to which database users have permission to drop the database. Most database users only need read and write privileges. The ideal would be a localhost-only user being allowed to perform potentially destructive actions. If a rogue database user starts deleting stories, it would get noticed a whole lot faster than a database being dropped all at once.</p> + +<p>But each of these is only one piece of a defense strategy. <a href="https://news.ycombinator.com/item?id=27613217">As this well-attended Hacker News thread from the day of the hack made clear</a>, a proper defense strategy can never rely on only one move. And for NewsBlur that move was a allowlist-only firewall that worked perfectly up until it didn’t.</p> + +<p>As usually, the real heros are backups. Regularly well tested backups are a necessary component to any web service. And with that, I’ll prepare to <a href="https://beta.newsblur.com">launch the big NewsBlur redesign later this week</a>.</p>I’m in the process of moving everything on NewsBlur over to Docker containers in prep for a big redesign launching next week. It’s been a great year of maintenance and I’ve enjoyed the fruits of Ansible + Docker for NewsBlur’s 5 database servers (PostgreSQL, MongoDB, Redis, Elasticsearch, and soon ML models). The day was wrapping up and I settled into a new book on how to tame the machines once they’re smarter than us when I received a strange NewsBlur error on my phone.Android app update: premium subscriptions, saved searches, in-app browser, auto-dark mode2020-11-03T07:41:03-05:002020-11-03T07:41:03-05:00https://blog2.newsblur.com/2020/11/03/android-app-update-premium-subscriptions-saved<p>For a point release this one sure is big. The Android app has been upgraded to include a bunch of features found on the web.</p> <p>For one, premium subscriptions can now be purchased in the Android app itself. Reading by folder, saved story tags, searching and saved searches are all premium features that you can unlock directly in the app.</p> diff --git a/blog/_site/index.html b/blog/_site/index.html index 37069593a..977fadc84 100644 --- a/blog/_site/index.html +++ b/blog/_site/index.html @@ -63,11 +63,42 @@
  • - Story of a Hacking + How a Docker footgun led to a vandal deleting NewsBlur's MongoDB database

    -

    I’d like to answer a few questions about what happened here.

    +

    I’m in the process of moving everything on NewsBlur over to Docker containers in prep for a big redesign launching next week. It’s been a great year of maintenance and I’ve enjoyed the fruits of Ansible + Docker for NewsBlur’s 5 database servers (PostgreSQL, MongoDB, Redis, Elasticsearch, and soon ML models). The day was wrapping up and I settled into a new book on how to tame the machines once they’re smarter than us when I received a strange NewsBlur error on my phone.

    + +
    "query killed during yield: renamed collection 'newsblur.feed_icons' to 'newsblur.system.drop.1624498448i220t-1.feed_icons'"
    +
    + +

    There are honestly no sets of words in that error message that I ever want to see again. What is the word drop doing in that error message? Better go find out.

    + +

    Logging into the MongoDB machine to check out what state the DB is in and I come across the following…

    + +
    nbset:PRIMARY> show dbs
    +READ__ME_TO_RECOVER_YOUR_DATA   0.000GB
    +newsblur                        0.718GB
    +
    +nbset:PRIMARY> use READ__ME_TO_RECOVER_YOUR_DATA
    +switched to db READ__ME_TO_RECOVER_YOUR_DATA
    +    
    +nbset:PRIMARY> db.README.find()
    +{ 
    +    "_id" : ObjectId("60d3e112ac48d82047aab95d"), 
    +    "content" : "All your data is a backed up. You must pay 0.03 BTC to XXXXXXFTHISGUYXXXXXXX 48 hours for recover it. After 48 hours expiration we will leaked and exposed all your data. In case of refusal to pay, we will contact the General Data Protection Regulation, GDPR and notify them that you store user data in an open form and is not safe. Under the rules of the law, you face a heavy fine or arrest and your base dump will be dropped from our server! You can buy bitcoin here, does not take much time to buy https://localbitcoins.com or https://buy.moonpay.io/ After paying write to me in the mail with your DB IP: FTHISGUY@recoverme.one and you will receive a link to download your database dump." 
    +}
    + +

    Two thoughts immediately occured:

    + +
      +
    1. Thank goodness I have some recently checked backups on hand
    2. +
    3. No way they have that data without me noticing
    4. +
    + +

    Three and a half hours before this happened, I switched the MongoDB cluster over to the new servers. When I did that, I shut down the original primary in order to delete it in a few days when all was well. And thank goodness I did that as it came in handy a few hours later. Knowing this, I realized that the hacker could not have taken all that data in so little time.

    + +

    With that in mind, I’d like to answer a few questions about what happened here.

    1. Was any data leaked during the hack? How do you know?
    2. @@ -75,50 +106,46 @@
    3. What will happen to ensure this doesn’t happen again?
    -

    Let’s start by talking about the importance of your data. As you may know, NewsBlur is open source and enjoys the added protection of having thousands of people looking at the codebase and dozens contributing back to it.

    +

    Let’s start by talking about the most important question of all which is what happened to your data.

    1. Was any data leaked during the hack? How do you know?

    I can definitively write that no data was leaked during the hack. I know this because of two different sets of logs showing that the automated attacker only issued deletion commands and did not transfer any data off of the MongoDB server.

    -

    This is what the day looks like. This 24 graph of bandwidth shows:

    +

    Below is a snapshot of the bandwidth of the db-mongo1 machine over 24 hours:

    -

    You can imagine the stress I experienced in the forty minutes between 9:35p, when the hack began, and 10:15p, when the fresh backup snapshot was identified and put into gear.

    +

    You can imagine the stress I experienced in the forty minutes between 9:35p, when the hack began, and 10:15p, when the fresh backup snapshot was identified and put into gear. Let’s breakdown each moment:

      -
    1. 6:10p:
    2. -
    3. 9:35p:
    4. -
    5. 10:15p:
    6. -
    7. 3:00a:
    8. -
    9. 4:30a:
    10. +
    11. 6:10p: The new db-mongo1 server was put into rotation as the new MongoDB primary server. This machine was the first of the new, soon-to-be private cloud.
    12. +
    13. 9:35p: Three hours later an automated hacking attempt opened a connection to the db-mongo1 server and immediately dropped the database. Downtime ensued.
    14. +
    15. 10:15p: Before the former primary server could be placed into rotation, a snapshot of the server was made to ensure the backup would not delete itself upon reconnection. This cost a few hours of downtime, but saved nearly 18 hours of a day’s data by not forcing me to go into the daily backup archive.
    16. +
    17. 3:00a: Snapshot completes, replication from original primary server to new db-mongo1 begins. What you see in the next hour and a half is what the transfer of the DB looks like in terms of bandwidth.
    18. +
    19. 4:30a: Replication, which is inbound from the old primary server, completes, and now replication begins outbound on the new secondaries. NewsBlur is now back up.
    +

    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.

    + +

    While the server was being snapshot, I used that time to figure out how the hacker got in.

    +

    2. How did NewsBlur’s MongoDB server get hacked?

    -

    It would make for a much more dramatic read if I was hit through a vulnerability in Docker instead of a footgun.

    +

    Turns out the ufw firewall I enabled and diligently kept on a strict allowlist with only my internal servers didn’t work on a new server because of Docker. When I containerized MongoDB, Docker helpfully inserted an allow rule into iptables, opening up MongoDB to the world. So while my firewall was “active”, doing a sudo iptables -L | grep 27017 showed that MongoDB was open the world.

    -
    nbset:PRIMARY> show dbs
    -READ__ME_TO_RECOVER_YOUR_DATA   0.000GB
    -admin                           0.000GB
    -local                          16.471GB
    -newsblur                        0.718GB
    +

    To be honest, I’m a bit surprised it took over 3 hours from when I flipped the switch to when a hacker/vandal dropped NewsBlur’s MongoDB collections and pretended to ransom about 250GB of data. This is the work of an automated hack and one that I was prepared for. NewsBlur was back online a few hours later once the backups were restored. And the Docker-made hole was immediately patched.

    -nbset:PRIMARY> use READ__ME_TO_RECOVER_YOUR_DATA -switched to db READ__ME_TO_RECOVER_YOUR_DATA +

    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. 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.

    -nbset:PRIMARY> show collections -README -system.profile +

    -nbset:PRIMARY> db.README.find() -{ "_id" : ObjectId("60d3e112ac48d82047aab95d"), "content" : "All your data is a backed up. You must pay 0.03 BTC to XXXXXXFTHISGUYXXXXXXX 48 hours for recover it. After 48 hours expiration we will leaked and exposed all your data. In case of refusal to pay, we will contact the General Data Protection Regulation, GDPR and notify them that you store user data in an open form and is not safe. Under the rules of the law, you face a heavy fine or arrest and your base dump will be dropped from our server! You can buy bitcoin here, does not take much time to buy https://localbitcoins.com or https://buy.moonpay.io/ After paying write to me in the mail with your DB IP: FTHISGUY@recoverme.one and you will receive a link to download your database dump." } -
    +

    The second reason we know that no data was taken comes from looking through the MongoDB access logs. With these rich and verbose logging sources we can invoke a pretty neat command to find everybody who is not one of the 100 known NewsBlur machines that has accessed MongoDB.

    -

    Looking at the MongoDB access logs, we can invoke a pretty neat command to find everybody who is not one of the 100 known NewsBlur machines that has accessed MongoDB.

    - -
    $ cat /var/log/mongodb/mongod.log | egrep -v "159.65.XX.XX|161.89.XX.XX|<< SNIP: A hundred more servers >>"
    +
    
    +$ cat /var/log/mongodb/mongod.log | egrep -v "159.65.XX.XX|161.89.XX.XX|<< SNIP: A hundred more servers >>"
     
     2021-06-24T01:33:45.531+0000 I NETWORK  [listener] connection accepted from 171.25.193.78:26003 #63455699 (1189 connections now open)
     2021-06-24T01:33:45.635+0000 I NETWORK  [conn63455699] received client metadata from 171.25.193.78:26003 conn63455699: { driver: { name: "PyMongo", version: "3.11.4" }, os: { type: "Linux", name: "Linux", architecture: "x86_64", version: "5.4.0-74-generic" }, platform: "CPython 3.8.5.final.0" }
    @@ -147,15 +174,27 @@ nbset:PRIMARY> db.README.find()
     2021-06-24T01:35:18.840+0000 I COMMAND  [conn63456637] dropDatabase newsblur - finished
     
    -

    What you see above…

    +

    The above is a lot, but the important bit of information to take from it is that by using a reductive filter, capturing everything that doesn’t match a known IP, I was able to find the two connections that were made a few seconds apart. Both connections from these unknown IPs occured only moments before the database-wide deletion. By following the connection ID, it became easy to see the hacker come into the server only to delete it seconds later.

    -

    When I visited the IP address of the two connections above, I found a Tor exit router:

    +

    Interestingly, when I visited the IP address of the two connections above, I found a Tor exit router:

    +

    This means that it is virtually impossible to track down who is responsible due to the anonymity preserving quality of Tor exit routers. Tor exit nodes have poor reputations due to the havoc they wreak. Site owners are split on whether to block Tor entirely, but some see the value of allowing anonymous traffic to hit their servers. In NewsBlur’s case, because NewsBlur is a home of free speech, allowing users in countries with censored news outlets to bypass restrictions and get access to the world at wide, the continuing risk of supporting anonymous Internet traffic is worth the cost.

    +

    3. What will happen to ensure this doesn’t happen again?

    -

    VPC all the way.

    +

    Of course, being in support of free speech and providing enhanced ways to access speech comes at a cost. So for NewsBlur to continue serving traffic to all of its worldwide readers, several changes have to be made.

    + +

    The first change is the one that, ironically, we were in the process of moving to. A VPC, a virtual private cloud, keeps critical servers only accessible from others servers in a private network. But in moving to a private network, I need to migrate all of the data off of the publicly accessible machines. And this was the first step in that process.

    + +

    The second change is to use database user authentication on all of the databases. We had been relying on the firewall to provide protection against threats, but when the firewall silently failed, we were left exposed. Now who’s to say that this would have been caught if the firewall failed but authentication was in place. I suspect the password needs to be long enough to not be brute-forced, because eventually, knowing that an open but password protected DB is there, it could very possibly end up on a list.

    + +

    Lastly, a change needs to be made as to which database users have permission to drop the database. Most database users only need read and write privileges. The ideal would be a localhost-only user being allowed to perform potentially destructive actions. If a rogue database user starts deleting stories, it would get noticed a whole lot faster than a database being dropped all at once.

    + +

    But each of these is only one piece of a defense strategy. As this well-attended Hacker News thread from the day of the hack made clear, a proper defense strategy can never rely on only one move. And for NewsBlur that move was a allowlist-only firewall that worked perfectly up until it didn’t.

    + +

    As usually, the real heros are backups. Regularly well tested backups are a necessary component to any web service. And with that, I’ll prepare to launch the big NewsBlur redesign later this week.

  • diff --git a/blog/assets/ornament-pill.png b/blog/assets/ornament-pill.png new file mode 100644 index 0000000000000000000000000000000000000000..28c247c4fa778f3bc9784c7e1460c0c376016029 GIT binary patch literal 1715 zcmY*a2~<*97=9=fk##|9|)U?tjm{?-hh>@;61= zAOQd{Wzcvixa4%XRcKXb{4WU?c!>FS007>^ z@bwOr!(RO0x@m;Eg!)41qmD1S8DjU1$jCplVoVhz^7gsB1|icd@<(J@7UULIY%NPF z2um2*EZEmJQq@Mzh@GjktA0Oq;c5D$9#sa1c_65O5h@daV*uD3q!v@HyoRa!si`hR z3mm_Jm}`6_j2(rI}ov9gKLoB>Cv@*cr! zM`34e5TTKz&r=XLn^Ks`SWPq`zlde_wto0 z^sZiqi#CFV)Q3uQEwkPXS4H@_dBD1O_vCnt^v{*k#Bqf*<#}QQPvWWn5YEjS9!<_G z_E^_}ymbFZ*|3rszS8AoGU3)*&Af2E{*K3l zeVH5G`D$&Wy+VJBY*oFwE+yS1GO+bV`pu4J&O(XsZE)(TOSg+&{C2Hy3;y!oZ?C*A zXHaVSv__8WsmbxhxngG;8QjlpKKEWfImi?i)l{IuHHX11{&|)&PN1eaq5bMDusnyZ z%VD|2?a+6mzdLv5!_dq3n00CB;9Uk2YssJ}khns?uxO1UFIR+#Q%@!qcLqEz(FA1P zRW8vUKjdfMl8xTS_Rcirm39|;*h-d0=WCD`x4&cQ!=5h#B%}H~O>%bJV1|))Vgqu* zD_>{BL1CKFzeJP;0E)WU{>4~4u0{?F6iz)pwjQSE-#|$?E}C5y)4iJU{-;haX^yS+8`$>?>T`QyVw=tzznvbk@jC9ARdN6OnTV3jl3U&VU8k;u zh2wXub)r&JJHjwKg_T(RcWf+ZOwZX*0|ZquzPyA(@5Bn9O*x~EnVP;qJG_*w)nzX| zk^)y&+Je3OD~lAINh~iI5InO^FQxJh7g(XEJPZO>W>l`<0dwzs=d9ixfTBu*-A0Dh zc2t*$=1ZO-_spt7?8jHY{r@P zrKh6a+?QCi@p5N=-#g-1{+y?%3faakF415KLp`sA(?^#LExXXcO`Ezp+AiO$%Ql>p zhOs!(+PCZgxicav@z;sv2jjK6T=uy*VcpXHu@xr4`&nlx+W7Ro^1V}>@OHv5_jFc} zt$)(Jlix%$vqWx;c$LYG&Q->o16T~OoiUw(hcV@RW$C&UNhBNvEHFC zvIad+rbf4Bbq