How to: Add Protection Against Bad Actors While Protecting Everyone’s Privacy

Mark
Geek Culture
Published in
6 min readNov 13, 2021

--

Undraw hacker mind

Background

If you’ve been following along, then you’ll know that I’ve been building a privacy-focused alternative to Facebook (err… Meta?). It’s more than a simple clone, in fact its a complete departure from everything that company is about.

It’s called Metamorphic and I’m currently getting ready to send out the first few Early Access invite codes (so close!). It’s a bootstrapped endeavor, a real labor of love — I built it so that my family and loved ones could connect and share without disempowering our lives (to roughly summate) — and because of the bootstrap nature of the service, I have to time scaling up the hardware with the amount of revenue coming in (my runway is shorter than the desk I’m typing on), which means that I’m currently running the production server on very under-powered hardware.

The fact that it works at all makes me always want to sing the praises of Elixir, Phoenix, and OTP, but it has got me thinking a lot about how vulnerable the service is before it even gets off the ground.

I kept trying to push it out of my mind because I’ve gone out of my way to not have the server pull people’s IP addresses (even if temporarily), see this post on using the session to rate limit log in attempts, but it kept coming back. I needed to do something.

And thanks to an Elixir Forum post, I learned about a library called plug_attack and realized that I could use that library with some simple-but-strong hashing to increase the protection against “bad actor IP addresses” without ever storing the IP address (even if temporarily).

Note: you’ll most likely want to use remote_ip as well, mentioned in Bonus Step 6.

So, without further ado, let me share with you the basics of how I went about doing this.

Prerequisites

For this post, we’re going to assume the following:

  • You have a Phoenix app up and running (1.6+)
  • You have Elixir 1.12+ and OTP 24+

That’s really all you need to have for this brief tutorial, but I’m assuming you have more of a full blown app with authentication and other business logic, though it won’t come in to play here.

I think you could also go as low as Elixir 1.4 (although you’ll most likely also be on a lower OTP version), but if you go any lower then you’ll need to follow an extra step to ensure that plug_attack is started before your application (and change your hash function most likely).

# Only necessary if using Elixir 1.3
def application do
[applications: [:plug_attack]]
end

Let’s dive in.

Step 1

First, add plug_attack to your dependencies in mix.exs, if using an umbrella app, then add accordingly (in my case, I added to the dependencies list in my web portion since it’s dealing directly with web requests).

# mix.exs
...
defp deps do
[
...
{:plug_attack, "~> 0.4.3"},
...
]
end

Then, run mix deps.get from your terminal (you may want to check the Hex package to see the latest version).

Onto the next.

Step 2

Next, we’re going to add a new environment variable to our application. You’ll need to create a .env file at the root of your application if you don’t already have one. Then, make sure you also add *.env to your .gitignore file or similar (be extra careful not to push your environment variables to your source control).

In your .env file add export PLUG_ATTACK_IP_SECRET=randombytes. You’ll want to generate some random bytes for the secret. You can do something like: :crypto.strong_rand_bytes(12) |> Base.encode64().

With that setup, we can move on.

Step 3

Next, let’s create our plug_attack.ex module. This will be a simple example that you can test immediately in your development environment, you can organize this as appropriate to your application. For me, I created a plugs folder inside my web folder, but it’s up to you!

With your code organization settled, you can start by simply copy/pasting the demo from the “Rate limiting headers” section with some minor adjustments:

Protect against “bad actor IP’s” while protecting everyone’s privacy

Okay, so what’s going on here? Quite a bit actually, but for our sake, the important bits are as follows:

  • @alg :sha512 — This will be used in our hashing function.
  • @ip_secret System.fetch_env!("PLUG_ATTACK_IP_SECRET") — This will be used in our hashing function.
  • hash_ip/2 — This is our hashing function that takes an algorithm, alg, and converted IP address, ip (you could name it differently to be more explicit). Then, we use Erlang’s :crypto.mac/4 function to hash the converted IP address using our provided algorithm, :sha512, and our @ip_secret.
  • convert_ip/1 — This function takes the IP address from the conn.remote_ip and converts it from IO data to a string that can be properly used in our hashing function.
  • rule "throttle by ip" — This “rule” enables us to throttle all requests by their IP addresses so that we can quickly test that it works in our development environment. You can later add a rule like rule "allow local", conn do to always allow requests from the localhost.

