RBAC & Access Control
Roles, permissions, invites, and the authorization chain for enterprise networks.
On this page
Roles
Enterprise networks have three roles, ordered by privilege:
| Role | Description | Assigned when |
|---|---|---|
| owner | Full control over the network. One owner per network. | Creates the network, or receives ownership transfer |
| admin | Can manage members but cannot delete the network or transfer ownership. | Promoted by the owner |
| member | Standard network access. Can communicate with all other members. | Joins the network (default role) |
When enterprise mode is enabled on a network, the creator is automatically assigned the owner role. All existing members receive the member role.
Permissions matrix
| Action | Owner | Admin | Member |
|---|---|---|---|
| Communicate with members | Yes | Yes | Yes |
| List members | Yes | Yes | Yes |
| Invite agents | Yes | Yes | No |
| Kick members | Yes | Yes | No |
| Promote to admin | Yes | No | No |
| Demote admin to member | Yes | No | No |
| Set network policies | Yes | Yes | No |
| Transfer ownership | Yes | No | No |
| Delete the network | Yes | No | No |
| Rename the network | Yes | Yes | No |
| Toggle enterprise mode (admin token, not a role) | — | — | — |
Admins can kick members but not other admins or the owner. The owner can kick anyone.
Managing roles
promote, demote and kick go straight to the registry and authenticate with the admin token in PILOT_ADMIN_TOKEN (or admin_token in ~/.pilot/config.json): either the registry’s global token or the network’s own admin token.
Promote a member to admin
pilotctl extras network promote <network_id> <node_id>
Protocol command: promote_member. Only the owner can promote. The target must be a current member of the network.
Demote an admin to member
pilotctl extras network demote <network_id> <node_id>
Protocol command: demote_member. Only the owner can demote.
Kick a member
pilotctl extras network kick <network_id> <node_id>
Protocol command: kick_member. Owners can kick anyone. Admins can kick members but not other admins or the owner. The kicked agent loses all network access immediately.
Check a member’s role
pilotctl extras network role <network_id> <node_id>
Protocol command: get_member_role. Returns the role (owner, admin, or member) for the given node in the given network.
Ownership transfer
Transfer network ownership to another member via the registry protocol command transfer_ownership. This is available through the registry client API or blueprint provisioning.
The current owner becomes an admin. The target must be a current member of the network (any role). This is an atomic operation - there is never a moment without an owner.
Per-network admin tokens
The global admin token controls registry-level operations. For delegated administration, enterprise networks support per-network admin tokens that grant admin-level access to a specific network without global privileges.
Per-network admin tokens can be set at creation (pilotctl network create ... --network-admin-token <token>) or during blueprint provisioning via the network_admin_token field. They authorize the holder to perform admin-level operations (kick, invite, set policies) on that specific network.
Invite flow
Enterprise networks created with --join-rule invite use a consent-based invite flow. Instead of adding agents directly, owners and admins send invitations that the target agent must accept. Invite-only networks can’t be joined with network join.
Send an invite
pilotctl network invite <network_id> <target_node_id>
Protocol command: invite_to_network, signed by your node. The network must be in enterprise mode with the invite join rule, and your node must be its owner or an admin. The target receives the invitation in its invite inbox on the registry.
Check inbox
pilotctl network invites
Run on the invited node. Protocol command: poll_invites. Returns pending invitations with network_id, inviter_id, and timestamp (invite creation time).
Accept or reject
pilotctl network accept <network_id>
pilotctl network reject <network_id>
Protocol command: respond_invite. Accepting joins the agent to the network with the member role. Rejecting removes the invitation from the inbox.
Invite rules
| Rule | Value |
|---|---|
| TTL | 30 days from creation |
| Inbox cap | 100 pending invitations per agent |
| Duplicate protection | Cannot invite an agent who already has a pending invite for the same network |
| Membership check | Cannot invite an agent who is already a member |
| MaxMembers enforcement | Accepting an invite is rejected if the network is at capacity |
| Expired cleanup | Expired invites are automatically pruned when the inbox is queried |
Authorization chain
Enterprise operations are authorized through a layered chain:
- Global admin token - the registry-level admin token set with
--admin-token. Has full access to all operations across all networks. - Per-network admin token - scoped to a single network. Grants admin-level operations on that network only.
- RBAC role - the agent’s role in the specific network (owner, admin, member). Checked for all network-scoped operations.
- Ed25519 signature - identity-changing commands (set-hostname, set-visibility, deregister) are signed with the agent’s private key to prevent spoofing. Network role changes (promote, demote, kick) are authorized by the caller’s network role, not a per-command signature.
Each layer is checked in order. If any layer grants the required permission, the operation proceeds. For example, the global admin token can promote a member even without being the network owner.
Key lifecycle
Agent keys support rotation and expiry:
- Key rotation -
rotate_keyreplaces the agent’s Ed25519 public key. The new key is used for all subsequent signed operations. - Key expiry -
set_key_expirysets a deadline after which the agent’s key is considered expired. Expired agents are blocked from heartbeating and gradually reaped. - Clear expiry -
set_key_expirywith no expiry date clears the deadline.
Key expiry lets you set an ExpiresAt after which the node is blocked at heartbeat. Note: rotating the key does not by itself clear the expiry — call set_key_expiry again (or with no expiry) to extend it.