640 Phishing Detector on Your Own Domain
0%

Phishing Detector on Your Own Domain

The task: train a phishing-URL classifier on 235,000 real URLs, then serve it from your own VM at https://phish.yourdomain.xyz — with Claude Code doing every bit of the server work from your laptop.

Steps
  1. •Before you start
  2. 11 — Add the phish subdomain
  3. 22 — Get the data
  4. 33 — Train the classifier
  5. 44 — Wrap it in a web service
  6. 55 — Deploy with no manual server work
  7. 66 — TLS and go live
  8. •Reference
  9. •Links
•

Before you start

Three earlier labs have to be finished first. This one builds on all three and repeats none of them.

Open Claude Code on your laptop and ask it for your VM's external IP — you need it for the DNS record in step 1:

Prompt 1 — read the VM’s IP

Prompt
Get the external IP of my Google Cloud VM g1 in zone us-central1-a with gcloud.

Print only the IP address on its own line. Show me the command you ran and its raw output.
Claude Code stays on your laptop for the whole lab. It reaches the server the same way you do, through gcloud compute ssh g1 --zone=us-central1-a --command "..." and gcloud compute scp. Nothing gets installed on the VM by hand, and Claude Code is never installed on the VM.
DNSdatatrainweb servicedeployTLS

Replace yourdomain.xyz with your own domain everywhere below.

1

1 — Add the phish subdomain at Namecheap

You need one new A record at Namecheap, next to the @ and lab records you already have. Pick one of two ways: let Claude in Chrome add it for you (option A), or add it by hand (option B).

Option A. This needs Claude in Chrome, set up in Claude in Chrome Setup. Log in to Namecheap in Chrome yourself first, then give Claude in Chrome this prompt, with the IP from Prompt 1 in place of YOUR_VM_IP:

Prompt 2 — option A: Claude in Chrome adds the record

Prompt
I am logged in to Namecheap in this Chrome tab. Go to Domain List, click Manage next to yourdomain.xyz, and open Advanced DNS.

Add one new record: type A Record, host phish, value YOUR_VM_IP, TTL Automatic. Save it.

Do not change or delete any other record, and do not touch the nameservers. When you are done, show me the full list of host records as they look now.

Option B — do it by hand instead

In the Namecheap dashboard: Domain List → Manage → Advanced DNS. Add one row next to the @ and lab records you already have:

TypeHostValueTTL
A Recordphishyour server's IPAutomatic

That gives you phish.yourdomain.xyz, pointing at the same VM as lab. Nothing else changes: the nameservers stay on Namecheap BasicDNS.

DNS is not instant — minutes usually. Back in Claude Code, check it from your laptop:

Prompt 3 — check the new name

Prompt
Run dig +short phish.yourdomain.xyz and tell me whether it prints the external IP of g1.

Then run curl -sI http://phish.yourdomain.xyz and tell me whether nginx answers.

If the name does not resolve yet, wait one minute and try again, up to five times. Show me every command you ran and its raw output.
⚠ The VM's external IP is ephemeral. Stop and start the VM and it gets a different IP, the A record you just typed points at someone else's machine, and the site goes dark — you then have to come back to Advanced DNS and enter the new IP. Leave g1 running, or reserve a static IP.
✅ Checkpoint 1: Prompt 3 shows phish.yourdomain.xyz resolving to your VM's external IP, and nginx answering on http://phish.yourdomain.xyz.
2

2 — Get the data

The dataset is the PhiUSIIL Phishing URL Dataset from the UCI Machine Learning Repository: about 235,000 URLs, roughly 50 features already computed for each one, and a label column saying phishing or benign. It downloads without a login.

Open Claude Code on your laptop and give it this:

Prompt 4 — download and inspect

Prompt
Make a new folder ~/phish-ml and work inside it.

Download this file:
https://archive.ics.uci.edu/static/public/967/phiusiil+phishing+url+dataset.zip

Unzip it, load the CSV with pandas, and tell me four things: how many rows there are, how many are phishing and how many are benign, the data type of every column, and whether any column has missing values.

Do not train anything yet.

What to look for:

  • Around 235,000 rows. A much smaller number means the zip did not extract fully.
  • The two classes are close to balanced — neither is a rare event. That matters for how you read the scores in the next step.
  • Most columns are numeric. A couple are text: the raw URL and the domain. Those are identifiers, not features, and they have to come out before training.
  • No missing values. If Claude reports some, ask it which columns and how many before you go further.
This dataset hands you the features already computed — URL length, digit counts, whether the host is a bare IP, and so on. That is why the lab fits in an afternoon. Deriving those features yourself from raw URLs is a different and much longer exercise.
✅ Checkpoint 2: ~/phish-ml holds the extracted CSV, and you can state the row count and the class balance from memory.
3

