Home Anemoi: Paranoid Dynamic DNS
Post
Cancel

Anemoi: Paranoid Dynamic DNS

If you know me, you know that I have serious, unresolved trust issues. Not so much with people, but with machines.

As soon as I hand off a VM or physical machine to someone, I assume the data on that machine is no longer fully mine even if I went to great lengths to secure the data at rest. There has to be a plan if the data (API credentials) on that machine are compromised and it shouldn’t involve rotating every credential under the sun.

The DNS provider privilege problem

I have a few VPN endpoints scattered across the southeastern United States. Some of these endpoints are in residential areas, so their IPs tend to change from time to time.

Dynamic DNS (DDNS) is the obvious solution here, but existing DDNS clients require placing an API key on a machine at each site. That API key potentially has control over either my entire account on the DNS service or at the very least, the target DNS zone. If any site’s VPN appliance was compromised, the attacker would have the keys to my public infrastructure castle.

Those dang API keys have too much access

I would take an actual bullet for the Porkbun pig.

This post is absolutely not sponsored by them, I just like the service. They make it easy to buy cheap domain names with almost any TLD on the market. I didn’t dive into their API system until this project, but in doing so I discovered that it leaves a lot to be desired in the way of security.

Setting up a Porkbun API key is fairly straightforward:

  1. Go to https://porkbun.com/account/api.
  2. Name it output of a successful API key creation. shows the "API Key" and "Secret Key", as well as a message that says "You will need both keys when communicating with our API. Store the secret key in a secure location as you will not see it again after this page refreshes."

Notice how there is no mention of limiting an API key to specific capabilities other than a message at the top of the page that instructs you to turn on API access on a per-domain basis: Text box from Porkbun that says 'The Porkbun API provides access to certain backend functionality. To use the API you will need API keys and enough programming knowledge to implement the commands. Please note, you will have to enable the domain for API access from the "Details" drop down panel next to the domain in the "Domain Management" console. Your account email address will also need to be verified before using the API.'

So, let’s turn on the API access for a particular domain:

turning on Porkbun API access for a domain
Application Programming Interface: Enabled

This is cool and all, but there is a slight issue: any API key I create will have access to any domain that has API access turned on.

Ideally, I should have something that can handle this sort of unscoped access, but at an even more granular level. Then I could do DDNS on hosts that I trust much less than a machine in my own house that only I have access to.

For example, let’s say you have a couple of networks that you want VPN access to with the domain names egress.business-name-1.mydomain.com and egress.business-name-2.mydomain.com. In the Porkbun API’s current state, you could place API keys that have access to your entire account on the on-site servers and run a simple dynamic DNS client on each one. But you don’t really want to do that, right?

Yes, you can encrypt the drives, put the server in a room with 12 deadbolts on a steel door, and STIG it until you can only log in from 3:00 to 3:15 PM on the 2nd Wednesday of each month. But that will never be enough. What if we get another OpenSSH RCE vuln that takes days to exploit?

What we really need here is a second layer of security. And this second layer should easily work with other DNS providers besides Porkbun, such as Cloudflare. That’s where Anemoi comes in.

The solution: Anemoi

Anemoi is an extendible least privilege Dynamic DNS server. You can limit the capabilities of your zone-editing on untrusted machines down to the FQDN. Nothing (except curl or wget) needs to be installed on the client machines that will check in with the server to keep their records up-to-date.

If a DDNS client machine is compromised, the most an attacker can do is change the DNS record for the specific FQDN that the machine is responsible for updating. Once the Anemoi server operator knew of the client’s compromise, they could just remove the client from Anemoi’s known client list and the attacker would be prevented from further updating the FQDN record. At no point in this process would the main DNS provider API key need to be rotated.

Depending on the DNS provider used, Anemoi can greatly enhance the security of your API keys.

In Porkbun’s case, using Anemoi for DDNS will prevent someone from not only compromising an entire domain (e.g. *.mydomain.com), but also any other domain that you have through Porkbun that has API Access enabled.

Cloudflare gives you the option to limit your API key to a specific DNS zone: Creating a new API key in Cloudflare. The API key has the permissions 'Zone : DNS : Edit' and under Zone Resources, it just has 'Include : Specific zone : dayt0n.com'. No Client IP Address Filtering option is given and the TTL lasts for a year.

