RBAC Configuration

Mezite uses role-based access control (RBAC) to govern who can SSH into which nodes. Roles define allow and deny rules that match against node labels, specify allowed OS logins, and set session options. This guide covers the role system in depth: structure, label matching, deny-overrides-allow semantics, template variables, built-in roles, and practical examples.


Role Structure

A Mezite role has three main sections: allow, deny, and options. The allow and deny sections specify which nodes a user can or cannot access. Options control session behavior.

Role definition files are JSON. A file has a metadata object (the role's name and description) and a spec object; anything else in the file is ignored. Every label selector value is a list of strings, never a bare string — {"env": ["staging"]}, not {"env": "staging"}. A bare string is rejected when the file is parsed.

example-role.json json
{
  "metadata": {
    "name": "example-role",
    "description": "Human-readable description of this role"
  },
  "spec": {
    "options": {
      "max_session_ttl": "12h",
      "require_session_mfa": "",
      "port_forwarding": true,
      "file_copy": true
    },
    "allow": {
      "node_labels": { "env": ["staging"] },
      "logins": ["ubuntu", "deploy"],
      "request_roles": ["ssh-production"],
      "review_roles": ["ssh-production"]
    },
    "deny": {
      "node_labels": { "sensitivity": ["restricted"] },
      "logins": ["root"]
    }
  }
}
  • options.max_session_ttl — maximum session TTL for holders of this role, written as a duration string.
  • options.require_session_mfa — MFA challenge required before each session; empty means none.
  • allow.node_labels — which nodes the role grants access to; allow.logins — which OS logins are permitted.
  • allow.request_roles / allow.review_roles — roles this user may request, and whose requests they may review.
  • deny — same shape, and always wins over allow.

Label-Based Matching

Labels are key-value pairs attached to nodes via the agent configuration. Roles grant or deny SSH access based on label selectors.

Exact Match

Exact label match — nodes with env=production AND team=backend json
"node_labels": {
  "env": ["production"],
  "team": ["backend"]
}

Wildcard Match

Wildcard label match json
// Any value for the env label
"node_labels": { "env": ["*"] }

// All nodes (any label, any value)
"node_labels": { "*": ["*"] }

Multi-Value Match

Multi-value label match — env is staging OR development json
"node_labels": {
  "env": ["staging", "development"]
}

Regex Match

Regex label match — team starts with eng- json
"node_labels": {
  "team": ["^eng-.*$"]
}

When multiple labels are specified, all must match (AND logic). When multiple values are specified for a single label, any can match (OR logic).


Deny Overrides Allow

Mezite uses a deny-overrides-allow evaluation model. When a user has multiple roles, the system:

  1. Collects all allow rules from every role assigned to the user.
  2. Collects all deny rules from every role assigned to the user.
  3. Grants access only if at least one allow rule matches AND no deny rule matches.

This means a single deny rule in any role will block access, even if other roles explicitly allow it. Use deny rules sparingly and intentionally.

ssh-all-production.json — allows SSH to all production nodes json
{
  "metadata": { "name": "ssh-all-production" },
  "spec": {
    "allow": {
      "node_labels": { "env": ["production"] },
      "logins": ["ubuntu", "deploy"]
    },
    "deny": {}
  }
}
deny-pci.json — denies SSH to PCI nodes json
{
  "metadata": { "name": "deny-pci" },
  "spec": {
    "allow": {},
    "deny": {
      "node_labels": { "compliance": ["pci"] },
      "logins": ["root", "ubuntu", "deploy"]
    }
  }
}

If a user holds both roles:

  • They CAN access env=production nodes (allowed by the first).
  • They CANNOT access compliance=pci nodes (denied by the second).
  • A production node that is also compliance=pci is DENIED — deny wins.

Allowed Logins

The logins field in a role specifies which OS-level usernames the Mezite user can authenticate as on remote nodes. The login requested at connection time (via msh ssh --login=) must appear in the allow.logins list and must not appear in the deny.logins list.

Allowed logins json
"spec": {
  "allow": {
    "node_labels": { "env": ["production"] },
    "logins": ["ubuntu", "deploy", "{{internal.logins}}"]
  },
  "deny": {
    "logins": ["root"]
  }
}

{{internal.logins}} expands to the user's own Mezite username. The deny entry here blocks root even if another of the user's roles permits it.


Template Variables

Roles support template variables that are expanded at evaluation time. This allows you to write generic roles that adapt to each user based on their identity and traits.

Variable Description Example Value
{{internal.logins}}Currently resolves to a single-element list containing the user's Mezite username — the auth server overrides any operator-supplied logins trait at certificate-issue time. Multi-login support via traits is not implemented yet.["alice"]
{{internal.<trait>}}Any other trait recorded on the user record{{internal.team}}platform
{{external.<trait>}}A trait propagated from an SSO connector (e.g. external.username, external.email)alice@example.com
{{email.local(external.email)}}Function: returns the local part of an email addressalice
team-scoped-ssh.json json
{
  "metadata": {
    "name": "team-scoped-ssh",
    "description": "SSH access scoped to the user's team nodes"
  },
  "spec": {
    "allow": {
      "node_labels": { "team": ["{{internal.team}}"] },
      "logins": ["{{internal.logins}}", "{{external.username}}"]
    },
    "deny": {}
  }
}

When a user with the team=platform trait uses this role, the label selector expands to team: platform, restricting them to nodes owned by their team.


Session Options

The options section of a role controls session behavior. When a user has multiple roles, the most restrictive option value wins.

Option Type Default Description
max_session_ttlduration12hTwo things: the ceiling on how long a live SSH session may run, and the cap on the lifetime of certificates issued cross-cluster under this role. The shortest non-zero value across all of a user's roles wins, and the cluster-wide proxy.max_session_duration is applied on top — the effective cap is the smaller of the two, and the cluster cap still applies to a role that sets no TTL.
idle_timeoutduration0 (disabled)Terminate an SSH session after this long with no terminal activity. Purely an RBAC setting — the shortest non-zero value across the user's roles wins. proxy.idle_timeout is a separate HTTP-listener setting and never kills an SSH session.
require_session_mfastring""Require a per-session MFA challenge before the session starts. One of totp, hardware_key, hardware_key_touch, hardware_key_pin, hardware_key_touch_and_pin, or empty. The most restrictive value across the user's roles wins, in that order (hardware_key_touch_and_pin strongest, totp weakest).
port_forwardingboolfalseAllow SSH local port forwarding (ssh -L), the tunnel that IDE remote-development tools such as VS Code Remote-SSH rely on. Enabled when any of the user's roles sets it true; otherwise the forwarding channel is refused and an access.denied.port_forwarding audit event (code T4002W) is emitted, while a granted forward records port_forwarding.start (T4001I) with the target address. Of the shipped roles only admin sets this true — ssh-access does not, so its holders cannot port-forward unless another role grants it. The field's bare zero value is false, so a custom role that omits it denies forwarding. Forwarded traffic is not session content and is not recorded.
disconnect_expired_certboolfalseForcibly disconnect a live session the moment the client certificate expires. Default (off) leaves an established session running past expiry — only the next connection has to re-authenticate — which matches normal SSH behavior. Turn it on for a hard cutoff (useful for long-lived IDE sessions). Enabled when any of the user's roles sets it true (OR across roles).
file_copyboolfalseAllow SCP / SFTP file transfers. Enabled when any of the user's roles sets it true; otherwise transfers are refused and the proxy emits access.denied.file_copy.
forward_agentboolfalseAllow SSH agent forwarding (ssh -A / ForwardAgent yes). Enabled when any role allows it.
create_host_userboolfalseAuto-provision the requested OS login on the node when it does not exist (agent-based nodes only). Pairs with the host_groups / host_sudoers fields in allow.
create_host_user_modestringoffCleanup policy for auto-provisioned host users: keep (leave the account in place after session end), drop (remove on last-session close), or off. Most-permissive wins (keep > drop > off).
create_host_user_default_shellstring""Default login shell for auto-provisioned host users. First non-empty value across the user's roles wins.
device_trust_modestring""Require the session to come from an enrolled trusted device. required rejects a login whose certificate carries no verified device ID; optional lets the login through without one; off (or empty) skips the check. required wins over optional across a user's roles. Enforced on SSH and on application TCP tunnels alike.
auditd_enabledboolfalseEmit auditd hand-off events for the session (Linux auditd). Any role with the option set to true enables it.
pin_source_ipboolfalsePin the issued certificate to the client IP at login time. A subsequent connection from a different IP is denied with access.denied / T4001W. Any role with the option set to true enables it.
require_session_joinlist[]Repeatable. Each entry requires a population of users (matched by a predicate over their roles) to join the session in a given mode (observer or peer) before the session unblocks. The session is held at start time until the join condition is met. on_leave: terminate ends the session if the required participants leave.

allow.host_groups, allow.host_sudoers

Used in combination with create_host_user: true. When a host user is auto-provisioned on a node, the agent adds them to every group listed in allow.host_groups across the user's roles, and writes any allow.host_sudoers fragments into /etc/sudoers.d/mezite-<login>. Both lists union across roles — write minimal roles and let assignment do the addition.

ssh-restricted.json json
{
  "metadata": { "name": "ssh-restricted" },
  "spec": {
    "options": {
      "max_session_ttl": "4h",
      "require_session_mfa": "totp",
      "port_forwarding": false,
      "file_copy": false
    },
    "allow": {
      "node_labels": {
        "env": ["production"],
        "sensitivity": ["high"]
      },
      "logins": ["ubuntu"]
    },
    "deny": {
      "logins": ["root"]
    }
  }
}

Built-in Roles

Mezite bootstraps four built-in roles on startup. You can assign these directly or use them as templates for custom roles.

admin

Full cluster administrator. SSH access to every node, all administrative resources, and the ability to join any moderated session in any mode.

Built-in: admin json
{
  "metadata": {
    "name": "admin",
    "description": "Full cluster administrator"
  },
  "spec": {
    "options": {
      "max_session_ttl": "12h",
      "forward_agent": true,
      "port_forwarding": true,
      "file_copy": true
    },
    "allow": {
      "logins": ["root", "{{internal.logins}}", "testuser", "ubuntu"],
      "node_labels": { "*": ["*"] },
      "app_labels": { "*": ["*"] },
      "join_sessions": [{ "name": "admin-any-session" }],
      "rules": [{ "resources": ["*"], "verbs": ["*"] }]
    }
  }
}

editor

Can manage users, roles, tokens, and connectors. Read access to sessions, audit events, and nodes — but no SSH access by default.

Built-in: editor json
{
  "metadata": {
    "name": "editor",
    "description": "Can manage users and roles but limited server access"
  },
  "spec": {
    "options": { "max_session_ttl": "8h" },
    "allow": {
      "rules": [
        { "resources": ["user", "role", "token", "connector"], "verbs": ["*"] },
        { "resources": ["session", "event", "node"], "verbs": ["read", "list"] }
      ]
    }
  }
}

viewer

Read-only access to cluster state — users, roles, tokens, nodes, sessions, and audit events. Cannot SSH into nodes.

Built-in: viewer json
{
  "metadata": {
    "name": "viewer",
    "description": "Read-only access to cluster state"
  },
  "spec": {
    "options": { "max_session_ttl": "4h" },
    "allow": {
      "rules": [
        {
          "resources": ["user", "role", "token", "node", "session", "event"],
          "verbs": ["read", "list"]
        }
      ]
    }
  }
}

ssh-access

Basic SSH access to non-production nodes. Allows logging in as the external SSO username or ubuntu on nodes labelled env=staging or env=dev; denies env=production.

Built-in: ssh-access json
{
  "metadata": {
    "name": "ssh-access",
    "description": "Basic SSH access to non-production nodes"
  },
  "spec": {
    "options": {
      "max_session_ttl": "8h",
      "forward_agent": true,
      "file_copy": true
    },
    "allow": {
      "logins": ["{{external.username}}", "ubuntu"],
      "node_labels": { "env": ["staging", "dev"] }
    },
    "deny": {
      "node_labels": { "env": ["production"] }
    }
  }
}

Note that ssh-access does not set port_forwarding, so its holders cannot open forwarded ports.


Creating Roles with mezctl

Use mezctl to create, update, and manage roles.

Role management commands bash
# Create a role from a JSON file. Only "metadata" and "spec" are read.
mezctl roles create --from-file=ssh-production.json

# Or inline with --name and --spec
mezctl roles create --name=ssh-production --spec='{"options":{},"allow":{}}'

# List all roles
mezctl roles ls
# NAME            VERSION  DESCRIPTION
# admin           v1       Full cluster administrator
# editor          v1       Can manage users and roles but limited server access
# viewer          v1       Read-only access to cluster state
# ssh-access      v1       Basic SSH access to non-production nodes
# ssh-production  v1       SSH access to production nodes

# Show a specific role (full spec rendered as JSON)
mezctl roles get ssh-production

# Update a role from a file
mezctl roles update --from-file=ssh-production.json

# Delete a role
mezctl roles delete ssh-production

# Assigning roles to a user
# There is no in-place "update user roles" CLI today. The roles list is
# set when the user is created (mezctl users create --roles=...); to
# change a user's roles, modify the user via the gRPC API directly or
# recreate the user with the new role set.

Example Role Definitions

SSH to Production (Non-Root)

ssh-production.json json
{
  "metadata": {
    "name": "ssh-production",
    "description": "SSH access to production nodes, non-root"
  },
  "spec": {
    "options": { "max_session_ttl": "8h" },
    "allow": {
      "node_labels": { "env": ["production"] },
      "logins": ["ubuntu", "deploy"]
    },
    "deny": {
      "logins": ["root"],
      "node_labels": { "sensitivity": ["restricted"] }
    }
  }
}

Team-Scoped Access with Template Variables

ssh-team-scoped.json json
{
  "metadata": {
    "name": "ssh-team-scoped",
    "description": "SSH access restricted to the user's team nodes"
  },
  "spec": {
    "options": { "max_session_ttl": "12h" },
    "allow": {
      "node_labels": { "team": ["{{internal.team}}"] },
      "logins": ["{{internal.logins}}", "ubuntu"]
    },
    "deny": {
      "logins": ["root"]
    }
  }
}

Staging-Only with File Copy Disabled

ssh-staging-readonly.json json
{
  "metadata": {
    "name": "ssh-staging-readonly",
    "description": "SSH to staging, no file transfers or port forwarding"
  },
  "spec": {
    "options": {
      "max_session_ttl": "4h",
      "port_forwarding": false,
      "file_copy": false
    },
    "allow": {
      "node_labels": { "env": ["staging"] },
      "logins": ["ubuntu"]
    },
    "deny": {}
  }
}

Requestable Production Access

No direct node access — only permission to request another role.

can-request-production.json json
{
  "metadata": {
    "name": "can-request-production",
    "description": "Can request temporary production SSH access"
  },
  "spec": {
    "allow": {
      "request_roles": ["ssh-production"]
    },
    "deny": {}
  }
}

Role Evaluation Order

When a user attempts to SSH into a node, Mezite evaluates roles in this order:

  1. Collect all roles assigned to the user.
  2. Merge all allow rules — the union of all allowed node labels and logins.
  3. Merge all deny rules — the union of all denied node labels and logins.
  4. Check if any allow rule matches the target node and requested login.
  5. Check if any deny rule matches the target node and requested login.
  6. Grant access only if step 4 is true and step 5 is false.

Debugging Role Evaluation

There is no dedicated mezctl access check command yet — debug role evaluation by inspecting the user and each of their assigned roles, and by watching the audit log for denial events (any access.denied* event records the user, the action that was attempted, and a short reason; it does not name the specific role or rule that fired the denial, so you have to cross-reference against the user's assigned roles).

Inspect users and roles bash
# List users and the roles each one holds
mezctl users list
# USERNAME  ROLES                       STATUS
# alice     ssh-access,ssh-production   active

# Print the full spec for a role
mezctl roles get ssh-production

# Watch the audit log for denial events emitted at session setup
mezctl audit ls --type=access.denied --since=1h
mezctl audit ls --type=access.denied.port_forwarding --since=1h
mezctl audit ls --type=access.denied.file_copy --since=1h

Next Steps

  • SSH Access — Apply SSH-specific RBAC policies.
  • Access Requests — Set up approval workflows for elevated access.
  • SSO Setup — Map SSO attributes to Mezite roles automatically.
  • Audit Logging — Monitor role evaluation in audit logs.