An email list without emails
In this cookbook, we'll see how you can use Vaulty to maintain an email list without ever storing any emails in plaintext!
We put together a demo app that asks users for their email, and sends an email confirmation via Mailgun.
Our goal is to modify the current app to achieve the following:
- Pseudonymize all emails in our database (replace them with tokens)
- Encrypt and store the emails in isolated storage (managed by Vaulty)
- Prevent application developers from accessing plain emails.
The best part, as you'll soon see, is that this entire change can be performed without changing any code.
Skeptical? Let's fix that.
before Vaulty
CodeThe entire source code of the demo app can be found here. We've kept things simple, so the repository only contains the code necessary for this working demo.
The architecture is pretty much what you'd expect:
- A user submits his email through the browser to our backend
- The backend saves the email in a database
- The backend sends a confirmation email via Mailgun
1. User submits email
We have a simple form for collecting emails, that gets submitted to /subscribe
:
2. Store the email in our database
Our Go backend handles the incoming requst:
3. Send email confirmation
We use the standard Mailgun Go library to send the confirmation email:
Running the app
Let's create a .env
file with the environment variables we need:
These are:
MG_DOMAIN
- Your Mailgun domainMG_API_KEY
- Mailgun API Key
Once you've added your enviroment variables, you can launch the app using docker-compose up
:
Now navigate to http://127.0.0.1:3000 to see the subscription form:
You can access the final screen at http://127.0.0.1:3000/subscribers โ it shows a dump of all of the subscribers stored in our database.
It's time to protect user's data! Let's add Vaulty to the demo app.
Adding Vaulty ๐ช
There are two things we'd need to do to wire Vaulty in:
- Pseudonymize (tokenize and encrypt) the user's email before passing it to the backend
- Detokenize the email before it's sent to Mailgun
This is what our updated architecture diagram would look like once this change is done:
To implement this change, we'll need to update the docker-compose.yml
file to add Vaulty:
Here's what changed:
- We now map port
:3000
to Vaulty (The backend is served at port:8080
instead). - We created a volume that allows our backend to access the root CA certificate generated by Vaulty.
- We run the Vaulty proxy in debug mode. Debug mode should never be used in production because it logs all incoming and outgoing requests.
Also, let's add the following Vaulty configuration parameters into .env file:
Our .env
file now looks like this:
1. Pseudonymize incoming emails
Start by creating a routes.json
that tokenizes the incoming email request (before it hits our backend):
Restart vaulty to load the new routes:
If you try subscribing now, you'll get an error response with an "Invalid email" message. That's because Mailgun can't send emails to tokenized emails (they end with a .local
):
โ Step 1 complete! Vaulty is successfully intercepting our requests, pseudonymizing the email and passing it to our backend.
2. Detokenize outgoing requests
Mailgun will need to receive the email in plain text so we have to detokenize it on the way out.
In order to do so, the Mailgun library has to connect to the Mailgun API via Vaulty (which serves as a Forward Proxy). This can easily be accomplished without any code changes, by setting the http_proxy
environment variable.
We already set it to http://x:${PROXY_PASS}@vaulty:8080
earlier.
(PROXY_PASS
is used for proxy authentication).
1. Add the Vaulty CA Certificate to our backend
Since Vaulty uses HTTPS, we'd also need the Mailgun HTTP client to verify TLS certificates generated by Vaulty. That means we'd need to add Vaulty's CA certificate into the backend container's CA certificates directory:
Now, just restart the backend service:
2. Update routes.json
Now, let's update our routes.json
to detokenize the email address on it's way to the Mailgun API:
This route uses form transformation to detokenize the to
field (which contains the email address).
If you're unsure about the exact request path, just look at the Vaulty logs! Since we enabled
debug mode
, all requests and responses are dumped there.
Finally, just restart Vaulty to reload routes:
Now, when you try to subscribe again:
Email was successfully sent to the recipient ๐
Summary
We've achieved everything we set out to do:
- Pseudonymize all emails in our database (replace them with tokens)
- Encrypt and store the emails in isolated storage (managed by Vaulty)
- Prevent application developers from accessing plain emails.
And all this with zero changes to our application code!.
Hooray, Vaulty!