dockmesh ships with role-based access control as part of the single binary. Five built-in roles form a privilege ladder; custom roles let you carve permissions and scope arbitrarily for larger teams.
Five roles exist out of the box. They are flagged builtin = 1 in the database — the UI will not let you edit or delete them, but the API returns them so you can read and inspect every grant.
| Role | Tier | What it covers |
|---|
Admin (admin) | Full | Every permission on every resource: users, roles, agents, system settings, hash-chain audit, system upgrade. |
Host-Admin (host-admin) | Full ops, no governance | Full read/write on stacks, containers, images, volumes, networks, hosts, backups, alerts, proxy, templates, registries. No user/role management, no system upgrade, audit is view + export but not write. Designed for engineers who own the fleet but not the platform. |
Deployer (deployer) | Author + run | Operator’s grants plus create/update/delete on stacks and create on images/volumes/networks. The “I write compose files and ship them” tier. |
Operator (operator) | Run existing | Read everything; start/stop/restart containers, shell-in, view logs, deploy/stop pre-existing stacks, run image scans. Cannot create new stacks, volumes, networks, or images. |
Viewer (viewer) | Read-only | List + inspect every resource, view audit log, view metrics. No mutations of any kind. |
Every user has exactly one role. New local users default to viewer; users created via SSO get whatever the provider’s group-mapping rule resolves to (see SSO). Change a user’s role from Users → Users tab → role dropdown (requires users.update).
Permissions follow the resource.verb pattern — one permission per (resource, action) pair, separated so a custom role can grant “view stacks” without “deploy stacks”, or “create networks” without “delete networks”.
The 17 resource families and their permissions:
| Permission | What it allows |
|---|
containers.view | List + inspect containers, view metrics |
containers.update | Start / stop / restart / pause / unpause / kill |
containers.delete | Remove a container |
containers.exec | Shell into a container (sensitive — full code exec) |
containers.logs | Stream live logs (sensitive — PII risk) |
| Permission | What it allows |
|---|
stacks.view | List + inspect stacks |
stacks.create | Write a new compose.yaml to disk |
stacks.update | Edit an existing compose.yaml |
stacks.delete | Remove a stack |
stacks.deploy | Deploy / stop / scale (sensitive — runs containers) |
stacks.migrate | Move a stack between hosts |
stacks.adopt | Adopt a Compose project already running on a host |
| Permission | What it allows |
|---|
volumes.view | List + inspect volumes |
volumes.create | Create a named volume |
volumes.delete | Remove + prune volumes |
volumes.browse | Walk the file tree inside a volume (PII risk) |
volumes.read_file | Read a single file’s content from a volume |
| Permission | What it allows |
|---|
networks.view | List + inspect networks |
networks.create | Create a Docker network |
networks.delete | Remove + prune networks |
| Permission | What it allows |
|---|
images.view | List images, inspect digests + layers |
images.create | Pull from a registry (= create a local copy) |
images.delete | Remove + prune images |
images.scan | Run a Grype CVE scan |
| Permission | What it allows |
|---|
registries.view | List configured registries (credentials masked) |
registries.create | Add a new registry credential |
registries.update | Edit an existing registry credential |
registries.delete | Remove a registry credential |
| Permission | What it allows |
|---|
hosts.view | List hosts and read their status / metrics |
hosts.create | Enrol a new agent (issue token) |
hosts.update | Drain, upgrade, edit an existing host |
hosts.delete | Revoke + remove a host |
hosts.tag | Edit a host’s tags |
| Permission | What it allows |
|---|
users.view | List + inspect users |
users.create | Create a local user |
users.update | Edit a user’s email, role, scope tags |
users.delete | Remove a user |
users.password_reset | Force-reset another user’s password |
users.suspend | Disable a user without deleting them |
| Permission | What it allows |
|---|
roles.view | List + inspect roles (built-in + custom) |
roles.create | Define a new custom role |
roles.update | Edit a custom role’s permissions or scope rows |
roles.delete | Remove a custom role (built-ins refuse) |
| Permission | What it allows |
|---|
tokens.view | List your own API tokens |
tokens.create | Create an API token for yourself |
tokens.delete | Revoke your own API token |
tokens.manage_others | Admin override: list / revoke others’ tokens |
| Permission | What it allows |
|---|
backups.view | List backup jobs, targets, runs |
backups.create | Define a new job or target |
backups.update | Edit an existing job or target |
backups.delete | Remove a job or target (runs are kept) |
backups.restore | Restore a stack from a run (sensitive — overwrites) |
| Permission | What it allows |
|---|
proxy.view | List proxy routes + see proxy on/off state |
proxy.create | Add a new route |
proxy.update | Edit an existing route (toggle proxy, change TLS mode) |
proxy.delete | Remove a route |
| Permission | What it allows |
|---|
alerts.view | List rules + channels + history |
alerts.create | Define a new rule or channel |
alerts.update | Edit an existing rule or channel |
alerts.delete | Remove a rule or channel |
| Permission | What it allows |
|---|
templates.view | List + inspect templates |
templates.create | Define a new template |
templates.update | Edit an existing template (built-ins refuse) |
templates.delete | Remove a template (built-ins refuse) |
| Permission | What it allows |
|---|
audit.view | Read the tamper-evident audit log + run chain verify |
audit.export | Stream the full log to an external sink (reserved) |
audit.write | Insert synthetic audit rows (admin-only, integrations) |
| Permission | What it allows |
|---|
system.view | View system metrics + settings |
system.update | Edit settings, manage global env vars, edit proxy config |
system.upgrade | Trigger a server self-update |
| Permission | What it allows |
|---|
metrics.view | Read /metrics Prometheus endpoint with a scoped token |
Total: ~50 permissions. The full enumeration ships in GET /api/v1/roles/permissions so the custom-role editor can render the live list — that endpoint is also the source of truth for any external automation.
Users & Roles → Roles tab lists every role with its permission count. Click the count for any role (including the built-in ones) to open a read-only modal showing exactly which permissions that role holds, grouped by prefix (container.*, stack.*, …).
Users & Roles → Roles tab → New role opens a form with:
- Name — lowercase identifier, used internally (e.g.
devops)
- Display name — shown in dropdowns (e.g.
DevOps Engineer)
- Permissions — checkbox matrix of the permissions table above, grouped by prefix with a per-group “all” toggle
Custom roles are saved to the database and appear alongside the built-ins in the user-role dropdown and in OIDC provider config.
Permissions answer “what can this role do?”. Scopes answer “which resources can it do it to?”. dockmesh has scopes at two layers, both optional:
- Role-level scopes (
role_scopes table) — define on the role itself, in the Roles tab. Each row is a (scope_type, scope_value) pair where scope_type ∈ {host, stack, host_tag} and scope_value is the host id, the stack name, or the tag string. Multiple entries OR together — a role scoped to host=prod-01 plus stack=monitoring matches either condition. Built-in roles have no scope entries → they apply globally.
- User-level scope tags (
users.scope_tags) — extra restriction layered on top of the role’s scopes. Edit from Users → Users tab → scope icon. Same <scope_type>:<scope_value> syntax (host:local, stack:web-platform, host_tag:prod). An empty list means “use whatever the role allows”.
When a request comes in, dockmesh verifies the permission then walks both scope layers — the request is allowed only if both layers grant it (or are empty, meaning unrestricted at that layer). The check fires in middleware before any mutation runs; bypassing the UI by calling REST directly hits the same gate.
Tag your hosts from Hosts → host detail → edit tags so the host_tag scope type has something to match against.
The common pattern:
- Tag hosts by team:
team-frontend, team-backend, team-data.
- Create a custom role
Team-Operator with the relevant permissions (e.g. containers.view/update/exec/logs, stacks.view/deploy, images.view/scan, audit.view) plus a role-scope row host_tag = team-frontend.
- Assign that role to the team’s users. No per-user scope tags needed because the role itself already restricts the reach.
Result: engineers in each team see and manage only their own team’s fleet without any per-user tag bookkeeping.
Buttons and forms for actions a user isn’t permitted to take are hidden, not just disabled — the UI reads GET /api/v1/me on login and gates every action through allowed(permission) checks. A Viewer never sees the “Deploy” button; an Operator never sees the “Edit compose” tab.
The backend re-checks the same permission before executing. Scopes are enforced there too: a scoped Operator who pokes at the API with a stack on a forbidden host gets a 403 regardless of what the UI shows.
Every RBAC-relevant action writes a row to the audit log: role changes, permission grants/revokes, scope edits, user create/delete, password resets, 2FA resets. See Audit log for the full coverage and the hash-chain integrity story.