Domain Verification
Prove you own a domain before the first pentest runs against it
A pentest sends real attack traffic at your application: login attempts, injection payloads, active probing. ModernPentest generates that traffic, so before the first pentest we ask you to prove that the target is yours. Every application needs this proof, whether you added it in the dashboard, over the MCP server or through the API, and no pentest can start until every host of the application is proven.
There is no approval queue and nobody to wait for. Publish a record or a file, press Check now, and the pentest starts.
What needs proof
Each host you entered for the application needs its own proof: the web application URL and, if you added a REST API, its base URL.
app.example.com and api.example.com are two hosts, so they need two records or two files; both are named after the host they prove.
Proof belongs to your organization and never expires. A host you have proven once is proven for every application you add that targets it. An agency and its client can both prove the same host, and one proving it never lets the other scan it.
Two ways to prove it
| DNS TXT record | HTTPS file | |
|---|---|---|
| What you publish | A TXT record at _modernpentest-verify.<host> | A text file at https://<host>/.well-known/modernpentest-verify.txt |
| What it proves | The exact host named in the record | The exact host that serves the file |
| How long it takes | Minutes to a few hours, depending on your DNS provider and TTL | Immediate, as soon as the file is deployed |
| Who can use it | Anyone with access to the host's DNS | Anyone who can deploy a static file to the host |
| Not available for | Shared hosting suffixes (see below) | Hosts that redirect or require authentication |
Both methods prove one host: the record and the file are named after the host, and a record for app.example.com says nothing about api.example.com.
Use DNS when the host redirects, sits behind a login, or is served by something you cannot deploy a file to.
Use the HTTPS file when you cannot touch DNS, when your host lives on a provider-owned suffix, or when you need the pentest to start right now.
The record is named after the host rather than the domain because that is what makes it a proof. Anyone who can add records under a shared DNS zone could publish a record at the zone's apex, so a record there would prove hosts that are not theirs; a record under the host's own name is the host owner's to publish and nobody else's.
The dashboard shows both methods for every host, with the exact values to copy. Publish either one; the first that checks out proves the host.
Method 1: DNS TXT record
Copy the record from the dashboard
Open the application and pick the DNS record tab of the verification panel for the host. If the tab shows Not started, press Get the value to publish; it then shows the record name and value for that host:
Type TXT
Name _modernpentest-verify.app.example.com
Value modernpentest-verify=3f9c1e7a8b2d4c6e0f1a2b3c4d5e6f70The name is always _modernpentest-verify. followed by the exact host you entered (app.example.com, not example.com; _modernpentest-verify.example.com only when the host is the bare domain).
The value is modernpentest-verify= followed by a 32-character token that is unique to your organization and this host.
An application with two hosts shows two records, one per host.
Add it at your DNS provider
Create a TXT record with that name and value in the zone of your domain.
Many providers append the domain to whatever you type in the Name field; if yours does, enter the name relative to the zone, _modernpentest-verify.app for app.example.com in the example.com zone.
Leave any other TXT records in place; the check ignores records that are not ours.
A short TTL (300 seconds) makes a typo cheaper to fix.
Press Check now
Back in the dashboard, press Check now. We look the record up from our own resolver, so it has to have reached the public DNS first. Most providers publish within minutes; some take an hour or more. A "no record found yet" answer spends one of the 25 attempts for that record, so wait a few minutes between checks rather than pressing repeatedly.
Leave the record in place after verification. A proof never expires, but the record is what would satisfy a periodic re-check if we add one.
Method 2: HTTPS file
Copy the file body from the dashboard
Open the HTTP file tab of the verification panel for the host. If the tab shows Not started, press Get the value to publish; it then shows the URL we will fetch and the one line the file must contain:
URL https://app.example.com/.well-known/modernpentest-verify.txt
Body modernpentest-verify=3f9c1e7a8b2d4c6e0f1a2b3c4d5e6f70Serve the file from the host
Deploy the file so that a plain GET of that URL returns it.
The rules the check applies:
- HTTPS only, on the default port, with a certificate browsers trust.
http://is never tried. - HTTP 200, no redirects.
A
301,302or308counts as "file not published", even when the destination would have served it. Ifexample.comredirects towww.example.com, verify the host you actually entered, or use DNS. - No authentication.
A
401or403counts as not published. - Plain text, one value per line.
The check reads the first 64 KB and looks for a line that is exactly
modernpentest-verify=<token>. Trailing whitespace, Windows line endings and a byte order mark are fine. The token embedded in a longer line, or inside HTML, is not. - Several lines are allowed. An agency and its client, or two organizations of your own, can each add their own line to the same file.
- 10 seconds to answer, including the body.
Press Check now
The file is live as soon as your deploy finishes, so the check succeeds on the first try.
Examples
Next.js serves everything in public/ from the site root, so a file at public/.well-known/modernpentest-verify.txt is served at /.well-known/modernpentest-verify.txt with no code.
modernpentest-verify=3f9c1e7a8b2d4c6e0f1a2b3c4d5e6f70If your middleware protects every route, exclude the path so an anonymous request gets the file rather than a redirect to sign-in:
export const config = {
matcher: ['/((?!\\.well-known/|_next/|favicon.ico).*)'],
};For a Next.js project the public/ file above is all you need.
For any other framework, put the file in the directory Vercel serves as static output: public/ for most frameworks, or the build output directory of a static site.
Two Vercel settings can get in the way:
- Deployment Protection.
A preview deployment behind Vercel Authentication answers
401, which counts as not published. Verify the production domain, or use DNS. - Redirects.
If the project redirects the apex to
www(or the other way round), verify the host that serves the file directly.
*.vercel.app hosts are shared hosting, so only the HTTPS file is offered for them.
A custom domain on the same project can use either method.
*.run.app hosts are shared hosting, so the HTTPS file is the only method.
Serve the path from your service and read the token from an environment variable, so a new token never needs a code change:
const lines = (process.env.MODERNPENTEST_VERIFY_TOKENS ?? "")
.split(",")
.map((token) => token.trim())
.filter(Boolean)
.map((token) => `modernpentest-verify=${token}`);
app.get("/.well-known/modernpentest-verify.txt", (_req, res) => {
res.type("text/plain").send(lines.join("\n") + "\n");
});gcloud run services update my-service \
--update-env-vars MODERNPENTEST_VERIFY_TOKENS=3f9c1e7a8b2d4c6e0f1a2b3c4d5e6f70The service must allow unauthenticated requests; a Cloud Run service that requires IAM authentication answers 403.
If you serve the same service from a custom domain you control, DNS is available for that domain too.
Create a .well-known directory next to your index.html and upload the file into it.
Some server configurations hide directories that start with a dot. On nginx, allow the path before any rule that denies hidden files:
location ^~ /.well-known/ {
allow all;
}Apache serves .well-known/ by default; its .ht* rule does not cover it.
On S3 behind CloudFront, upload the object with the key .well-known/modernpentest-verify.txt and the content type text/plain.
*.cloudfront.net hosts are shared hosting; a custom domain in front of the distribution can use DNS.
Shared hosting: HTTPS file only
When your host lives on a suffix owned by the hosting provider, the DNS for that suffix is the provider's, not yours, so you cannot publish the record for your host and the DNS method is not offered. The dashboard shows a note in place of the DNS record tab, and the HTTPS file is the way to prove the host.
These suffixes count as shared hosting:
run.app, vercel.app, netlify.app, herokuapp.com, fly.dev, pages.dev, web.app, firebaseapp.com, azurewebsites.net, github.io, onrender.com, railway.app, ngrok.app, ngrok-free.app, amplifyapp.com, appspot.com, replit.app, koyeb.app, deno.dev, workers.dev, cloudfront.net, convex.site, supabase.co.
A custom domain pointed at the same service is yours, so both methods are offered for it. If your provider's suffix is missing from this list, the DNS record tab is still shown, but a record under the provider's zone is not yours to publish; use the file.
After the proof lands
The moment a check succeeds, the host shows Proven, and once every host is proven the application's Run Pentest button is enabled.
If you chose Start the first pentest as soon as ownership is verified when adding the application, the first pentest starts on its own within moments of the last host being proven, and the dashboard takes you to it. You do not need to keep the page open. The request is kept for 30 days; after that, or if you chose Save only, start the pentest yourself from the application page. If you chose Schedule for later, the pentest runs on the date you picked as long as ownership is proven by then; otherwise that run is skipped and the next one moves to the following cycle.
The automatic start follows the same rules as pressing Run Pentest: it spends your plan's quota, otherwise a pentest credit, otherwise your one free preview, whose findings are listed but stay locked until a credit or a plan unlocks them.
Attempts and failure states
Checks run only when you ask for them: Check now in the dashboard, or verify_application_domain over MCP.
In the dashboard, Check now checks the method whose tab is open for each host, so the attempts of the method you are not using are left alone; verify_application_domain checks every pending method.
Nothing polls in the background, so publish first and check second.
Each record or file gets 25 attempts. An attempt is spent when the check reaches your DNS or your host and gets an answer that says the proof is not there yet:
| Spends an attempt | Does not spend an attempt |
|---|---|
| No TXT record at the name yet | The DNS lookup timed out or the resolver failed |
| A TXT record exists but none carries the value | The host answered with a 5xx |
The file answers 404, another 4xx, or a redirect | The host could not be reached, the certificate failed, or the fetch timed out |
| The file is served but no line matches |
Each host carries one status chip, and each method tab says where that method stands:
| Chip | Meaning | What to do |
|---|---|---|
| Not started | No value has been issued for the host yet | Open the HTTP file or DNS record tab and press Get the value to publish |
| Waiting for proof | A value is issued and waiting for a successful check | Publish the value, then press Check now |
| Proven | An active proof covers the host, and it never expires | Nothing; the host is proven for every application that targets it |
| Check failed | A method spent all 25 attempts and no other method is still waiting | Press Start over with a new value on that method, publish the new value, and check again |
| Revoked | ModernPentest support withdrew a proof | Contact support to learn why; Start again with a new value on the DNS record or HTTP file issues a new token |
A failed record's token is dead for good; the new token replaces it, so update the record or file before checking again.
The Last check line under a waiting method is the reason the last check gave, for example No file found at ... (HTTP 404) or A TXT record exists at ... but none matched the expected verification value.
What an operator can still do
Some targets can never be proven by you: an IP address has no DNS to publish under and no certificate to serve a file with, and internal hostnames are rejected outright. For those, and for a host that is stuck for a reason outside your control, contact support. An operator can approve a domain by hand for your organization; the approval is recorded with a reason and the approver's name, and unlike your own record or file it covers every host under that domain. An operator can also withdraw any proof, yours or theirs, which puts the application back behind the gate until it is proven again.
If you verified a domain before September 2026, when one record at _modernpentest-verify.<domain> covered every host under it, that record keeps proving those hosts and nothing needs to be republished; the dashboard shows it as the proof for each host.
A record you publish from now on proves one host, and an operator withdrawing an old domain-wide record means proving each host again, per host.
Operators do not approve organizations any more. Proving your domain is the whole authorization.
Doing it from an agent
The same proof can be driven end to end over the MCP server: register_application starts verification for every target and returns the values to publish, and verify_application_domain runs the check.
See Agents as customers.
Next steps
- Adding Applications for every option in the wizard
- Running Pentests for what happens once the gate opens
- WAF Bypass Configuration if your host sits behind a firewall the scanner needs to pass
Last updated: September 7, 2026