Skip to main content

RBAC Guide

AlmaForge uses role-based access control (RBAC). Permissions are attached to roles, not to users, and users gain permissions only by being assigned one or more roles. This page is the field-by-field reference for the Role resource that carries those permissions. Human users receive roles through their SSO connector and can receive temporary elevation through an Access Request. Keep permanent assignments in the connector's claim mapping rather than editing temporary SSO user records.

A role has two halves:

  • allow: what holders of this role can do.
  • deny: what they cannot do, even if allow would otherwise permit it.

AlmaForge evaluates a user's roles when their certificate is issued and on every connection to a resource. Additional roles can grant more permissions, but a connection requires one role to allow both the target and the requested identity, such as a Linux login or database user. A matching deny condition takes precedence over an allow. Resource discovery, connection access, and administrative API permissions are distinct. A role can let a user find a target without granting a connection to it.

AlmaForge ships five built-in roles you can assign as-is or use as a starting point.

Resource shape​

role-shape.yaml:

examples/rbac/role-shape.yaml
# Minimal Role resource showing the canonical document shape.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: example
spec:
options: {}
allow: {}
deny: {}

spec and options field names are camelCase. Snake_case input from older clusters is accepted on read. Output is always camelCase.

Manage roles with the Web UI, with alma, or through the API:

Terminal
alma get rolealma get role/example -o yamlalma apply -f role.yaml

Keep an existing admin session available while changing access roles, then test the affected user's sign-in and resource access. See break-glass access for recovery if a role change removes administrative access.

Built-in roles​

AlmaForge ships five preset roles. They are real Role resources. You can read, edit, or delete them like any other role. The labels on each preset (internal/resource-type: preset) flag them in the Web UI.

access​

Connect to enrolled servers, applications, databases, and Kubernetes clusters, and nothing else. It carries no rules at all, so it reads neither the audit log, nor session recordings, nor the instance inventory. The preset member role can request this role. Its certificate lifetime is capped at twelve hours. Principal lists template from the user's traits and fall back to the accounts common Linux cloud images ship with.

access role YAML
examples/rbac/preset/access.yaml
apiVersion: almaforge.com/v1
kind: Role
metadata:
annotations:
almaforge.com/description: Connect to every resource without administering the cluster
labels:
internal/resource-type: preset
name: access
spec:
allow:
appLabels:
'*': '*'
databaseLabels:
'*': '*'
databaseNames:
- '{{internal.db_names}}'
databaseRoles:
- '{{internal.db_roles}}'
databaseServiceLabels:
'*': '*'
databaseUsers:
- '{{internal.db_users}}'
kubernetesGroups:
- '{{internal.kubernetes_groups}}'
kubernetesLabels:
'*': '*'
kubernetesResources:
- kind: '*'
name: '*'
namespace: '*'
verbs:
- '*'
kubernetesUsers:
- '{{internal.kubernetes_users}}'
serverLabels:
'*': '*'
serverLogins:
- '{{internal.logins}}'
- root
- ec2-user
- ubuntu
deny: {}
options:
forwardAgent: true
maxSessionTTL: 12h0m0s
portForwarding: true
sshFileCopy: true

admin​

Manage every resource, including the roles and users that decide who else has access, and connect to everything the cluster knows about as root, system:masters or any database user. Certificate lifetime is capped at one hour.

admin role YAML
examples/rbac/preset/admin.yaml
apiVersion: almaforge.com/v1
kind: Role
metadata:
annotations:
almaforge.com/description: Full access to manage every resource, and to connect to all of them as root, system:masters or any database user
labels:
internal/resource-type: preset
name: admin
spec:
allow:
appLabels:
'*': '*'
databaseLabels:
'*': '*'
databaseNames:
- '*'
databaseServiceLabels:
'*': '*'
databaseUsers:
- '*'
kubernetesGroups:
# Hardwired into the Kubernetes API server's authorizer, which means it
# bypasses that cluster's own RBAC.
- system:masters
kubernetesLabels:
'*': '*'
kubernetesResources:
- kind: '*'
name: '*'
namespace: '*'
verbs:
- '*'
rules:
- resources:
- '*'
verbs:
- '*'
serverLabels:
'*': '*'
serverLogins:
# The accounts common Linux cloud images ship with.
- root
- ec2-user
- ubuntu
deny: {}
options:
forwardAgent: true
# Short by design: this bounds every session the role grants, including a
# redeemed break-glass session.
maxSessionTTL: 1h0m0s
portForwarding: true
sshFileCopy: true