Combined with Anemoi, you can further limit the DDNS client machine to only update A/AAAA records for my-subdomain.mydomain.com instead of any record type on Cloudflare’s *.mydomain.com zone.

Using Anemoi

First, install the latest version of Anemoi from PyPi:

1
pip install anemoi-dns

All of the configuration for the Anemoi server is done in a YAML config file. This config file looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
domains:
  - zone: mydomain.com
    provider: cloudflare
    token: AAAAAAAAAAAAAAAAAAAA

  - zone: my-other-domain.com
    provider: porkbun
    apikey: pk1_AAAAAAAAAAAAAAAAAAA
    secret: sk1_BBBBBBBBBBBBBBBBBBB
backend:
  type: database
  vendor: sqlite
  path: /path/to/test-db.db

In production, you can run the server on port 80 with:

1
gunicorn -b 0.0.0.0:80 'anemoi.server:setup_server("/path/to/config.yml")'

Nginx forwarding and certs are left as an exercise to the reader.

For a development instance, use the anemoi server command:

1
anemoi server -c /path/to/config.yml -p 8080

Anemoi allows you to choose from multiple data storage backends. I initially wrote it using just TinyDB to store client data. But recently I added different data storage options, like PostgreSQL, SQLite, and MySQL.

Adding a client can be done at any time, even while Anemoi is running as a service in the background. Let’s say you want to create credentials for a client to keep the egress.business-name-1.mydomain.com up-to-date. All you have to do is:

1
2
3
4
5
$ anemoi client add -d egress.business-name-1.mydomain.com

- Client info -
uuid: d7a97488-60b5-4afa-9cfa-4d4864aa6e0d
secret: j5Y3bINOM8sz05sZ42Kp3hCs0UXE4ge5WpI6xeNLhW5uOxXPLTVwuFFyvEVmCyiDZgAtrDbK_QLc40vC6urMPA

Then, on the somewhat-untrusted client, create a cron job or systemd service that periodically runs the following command every few minutes:

1
2
curl -X POST https://an.anemoi-server.com/check-in -H 'Content-Type: application/json' \
-d '{"uuid":"d7a97488-60b5-4afa-9cfa-4d4864aa6e0d", "secret":"j5Y3bINOM8sz05sZ42Kp3hCs0UXE4ge5WpI6xeNLhW5uOxXPLTVwuFFyvEVmCyiDZgAtrDbK_QLc40vC6urMPA"}'

If one of your client machines is compromised, simply delete the client for that FQDN with:

1
anemoi client delete -d egress.business-name-1.mydomain.com

Further information on using Anemoi can be found on the Anemoi repo.

Nice-to-haves that we don’t have yet

It would be cool to have API keys stored in something like Vault, but for now they are just in the config file on the server. The basic idea of this project is that the server is the only thing you have 100% control over, so leaving the API keys on there is okay for now.

Extending the web interface to allow an admin to add clients from the Flask server would certainly make the client creation process easier for some folks. Initially, I did not add this because my client setup is part of an Ansible playbook which runs the client setup commands on the server and then passes the UUID/secret to the actual client in one go.

Of course, the more DNS providers Anemoi supports, the better.

Contributing

For the coders out there, adding a new DNS provider is fairly simple. The Anemoi repo has more detailed instructions, but you essentially need to:

  1. Create a new file in the project with the name of your provider, such as cloudflare.
  2. Add a new class called CloudflareProvider(Provider).
    1. Write out the config file parsing to retrieve the API key or other custom fields you need.
    2. Implement the requests for retrieving/updating existing records and creating new records when necessary.

CloudflareProvider and PorkbunProvider already exist at the time of writing this post, so both should give you a good idea of what the code should look like.

If you want to make a different client data storage backend for some reason, that should be of equivalent difficulty.

PRs are welcome!

Shameless plug zone

I’ve been using Anemoi in production for about a year now. It is stable and I feel that it is mature enough to release into the wild, so have fun and make an issue on GitHub if you run into any trouble!

If you like Anemoi, feel free to support its development.

I do freelance / consulting work on the side, so if you or your business is interested in secure-by-design software and infrastructure, feel free to reach out!

This post is licensed under CC BY 4.0 by the author.