6.2 KiB
ZeroTier Kill Switch
Overview
The kill-switch mechanism provides emergency deactivation of ZeroTier network access at three granularities: a single device membership, all memberships for a user, or all memberships on a network. All kill operations are reversible — they set active=False and (in most cases) status=SUSPENDED but do not delete records, so affected users can re-activate or re-authenticate.
Three Kill Operations
| Granularity | Endpoint | Admin-only | Behavior |
|---|---|---|---|
| Device X on network Y | POST /orgs/<id>/memberships/<id>/deactivate |
No (owner can self-deactivate) | Sets active=False. Status stays APPROVED. |
| All devices for a user | POST /orgs/<id>/kill-switch |
Yes | Sets active=False, status=SUSPENDED for every active membership in the org (optionally filtered to specific networks). |
| All devices on a network | POST /orgs/<id>/networks/<id>/kill-switch |
Yes | Sets active=False, status=SUSPENDED for every active membership on the network, across all users. |
Detailed Endpoint Reference
1. Kill a Single Membership (Device + Network)
POST /api/v1/organizations/<org_id>/memberships/<membership_id>/deactivate
Auth: @login_required, @full_access_required
Admin override: Admins can deactivate any membership; non-admins can only deactivate their own.
Request body: None
Response (200):
{
"success": true,
"data": {
"request": {
"id": "...",
"active": false,
"status": "approved",
...
}
},
"message": "Request deactivated successfully"
}
Behavior:
- Ends the active
ActivationSessionwith reasonmanual_revoke - De-authorizes the device node in the ZeroTier controller
- Sets
request.active = False(status unchanged — staysapproved) - Logs
zt.membership.deactivatedaudit event
Re-activation: The user can re-activate via POST /memberships/<id>/activate or POST /memberships/activate-all.
2. Kill All Devices for a User
POST /api/v1/organizations/<org_id>/kill-switch
Auth: @login_required, @require_admin, @full_access_required
Request body:
{
"target_user_id": "uuid-of-user-to-kill",
"scope": "organization",
"network_ids": ["uuid-of-network-1", "uuid-of-network-2"],
"reason": "Security incident — force deactivation"
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
target_user_id |
string (UUID) | yes | — | The user to deactivate |
scope |
string | no | "organization" |
"organization" (all networks) or "selected_networks" |
network_ids |
array of UUIDs | no | null |
Required when scope is selected_networks |
reason |
string | no | null |
Max 500 chars |
Response (200):
{
"success": true,
"data": {
"affected_count": 3
},
"message": "Kill switch triggered successfully"
}
Behavior:
- Queries all active, non-deleted
NetworkAccessRequestrows for the target user - For each: ends session (reason
kill_switch), de-authorizes in ZT - Sets
active = False, and setsstatus = SUSPENDEDif currentlyAPPROVED - Logs
zt.kill_switch.activatedaudit event withaffected_countandscope
Re-activation: The user's memberships are in SUSPENDED state. An admin must explicitly re-approve (change status back to APPROVED) before the user can re-activate.
3. Kill All Devices on a Network
POST /api/v1/organizations/<org_id>/networks/<network_id>/kill-switch
Auth: @login_required, @require_admin, @full_access_required
Request body:
{
"reason": "Network compromised — emergency deactivation"
}
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
reason |
string | no | null |
Max 500 chars |
Response (200):
{
"success": true,
"data": {
"affected_count": 12
},
"message": "Network kill switch triggered successfully"
}
Behavior:
- Queries all active, non-deleted
NetworkAccessRequestrows for the network, regardless of user - For each: ends session (reason
kill_switch), de-authorizes in ZT - Sets
active = False, andstatus = SUSPENDEDif currentlyAPPROVED - Logs
zt.network_kill_switch.activatedaudit event withaffected_count
Re-activation: Same as user kill switch — each affected membership is SUSPENDED and needs admin re-approval.
Comparison: Deactivation vs. Deletion
| Operation | active | status | deleted_at | DB row | Reversible? |
|---|---|---|---|---|---|
POST /memberships/<id>/deactivate |
false |
unchanged | null |
preserved | Yes — re-activate |
POST /kill-switch (user) |
false |
suspended |
null |
preserved | Yes — admin re-approve |
POST /networks/<id>/kill-switch |
false |
suspended |
null |
preserved | Yes — admin re-approve |
DELETE /memberships/<id> (soft) |
false |
unchanged | set | preserved | Partial — depends on join logic |
DELETE /admin/memberships/<id> |
false |
— | — | hard-deleted | No |
Audit Events
| Event | Trigger |
|---|---|
zt.membership.deactivated |
Single membership deactivated (endpoint #1) |
zt.kill_switch.activated |
User kill switch triggered (endpoint #2) |
zt.network_kill_switch.activated |
Network kill switch triggered (endpoint #3) |
All audit entries include organization_id, user_id (the actor), resource_type, resource_id, and metadata (affected count, scope, network IDs).
Key Source Files
| File | Purpose |
|---|---|
gatehouse_app/api/v1/zerotier.py |
Route handlers for all three endpoints |
gatehouse_app/services/network_access_service.py |
deactivate_request(), kill_switch(), kill_switch_network() |
gatehouse_app/services/zerotier_api_service.py |
deauthorize_member() — ZT controller call |
gatehouse_app/utils/constants.py |
AuditAction and KillSwitchScope enums |
gatehouse_app/models/zerotier/network_access_request.py |
NetworkAccessRequest model |
gatehouse_app/models/zerotier/activation_session.py |
ActivationSession model |
gatehouse_app/models/zerotier/kill_switch_event.py |
KillSwitchEvent model |
tests/integration/test_zerotier.py |
Integration tests in TestZeroTierMembership |