So, we’re using the :sha512 algorithm with a salt from our @ip_secret to deterministically hash the incoming IP address and then storing that hash temporarily in our Erlang Term Storage (ETS) table.

We’re then storing the hash for period: 60_000 milliseconds (1 minute) and blocking any requests from that hashed IP address that exceed 10, limit: 10, in the same minute.

Okay, with that we’re almost there. In our application.ex file we’ll need to add our ETS storage to our supervision tree that we listed in our PlugAttack rule above, storage: {PlugAttack.Storage.Ets, YourAppWeb.PlugAttack.Storage}:

# your_app_web/application.exdefmodule YourAppWeb.Application do
@moduledoc false
use Application

def start(_type, _args) do
...
children = [
...
# Start PlugAttack storage
{PlugAttack.Storage.Ets, name: YourAppWeb.PlugAttack.Storage, clean_period: 60_000},
...
]
...
end
...
end

And that’s that. Again, you can read more about your storage options and starting strategies in the PlugAttack docs.

Step 4

Head on over to your router.ex file in the web portion of your application and add your plug in the appropriate pipeline:

# router.ex
defmodule YourAppWeb.Router do
use YourAppWeb, :router
...
alias YourAppWeb.Plugs.PlugAttack
...
pipeline :browser do
plug PlugAttack
...
end
...
end

And that’s it! We alias our PlugAttack module and then drop the plug into our :browser pipeline, piping through it before we do anything else to ensure that the request coming through is allowed.

You can go ahead and test it out by hitting the refresh button a bunch of times until you hit the Forbidden screen.

Now, as you might have already guessed, this can be greatly modified and expanded to fit your desired needs. The important thing is that we now know how to take an incoming IP address, hash it to protect the person’s privacy, store it temporarily (it’s all ephemeral!), and then compare hashes against each other to decide whether or not to allow a request.

Pretty cool, but we’re actually not quite done. Let’s look at another step we can take that utilizes the fail2ban/2 function inspired by the similarly named algorithm.

Bonus Step 5

For this final step, we’re going to replace our rule "throttle by ip" with a rule implementing the PlugAttack.Rule.fail2ban/2 algorithm.

From the docs:

This intends to catch misbehaving clients early and for longer amounts of time. The `key` differentiates different clients, you can use, for example, `conn.remote_ip` for per IP tracking. If the `key` is falsey the action is skipped and next rules are evaluated.

Protect against “bad actor IP’s” while preserving everyone’s privacy with fail2ban

So, we swapped out our rule and set the ban for 90 seconds, ban_for: 90_000. This is so that we can test it out in development with little headache. For production, you’ll likely want to increase the :ban_for period.

Speaking of production…

Bonus Step 6

If your production server is behind Heroku’s router or some other host where the request is being forwarded, then you may need to also add remote_ip to your dependencies list and drop it into your endpoint.ex file right before you call your plug YourAppWeb.Router.

# mix.exs
...
defp deps do
[
...
{:remote_ip, "~> 1.0"}
]
end
...
# your_app_web/endpoint.exdefmodule YourAppWeb.Endpoint.ex do
...
plug RemoteIp
plug YourAppWeb.Router
end

There are other ways to configure and use remote_ip, so I encourage you to read up on the docs if you need to use it.

Also, per Kip Cole’s post on Elixir Forum, you may want to use remote_ip to address issues around IP spoofing.

Update 10/30/2022

To handle invalid Unicode code point errors, one option is to update the convert_ip/1 function to:

defp convert_ip(ip) do
ip
|> Tuple.to_list()
|> Enum.map(fn(i) -> Integer.to_string(i) end)
|> List.to_string()
end

Conclusion

That’s it!

Thanks for following along and I hope this gives you ideas for how you can protect your applications while still respecting everyone’s privacy.

If you’re interested in learning more about Metamorphic, then follow along with our company podcast and sign up for invite codes to our Early Access launch— a transformation to our online life is coming soon!

Always open to improvements and thoughts, thank you!

💚 Mark

--

--

Mark
Geek Culture

Creator @ Metamorphic | Co-founder @ Moss Piglet