Diagnostic Security / Takeover Risk
WHOIS or RDAP change signals a possible domain hijack
A domain does not usually get stolen in one dramatic move. It gets stolen quietly first: an attacker gets into the registrar account or the registrant's email, then edits a few fields in the domain's RDAP record. The transfer lock disappears. The nameservers change. Nobody is watching, so nobody notices until the domain, or its traffic, is already gone. Here is what that quiet change looks like and how to catch it while there is still time to act.
A domain hijack almost always starts with the attacker changing something in the RDAP record before the actual takeover: the transfer lock is removed, the nameservers point somewhere new, or the registrant contact changes. These edits usually happen hours to days ahead of the transfer or DNS redirect. If nothing is watching RDAP for these changes, nobody notices in time. Poll RDAP on a schedule, compare each result to a known-good snapshot, and alert the moment a lock, a nameserver, or a registrant field changes without a matching change you made. The repair itself, re-locking the domain, resetting registrar credentials, or stopping a pending transfer, happens at the registrar, not through a DNS API. Full code, tests, and the detection logic are below.
The problem in plain words
Every domain has a public record called RDAP, the modern replacement for WHOIS. It lists a handful of fields that matter a lot more than they look: the domain's status codes, its nameservers, who the registrant is, and a log of recent events. Normally these barely ever change. A domain you set up once and left alone should show the same status, the same nameservers, and the same registrant for years.
An attacker who gets into your registrar account, or into the email address tied to it, does not usually flip a switch and take the domain in one step. They edit the RDAP-visible fields first: they remove the lock that blocks transfers, they swap the nameservers to servers they control, or they change the registrant contact. Each of those is a small, quiet edit. If nobody is comparing today's RDAP record to yesterday's, that edit just sits there, unnoticed, while the actual transfer or DNS redirect gets set up behind it.
Why it happens
RDAP, defined in RFC 9082 and RFC 9083, replaced the old free-text WHOIS protocol with a structured JSON format, but it did not change the underlying reality: these fields are the registry's source of truth for who controls a domain, and they can be changed by anyone with registrar-account access. A few reasons the change slips past unnoticed:
- The registrant's email account gets compromised first, so the attacker can approve registrar password resets and confirmation emails without the owner seeing them.
- Nobody set up any kind of RDAP or WHOIS monitoring in the first place, so there is no baseline to compare against and no alert to fire.
- The transfer lock (the EPP status code
clientTransferProhibited) gets removed as one of the first steps, since it is the one thing standing between the attacker and an actual transfer request. - The domain is old and "boring," the kind nobody logs into for years, which makes it both a common hijack target and the least likely to have anyone watching it.
- ICANN's Transfer Policy requires a 60 day lock after most registrant or registrar changes specifically to create a detection window, but that window only helps if someone is actually looking during it.
The result is the same every time: the RDAP record changed first, quietly, and the loss of the domain came later. A monitor's whole job is to close the gap between those two moments.
RDAP is a public, free, structured record that already contains every early warning sign of a hijack: status codes, nameservers, registrant handle, and a log of recent events. You do not need special access to read it. You only need to check it regularly and compare it to what it looked like yesterday. The moment a lock disappears or a nameserver you did not set appears, that is the signal, often before anything else has visibly broken.
The fix, as a flow
This is not a record you push through a DNS API. It is a watch loop: fetch RDAP, normalize the fields that matter, compare against the last known-good snapshot, and alert loudly the instant something changed that you did not do yourself. The actual repair, when an alert fires, happens by hand at the registrar.
How to fix it
Pull the current RDAP record
Use rdap.org, ICANN's public RDAP bootstrap redirector defined by RFC 9082 and RFC 9083, which forwards your query to the domain's authoritative registry RDAP server. Pull the status codes, nameservers, entity roles, and events in one call.
curl -s -L "https://rdap.org/domain/example.com" \
| jq '{status, nameservers: [.nameservers[].ldhName], entities: [.entities[].roles], events}'
Cross-check with legacy WHOIS and a live NS query
RDAP is the modern, structured view, but it is worth confirming against the classic WHOIS fields and against what the registry itself is delegating to. A stale local cache can hide a change for a while, so query the registry's own servers directly.
whois example.com
dig +short NS example.com @a.gtld-servers.net
dig +short NS example.com
Know what a bad result looks like
You are looking for any one of these on a domain that was previously quiet: the status array losing clientTransferProhibited or clientUpdateProhibited with no change ticket from you; nameservers[].ldhName pointing at servers you did not configure; the registrant or registrar entity changing; or an events entry showing a "last changed" or "transfer" timestamp you cannot account for. Any single one of these is the hijack signal, even if everything else still looks normal.
Build a poller that runs on a schedule
Fetch RDAP for the domain every 15 to 60 minutes, either through rdap.org or a direct registry RDAP base URL pulled from the IANA RDAP bootstrap file, and store the normalized fields (status list, nameservers, registrant and registrar entity handles, and the latest "last changed" event) keyed by domain.
Diff every poll against the last known-good snapshot
Each time the poller runs, compare the new snapshot field by field against the last one that was confirmed good. If a lock disappeared, or the nameservers or registrant handle differ, that is a hijack signal, not noise.
Fire an alert immediately, then act at the registrar
Send the diff to email, Slack, or PagerDuty the moment it is detected, with the old and new values side by side. The response itself, re-locking the domain, resetting registrar-account credentials and MFA, contacting the registrar's abuse team, or invoking ICANN's Transfer Emergency Action Contact (TEAC) to halt an in-flight transfer, has to be done in the registrar's control panel. No Cloudflare-style zone API can re-lock a domain or undo a registrar-level ownership change.
How to check it worked
After you fix things at the registrar, re-poll RDAP and confirm the record matches the trusted baseline again.
# Locks should be back
curl -s -L "https://rdap.org/domain/example.com" | jq '.status'
# Nameservers should only list your own
curl -s -L "https://rdap.org/domain/example.com" | jq '[.nameservers[].ldhName]'
# Delegation should trace to your legitimate authoritative servers
dig +trace example.com
# Registrar-of-record and expiry should be unchanged
whois example.com
A good result looks like this: status shows clientTransferProhibited (or your registrar's equivalent lock codes) restored, the nameserver list contains only your own servers, dig +trace resolves cleanly to your legitimate authoritative servers, and there is no pending transfer event in events[]. Your monitor's diff should show zero deltas across several consecutive polls before you consider the domain safe again.
The full code
This problem is detect-only. A script can fully automate the RDAP polling and the diff, but the actual repair, re-locking a domain, changing the registrant, or halting a transfer, is a registrar and EPP-level action outside the reach of any DNS-zone API. The scripts below poll RDAP, normalize the fields, and diff them, and leave the repair as a manual registrar step.
View this code on GitHub Full runnable folder with tests in the dns-fixes repo.
"""Poll RDAP for a domain and diff it against a stored known-good snapshot to
catch the early signal of a hijack: a lost transfer lock, a nameserver you did
not configure, or a registrant/registrar change. Detect only: re-locking the
domain, resetting registrar credentials, or halting a transfer are registrar
and EPP-level actions this script cannot perform through the Cloudflare DNS
API, so it never writes anything, it only reports what it finds.
"""
import os
import json
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("check_rdap_hijack_signal")
DNS_DOMAIN = os.environ.get("DNS_DOMAIN", "example.com")
CLOUDFLARE_API_TOKEN = os.environ.get("CLOUDFLARE_API_TOKEN", "")
CLOUDFLARE_ZONE_ID = os.environ.get("CLOUDFLARE_ZONE_ID", "")
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"
SNAPSHOT_PATH = os.environ.get("SNAPSHOT_PATH", "rdap_snapshot.json")
def diff_rdap_snapshot(baseline, current):
"""Pure function, no I/O. baseline/current are shaped as:
{"status": list[str], "nameservers": list[str], "registrant_handle": str,
"registrar_handle": str, "last_changed": str}
Returns a list of human-readable alert strings. An empty list means no
hijack signal was detected.
"""
alerts = []
baseline_status = set(baseline.get("status", []))
current_status = set(current.get("status", []))
lost_locks = baseline_status - current_status
for lock in sorted(lost_locks):
alerts.append(f"status lost {lock}")
baseline_ns = baseline.get("nameservers", [])
current_ns = current.get("nameservers", [])
if set(baseline_ns) != set(current_ns):
alerts.append(f"nameservers changed: {baseline_ns} -> {current_ns}")
if baseline.get("registrant_handle") != current.get("registrant_handle"):
alerts.append("registrant_handle changed")
if baseline.get("registrar_handle") != current.get("registrar_handle"):
alerts.append("registrar_handle changed")
if baseline.get("last_changed") != current.get("last_changed"):
alerts.append(
f"last_changed event moved: {baseline.get('last_changed')} -> {current.get('last_changed')}"
)
return alerts
def normalize_rdap(data):
"""Turn a raw RDAP JSON document into the flat shape diff_rdap_snapshot expects."""
status = list(data.get("status", []))
nameservers = sorted(
ns["ldhName"] for ns in data.get("nameservers", []) if ns.get("ldhName")
)
registrant_handle = None
registrar_handle = None
for entity in data.get("entities", []):
roles = entity.get("roles", [])
if "registrant" in roles and registrant_handle is None:
registrant_handle = entity.get("handle")
if "registrar" in roles and registrar_handle is None:
registrar_handle = entity.get("handle")
last_changed = None
for event in data.get("events", []):
if event.get("eventAction") in ("last changed", "transfer"):
last_changed = event.get("eventDate")
return {
"status": sorted(status),
"nameservers": nameservers,
"registrant_handle": registrant_handle,
"registrar_handle": registrar_handle,
"last_changed": last_changed,
}
def fetch_rdap(domain):
"""RDAP over HTTP via ICANN's public bootstrap redirector (RFC 9082 / 9083)."""
import requests
r = requests.get(f"https://rdap.org/domain/{domain}", timeout=15, allow_redirects=True)
r.raise_for_status()
return r.json()
def load_baseline(path):
if not os.path.exists(path):
return None
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def save_baseline(path, snapshot):
with open(path, "w", encoding="utf-8") as f:
json.dump(snapshot, f, indent=2, sort_keys=True)
def run():
log.info("Polling RDAP for %s (DRY_RUN=%s)", DNS_DOMAIN, DRY_RUN)
raw = fetch_rdap(DNS_DOMAIN)
current = normalize_rdap(raw)
baseline = load_baseline(SNAPSHOT_PATH)
if baseline is None:
log.info("No baseline snapshot yet. Saving current RDAP state as the trusted baseline.")
save_baseline(SNAPSHOT_PATH, current)
return
alerts = diff_rdap_snapshot(baseline, current)
if not alerts:
log.info("OK: RDAP record matches the trusted baseline. No hijack signal.")
return
log.warning("HIJACK SIGNAL for %s:", DNS_DOMAIN)
for alert in alerts:
log.warning(" - %s", alert)
log.warning(
"This is a registrar-side action, not something the Cloudflare DNS API can fix. "
"Re-lock the domain, reset registrar credentials and MFA, or contact the registrar's "
"abuse team / ICANN's Transfer Emergency Action Contact (TEAC)."
)
# Note for future readers: CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID are
# accepted for consistency with the other fixes in this repo, and would be
# used to manage records inside a zone already delegated to Cloudflare via
# https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records, but
# that endpoint has no way to re-lock a domain or change registrar-level
# ownership fields, so this script never calls it.
if not DRY_RUN:
log.info("DRY_RUN is false, but this check never writes. Fix the registrar by hand.")
if __name__ == "__main__":
run()
/**
* Poll RDAP for a domain and diff it against a stored known-good snapshot to
* catch the early signal of a hijack: a lost transfer lock, a nameserver you
* did not configure, or a registrant/registrar change. Detect only:
* re-locking the domain, resetting registrar credentials, or halting a
* transfer are registrar and EPP-level actions this script cannot perform
* through the Cloudflare DNS API, so it never writes anything, it only
* reports what it finds.
*/
import { pathToFileURL } from "node:url";
const DNS_DOMAIN = process.env.DNS_DOMAIN || "example.com";
const CLOUDFLARE_API_TOKEN = process.env.CLOUDFLARE_API_TOKEN || "";
const CLOUDFLARE_ZONE_ID = process.env.CLOUDFLARE_ZONE_ID || "";
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";
const SNAPSHOT_PATH = process.env.SNAPSHOT_PATH || "rdap_snapshot.json";
/**
* Pure function, no I/O. baseline/current are shaped as:
* { status: string[], nameservers: string[], registrant_handle: string,
* registrar_handle: string, last_changed: string }
* Returns a list of human-readable alert strings. An empty array means no
* hijack signal was detected.
*/
export function diffRdapSnapshot(baseline, current) {
const alerts = [];
const baselineStatus = new Set(baseline.status || []);
const currentStatus = new Set(current.status || []);
const lostLocks = [...baselineStatus].filter((s) => !currentStatus.has(s)).sort();
for (const lock of lostLocks) {
alerts.push(`status lost ${lock}`);
}
const baselineNs = baseline.nameservers || [];
const currentNs = current.nameservers || [];
const nsBaselineSet = new Set(baselineNs);
const nsCurrentSet = new Set(currentNs);
const nsEqual =
nsBaselineSet.size === nsCurrentSet.size &&
[...nsBaselineSet].every((ns) => nsCurrentSet.has(ns));
if (!nsEqual) {
alerts.push(`nameservers changed: ${JSON.stringify(baselineNs)} -> ${JSON.stringify(currentNs)}`);
}
if (baseline.registrant_handle !== current.registrant_handle) {
alerts.push("registrant_handle changed");
}
if (baseline.registrar_handle !== current.registrar_handle) {
alerts.push("registrar_handle changed");
}
if (baseline.last_changed !== current.last_changed) {
alerts.push(`last_changed event moved: ${baseline.last_changed} -> ${current.last_changed}`);
}
return alerts;
}
/** Turn a raw RDAP JSON document into the flat shape diffRdapSnapshot expects. */
export function normalizeRdap(data) {
const status = [...(data.status || [])].sort();
const nameservers = (data.nameservers || [])
.map((ns) => ns.ldhName)
.filter(Boolean)
.sort();
let registrantHandle = null;
let registrarHandle = null;
for (const entity of data.entities || []) {
const roles = entity.roles || [];
if (roles.includes("registrant") && registrantHandle === null) {
registrantHandle = entity.handle || null;
}
if (roles.includes("registrar") && registrarHandle === null) {
registrarHandle = entity.handle || null;
}
}
let lastChanged = null;
for (const event of data.events || []) {
if (event.eventAction === "last changed" || event.eventAction === "transfer") {
lastChanged = event.eventDate || null;
}
}
return {
status,
nameservers,
registrant_handle: registrantHandle,
registrar_handle: registrarHandle,
last_changed: lastChanged,
};
}
async function fetchRdap(domain) {
// RDAP over HTTP via ICANN's public bootstrap redirector (RFC 9082 / 9083).
const res = await fetch(`https://rdap.org/domain/${domain}`, { redirect: "follow" });
if (!res.ok) throw new Error(`RDAP lookup failed: ${res.status}`);
return res.json();
}
async function loadBaseline(path) {
const fs = await import("node:fs/promises");
try {
const text = await fs.readFile(path, "utf-8");
return JSON.parse(text);
} catch (err) {
if (err.code === "ENOENT") return null;
throw err;
}
}
async function saveBaseline(path, snapshot) {
const fs = await import("node:fs/promises");
await fs.writeFile(path, JSON.stringify(snapshot, null, 2), "utf-8");
}
export async function run() {
console.log(`Polling RDAP for ${DNS_DOMAIN} (DRY_RUN=${DRY_RUN})`);
const raw = await fetchRdap(DNS_DOMAIN);
const current = normalizeRdap(raw);
const baseline = await loadBaseline(SNAPSHOT_PATH);
if (baseline === null) {
console.log("No baseline snapshot yet. Saving current RDAP state as the trusted baseline.");
await saveBaseline(SNAPSHOT_PATH, current);
return;
}
const alerts = diffRdapSnapshot(baseline, current);
if (alerts.length === 0) {
console.log("OK: RDAP record matches the trusted baseline. No hijack signal.");
return;
}
console.warn(`HIJACK SIGNAL for ${DNS_DOMAIN}:`);
for (const alert of alerts) {
console.warn(` - ${alert}`);
}
console.warn(
"This is a registrar-side action, not something the Cloudflare DNS API can fix. " +
"Re-lock the domain, reset registrar credentials and MFA, or contact the registrar's " +
"abuse team / ICANN's Transfer Emergency Action Contact (TEAC)."
);
// Note for future readers: CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID are
// accepted for consistency with the other fixes in this repo, and would be
// used to manage records inside a zone already delegated to Cloudflare via
// https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records, but
// that endpoint has no way to re-lock a domain or change registrar-level
// ownership fields, so this script never calls it.
if (!DRY_RUN) {
console.log("DRY_RUN is false, but this check never writes. Fix the registrar by hand.");
}
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
run().catch((e) => { console.error(e); process.exit(1); });
}
Add a test
The part worth testing is the diff itself, since it decides whether an alert fires. It takes two plain snapshot objects in and returns a list of alert strings, so the test needs no network and no real domain.
from check_rdap_hijack_signal import diff_rdap_snapshot
def snapshot(**over):
base = {
"status": ["clientTransferProhibited", "clientUpdateProhibited"],
"nameservers": ["ns1.cf.com", "ns2.cf.com"],
"registrant_handle": "REG-1",
"registrar_handle": "REGR-1",
"last_changed": "2026-01-01T00:00:00Z",
}
base.update(over)
return base
def test_no_alerts_when_nothing_changed():
baseline = snapshot()
current = snapshot()
assert diff_rdap_snapshot(baseline, current) == []
def test_alerts_when_transfer_lock_lost():
baseline = snapshot()
current = snapshot(status=["clientUpdateProhibited"])
alerts = diff_rdap_snapshot(baseline, current)
assert "status lost clientTransferProhibited" in alerts
def test_alerts_when_nameservers_change():
baseline = snapshot()
current = snapshot(nameservers=["ns1.evil.net"])
alerts = diff_rdap_snapshot(baseline, current)
assert any("nameservers changed" in a for a in alerts)
def test_alerts_when_registrant_handle_changes():
baseline = snapshot()
current = snapshot(registrant_handle="REG-2")
assert "registrant_handle changed" in diff_rdap_snapshot(baseline, current)
def test_alerts_when_registrar_handle_changes():
baseline = snapshot()
current = snapshot(registrar_handle="REGR-2")
assert "registrar_handle changed" in diff_rdap_snapshot(baseline, current)
def test_alerts_when_last_changed_moves():
baseline = snapshot()
current = snapshot(last_changed="2026-06-01T00:00:00Z")
alerts = diff_rdap_snapshot(baseline, current)
assert any("last_changed event moved" in a for a in alerts)
def test_nameserver_order_does_not_trigger_a_false_alert():
baseline = snapshot(nameservers=["ns1.cf.com", "ns2.cf.com"])
current = snapshot(nameservers=["ns2.cf.com", "ns1.cf.com"])
assert diff_rdap_snapshot(baseline, current) == []
def test_multiple_changes_all_reported():
baseline = snapshot()
current = snapshot(status=[], nameservers=["ns1.evil.net"], registrant_handle="REG-2")
alerts = diff_rdap_snapshot(baseline, current)
assert len(alerts) >= 3
import { test } from "node:test";
import assert from "node:assert/strict";
import { diffRdapSnapshot } from "./check-rdap-hijack-signal.js";
const snapshot = (over = {}) => ({
status: ["clientTransferProhibited", "clientUpdateProhibited"],
nameservers: ["ns1.cf.com", "ns2.cf.com"],
registrant_handle: "REG-1",
registrar_handle: "REGR-1",
last_changed: "2026-01-01T00:00:00Z",
...over,
});
test("no alerts when nothing changed", () => {
assert.deepEqual(diffRdapSnapshot(snapshot(), snapshot()), []);
});
test("alerts when transfer lock lost", () => {
const alerts = diffRdapSnapshot(snapshot(), snapshot({ status: ["clientUpdateProhibited"] }));
assert.ok(alerts.includes("status lost clientTransferProhibited"));
});
test("alerts when nameservers change", () => {
const alerts = diffRdapSnapshot(snapshot(), snapshot({ nameservers: ["ns1.evil.net"] }));
assert.ok(alerts.some((a) => a.includes("nameservers changed")));
});
test("alerts when registrant handle changes", () => {
const alerts = diffRdapSnapshot(snapshot(), snapshot({ registrant_handle: "REG-2" }));
assert.ok(alerts.includes("registrant_handle changed"));
});
test("alerts when registrar handle changes", () => {
const alerts = diffRdapSnapshot(snapshot(), snapshot({ registrar_handle: "REGR-2" }));
assert.ok(alerts.includes("registrar_handle changed"));
});
test("alerts when last_changed moves", () => {
const alerts = diffRdapSnapshot(snapshot(), snapshot({ last_changed: "2026-06-01T00:00:00Z" }));
assert.ok(alerts.some((a) => a.includes("last_changed event moved")));
});
test("nameserver order does not trigger a false alert", () => {
const baseline = snapshot({ nameservers: ["ns1.cf.com", "ns2.cf.com"] });
const current = snapshot({ nameservers: ["ns2.cf.com", "ns1.cf.com"] });
assert.deepEqual(diffRdapSnapshot(baseline, current), []);
});
test("multiple changes all reported", () => {
const baseline = snapshot();
const current = snapshot({ status: [], nameservers: ["ns1.evil.net"], registrant_handle: "REG-2" });
const alerts = diffRdapSnapshot(baseline, current);
assert.ok(alerts.length >= 3);
});
Case studies
The domain nobody had logged into in three years
An old marketing domain still forwarded to the main site, but nobody had touched its registrar account since it was set up. An attacker who had phished the founder's old email account years earlier finally used it to reset the registrar password and quietly remove clientTransferProhibited.
An RDAP poller running on a fifteen minute schedule flagged the missing lock within the hour. The team re-locked the domain, rotated the registrar credentials and enabled MFA, and the transfer request that had just been filed was cancelled before it could complete.
Traffic quietly redirected before anyone noticed a lock change
A support subdomain started returning odd content. Nobody suspected DNS at first, since the main site looked fine. A look at RDAP showed the nameservers had changed two days earlier, from the company's usual Cloudflare pair to a pair nobody recognized, while the transfer lock was still in place.
Because the nameserver diff was caught by a scheduled check rather than by accident, the team could tell exactly when the change happened and confirm with the registrar that it was unauthorized, well within the window where support could still intervene.
Once a poller is watching RDAP on a schedule, a hijack attempt stops being a surprise you discover when the domain is already gone. It becomes an alert that lands in your inbox the same hour the lock disappears or the nameservers change, while you still have time to re-lock the domain, rotate credentials, and stop a pending transfer at the registrar.
FAQ
What is the first sign of a domain hijack?
Usually a quiet change in the domain's RDAP record: a transfer lock like clientTransferProhibited disappearing, the nameservers switching to servers you did not configure, or the registrant contact changing. These changes typically happen hours to days before the domain or its traffic is actually taken.
Can a script fix a hijacked domain automatically?
No. A script can detect the RDAP change and alert you, but re-locking the domain, resetting registrar credentials, or halting a transfer has to happen in the registrar's control panel or through ICANN's Transfer Emergency Action Contact. There is no DNS-zone API that can undo a registrar-level ownership change.
How often should I poll RDAP for a domain I care about?
Every 15 to 60 minutes is enough for most domains. RDAP lookups are lightweight and free, and the goal is to catch a bad status or nameserver change within the same hour it happens, well inside the window before an unauthorized transfer can complete.
Related field notes
Citations
On the problem:
- RFC 9083, JSON Responses for the Registration Data Access Protocol (RDAP). datatracker.ietf.org/doc/html/rfc9083
- RFC 9082, Registration Data Access Protocol (RDAP) Query Format. datatracker.ietf.org/doc/html/rfc9082
- ICANN: About Locked Domain / Transfer Policy. icann.org/resources/pages/locked-2013-05-03-en
On the solution:
- IANA RDAP Bootstrap Service Registry for Domain Name Space. data.iana.org/rdap/dns.json
- RDAP.ORG, about the RDAP bootstrap redirector. about.rdap.org
- ICANN Transfer Policy, including the Transfer Emergency Action Contact (TEAC). icann.org/resources/pages/transfer-policy-2016-06-01-en
Stuck on a tricky one?
If you have a DNS, delegation, or domain security problem you would rather hand off, this is the kind of work I do. Message me and we can work through it together.
Did this help you catch a hijack early?
If this saved you a stolen domain or a scary registrar email, you can buy me a coffee. It is the best way to keep these field notes free and growing.
Buy me a coffee on Ko-fi