User roles and permissions

Updated 2026-06-22

Last Updated: 2026-06-22 · Applies to: OpenWatch 0.2.0-rc series (Go single-binary)

This guide describes the role-based access control (RBAC) system in the Go-era OpenWatch. It covers the five built-in roles, the permissions they grant, and how you create users and assign roles from the single openwatch binary.

OpenWatch runs as one Go binary that serves the REST API and the embedded React UI over HTTPS on port 8443. All RBAC state lives in PostgreSQL. There is no separate web tier, container runtime, or Python service.

Source of truth

RBAC is registry-driven. Do not hand-edit role or permission lists; they are generated from one file.

ArtifactPathRole
Permission and role registryauth/permissions.yamlThe single source you edit
Generated permission constantsinternal/auth/permissions.gen.goTyped Go constants (do not edit)
Generated role definitionsinternal/auth/roles.gen.goBuilt-in roles with wildcards expanded (do not edit)
Design referencedocs/engineering/rbac_registry.mdRationale, codegen workflow, custom-role design
API contractapi/openapi.yamlx-required-permission per operation, paths under /api/v1

When this guide and the registry disagree, the registry wins. Regenerate the Go code with make generate-rbac after changing auth/permissions.yaml.

Built-in roles

OpenWatch ships five built-in roles. They form a single privilege ladder from read-only to full administration; there is no parallel "compliance officer" or "guest" track. Built-in roles are loaded into the roles table by migration with is_built_in = true, so the API rejects attempts to modify them.

Role IDDescriptionPermission count
viewerRead-only access across the platform16
auditorRead-only plus exception authority and audit export20
ops_leadDay-to-day operations: hosts, scans, alerts32
security_adminFull security operations including dangerous and license-gated actions56
adminFull system administrationAll permissions (bare * wildcard)

A user may hold more than one role. Their effective permission set is the union of every assigned role's permissions.

viewer

Read-only across every domain. Grants *:read-style permissions for hosts, scans, scan templates, compliance state, baselines, exceptions, alerts, notifications, license, policy, remediation, integrations, audit, system, and roles, plus auth:read for the user's own profile.

Cannot write, execute, export, approve, or administer anything.

auditor

Everything viewer has, plus the exception workflow authority an auditor needs: exception:request, exception:comment, and exception:approve. Adds audit:export (license-gated by the audit_export feature) and auth:write so the auditor can manage their own password, MFA, and sessions.

Cannot create or modify hosts, run scans, or touch system configuration.

ops_lead

The day-to-day operator. Adds write and execute authority over the operational surface: host:write, host:connectivity_check, host:intelligence_refresh, credential:read, scan:execute, scan:cancel, scan_template:write, baseline:write, alert acknowledge/resolve, notification:test, remediation:request, and the exception request/comment verbs.

Cannot delete hosts, manage credentials beyond reading them, approve remediations, install licenses or policies, or manage users.

security_admin

Full security operations. Grants category wildcards (host:*, credential:*, scan:*, scan_template:*, baseline:*, exception:*, alert:*, notification:*, remediation:*, integration:*, audit:*) plus user:read, user:write, license:install, and the policy reload/install verbs. This includes the dangerous and license-gated actions remediation:execute and remediation:rollback (both gated by the remediation_execution feature).

Cannot perform the high-privilege admin:* bundle: managing other users' roles, SSO providers, retention policy, system settings, or user:delete.

admin

Full system administration. Holds the bare * wildcard, which is reserved exclusively for this built-in role and cannot be granted to a custom role. Adds the admin:* bundle (user_manage, role_manage, retention_policy, sso_provider, system_setting), user:delete, role:assign, role:write, license:revoke, and system:config_write.

Permission model

Permissions are named resource:action, both lowercase (for example host:read, scan:execute, remediation:rollback). The registry defines 19 categories. Two attributes affect enforcement:

  • dangerous: true marks destructive or high-impact actions (for example host:delete, license:install, user:delete). The UI uses this for confirmation prompts and the audit middleware records denials at high priority.
  • license_gated: <feature> makes a permission inert unless the active license enables that feature. A role may grant the permission, but the combined RBAC-plus-license middleware denies the call with 402 until the license enables it. Today this applies to audit:export (audit_export) and remediation:execute / remediation:rollback (remediation_execution).

Enforcement happens in middleware generated from the OpenAPI x-required-permission extension, so handlers never check RBAC inline. A request with a missing or insufficient permission returns 403 with error.code = "authz.permission_denied" and emits an authz.permission_denied audit event.

Permissions matrix

Y = granted, - = not granted. License-gated permissions are marked (LG); they are granted by the role but require the matching license feature at runtime.