auditor​

Read every resource, plus the audit log and session recordings, and change none of it. It also sees the server, database and Kubernetes inventory, because what is enrolled is itself audit evidence, and it can connect to none of it: reaching any of them needs an identity on top of a label match and this role has none. The certificate it receives carries a login that cannot name a real Linux account.

Applications are the exception and are deliberately not included. A plain web or TCP application authorizes on the label match alone, so listing them here would grant access rather than sight.

auditor role YAML
examples/rbac/preset/auditor.yaml
apiVersion: almaforge.com/v1
kind: Role
metadata:
annotations:
almaforge.com/description: Read-only review of audit events, recordings, and configuration
labels:
internal/resource-type: preset
name: auditor
spec:
allow:
# Wildcard labels make the inventory visible. With no login, Kubernetes
# identity or database user anywhere in the role, none of it can be
# connected to. Applications are absent on purpose: a plain web or TCP
# application authorizes on the label match alone, so a grant here would
# be access rather than sight.
databaseLabels:
'*': '*'
databaseServiceLabels:
'*': '*'
kubernetesLabels:
'*': '*'
kubernetesResources:
- kind: '*'
name: '*'
namespace: '*'
verbs:
- '*'
serverLabels:
'*': '*'
rules:
# The auditor reads everything at the redacted tier: list plus
# read, where read returns secrets masked. Secret-bearing kinds
# served outside the redaction chokepoint (tokens, trusted
# clusters, CAs) require readwithsecrets, which this does not
# grant, so the auditor is denied there rather than shown a
# secret. Session read authorizes recording replay.
- resources:
- '*'
verbs:
- list
- read
deny: {}
options:
maxSessionTTL: 8h0m0s

member​

The role a user starts with. It carries no rules, labels, or logins, so it can see every resource in the cluster and open none of them. What it can do is ask: a member requests either the access role or specific resources, and a reviewer decides.

member role YAML
examples/rbac/preset/member.yaml
apiVersion: almaforge.com/v1
kind: Role
metadata:
annotations:
almaforge.com/description: See every resource and request access to it
labels:
internal/resource-type: preset
name: member
spec:
allow:
request:
# searchAsRoles makes every resource visible for the purpose of
# asking for it. roles is what an approved request grants, narrowed
# to the resources named in the request.
searchAsRoles:
- access
roles:
- access
deny: {}
options:
maxSessionTTL: 24h0m0s

reviewer​

Approve or deny Access Requests. Approving is a privilege of its own: the role grants no access to anything it approves, and nobody can review their own request, so manual approval requires another reviewer.

reviewer role YAML
examples/rbac/preset/reviewer.yaml
apiVersion: almaforge.com/v1
kind: Role
metadata:
annotations:
almaforge.com/description: Approve or deny access requests
labels:
internal/resource-type: preset
name: reviewer
spec:
allow:
reviewRequests:
previewAsRoles:
- access
roles:
- access
deny: {}
options:
maxSessionTTL: 12h0m0s

Examples​

Each example below is a complete role you can copy and apply.

SSH server access​

A developer role: SSO-templated Linux logins, host user auto-provisioning, access limited to dev and staging environments.

examples/rbac/dev-ssh.yaml
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: dev-ssh
spec:
options:
maxSessionTTL: 8h
createHostUserMode: keep
allow:
# Use the claim name supplied by the OIDC provider.
# The connector name is not part of the trait key.
serverLogins:
- '{{external.preferred_username}}'
hostGroups: ['developers', 'docker']
serverLabels:
env: ['dev', 'staging']

Database access with auto-provisioned users​

Read-only access to staging databases. AlmaForge creates a database user named after the SSO email's local part on first connect.