3 — Train the classifier

Training happens on your laptop, not on the VM. g1 has 1 GB of RAM and two shared vCPU bursts; it will not fit 235,000 rows and a random forest comfortably, and the swap file will thrash if you try. The VM's job is to serve one finished model file, which costs almost nothing.

Prompt 5 — two models, one winner

Prompt
In ~/phish-ml, train on that CSV with scikit-learn.

Drop every column that is not a feature, including the raw URL text and the domain name, and use the label column as the target. Split the data 80/20, stratified on the label, with random_state=42.

Train two models: logistic regression as the baseline, and a random forest. For each one, print accuracy, precision, recall, macro F1 and the confusion matrix on the test set.

Keep the model with the better macro F1. Save it to model.joblib together with the exact list of feature column names in the order used for training, and print that list for me.

What to look for:

  • Macro F1 is the number to read, not accuracy. Accuracy on this dataset flatters everything: a model that is lazy about one class still scores high, because both classes are large. Macro F1 averages the per-class F1 scores without weighting by class size, so a model that quietly gives up on phishing URLs cannot hide behind it.
  • The confusion matrix tells you which mistake the model makes. A false negative is a phishing URL waved through; a false positive is a real site blocked. They are not equally expensive, and the matrix is where you see the trade.
  • The random forest will almost certainly beat the baseline. Keep the baseline number anyway — without it you have nothing to say about whether the forest is worth its extra cost.
  • A score near 100% should make you suspicious, not pleased. Ask Claude Code which features carry the most weight; if one feature alone explains the label, it is probably leaking the answer.
✅ Checkpoint 3: model.joblib exists in ~/phish-ml, and you can say the macro F1 of both models and which one got saved.
4

4 — Wrap it in a web service

The model takes a row of numbers. A visitor types a URL. Something has to turn one into the other, and that something is the part most likely to go wrong.

Prompt 6 — the Flask app

Prompt
In ~/phish-ml, write app.py: a small Flask app that loads model.joblib and the saved feature list.

GET / shows one text box for a URL and a Submit button.

POST / takes that URL, computes the same features the model was trained on in the exact order of the saved feature list, and shows "phishing" or "benign" with the model's probability as a percentage.

GET /health returns JSON with the status, the model type and the number of features.

Put the feature extraction in one function, and at start-up check that the names and order it produces match the saved feature list exactly — refuse to start if they do not. Keep it to app.py plus a requirements.txt.

Then run it locally so I can try it at http://127.0.0.1:5000.
⚠ Feature-order drift. This is the classic bug in deployed models, and it fails silently. The model does not know your column names; it only sees position 0, position 1, position 2. Swap two columns between training and serving and nothing crashes — you just get confident nonsense. That is why the prompt asks for one shared extraction function and a start-up check against the saved list, rather than a second copy of the logic written for serving.

Now test it on URLs you can judge yourself: two real sites, and a made-up one with a bare IP for a hostname that looks like phishing. Claude Code also confirms the service answers a machine as well as a browser:

Prompt 7 — test it locally

Prompt
The app from the last prompt is running at http://127.0.0.1:5000. Leave it running.

Run curl -s http://127.0.0.1:5000/health.

Then POST each of these three URLs to / the same way the form does, and tell me the verdict and the probability for each one:
https://www.wikipedia.org
https://www.python.org
http://203.0.113.45/paypal.com-login/verify/account.php?session=99812

Show me every command you ran and its raw output.

You can still open http://127.0.0.1:5000 in your browser and try your own domain or your university.

✅ Checkpoint 4: Prompt 7 shows a verdict for each of the three URLs, /health returns JSON, and the feature count in that JSON matches the list printed in step 3.
5

5 — Deploy with no manual server work

You will not open an SSH session in this step. Claude Code runs every server command from your laptop through gcloud, and you read back what it did.

Prompt 8 — the whole server side

Prompt
Deploy ~/phish-ml to my Google Cloud VM. Do all of it from here. Do not ask me to log in to the server myself.

The VM is g1 in zone us-central1-a. Run remote commands as
gcloud compute ssh g1 --zone=us-central1-a --command "..."
and copy files with gcloud compute scp. nginx is already installed and serving on port 80.

1. Install python3-venv and python3-pip on the VM if they are missing.
2. Create /opt/phish-ml, make a virtual environment inside it, and pip install flask, gunicorn, scikit-learn, joblib and pandas into that environment.
3. Copy app.py, requirements.txt and model.joblib into /opt/phish-ml.
4. Write /etc/systemd/system/phish-ml.service so it runs the app with gunicorn from that virtual environment, with exactly one worker, bound to 127.0.0.1:8000, restarting on failure. Enable it and start it.
5. Write /etc/nginx/sites-available/phish-ml with server_name phish.yourdomain.xyz, proxy_pass to http://127.0.0.1:8000 and the usual proxy headers. Symlink it into sites-enabled, run nginx -t, and reload nginx only if the test passes.

