Moving the administrative interface to admin.example.com is a real improvement: it keeps admin cookies away from public pages, gives the two applications separate caches and lets you point a stricter network policy at one hostname. It is not a CSRF control, and it does not make the public site's code safe for an authenticated administrator to have open. The separation changes what the browser treats as a boundary, not what the server checks, so work these four checks before you call it finished.

Read origin and site as two different words

An origin is scheme, host and port, so https://www.example.com and https://admin.example.com are different origins. A site, in the SameSite sense, is scheme plus registrable domain, so those same two hostnames are usually the same site. That difference is the whole trap: the SameSite attribute will not help you across sibling subdomains, because the browser does not consider the request cross-site at all. Point the admin interface at a genuinely different registrable domain, such as admin-example.net, and the request does become cross-site, which is where SameSite=Lax or Strict starts doing work you can rely on.

Test it in the browser rather than reasoning about it.

Load the public site, open developer tools, and look at which cookies the browser attaches to a request aimed at the admin host. If the cookie has no Domain attribute it is host-only and stays on its own hostname; if it carries Domain=example.com it travels to every sibling, and the new subdomain has bought you nothing for that cookie.

Separate "can read the response" from "can send the request"

The same-origin policy restricts reading responses across origins. It does not stop a request from being sent, and it does not stop a form post from carrying credentials the browser already holds. A simple request, one with a form content type or a plain GET, is delivered without a preflight; CORS then decides whether the calling script may read the answer. CSRF lives entirely on the sending side.

That is why the server needs an explicit check for state-changing operations. A synchronizer token tied to the session, or a double-submit cookie compared server-side, covers the case; an Origin or Referer comparison is worth adding as a second gate but not as the only one, because not every request carries the header and not every proxy preserves it. OWASP's CSRF prevention guidance treats SameSite as one layer among several for exactly this reason.

Decide what the network gate actually proves

A VPN exit allowlist in front of the admin hostname is a good control and a weak identity claim. It proves that the request came from an address the VPN hands out, which may be shared by everyone on that exit, and it says nothing about which person or device is behind it. Treat it as an entry filter, not as authentication.

Keep it independent of the request-level checks. If the allowlist is the only thing standing between an attacker and a state-changing endpoint, one misconfigured route, one split-tunnel client or one borrowed laptop removes the control entirely.

Log rejects from outside the expected ranges as well. A burst of them is the most useful signal that someone is probing the boundary you just built, and silence is the signal that the filter is not being reached at all.

Keep cookies, sign-in and authorization explicit

Cookie scope decides most of the outcome. A parent-domain cookie is sent to the admin host, so a script injection on the public site can act with an administrator's session; a host-only cookie cannot. Prefer the host-only form, add the Secure attribute, and use the __Host- prefix where the platform supports it, since that prefix requires Secure, Path=/ and no Domain attribute. Read the response header rather than the cookie jar: Set-Cookie: __Host-admin_session=...; Secure; Path=/; HttpOnly; SameSite=Lax, with no Domain attribute. A browser drops a __Host- cookie that carries Domain, so a cookie that still appears on sibling hosts is proof the prefix never applied.

Then keep authorization on the server, per request, on every administrative endpoint rather than only at login. A request arriving from an allowed network still has to be authenticated and authorized for the operation it asks for; the hostname is not an actor. If the public and admin applications share a sign-in flow or a session store, write that relationship down, because it is the shortest path from a public-site bug to an admin action.

Re-check the boundary after the split

Run the negative tests, not just the happy path: a cross-site form post without a token must be rejected, a token from another session must be rejected, and no GET should change state. Check the cookie attributes in developer tools, confirm the Origin check fires on a mismatched request, and try the admin host from outside the VPN range to see the filter work.

None of this removes an XSS flaw inside the admin application itself. A separate hostname is a browser-side isolation boundary with real value, and it is not a substitute for preventing unsafe code execution or for keeping server-side request protection. After the split, the checklist is shorter than it looks: distinct origin, host-only cookies, a token on every mutation, an entry filter you do not trust as identity, and authorization decided per request.

Barruulee wal qabatan