Files
gatehouse-api/docs/zerotier-device-membership.md

8.9 KiB

ZeroTier Device Membership

Overview

This document covers the ZeroTier device membership model — how devices are registered, approved, activated, and deactivated on ZeroTier networks. It explains the schema design, the distinction between active and approved, the user activation flow, and audit coverage.

Schema Design

The core model is NetworkAccessRequest (table: network_access_requests). It replaces the legacy two-table approach (UserNetworkApproval + DeviceNetworkMembership) with a single per-device, per-network row.

network_access_requests table

Column Type Purpose
id UUID Primary key
organization_id FK -> organizations Org scope
user_id FK -> users Requesting user
device_id FK -> devices Target device
portal_network_id FK -> portal_networks Target network
granted_by_user_id FK -> users (nullable) Approving manager
grant_type enum requested or assigned
status enum pending, approved, rejected, revoked, suspended
active boolean Currently authorized on controller?
justification text User's reason for requesting
join_seen boolean Controller observed the device join
deleted_at timestamp (nullable) Soft delete support

Unique constraint: (user_id, device_id, portal_network_id, deleted_at) — ensures exactly one active record per device per network.

Supporting models

Model Table Purpose
Device devices A user-registered ZeroTier node (10-char node ID)
PortalNetwork portal_networks A managed ZeroTier network (scoped to org)
ActivationSession activation_sessions Temporary authorization window (TTL-based)
ZeroTierMembership zerotier_memberships Cache of controller-side state
KillSwitchEvent kill_switch_events Append-only audit of kill switch actions

Entity relationships

Device (1) ──> (N) NetworkAccessRequest (N) <── (1) PortalNetwork
                           │
                     (1 or 0)
                           │
                    ActivationSession
                           │
                     (1 or 0)
                           │
                   ZeroTierMembership

active vs approved — the key distinction

These are orthogonal concepts:

Concept Represents Set by Persists across sessions?
status = approved Administrative permission to use the network Manager approval Yes — once approved, stays approved until revoked
active = True Device is currently authorized on the ZT controller User activation No — toggled on/off per session

A request can be in any of these states:

status active Meaning
approved false Manager said yes, but user hasn't activated yet
approved true Manager said yes, device is actively connected
pending false Awaiting manager decision
rejected / revoked / suspended false Access denied or removed

The active flag is not a persistent grant — it's a run-time operational state tied to an ActivationSession with a finite TTL (default 8 hours).

User activation flow — "turning on" ZeroTier

User                         API                           Service                    ZT Controller
 │                            │                              │                            │
 │  POST /orgs/<id>/approvals │                              │                            │
 ├───────────────────────────>│ request_access()             │                            │
 │                            ├──> creates NetworkAccessRequest (status=PENDING)          │
 │                            ├──> _ensure_zerotier_member()                              │
 │                            │    └──> provisions member (de-authorized) ────────────────>│
 │                            │                              │                            │
 │  <── 201 { status: pending }                              │                            │
 │                            │                              │                            │
 │  [Admin approves]          │                              │                            │
 │  POST /orgs/<id>/approvals │                              │                            │
 │  /<id>/approve             │                              │                            │
 ├───────────────────────────>│ approve_request()            │                            │
 │                            ├──> sets status=APPROVED      │                            │
 │  <── 200 { status: approved }                             │                            │
 │                            │                              │                            │
 │  POST /orgs/<id>/memberships/<id>/activate                │                            │
 │  ─────────────────────────>│ activate_request()           │                            │
 │    "turn on ZeroTier"      ├──> creates ActivationSession (TTL=8h)                    │
 │                            ├──> sets request.active=True  │                            │
 │                            ├──> _authorize_in_zerotier()  │                            │
 │                            │    └──> authorizes member ───────────────────────────────>│
 │                            │                              │                            │
 │  <── 200 { active: true, session: {...} }                 │                            │

Endpoints

Method Path Action
POST /organizations/<id>/devices Register a device (prerequisite)
POST /organizations/<id>/approvals Request network access
POST /organizations/<id>/approvals/<id>/approve Admin approves
POST /organizations/<id>/memberships/<id>/activate Turn on ZeroTier
POST /organizations/<id>/memberships/activate-all Bulk-activate all approved
POST /organizations/<id>/memberships/<id>/deactivate Turn off ZeroTier
POST /organizations/<id>/devices/<id>/join-network/<id> Direct join (open networks)
POST /organizations/<id>/kill-switch Emergency deactivation

Session expiry and audit

When the ActivationSession TTL expires, the reconciliation worker handles it:

Reconciliation worker (runs every 2 min)
    │
    ├── reconcile_expired_activations()
    │    └── for each expired ActivationSession:
    │         ├── _deauthorize_in_zerotier() ───> ZT controller de-authorizes member
    │         ├── sets request.active = False
    │         └── logs audit event
    │
    └── reconcile_all()
         └── for each network:
              ├── sync ZeroTierMembership cache
              └── detect/repair drift (portal vs controller state mismatch)

Every authorization state change is audited:

Event When
zt.approval.requested User requests access
zt.approval.granted Manager approves/assigns
zt.approval.rejected Manager rejects
zt.approval.revoked Manager revokes
zt.membership.activated User activates (session created)
zt.membership.deactivated User deactivates (session ended)
zt.member.authorized ZT controller authorize call succeeds
zt.member.deauthorized ZT controller de-authorize call succeeds
zt.activation.expired Session expired by reconciliation worker
zt.kill_switch.activated Admin triggers kill switch

All audit entries are stored in the audit_logs table with organization_id, user_id, resource_type, resource_id, ip_address, and extra_data (JSON) for full traceability.

Key source files

File Purpose
gatehouse_app/models/zerotier/network_access_request.py NetworkAccessRequest model
gatehouse_app/models/zerotier/activation_session.py ActivationSession model
gatehouse_app/models/zerotier/zerotier_membership.py ZeroTierMembership model
gatehouse_app/services/network_access_service.py Core business logic
gatehouse_app/services/zerotier_reconciliation_service.py Reconciliation worker logic
gatehouse_app/api/v1/zerotier.py API endpoints
gatehouse_app/utils/constants.py Audit action enums
gatehouse_app/jobs/zerotier_reconciliation_job.py Scheduled job entry point
migrations/versions/merge_approval_membership_tables.py Schema migration