Skip to main content
Site Architecture

Auth: Network Lock Removal

Removal of IP-based network-lock authentication layer in favor of CF Access-only auth

March 3, 2026

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.x range) -- 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 /24 range, 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_DISABLED env 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

  1. 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.

  2. 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."

  3. 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.

  4. 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.

authenticationsecuritynetwork-lockcloudflare-access