examples/rbac/db-readonly.yaml
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: db-readonly
spec:
options:
createDbUserMode: keep
allow:
databaseLabels:
env: 'staging'
# Provision a database user named after the local part of the
# SSO email claim supplied by the identity provider.
databaseUsers:
- '{{email.local(external.email)}}'
databaseNames: ['*']
databaseRoles: ['reader']

Kubernetes namespace access​

Access to staging clusters with broad read across the namespace and exec/portforward limited to pods. Cluster-wide resources stay out of reach.

examples/rbac/k8s-team-alpha.yaml
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: k8s-team-alpha
spec:
allow:
kubernetesLabels:
env: 'staging'
kubernetesGroups: ['developers']
kubernetesResources:
# Read across the namespace.
- kind: '*'
namespace: 'team-alpha'
name: '*'
verbs: [get, list, watch]
# Exec and port-forward, but only into pods.
- kind: pod
namespace: 'team-alpha'
name: '*'
verbs: [exec, portforward]

Application access​

Web and TCP app access scoped by label. Start here and tighten the label selector before production.

examples/rbac/aws-engineer.yaml
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: aws-engineer
spec:
allow:
# Launch web and TCP apps labeled env=dev or env=staging. Add or
# tighten labels to scope which apps this role can reach.
appLabels:
env: ['dev', 'staging']

Request elevated access​

A baseline role that lets its holder request temporary elevation to a more privileged role. The grant needs approval and expires on its own. Attach it alongside the user's everyday role.

examples/rbac/access-requester.yaml
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: access-requester
spec:
allow:
request:
# Roles this user may request temporarily. The grant needs
# approval and expires automatically. Attach this role
# alongside the user's baseline (non-elevated) role.
roles: ['dba']
maxDuration: 2h

Approve access requests​

The reviewer side of the same flow. This role lets its holder approve or deny requests for a privileged role, without granting that role's access. Attach it to your reviewers alongside their everyday role.

examples/rbac/access-approver.yaml
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: access-approver
spec:
allow:
reviewRequests:
# Roles whose access requests a holder may approve or deny.
# Attach this role to your reviewers alongside their everyday
# role. It grants no infrastructure access of its own.
roles: ['dba']
# previewAsRoles lets a reviewer see the resources a request
# covers, so they can judge it before approving.
previewAsRoles: ['dba']

Restrict access to your own session recordings​

warning

The preset auditor role grants access to all sessions and recordings. A user holding auditor is not limited by this rule.

examples/rbac/own-recordings.yaml
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: own-recordings
spec:
allow:
rules:
- resources: [Recording]
verbs: [list, read]
# An SSH recording lists everyone who was in the session, so a
# user who joined one sees it as well as one they started. A
# database session has a single identity and no participants, so
# the second half is what matches it.
where: contains(recording.participants, user.metadata.name) || equals(recording.user, user.metadata.name)

Restrict access to your own active sessions​

Session access defaults to permissive for the user's own sessions, so this uses deny to filter out everything else:

examples/rbac/own-active-sessions.yaml
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: own-active-sessions
spec:
allow:
rules:
- resources: [Session]
verbs: ['*']
deny:
rules:
- resources: [Session]
verbs: [list, read, update, delete]
where: '!contains(session.participants, user.metadata.name)'

spec.options​

When a user has multiple roles, numeric limits merge to the strictest value: the shortest TTL, the shortest timeout, and the lowest connection limit. Recording policy merges the same way, with strict overriding bestEffort.

The booleans in the table below merge with OR. Agent forwarding, port forwarding, X11, and file copy are on if any role grants them, and pinSourceIp and disconnectExpiredCert are on if any role sets them. This covers the booleans the table documents and no others. createHostUserMode is not one of them, and merges the other way.

