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 |