Permissionviewerauditorops_leadsecurity_adminadmin
auth:readYYYYY
auth:write-YYYY
user:read---YY
user:write---YY
user:delete----Y
host:readYYYYY
host:write--YYY
host:delete---YY
host:connectivity_check--YYY
host:intelligence_refresh--YYY
credential:read--YYY
credential:write---YY
credential:delete---YY
scan:readYYYYY
scan:execute--YYY
scan:cancel--YYY
scan_template:readYYYYY
scan_template:write--YYY
scan_template:delete---YY
compliance:readYYYYY
baseline:readYYYYY
baseline:write--YYY
baseline:delete---YY
exception:readYYYYY
exception:request-YYYY
exception:comment-YYYY
exception:approve-Y-YY
exception:revoke---YY
alert:readYYYYY
alert:acknowledge--YYY
alert:resolve--YYY
alert:write---YY
notification:readYYYYY
notification:write---YY
notification:delete---YY
notification:test--YYY
license:readYYYYY
license:install---YY
license:revoke----Y
policy:readYYYYY
policy:reload---YY
policy:install---YY
remediation:readYYYYY
remediation:request--YYY
remediation:approve---YY
remediation:execute (LG)---YY
remediation:rollback (LG)---YY
integration:readYYYYY
integration:write---YY
integration:execute---YY
audit:readYYYYY
audit:export (LG)-Y-YY
system:readYYYYY
system:config_write----Y
role:readY---Y
role:write----Y
role:assign----Y
admin:user_manage----Y
admin:role_manage----Y
admin:retention_policy----Y
admin:sso_provider----Y
admin:system_setting----Y

security_admin grants audit:*, which includes audit:export; the auditor row grants audit:export explicitly. Both depend on the audit_export license feature at runtime.

Creating the first admin

The first admin is created from the CLI, not the API. The create-admin subcommand creates the user and assigns the built-in admin role in one step. It requires --username and --email; the password is read from stdin when --password is omitted, and is held to the 15-character admin policy.

sudo -u openwatch env $(cat /etc/openwatch/secrets.env | xargs) \
  openwatch --config /etc/openwatch/openwatch.toml \
  create-admin --username admin --email admin@example.com

The command connects to PostgreSQL using OPENWATCH_DATABASE_DSN from /etc/openwatch/secrets.env and exits non-zero if the user is created but the role assignment fails, so you can detect a partial state. See docs/guides/INSTALLATION.md for the full install sequence (openwatch migrate, create-admin, systemctl enable --now openwatch).

Managing users and roles through the API

Once an admin exists, manage users over HTTPS at https://<host>:8443 under /api/v1. Authenticate at POST /api/v1/auth/login to obtain a bearer token, then call the user and role endpoints. The required permission for each is below.

OperationMethod and pathRequired permission
List usersGET /api/v1/usersuser:read
Fetch a userGET /api/v1/users/{id}user:read
Create a userPOST /api/v1/usersuser:write
Soft-delete a userDELETE /api/v1/users/{id}user:delete
Assign a rolePOST /api/v1/users/{id}/roles:assignrole:assign
Remove a rolePOST /api/v1/users/{id}/roles:unassignrole:assign
List built-in rolesGET /api/v1/rolesrole:read
Create a custom rolePOST /api/v1/roles:createrole:write
Effective permissions for the callerGET /api/v1/auth/me/permissionsauthenticated
Full RBAC registryGET /api/v1/auth/permissions:registryauthenticated

Creating a user does not assign a role. POST /api/v1/users takes only username, email, and password; role assignment is a separate roles:assign call. Among the built-in roles, only admin holds role:assign and user:delete.

Assign a role by posting the role id:

curl -sS -X POST "https://<host>:8443/api/v1/users/<user_id>/roles:assign" \
  -H "Authorization: Bearer <admin_token>" \
  -H "Content-Type: application/json" \
  -d '{"role_id": "ops_lead"}'

A roles:assign call with an unknown role id returns 400. The roles:unassign call is idempotent and returns 204 whether or not the role was present.

Custom roles

The registry supports custom, DB-stored roles created at runtime via POST /api/v1/roles:create (requires role:write). A custom role may grant any registry permission and category wildcards such as host:*, but not the bare * wildcard, which is reserved for the built-in admin role. Every permission a custom role lists is validated against the registry; unknown permissions are rejected with 400.

For the custom-role design, validation rules, and the relationship between wildcards and newly added permissions, see docs/engineering/rbac_registry.md.

  • docs/engineering/rbac_registry.md — RBAC design, codegen workflow, custom roles
  • docs/guides/INSTALLATION.md — install, migrate, create-admin, service start
  • api/openapi.yaml — API contract and x-required-permission per operation
  • auth/permissions.yaml — the editable permission and role registry

Edit this page on GitHub →