FieldDescription
maxSessionTTLMaximum lifetime of issued user certificates.
clientIdleTimeoutDisconnect idle interactive sessions after this duration.
disconnectExpiredCertTerminate active sessions when the client's certificate expires.
maxSessionsMaximum session channels per SSH connection.
maxConnectionsMaximum concurrent sessions per user, cluster-wide.
maxKubernetesConnectionsMaximum concurrent Kubernetes sessions per user.
forwardAgentPermit SSH agent forwarding.
portForwardingPermit TCP port forwarding over SSH.
permitX11ForwardingPermit X11 forwarding through OpenSSH clients.
sshFileCopyPermit SCP and SFTP. Not permitted unless set.
pinSourceIpBind issued SSH certificates to the client IP that authenticated.
recordSessionPer-protocol session recording mode (see below). Audit modes.
lockLocking mode. One of bestEffort, strict. strict wins on merge.
requestAccessAccess Request strategy. One of optional, always, reason.
requestPromptPrompt shown to users for the Access Request "reason" field.
createHostUserModeAuto-provision Linux host users. One of off, drop, keep.
createDbUserModeAuto-provision database users. One of off, keep, bestEffortDrop.
certExtensionsCustom extensions added to issued SSH certificates (see below).

recordSession has default and protocol-specific fields such as ssh. Each accepts bestEffort or strict. certExtensions is a list of extension names and values.

options.yaml:

examples/rbac/options.yaml
# Role options for strict SSH recording and a custom certificate extension.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: recorded-github-access
spec:
options:
recordSession:
default: bestEffort
ssh: strict
certExtensions:
- name: login@github.com
value: '{{external["github_login"]}}'

spec.allow and spec.deny​

Both sections accept the same fields. deny is evaluated first, so it overrides allow.

Labels​

A role grants access to enrolled resources by matching their labels. A label entry is a key plus one or more values:

label-allow-deny.yaml:

examples/rbac/label-allow-deny.yaml
# Role that allows staging servers except database and backup workloads.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: staging-servers
spec:
allow:
serverLabels:
env: stage
region: [us-west-1, eu-central-1]
deny:
serverLabels:
workload: [database, backup]

This grants access to nodes labeled env=stage and in either region, except those also labeled workload=database or workload=backup. Multiple keys are AND. Multiple values for one key are OR. Values can be a literal string, a list, the wildcard *, or a Go regular expression anchored with ^ and $.

The label fields by resource kind:

FieldResource
serverLabelsSSH servers
appLabelsApplications
databaseLabelsDatabases
databaseServiceLabelsDatabase Service agents
kubernetesLabelsKubernetes clusters

Every *Labels field has a parallel *LabelsExpression predicate field (serverLabelsExpression, appLabelsExpression, etc.). Use the predicate form when you need OR across keys, negation, or matching on label keys:

label-expression.yaml:

examples/rbac/label-expression.yaml
# Role that matches staging servers or servers owned by one of the user's teams.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: team-servers
spec:
allow:
serverLabelsExpression: |
labels["env"] == "staging" ||
contains(user.spec.traits["teams"], labels["team"])

When a role sets both *Labels and *LabelsExpression, both must match in allow. Either match denies access in deny.

tip

Resource labels can be dynamic. An agent can set them from the output of a command at startup, letting you write rules like "any host running PostgreSQL is reachable only by the DBA group".

Principals​

These fields list the identities a user can assume on a target resource. Group them by infrastructure type:

  • Servers: serverLogins, hostGroups, hostSudoers
  • Kubernetes: kubernetesGroups, kubernetesUsers, kubernetesResources
  • Database: databaseUsers, databaseNames, databaseRoles

hostGroups and hostSudoers apply when host user auto-provisioning is enabled via createHostUserMode.

kubernetesResources is structured. Each entry is {kind, namespace, name, verbs} and constrains which objects in a matched cluster the user may touch. Wildcards and anchored regular expressions are supported on namespace and name. See Kubernetes Access for the full kind and verb lists.

Rules​

rules controls access to dynamic AlmaForge resources such as roles, users, tokens, and audit events. Each rule is a list of resource kinds plus a list of verbs, with an optional where predicate:

resource-rules.yaml:

examples/rbac/resource-rules.yaml
# Administrative and audit permissions scoped by resource kind.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: resource-manager
spec:
allow:
rules:
- resources: [Role, User, AuthConnector]
verbs: [list, create, read, update, delete]
- resources: [Recording, Event]
verbs: [list, read]
- resources: [Recording]
verbs: [list, read]
where: contains(recording.participants, user.metadata.name)

