Auth: Network Lock Removal
Removal of IP-based network-lock authentication layer in favor of CF Access-only auth
Auth: Network Lock Removal
The Network-Aware Session Lock system (src/lib/network-lock.ts) has been disabled. It checked client IPs against a trusted list and restricted admin mutations on untrusted networks. The system was removed because it fundamentally could not work with mobile carrier networks behind Cloudflare Access. This document records what was removed, why, what remains, and what replaces it.
What the Network Lock Did
When enabled, the network lock added an IP-based trust layer on top of Cloudflare Access authentication:
- Trusted network (home LAN IPs in
10.x.x.xrange) -- full admin access, no restrictions - Untrusted network (any other IP) -- view-only mode until unlocked via TOTP, passphrase, or recovery code
- The unlock cookie was bound to the client's IP
/24range, so moving between subnets required re-authentication
Lockout Prevention
The system included several safeguards against permanent lockout:
- Passphrase unlock -- manual passphrase entry to unlock from any network
- Recovery codes -- one-time-use codes stored in KV
- Trusted network bypass -- automatic unlock when connecting from a trusted IP
NETWORK_LOCK_DISABLEDenv var -- kill switch to disable the system entirely- Fail-open on KV errors -- if the KV store was unreachable, the lock would not engage
Why It Was Removed
The system failed on mobile phones using carrier networks. The root cause is architectural:
Phone on carrier network:
Phone → Carrier IPv6 → Public Internet → Cloudflare → Worker
Phone on Tailscale VPN:
Phone → Carrier IPv6 → Public Internet → Cloudflare → Worker
(Tailscale tunnel is separate -- CF still sees the carrier IP)
Failure chain
Cloudflare sees the phone's carrier IPv6 address, not the Tailscale IP. Traffic flows from the phone through the public internet to Cloudflare's edge, regardless of whether Tailscale is running. The Tailscale tunnel is a separate overlay network that does not affect the path to Cloudflare.
The trusted IP list only contained local LAN IPs (
10.x.x.x). Any phone on a carrier network presented an IPv6 address outside this range, so it was always classified as "untrusted."Once locked, the phone was never prompted to re-authenticate. Cloudflare Access had already validated the device and cached its session state. The CF Access JWT was valid, but the network lock layer silently blocked admin mutations without surfacing an unlock prompt.
The unlock mechanism was unreachable. The network lock banner and unlock UI relied on the admin layout rendering correctly, but the blocked state prevented the necessary API calls from completing.
The result: admin users on mobile carrier networks were permanently locked into view-only mode with no visible way to unlock.
Where It Was Called
The network lock was integrated at three points in the request lifecycle:
Middleware (src/middleware.ts)
Blocked admin API mutations (POST, PUT, DELETE, PATCH) when the user was network-locked:
// REMOVED: Network lock mutation block (lines 70-93)
if (user.networkLocked && isAdminApi && isMutation) {
return new Response(JSON.stringify({
error: 'Network locked',
message: 'Admin mutations are blocked on untrusted networks',
}), { status: 403 });
}
Role Resolution (src/lib/roles.ts)
resolveAuthState() called checkNetworkLock() for admin users and set networkLocked = true on the AuthUser object:
// REMOVED: checkNetworkLock() call and networkLocked property
const lockState = await checkNetworkLock(request, env);
if (lockState.locked) {
authUser.networkLocked = true;
}
The networkLocked property has been removed from the AuthUser type.
Layout (src/layouts/CosmicLayout.astro)
Displayed the NetworkLockBanner component and set a data-network-locked attribute on the page body:
<!-- REMOVED: NetworkLockBanner import, variable, component, and data attribute -->
import NetworkLockBanner from '../components/admin/NetworkLockBanner.astro';
const networkLocked = Astro.locals.user?.networkLocked ?? false;
<NetworkLockBanner locked={networkLocked} />
<body data-network-locked={networkLocked}>
What Remains Unchanged
The rest of the authentication and authorization stack is unaffected:
| Layer | File | Status |
|---|---|---|
| Cloudflare Access JWT validation | src/lib/auth.ts |
Unchanged |
| Role-based permissions (admin/member/demo) | src/lib/roles.ts |
Unchanged (networkLocked property removed) |
CF_AppSession cookie validation |
src/lib/auth.ts |
Unchanged |
ADMIN_EMAILS allowlist |
src/lib/roles.ts |
Unchanged |
| Service-to-service bearer token auth | src/middleware.ts |
Unchanged |
| Route classification and permission checks | src/middleware.ts |
Unchanged |
| Dev mode admin bypass | src/middleware.ts |
Unchanged |
The existing security model -- Cloudflare Access JWT + role-based permissions + admin email allowlist -- continues to enforce authentication and authorization on all protected routes.
Files Changed
| File | Change |
|---|---|
src/middleware.ts |
Removed network-lock mutation block (lines 70-93) |
src/lib/roles.ts |
Removed checkNetworkLock() call and networkLocked property from AuthUser |
src/layouts/CosmicLayout.astro |
Removed NetworkLockBanner import, variable, component, and data-network-locked attribute |
Dead Code (Not Yet Deleted)
The following files still exist in the codebase but are no longer called or rendered. They can be deleted in a future cleanup pass:
| File | Purpose |
|---|---|
src/lib/network-lock.ts |
Full network lock implementation (IP checking, unlock logic, cookie management) |
src/components/admin/NetworkLockBanner.astro |
Amber banner component showing lock state and unlock controls |
src/pages/api/admin/network-lock.ts |
API endpoints for unlock (TOTP, passphrase, recovery code) |
Replacement: Phase 2 Plan
The network lock will be replaced by a device-identity system that does not depend on client IP addresses:
- mTLS (mutual TLS) or Tailscale device identity -- authenticate the device, not the network
- Device certificates -- issued per-device, validated at the edge
- Network-independent -- works from any network (carrier, WiFi, VPN, roaming)
- No IP dependency -- eliminates the root cause of the mobile failure
Until Phase 2 is implemented, the site relies on Cloudflare Access JWT validation and role-based permissions as the sole authentication and authorization layers.