happyDomain holds something valuable: the API credentials that let you edit your DNS zones at your registrar or DNS provider. Were we really handling them correctly? Was there, somewhere, still a path allowing someone to use another person’s keys? We have been asking ourselves that for years, with the eyes of the people who wrote the code.

Thanks to our NGI Zero Core grant from NLnet, Radically Open Security has audited happyDomain during July 2026.

What did they find? Six issues, which is a rather positive outcome. Needless to say, we fixed all of them in the versions released alongside this article, 0.7.2 and 0.8.0-rc2.
We encourage you to update your instances without delay.

Why bring in outsiders?

We ran a security review of our own a few months ago, with the help of Claude, covering authentication, session management and the OIDC flow (/release-0.6.0/). It proved useful: 14 issues were fixed as a result.

But we had written the questions ourselves, out of the same mental model that produced the code. A review only looks where you ask it to look, and our blind spots were exactly where we had not thought to send anyone.

An outside team does not inherit that model: they read the code as an attacker would, ask the questions we stopped asking long ago, then actually try the attack against a running instance. That does not make it exhaustive. A week is not enough to go through all of happyDomain, and we are the ones who pointed out the areas deserving the most attention: the outside view remained partly guided by our own. Entire parts of the code were never opened.

So what follows says what was looked at and what was found there, not that the rest is sound.

Who Radically Open Security are

Radically Open Security is an Amsterdam-based computer security consultancy, structured as a not-for-profit.

Their working method matches the name. The audit ran in the open with us: each finding arrived as soon as it was confirmed, in a shared tracker, with the exact requests needed to reproduce it. No six-week silence followed by a PDF. We were able to start fixing from the first days, without waiting for the report, and to discuss each fix with them while they carried on testing.

The full report is public, available at this address:

happyDomain Code Audit Report, v1.0

You will find in it the complete technical descriptions, the proofs of concept and the screenshots of our own interface being abused.

What was in scope

The audit covered happyDomain itself: the Go REST API, the web interface and the admin interface, reviewed at the latest commit of the master branch at the time (a1de404f), with findings confirmed on a live instance backed by a PowerDNS setup.

The question asked is the one that matters for a multi-tenant service holding API credentials: what stands between a registered but malicious user and another user’s credentials, or another user’s zone? The worst case being takeover of the DNS for a domain that is not theirs.

The good news first

That core question came back unanswered for the attacker.

Cross-user isolation holds. Radically Open Security tried to reach another user’s credentials through the single-provider read, through the provider list and through export.json, then built a full cross-access matrix on a running instance, extended to domains and zones. No leaks.

Cross-user zone writes hold too. They tried direct apply and deploy, provider retargeting, forged correction-record injection, as well as the deferred variant where one user writes into another’s pending zone hoping the owner will publish it. The boundary held on every path.

Session fixation holds: a pre-set session ID does not survive a login, because the record is deleted and a fresh random ID is generated. Finally, no state-changing route is reachable without being logged in. The usual bypasses were tried without success: changing the request’s HTTP method, writing the URL differently (with or without a trailing slash, playing with case or encoding) to slip past the authentication check, or sending the same parameter several times hoping the check reads one value and the processing another.

The six findings

An empty recovery key reset any account (High)

The problem. Password recovery compared the key sent by email against the one supplied by the caller. When no recovery was in progress, the stored key was empty: submitting an empty key passed the check. Any account that had changed its password at least once was in that state permanently.

The impact. A single unauthenticated request was enough to take control of an account: its domains, its zones and its provider API keys.

What was done. The request is rejected if no recovery is in progress or no key was submitted (8db85c61), and the exact attack scenario is covered by an automated test so it cannot come back. The same review hardened two neighbouring points: key comparisons can no longer be guessed from response timing, and those unauthenticated endpoints now sit behind a limit on the number of attempts.

The BIND provider gave access to the server’s files (Elevated)

The problem. An optional feature (-with-bind-provider) allows reading and writing zone files on the server. The location of those files was chosen by the user, without any restriction.

The impact. On an instance where this option was enabled, a registered user could read or write arbitrary files on the server. The documentation already advised against this option on a shared instance, but a recommendation prevents nothing.

What was done. The option now takes the list of allowed directories, and any access outside them is refused, including for providers already stored (b671165e).

Anti-abuse limits could be bypassed (Moderate)

The problem. happyDomain identified its visitors from a piece of information the visitor supplied themselves, and could therefore change at will.

The impact. Protections against repeated login attempts became ineffective, making large-scale password spraying across many accounts practical.

What was done. Identification now relies on the real connecting address. An administrator who puts happyDomain behind a proxy declares it explicitly (-trusted-proxy) (da31f536). This work also revealed, on our side, that counting per IPv6 address was too fine-grained to be effective: it is now computed per subscriber prefix (8c1e5f13).

happyDomain would connect anywhere you pointed it (Moderate)

The problem. Several features let the user name an address that happyDomain then connects to: self-hosted providers, the resolver tool, certificate probing, webhooks… No check was made on the destination.

The impact. It was possible to have happyDomain probe the internal network it runs on, and, worse, to make it send a provider API key to an address of the attacker’s choosing.

What was done. A centralised check now validates every destination before connecting and refuses internal networks by default, including after a redirect. Provider destinations are validated as soon as they are registered, before any key travels (3b054501). For legitimate cases, such as a DNS server hosted next to happyDomain, the administrator explicitly allows the addresses concerned.

The admin interface had no password (Moderate)

The problem. The administration interface introduced in 0.6.0 relied solely on being reachable locally only. Again, this was documented, but nothing enforced it.

The impact. Anyone reaching that interface could download a full copy of the database, provider API keys in cleartext included, or change the password of any account.

What was done. The administration interface is protected by a password (c1adebbe), and happyDomain refuses to start if that interface is exposed on the network without a configured password.

This is the thing to check first if you self-host happyDomain and had opened that port for convenience.

A password change left other sessions open (Low)

The problem. Changing or resetting your password did not close the account’s other sessions, which stayed valid for up to 15 days.

The impact. A stolen session survived the very password reset meant to take back control.

What was done. A password change closes the sessions opened through the web interface (bd0236dd). Tokens created for automated use through the API are kept on purpose: they are not obtained with the password and are revoked separately.

What comes next: encrypting credentials at rest

One item was deliberately left out of this round, by agreement with the auditors, and it is the one we consider most important.

happyDomain stores DNS provider API keys in cleartext in its database. That is the durable exposure behind several of the findings above: a leaked key keeps controlling your DNS at the provider, independently of happyDomain, until you rotate it. Patching the leak does not undo the leak.

NLnet has agreed to fund this work as an additional step, and it is starting now. The goal: that a copy of the database alone is no longer enough to read your keys, that they no longer come back out through the API or backups, and that the encryption key stays managed separately.

Where we stand

None of the six findings is exotic. They are the mistakes that happen when a project grows. And that is precisely the point of having someone else read it.

Here is what we commit to, without ceremony:

  • The full report is public.
  • The findings are fixed and shipped today, not tracked. All six are in 0.7.2 and 0.8.0-rc2.
  • Report anything you find, to security@happydomain.org or through the process described in our SECURITY.md.

Thanks to NLnet for funding this work, and to Harry for doing it well.