Verbs:

VerbUse
listEnumerate resources of a kind.
readRead one resource by name, with secret fields redacted.
readwithsecretsRead one resource including its secret fields.
createCreate a new resource.
updateModify an existing resource.
deleteDelete a resource.
useUse a resource (for example, an OIDC integration).
*All verbs above.

Every user can read the roles they hold without a rule granting it. A role definition is the user's own permission set, so a user sees what they are allowed to do. It covers no other role, it does not grant list, and it does not include readwithsecrets. A deny rule on Role overrides it, as deny overrides any allow.

Resource kinds are PascalCase. Common kinds, grouped by purpose:

  • Identity: Role, User
  • SSO: AuthConnector, OIDC, OIDCConnector, Github, GithubConnector
  • Targets: Server, AppTarget, DatabaseTarget, KubernetesTarget
  • Sessions: Recording (recordings), Session (active), Event (audit)
  • Trust: CertificateAuthority
  • Operations: Lock, Token, AccessRequest, AccessPluginData
  • Cluster config: AuthConfig, RecordingConfig, NetworkConfig, ClusterName, StorageConfig

The full kind list is in the Declarative Configuration.

warning

A user with create on Token can mint a join token for any agent type and any trust relationship. Treat any role that grants Token write access as administrative.

Impersonation​

impersonate permits issuing certificates on behalf of other users and roles. Always scope it with where. An unscoped impersonate role is equivalent to admin.

impersonation.yaml:

examples/rbac/impersonation.yaml
# Impersonation role restricted to roles carrying the user's group label.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: ci-impersonation
spec:
allow:
impersonate:
users: ['*']
roles: [jenkins]
where: >
contains(user.spec.traits["group"],
impersonate_role.metadata.labels["group"])

Access Requests​

request is what a user can ask for. reviewRequests is what they can approve. Request roles support glob and ^...$ regular expression matchers. searchAsRoles lets the user temporarily assume roles to search for Resource Access Request targets. maxDuration caps requested duration at 7 days. claimsToRoles derives role names from SSO claims using regex capture groups. See Access Request roles and the full operator flow in Just-in-Time Access.

access-request-policy.yaml:

examples/rbac/access-request-policy.yaml
# Request and review policy with thresholds and claim-derived roles.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: access-request-policy
spec:
allow:
request:
roles: [common, 'dev-*']
searchAsRoles: [access]
maxDuration: 7d
thresholds:
- approve: 2
deny: 1
claimsToRoles:
- claim: projects
value: '^product-(.*)$'
roles: ['$1-admin']
annotations:
groups: ['{{external["groups"]}}']
reviewRequests:
roles: [dbadmin]
previewAsRoles: [dbadmin]
where: 'contains(reviewer.traits["team"], "dbadmin")'

Templates​

Most role fields accept template variables that resolve at login from the user's traits and SSO claims. Variables appear in {{ ... }}.

Templates work in label fields, principal fields, impersonate.where, and reviewRequests.where. They do not expand inside the role-name lists request.roles, request.searchAsRoles, reviewRequests.roles, and reviewRequests.previewAsRoles. Those use claimsToRoles.

Internal variables​

Internal variables resolve from the traits available when a template is evaluated. The variables below are the ones AlmaForge sets, and an unrecognized name produces an unsupported-variable error. Variable names use snake_case.

VariableValue
{{internal.logins}}Linux logins.
{{internal.kubernetes_users}}Kubernetes users.
{{internal.kubernetes_groups}}Kubernetes groups.
{{internal.db_users}}Database users.
{{internal.db_names}}Database names.
{{internal.db_roles}}Database roles for automatic database user provisioning.
{{internal.jwt}}JWT header for application access.

External variables​

External traits come from the user's SSO assertion. OIDC claim names are used directly: external.email, external.groups, and external.preferred_username. GitHub supplies external.logins and external.teams. Check the user's spec.traits to see which values their provider supplies.

For example, set a serverLogins entry to '{{external.preferred_username}}'.

Use bracket syntax for claim names containing /. Dot syntax only works for plain identifiers without /. AlmaForge expands an external variable to a string or a list of strings. Configure your identity provider to emit a supported claim type.

