diff --git a/docs/_subsections/configuring-the-helm-chart.md b/docs/_subsections/configuring-the-helm-chart.md
index e26c3f7..a51e216 100644
--- a/docs/_subsections/configuring-the-helm-chart.md
+++ b/docs/_subsections/configuring-the-helm-chart.md
@@ -10,9 +10,9 @@ When installed on OpenShift with authentication.openshift.enabled=trueauthentication.openshift.enabled=false but oauth2Proxy.tls.selfSigned.enabled=true then a self-signed **TLS** certificate will be generated at installation time to serve similar purposes. These **TLS** certificates are not managed, will not automatically rotate, and will expire after 365 days. You will need to manually rotate the certificates, or reinstall the chart, or else apply your own customizations to the **Kubernetes** manifests to automate **TLS** certificate issuance and rotation.
-## Configuration Options
+### Configuration Options
-### Cryostat Container
+#### Cryostat Container
| Name | Description | Value |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
@@ -65,7 +65,7 @@ When installed with authentication.openshift.enabled=false but authentication.openshift.enabled=false but authentication.openshift.enabled=false but authentication.openshift.enabled=false but authentication.openshift.enabled=false but authentication.openshift.enabled=false but authentication.openshift.enabled=false but authentication.openshift.enabled=false but authentication.openshift.enabled=false but authentication.openshift.enabled=false but :` key (for example `activerecordings:read`) and is mapped to a **Kubernetes** resource/verb pair (for example `pods/exec:create`). The built-in default mapping for every permission is `pods/exec:create`. Admins can override individual permissions via `core.config.extra.envVars` by setting environment variables of the form `CRYOSTAT_SECURITY_RBAC_PERMISSIONS____` to a value of the form `resource[/subresource]:verb`.
+
+**Cryostat** caches authorization decisions for one minute. Changes to user Roles or RoleBindings may take up to one minute to take effect.
+
+> **Note:** When Basic authentication (`authentication.basicAuth.enabled=true`) is also enabled, fine-grained RBAC checks are bypassed. Access becomes all-or-nothing for all authenticated users.
+
+###### Granting read-only access
+
+To give some users read-only access and others full access, remap every `read` permission to `pods:get` (a lower privilege granted by the built-in **OpenShift** `view` role) while leaving mutating permissions at the default `pods/exec:create` (granted by `admin` or `edit`).
+
+Pass the permission overrides via `core.config.extra.envVars` when installing or upgrading the chart. The example below shows `helm upgrade` syntax using `--set-json`. This replaces the entire `core.config.extra.envVars` array, so preserve and include any existing entries or provide a complete values file instead:
+
+```bash
+helm upgrade cryostat ./charts/cryostat -n \
+ --set authentication.openshift.enabled=true \
+ --set-json 'core.config.extra.envVars=[
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__ACTIVERECORDINGS_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__ARCHIVEDRECORDINGS_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__ASYNCPROFILER_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__AUDIT_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__AUTOMATEDRULES_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__CERTIFICATES_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__CREDENTIALS_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__DISCOVERYNODES_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__DISCOVERYPLUGINS_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__EVENTTEMPLATES_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__EVENTTYPES_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__HEAPDUMPS_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__MATCHEXPRESSIONS_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__PROBES_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__PROBETEMPLATES_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__RECORDINGMETADATA_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__REPORTS_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__TARGETS_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__THREADDUMPS_READ_","value":"pods:get"},
+ {"name":"CRYOSTAT_SECURITY_RBAC_PERMISSIONS__UNIFIEDLOGS_READ_","value":"pods:get"}
+ ]'
+```
+
+With this configuration, apply **OpenShift** RBAC as follows:
+
+- Grant `view` in the **Cryostat** installation namespace for read-only users:
+ ```
+ oc adm policy add-role-to-user -n view
+ ```
+- Grant `admin` or `edit` in the **Cryostat** installation namespace for full-access users:
+ ```
+ oc adm policy add-role-to-user -n admin
+ ```
+
+A user who passes the `get pods` proxy access review can reach the **Cryostat** application, but individual **Cryostat** API requests may still be rejected with `403 Forbidden` until they are granted an appropriate role.
+
+For a full list of available permission keys and their defaults, see the [Full permission reference](#full-permission-reference) table in the Operator configuration section.
+
+
+#### Other Parameters
| Name | Description | Value |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
@@ -270,4 +330,4 @@ When installed with authentication.openshift.enabled=false but :` key (for example `activerecordings:read`) and is mapped to a **Kubernetes** resource/verb pair (for example `pods/exec:create`).
+When a user makes a request, **Cryostat** checks whether they are permitted to perform the mapped **Kubernetes** action, scoped to the **Cryostat** installation namespace by default. If the user passes the
+check, the request proceeds; otherwise it is rejected with a `403 Forbidden` response.
+
+The built-in default mapping for every permission is `pods/exec:create`. This means that, out of the box, a user needs the (Cluster)Role which would grant them the ability to invoke shell commands
+within Pods in the **Cryostat** installation namespace. Admins can override this default; **Cryostat** resolves the effective mapping for each permission in order from most to least specific:
+
+1. An explicit entry in `spec.authorizationOptions.rbacPermissions` for that exact `:` key.
+2. A verb-class fallback in `spec.authorizationOptions.rbacDefaultPermissions` (`defaultReadPermission`, `defaultWritePermission`, or `defaultDeletePermission`).
+3. The global catch-all `spec.authorizationOptions.rbacDefaultPermissions.defaultPermission`.
+4. The built-in application default: `pods/exec:create` for all permissions.
+
+**Cryostat** caches authorization decisions for one minute by default. Changes to user Roles or RoleBindings may take up to one minute to take effect.
+
+> **Note:** When Basic authentication is enabled alongside **OpenShift** SSO, fine-grained RBAC checks are bypassed for all authenticated users. Access becomes all-or-nothing for any user who successfully authenticates.
+
+##### Granting read-only access
+
+The example below remaps every `read` permission to `pods:get`, while leaving all mutating operations mapped to the default `pods/exec:create`. A user who holds the built-in **OpenShift** `view` role
+in the **Cryostat** installation namespace passes `pods:get` checks and therefore gets read-only access. A user who holds `admin` or `edit` passes `pods/exec:create` checks and therefore gets full access.
+
+The most concise way to achieve this is via `spec.authorizationOptions.rbacDefaultPermissions`:
+
+```yaml
+apiVersion: operator.cryostat.io/v1beta2
+kind: Cryostat
+metadata:
+ name: cryostat-sample
+spec:
+ authorizationOptions:
+ rbacDefaultPermissions:
+ defaultReadPermission: pods:get
+```
+
+Alternatively, individual permissions can be remapped explicitly via `spec.authorizationOptions.rbacPermissions`:
+
+```yaml
+apiVersion: operator.cryostat.io/v1beta2
+kind: Cryostat
+metadata:
+ name: cryostat-sample
+spec:
+ authorizationOptions:
+ rbacPermissions:
+ activerecordings:read: pods:get
+ archivedrecordings:read: pods:get
+ asyncprofiler:read: pods:get
+ audit:read: pods:get
+ automatedrules:read: pods:get
+ certificates:read: pods:get
+ credentials:read: pods:get
+ discoverynodes:read: pods:get
+ discoveryplugins:read: pods:get
+ eventtemplates:read: pods:get
+ eventtypes:read: pods:get
+ heapdumps:read: pods:get
+ matchexpressions:read: pods:get
+ probes:read: pods:get
+ probetemplates:read: pods:get
+ recordingmetadata:read: pods:get
+ reports:read: pods:get
+ targets:read: pods:get
+ threaddumps:read: pods:get
+ unifiedlogs:read: pods:get
+```
+
+With this configuration, apply **OpenShift** RBAC as follows:
+
+- Grant `view` in the **Cryostat** installation namespace for read-only users:
+ ```
+ oc adm policy add-role-to-user -n view
+ ```
+- Grant `admin` or `edit` in the **Cryostat** installation namespace for full-access users:
+ ```
+ oc adm policy add-role-to-user -n admin
+ ```
+
+A user who has neither role may be able to log in (pass the `get pods` proxy access review), but all **Cryostat** API requests will be rejected with `403 Forbidden` until they are granted an appropriate role.
+
+##### RBAC cache options
+
+**Cryostat** maintains two in-process caches in fine-grained RBAC mode to reduce the number of **Kubernetes** API calls:
+
+- **Per-user client cache**: holds a per-user **Kubernetes** client. Defaults to a 5-minute idle TTL and a maximum of 1000 entries.
+- **Decision cache**: caches `SelfSubjectAccessReview` results. Defaults to a 1-minute write TTL and a maximum of 10000 entries.
+
+Both caches can be tuned via `spec.authorizationOptions.rbacCacheOptions`:
+
+```yaml
+apiVersion: operator.cryostat.io/v1beta2
+kind: Cryostat
+metadata:
+ name: cryostat-sample
+spec:
+ authorizationOptions:
+ rbacCacheOptions:
+ clientCacheExpireAfterAccess: "5m" # idle TTL for the per-user client cache (Go duration)
+ clientCacheMaximumSize: 1000 # max entries; set to 0 to disable
+ decisionCacheTTL: "1m" # write TTL for SSAR decision cache (Go duration)
+ decisionCacheMaximumSize: 10000 # max entries; set to 0 to disable
+```
+
+Setting `decisionCacheTTL: "0s"` or `decisionCacheMaximumSize: 0` disables the decision cache entirely so every request issues a fresh `SelfSubjectAccessReview`. Similarly, setting
+`clientCacheExpireAfterAccess: "0s"` or `clientCacheMaximumSize: 0` disables the client cache.
+
+##### Cluster-scoped RBAC
+
+By default, access reviews are scoped to the **Cryostat** installation namespace (i.e. the namespace where the **CR** is created), so users only need a `Role` and `RoleBinding` within that namespace to pass the various permissions checks. If you prefer cluster-scoped authorization (requiring a `ClusterRole` and `ClusterRoleBinding`), set `spec.authorizationOptions.namespacedRBACPermissions` to `false`:
+
+```yaml
+apiVersion: operator.cryostat.io/v1beta2
+kind: Cryostat
+metadata:
+ name: cryostat-sample
+spec:
+ authorizationOptions:
+ namespacedRBACPermissions: false
+```
+
+Setting `namespacedRBACPermissions: false` will require users to be able to pass the various permissions checks across all namespaces across the cluster, rather than only within the **CR**'s namespace. The `namespacedRBACPermissions` property is unset by default, which is equivalent to setting it to `true`: permissions are checked within the installation namespace only.
### Security Context