Use one worker, not more — the VM has 1 GB of RAM. Show me every command you ran and what it printed.

Check its work: Claude Code reports success in its own words. This prompt makes it show you the raw evidence, so you can see whether it is true.

Prompt 9 — check the deployment

Prompt
Check the deployment on g1 in us-central1-a. Run the server commands over gcloud compute ssh from here. Do not fix anything yet.

1. On the VM, run systemctl status phish-ml --no-pager.
2. On the VM, run sudo journalctl -u phish-ml -n 40 --no-pager.
3. From here, run curl -s http://phish.yourdomain.xyz/health.
4. From here, run curl -s http://phish.yourdomain.xyz/ and tell me whether the form HTML comes back.

Show me every command you ran and its raw output.

active (running), JSON from /health and the form's HTML mean it worked. If the status is failed, tell Claude Code to read those logs and fix its own deployment — a missing wheel on the VM's Python version is the usual cause.

gunicorn listens on 127.0.0.1:8000, which is reachable only from the VM itself. The internet only ever talks to nginx, and nginx forwards. Nothing extra needs opening in the firewall, and port 8000 stays private.
✅ Checkpoint 5: Prompt 9 shows active (running), and http://phish.yourdomain.xyz returns your form's HTML. You never typed a command on the server.
6

6 — TLS and go live

One server change left. No firewall change is needed: port 443 was opened by the allow-https rule when you built the VM, and g1 already carries the https-server tag. Certbot only has to prove the name and rewrite the nginx site.

Prompt 10 — certificate and redirect

Prompt
Same VM, g1 in us-central1-a, again entirely over gcloud compute ssh from here.

Install certbot and its nginx plugin, then issue a Let's Encrypt certificate for phish.yourdomain.xyz using the nginx plugin, non-interactively, agreeing to the terms with my email address, and choose the option that redirects all HTTP traffic to HTTPS.

Then run nginx -t, reload nginx, and show me the certificate's expiry date and that the renewal timer is active.

Now check it the way a visitor would, with your own domain in place of the placeholder:

Prompt 11 — check it is live

Prompt
Check that phish.yourdomain.xyz is live over HTTPS. Run everything from here.

1. Run curl -sIL http://phish.yourdomain.xyz and tell me whether the first answer is a 301 redirect to https.
2. Show me who issued the certificate for phish.yourdomain.xyz and when it expires.
3. POST https://www.wikipedia.org to https://phish.yourdomain.xyz/ the same way the form does, and tell me the verdict and the probability.

Show me every command you ran and its raw output.

A 301 from http:// to https://, a Let's Encrypt certificate, and phish or benign with a probability mean you are live. That is the lab.

Optional: to see the padlock the way a visitor does, give this to Claude in Chrome (Claude in Chrome Setup):

Prompt 12 — optional: padlock check in Chrome

Prompt
Open https://phish.yourdomain.xyz in a new tab. Tell me whether Chrome shows the connection as secure, and who issued the certificate.

Then type https://www.wikipedia.org into the URL box, press Submit, and tell me exactly what the page shows.
⚠ The free tier allows about 1 GB of egress a month. A text form and a JSON endpoint will never reach it. Serving images, or copying a large model file back and forth repeatedly, will — after that it is roughly $0.12/GB. Keep model.joblib out of any loop that re-uploads it.
The certificate lasts 90 days. Certbot installs a systemd timer that renews it on its own, which is the only reason this does not need revisiting — as long as the A record still points at this VM when renewal comes round.
✅ Checkpoint 6: Prompt 11 shows the redirect to https://phish.yourdomain.xyz, a valid certificate, and a verdict for the URL it sent.

Reference

App directory on the VM/opt/phish-ml — holds app.py, model.joblib and the virtual environment
systemd unit/etc/systemd/system/phish-ml.service
nginx site/etc/nginx/sites-available/phish-ml, symlinked into sites-enabled
gunicorn bind address127.0.0.1:8000, one worker — local only, nginx proxies to it
Service statusAsk Claude Code: “Show me the status of phish-ml on g1.”
Read the logsAsk Claude Code: “Show me the last 40 lines of the phish-ml logs on g1.”
Restart after a new modelAsk Claude Code: “Restart phish-ml on g1 and show me its status.”
Test and reload nginxAsk Claude Code: “Test the nginx config on g1 and reload nginx only if the test passes.”
Health checkAsk Claude Code: “Check https://phish.yourdomain.xyz/health and show me the raw JSON.”
Anything on the serverAsk Claude Code; it uses gcloud for you.