Functions​

FunctionUse
email.local(variable)Local part of an email address. email.local("alice@example.com") returns alice.
regexp.replace(variable, expression, replacement)Apply a Go regular expression substitution. Capture groups are referenced as $1, $2, etc. Values that do not match are filtered out.
regexp.match(expression)True if any of the user's roles matches the expression. Used in Access Request fields.
regexp.not_match(expression)True if no role matches the expression.

A regexp expression is treated as a Go regular expression when it starts with ^ and ends with $. Otherwise it is a glob, with * as the wildcard for one or more characters.

Evaluation rules​

How a template field renders for a logged-in user:

  • A field can mix literals and variables. 'IAM#{{external["email"]}};' becomes 'IAM#alice@example.com;'.
  • A list-typed variable expands into list entries. kubernetesGroups: ['{{external["groups"]}}'] becomes ['admins', 'devs'] when the claim is ["admins", "devs"].
  • An unset trait renders to nothing. serverLogins: ['{{external["preferred_username"]}}', 'admin'] becomes ['admin'] if the claim is missing.
  • regexp.replace filters as well as substitutes. Values that do not match the expression are dropped from the result.

Access Request roles​

The role-name lists in request and reviewRequests do not expand {{external.x}}. Use claimsToRoles to derive role names from SSO claims. Each entry matches a claim against a regular expression and emits one or more role names. Capture groups are available as $1, $2, and so on.

claim-derived-request-roles.yaml:

examples/rbac/claim-derived-request-roles.yaml
# Access-request roles derived from project names in an SSO claim.
apiVersion: almaforge.com/v1
kind: Role
metadata:
name: project-access
spec:
allow:
request:
claimsToRoles:
- claim: projects
value: '^product-(.*)$'
roles: ['$1-admin']

A user whose projects claim contains product-alpha and product-beta can request the alpha-admin and beta-admin roles.

Predicate language​

Roles use two predicate dialects with different field models. Both share &&, ||, and ! as operators.

where clauses​

Used in rules[].where, impersonate.where, and reviewRequests.where. Two functions:

FunctionUse
contains(field, value)True if the list field contains value.
equals(field, value)True if field equals value. value may be another field or a string literal.

Fields:

FieldMeaning
user.metadata.nameThe user's name.
user.spec.rolesThe user's role names.
user.spec.traitsThe user's traits, including SSO claims.
recording.participantsParticipants on an SSH session recording.
recording.userThe user a session recording belongs to.
session.participantsParticipants on an active session.
impersonate_userThe user being impersonated.
impersonate_roleThe role being impersonated.
reviewer.spec.traitsThe reviewing user's traits.

For example: contains(recording.participants, user.metadata.name) || equals(recording.user, user.metadata.name).

Only an SSH recording carries recording.participants, so a rule naming it alone matches no database recording. Naming recording.user beside it covers both: an SSH session a user joined, and a database session that is theirs. A field the recording does not carry is a mismatch rather than an error, so either half of the rule can carry it alone.

Label expressions​

Used in every *LabelsExpression field. Adds == and != to the operator set.

Fields:

FieldTypeMeaning
labelsmap[string]stringStatic and dynamic labels of the resource.
user.spec.traitsmap[string][]stringAll traits of the user.

Functions (any argument named list may be a list-typed value, a single string, or a string literal):

FunctionReturnsUse
contains(list, item)boolTrue if list contains an exact match for item.
contains_any(list, items)boolTrue if list contains any element of items.
contains_all(list, items)boolTrue if list contains every element of items.
regexp.match(list, re)boolTrue if any item in list matches re.
regexp.replace(list, re, replacement)[]stringReplace matches of re in each item of list.
email.local(list)[]stringLocal part of each email. Errors if any value is not an email.
strings.upper(list)[]stringUppercase each item.
strings.lower(list)[]stringLowercase each item.
labels_matching(re)[]stringAggregate of all label values whose key matches re (glob or regexp).

For example: labels["env"] == "staging" || contains(user.spec.traits["teams"], labels["team"]).

Further reading​