Building Data Breach Resistant Applications: Bring Your Own Database
Build applications and protect them against cyber attacks is a tough challenge. What if we could build an application that, even if breached, won't expose customer data? There are solutions for this using end-to-end encryption, which is great, although they're not open source or widespread. But I just thought about another model: bring your own database.
Before diving into cryptographic solutions to build data breach-resistant applications, I began to think of alternatives, and one of them was to let users store their own data. It opens up another way to think about applications in general, with some advantages and drawbacks that will be discussed further in this post.
For the sake of this post, I've built a proof of concept (one file application) using Sinatra, a very simple Ruby framework. Let's call this application "Bring Your Own Database (BYOD) Application" - or BYODA for short.
Here's how it works:
- User signs up and registers their database address and credentials with BYODA.
- BYODA creates tables on the user's Database Management System (DMS).
- BYODA performs any operations and reads/stores data in the user's DMS.
Requirements for this model to work:
- Users still have to sign up for BYODA to store their database address and credentials.
- Users must manage, keep up-to-date, and make their DMS available.
- BYODA must support data segregation across multiple DMSes.
Challenges:
- It's hard, costly, or burdensome for users to set up their DMSes.
- Applications don't have built-in Object-Relational Mappers (ORMs) to handle data read/persistence across multiple DMSes.
- There is an inherent performance penalty when using high-latency databases or handling multiple database connections with multiple threads.
- Users don't know how to protect their data, even if they have it under their control.
- In the case of closed-source code, there is no guarantee that the application itself won't make a copy of records before/after transmitting them to/from the user's DMS.
Solutions:
- Offer a no-brainer service to set up a DMS.
- Develop custom code to handle data read/persistence across multiple DMSes, or wait for a kind soul to do it for each language/framework. It's not trivial to do it correctly because there is a need to manage connection threads as well.
- The (1) solution must set up the DMS NEAR or IN THE SAME data center as the BYODA.
- Opportunity for a new market? Build defenses for users' hosted DMSes. One defense, for example, could map BYODA behavior and deny queries that are considered anomalies. Another would be to allow DMS access only from BYODA.
- Well, when installed on-premises, it's possible to monitor extra requests or an increase in file system size. When it's not on-premises, it's a problem.
Benefits:
- Users now control their data.
- Users can now build defenses as they wish.
- Users can, for example, shut down the database when not using BYODA.
Getting Practical:
Okay, let's get our hands dirty with the proof of concept. It's everything listed in the repository README, but I've cloned it here. These are the steps:
- Requirements
- Ruby 2.3+
- Docker
- cURL
- Set up MySQL Databases
- One for BYODA (this application)
- One for User A
- One for User B
- For each user
- Create User
- Let BYODA create tables on the user-specified MySQL
- Create a Task
- List Tasks
Getting your hands dirty:
Initialize all three MySQL containers:
# Create MySQL containers
docker run --name mysql-poc -d -e MYSQL_ROOT_HOST=172.17.0.1 -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql/mysql-server;
docker run --name mysql-poc2 -d -e MYSQL_ROOT_HOST=172.17.0.1 -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql/mysql-server;
docker run --name mysql-poc3 -d -e MYSQL_ROOT_HOST=172.17.0.1 -e MYSQL_ALLOW_EMPTY_PASSWORD=yes mysql/mysql-server;
# Wait for them to initialize
sleep 90;
# Create Databases
docker exec -it --user=root mysql-poc mysql -u root -D mysql -e "CREATE DATABASE byoda;";
docker exec -it --user=root mysql-poc2 mysql -u root -D mysql -e "CREATE DATABASE byoda2;";
docker exec -it --user=root mysql-poc3 mysql -u root -D mysql -e "CREATE DATABASE byoda3;";
# Retrieve IP Addresses
export MYSQL_POC_IP=$(docker inspect -f '' mysql-poc);
export MYSQL_POC_IP2=$(docker inspect -f '' mysql-poc2);
export MYSQL_POC_IP3=$(docker inspect -f '' mysql-poc3);
Testing the PoC:
# Solve dependencies for 'byoda.rb'
bundle install
# Start Application
# Requires access to the "MYSQL_POC_IP" env variable
ruby byoda.rb &
# Create BYODA "User table" on "mysql-poc" container
# Used to store users' DMS credentials
curl http://localhost:8080/setup --data ""
# {
# "success": true
# }
# Create "User A"
text=''
text="${text}email=a@a.com"
text="${text}&password=letmein"
text="${text}&dms_adapter=mysql2"
text="${text}&dms_host=$MYSQL_POC_IP2"
text="${text}&dms_username=root"
text="${text}&dms_password="
text="${text}&dms_database=byoda2"
curl http://localhost:8080/users --data $text
# {
# "success": true
# }
# Set up "User A" DMS
# This operation creates the "tasks" table
curl http://localhost:8080/users/1/setup --data ""
# {
# "success": true
# }
# Create a task for "User A"
curl http://localhost:8080/users/1/tasks --data "task[title]=MyTitle&task[description]=Something"
# {
# "success": true
# }
# Load tasks for "User A"
curl http://localhost:8080/users/1/tasks
# [
# {
# "id": 1,
# "title": "MyTitle",
# "description": "Something"
# }
# ]
# Create "User B"
text=''
text="${text}email=b@b.com"
text="${text}&password=letmein"
text="${text}&dms_adapter=mysql2"
text="${text}&dms_host=$MYSQL_POC_IP3"
text="${text}&d
ms_username=root"
text="${text}&dms_password="
text="${text}&dms_database=byoda3"
curl http://localhost:8080/users --data $text
# {
# "success": true
# }
# Set up "User B" DMS
# This operation creates the "tasks" table
curl http://localhost:8080/users/2/setup --data ""
# {
# "success": true
# }
# Create a task for "User B"
curl http://localhost:8080/users/2/tasks --data "task[title]=MyTitle2&task[description]=Something2"
# {
# "success": true
# }
# Load tasks for "User B"
curl http://localhost:8080/users/2/tasks
# [
# {
# "id": 1,
# "title": "MyTitle2",
# "description": "Something2"
# }
# ]
# List all users
curl http://localhost:8080/users
# [
# {
# "id": 1,
# "email": "a@a.com",
# "password": "letmein",
# "dms_adapter": "mysql2",
# "dms_host": "172.17.0.3",
# "dms_username": "root",
# "dms_password": "",
# "dms_database": "byoda2"
# },
# {
# "id": 2,
# "email": "b@b.com",
# "password": "letmein",
# "dms_adapter": "mysql2",
# "dms_host": "172.17.0.4",
# "dms_username": "root",
# "dms_password": "",
# "dms_database": "byoda3"
# }
# ]
That's all for today. Thank you.