diff --git a/charts/rstudio-workbench/Chart.yaml b/charts/rstudio-workbench/Chart.yaml
index 6c0175ba..36d2cf72 100644
--- a/charts/rstudio-workbench/Chart.yaml
+++ b/charts/rstudio-workbench/Chart.yaml
@@ -1,6 +1,6 @@
name: rstudio-workbench
description: Official Helm chart for Posit Workbench
-version: 0.20.0
+version: 0.21.1
apiVersion: v2
appVersion: 2026.04.0
icon: https://raw.githubusercontent.com/rstudio/helm/main/images/posit-icon-fullcolor.svg
diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md
index 0e027dfe..36db2970 100644
--- a/charts/rstudio-workbench/NEWS.md
+++ b/charts/rstudio-workbench/NEWS.md
@@ -1,5 +1,14 @@
# Changelog
+## 0.21.1
+
+- Workbench pods can now run as a non-root user. Set `pod.runAsRoot: false` (new value, default `true`) to run unprivileged.
+- When running unprivelaged SCIM must be used for user management.
+- A new `config.sssd` block controls the bundled SSSD daemon (replaces the deprecated `config.userProvisioning`).
+- `config.userProvisioning` is deprecated; use `config.sssd.conf`. A warning is emitted when it is set.
+- The Workbench and launcher prestart scripts (`prestart-workbench.bash`, `prestart-launcher.bash`) no longer perform root-only operations.
+- A writable `emptyDir` is now mounted at `/mnt/load-balancer` when load balancing is active (`replicas > 1` or `loadBalancer.forceEnabled: true`), so the prestart script can write the load-balancer config when the pod runs unprivileged (`pod.runAsRoot: false`).
+
## 0.20.0
- **BREAKING**: Default images now pull from the `posit/` namespace on Docker Hub
diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md
index 78407858..a56a62b0 100644
--- a/charts/rstudio-workbench/README.md
+++ b/charts/rstudio-workbench/README.md
@@ -1,6 +1,6 @@
# Posit Workbench
- 
+ 
#### _Official Helm chart for Posit Workbench_
@@ -24,11 +24,11 @@ To ensure a stable production deployment:
## Installing the chart
-To install the chart with the release name `my-release` at version 0.20.0:
+To install the chart with the release name `my-release` at version 0.21.1:
```{.bash}
helm repo add rstudio https://helm.rstudio.com
-helm upgrade --install my-release rstudio/rstudio-workbench --version=0.20.0
+helm upgrade --install my-release rstudio/rstudio-workbench --version=0.21.1
```
To explore other chart versions, look at:
@@ -62,20 +62,22 @@ To function, this chart requires the following:
* If using load balancing (by setting `replicas > 1`), you need similar storage defined for `sharedStorage` to
store shared project configuration. However, you can also configure the product to store its shared data underneath `/home` by
setting `config.server.rserver\.conf.server-shared-storage-path=/home/some-shared-dir`.
-* A method to join the deployed `rstudio-workbench` container to your auth domain. The default `posit/workbench` image has `sssd` installed and started by default.
- You can include `sssd` configuration in `config.userProvisioning` like so:
+* A method to join the deployed `rstudio-workbench` container to your auth domain. The `posit/workbench` image ships `sssd` for legacy LDAP/Active Directory provisioning. SSSD must run as root, so the bundled daemon starts by default only on root deployments (`config.sssd.enabled: true`, `pod.runAsRoot: true`) and is automatically skipped when `pod.runAsRoot: false`. Modern provisioning (SCIM / native) does not require SSSD.
+ Provide its configuration in `config.sssd.conf` like so (set `config.sssd.enabled: false` to disable the daemon entirely):
```yaml
config:
- userProvisioning:
- mysssd.conf:
- sssd:
- config_file_version: 2
- services: nss, pam
- domains: rstudio.com
- domain/rstudio.com:
- id_provider: ldap
- auth_provider: ldap
+ sssd:
+ enabled: true
+ conf:
+ mysssd.conf:
+ sssd:
+ config_file_version: 2
+ services: nss, pam
+ domains: rstudio.com
+ domain/rstudio.com:
+ id_provider: ldap
+ auth_provider: ldap
```
## Licensing
@@ -302,9 +304,9 @@ the `XDG_CONFIG_DIRS` environment variable.
- It is mounted into the pod at `/scripts/`.
- `prestart-workbench.bash` is used to start workbench.
- `prestart-launcher.bash` is used to start launcher.
-- User Provisioning Configuration:
- - These configuration files are used for configuring user provisioning (i.e., `sssd`).
- - Located at:
`config.userProvisioning.<< name of file >>` Helm values
+- SSSD Configuration:
+ - These configuration files configure the bundled `sssd` daemon (legacy LDAP/AD provisioning), which is started only when `config.sssd.enabled=true`.
+ - Located at:
`config.sssd.conf.<< name of file >>` Helm values (the deprecated `config.userProvisioning` is honored as a fallback)
- Mounted onto:
`/etc/sssd/conf.d/` with `0600` permissions by default.
- Custom Startup Configuration:
- `supervisord` service / unit definition `.conf` files.
@@ -355,9 +357,9 @@ Provisioning users in Workbench containers is challenging. Session images create
consistent UIDs / GIDs). However, creating users in the Workbench containers is a responsibility that falls to the
administrator.
-The most common way to provision users is via `sssd`.
-The [latest Workbench container](https://github.com/rstudio/rstudio-docker-products/tree/main/workbench#user-provisioning)
-has `sssd` included and running by default (see `userProvisioning` configuration files above).
+Posit Workbench's native user provisioning (SCIM / just-in-time) is the recommended approach and does not require SSSD.
+The legacy approach is `sssd`: the [latest Workbench container](https://github.com/rstudio/rstudio-docker-products/tree/main/workbench#user-provisioning)
+includes `sssd`, but it is started only when `config.sssd.enabled=true` (which requires `pod.runAsRoot: true`; see `config.sssd` above).
The other way that this can be managed is via a lightweight startup service (runs once at startup and then sleeps forever)
or a polling service (checks at regular intervals). Either can be written easily in `bash` or another programming language.
@@ -605,7 +607,7 @@ To activate the use of `SealedSecret` templates instead of `Secret` templates in
- `config.secret`
- `config.sessionSecret`
-- `config.userProvisioning`
+- `config.sssd.conf` (or the deprecated `config.userProvisioning`)
- `launcherPem`
- `secureCookieKey` (or `global.secureCookieKey`)
@@ -616,7 +618,7 @@ Use of [Sealed secrets](https://github.com/bitnami-labs/sealed-secrets) disables
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| affinity | object | `{}` | A map used verbatim as the pod's "affinity" definition |
-| args | list | `[]` | args is the pod container's run arguments. |
+| args | list | `[]` | args is the pod container's run arguments. When unset, the container's default arguments are used. |
| chronicleAgent.agentEnvironment | string | `""` | An environment tag to apply to all metrics reported by this agent ([reference](https://docs.posit.co/chronicle/appendix/library/advanced-agent.html#environment)) |
| chronicleAgent.autoDiscovery | bool | `true` | If true, the chart will attempt to lookup the Chronicle Server address and version in the cluster |
| chronicleAgent.enabled | bool | `false` | Creates a Chronicle agent sidecar container in the pod if true |
@@ -634,7 +636,7 @@ Use of [Sealed secrets](https://github.com/bitnami-labs/sealed-secrets) disables
| chronicleAgent.workbenchApiKey | object | `{"value":"","valueFrom":{}}` | A read-only administrator permissions API key generated for Workbench for the Chronicle agent to use, API keys can only be created after Workbench has been deployed so this value may need to be filled in later if performing an initial deployment ([reference](https://docs.posit.co/connect/user/api-keys/#api-keys-creating)) |
| chronicleAgent.workbenchApiKey.value | string | `""` | Workbench API key as a raw string to set as the `CHRONICLE_WORKBENCH_APIKEY` environment variable (not recommended) |
| chronicleAgent.workbenchApiKey.valueFrom | object | `{}` | Workbench API key as a `valueFrom` reference (ex. a Kubernetes Secret reference) to set as the `CHRONICLE_WORKBENCH_APIKEY` environment variable (recommended) |
-| command | list | `[]` | command is the pod container's run command. By default, it uses the container's default. However, the chart expects a container using `supervisord` for startup |
+| command | list | `[]` | command is the pod container's run command. When unset, the container's default command (`supervisord`) is used, which supports both root and non-root (`serviceAccountUser`) operation. |
| components | object | `{"enabled":true,"positron":{"image":{"repository":"posit/workbench-positron-init","tag":""},"version":""},"sessionInit":{"image":{"repository":"posit/workbench-session-init","tag":""}}}` | Session component delivery via init containers. When enabled (default), the chart configures rserver.conf so the launcher injects init containers into session pods at startup. Set `enabled: false` and change `session.image.repository` to `rstudio/r-session-complete` to use the classic all-in-one session image instead. |
| components.enabled | bool | `true` | Enable session component delivery via init containers. When false, no init containers are configured and session.image must be a self-contained image like r-session-complete. |
| components.positron.image.repository | string | `"posit/workbench-positron-init"` | The image repository for the Positron init container |
@@ -662,9 +664,12 @@ Use of [Sealed secrets](https://github.com/bitnami-labs/sealed-secrets) disables
| config.serverDcf | object | `{"launcher-mounts":[]}` | a map of server-scoped config files (akin to `config.server`), but with .dcf file formatting (i.e. `launcher-mounts`, `launcher-env`, etc.) |
| config.session | object | `{"notifications.conf":{},"repos.conf":{"CRAN":"https://packagemanager.posit.co/cran/__linux__/jammy/latest"},"rsession.conf":{},"rstudio-prefs.json":"{}\n"}` | a map of session-scoped config files. Mounted to `/mnt/session-configmap/rstudio/` on both server and session, by default. |
| config.sessionSecret | object | `{}` | a map of secret, session-scoped config files (odbc.ini, etc.). Mounted to `/mnt/session-secret/` on both server and session, by default |
+| config.sssd | object | `{"conf":{},"enabled":true}` | Bundled SSSD daemon for legacy LDAP/Active Directory user provisioning. On by default; automatically skipped when the pod runs unprivileged (`pod.runAsRoot: false`), since SSSD requires root. Modern provisioning (SCIM / native) does not require SSSD. |
+| config.sssd.conf | object | `{}` | a map of sssd config files, mounted to `/etc/sssd/conf.d/` with 0600 permissions. Replaces the deprecated `config.userProvisioning`. |
+| config.sssd.enabled | bool | `true` | whether to start the bundled SSSD daemon. Automatically skipped (not an error) when `pod.runAsRoot` is false. Set to `false` to disable entirely. |
| config.startupCustom | object | `{}` | a map of supervisord .conf files to define custom services. Mounted into the container at /startup/custom/ |
-| config.startupUserProvisioning | object | `{"sssd.conf":"[program:sssd]\ncommand=/usr/sbin/sssd -i -c /etc/sssd/sssd.conf --logger=stderr\nautorestart=false\nnumprocs=1\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstdout_logfile_backups=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\nstderr_logfile_backups=0\n"}` | a map of supervisord .conf files to define user provisioning services. Mounted into the container at /startup/user-provisioning/ |
-| config.userProvisioning | object | `{}` | a map of sssd config files, used for user provisioning. Mounted to `/etc/sssd/conf.d/` with 0600 permissions |
+| config.startupUserProvisioning | object | `{}` | a map of supervisord .conf files to define user provisioning services. Mounted into the container at /startup/user-provisioning/. The bundled SSSD service is now controlled by `config.sssd.enabled`; use this only for custom provisioning daemons. |
+| config.userProvisioning | object | `{}` | DEPRECATED: use `config.sssd.conf` instead. A map of sssd config files, mounted to `/etc/sssd/conf.d/` with 0600 permissions. Only applied when `config.sssd.enabled=true`. |
| dangerRegenerateAutomatedValues | bool | `false` | |
| deployment.annotations | object | `{}` | Additional annotations to add to the rstudio-workbench deployment |
| diagnostics | object | `{"directory":"/var/log/rstudio","enabled":false}` | Settings for enabling server diagnostics |
@@ -727,7 +732,10 @@ Use of [Sealed secrets](https://github.com/bitnami-labs/sealed-secrets) disables
| pod.labels | object | `{}` | Additional labels to add to the rstudio-workbench pods |
| pod.lifecycle | object | `{}` | container lifecycle hooks |
| pod.port | int | `8787` | The containerPort used by the main pod container |
+| pod.runAsRoot | bool | `true` | Whether the pod's containers run as the root OS user. `true` (default) preserves historical behavior: pod runs as root (`runAsUser: 0`), SSSD starts, secrets mount 0600, PAM sessions on. `false` runs unprivileged: pod sets `runAsNonRoot: true` and `runAsUser`/`fsGroup` to `pod.serviceAccountUserId`, SSSD is skipped, secrets mount 0640, and non-root `rserver.conf`/`launcher.conf` defaults are applied. |
| pod.securityContext | object | `{}` | Values to set the `securityContext` for the service pod |
+| pod.serviceAccountUser | string | `"rstudio-server"` | The OS user written to `rserver.conf` as `server-user`. Set to `""` to omit `server-user` from `rserver.conf` entirely. |
+| pod.serviceAccountUserId | int | `999` | The UID used as `runAsUser`/`fsGroup` in the pod's securityContext when `pod.runAsRoot` is false. Must match the UID of `pod.serviceAccountUser` baked into the Workbench image. |
| pod.sidecar | list | `[]` | sidecar is an array of containers that will be run alongside the main container |
| pod.terminationGracePeriodSeconds | int | `120` | The termination grace period seconds allowed for the pod before shutdown |
| pod.volumeMounts | list | `[]` | volumeMounts is injected as-is into the "volumeMounts:" component of the pod.container spec |
diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl
index df7239fb..7302e18c 100644
--- a/charts/rstudio-workbench/README.md.gotmpl
+++ b/charts/rstudio-workbench/README.md.gotmpl
@@ -33,20 +33,22 @@ To function, this chart requires the following:
* If using load balancing (by setting `replicas > 1`), you need similar storage defined for `sharedStorage` to
store shared project configuration. However, you can also configure the product to store its shared data underneath `/home` by
setting `config.server.rserver\.conf.server-shared-storage-path=/home/some-shared-dir`.
-* A method to join the deployed `rstudio-workbench` container to your auth domain. The default `posit/workbench` image has `sssd` installed and started by default.
- You can include `sssd` configuration in `config.userProvisioning` like so:
+* A method to join the deployed `rstudio-workbench` container to your auth domain. The `posit/workbench` image ships `sssd` for legacy LDAP/Active Directory provisioning. SSSD must run as root, so the bundled daemon starts by default only on root deployments (`config.sssd.enabled: true`, `pod.runAsRoot: true`) and is automatically skipped when `pod.runAsRoot: false`. Modern provisioning (SCIM / native) does not require SSSD.
+ Provide its configuration in `config.sssd.conf` like so (set `config.sssd.enabled: false` to disable the daemon entirely):
```yaml
config:
- userProvisioning:
- mysssd.conf:
- sssd:
- config_file_version: 2
- services: nss, pam
- domains: rstudio.com
- domain/rstudio.com:
- id_provider: ldap
- auth_provider: ldap
+ sssd:
+ enabled: true
+ conf:
+ mysssd.conf:
+ sssd:
+ config_file_version: 2
+ services: nss, pam
+ domains: rstudio.com
+ domain/rstudio.com:
+ id_provider: ldap
+ auth_provider: ldap
```
{{ template "rstudio.licensing" . }}
@@ -248,9 +250,9 @@ the `XDG_CONFIG_DIRS` environment variable.
- It is mounted into the pod at `/scripts/`.
- `prestart-workbench.bash` is used to start workbench.
- `prestart-launcher.bash` is used to start launcher.
-- User Provisioning Configuration:
- - These configuration files are used for configuring user provisioning (i.e., `sssd`).
- - Located at:
`config.userProvisioning.<< name of file >>` Helm values
+- SSSD Configuration:
+ - These configuration files configure the bundled `sssd` daemon (legacy LDAP/AD provisioning), which is started only when `config.sssd.enabled=true`.
+ - Located at:
`config.sssd.conf.<< name of file >>` Helm values (the deprecated `config.userProvisioning` is honored as a fallback)
- Mounted onto:
`/etc/sssd/conf.d/` with `0600` permissions by default.
- Custom Startup Configuration:
- `supervisord` service / unit definition `.conf` files.
@@ -301,9 +303,9 @@ Provisioning users in Workbench containers is challenging. Session images create
consistent UIDs / GIDs). However, creating users in the Workbench containers is a responsibility that falls to the
administrator.
-The most common way to provision users is via `sssd`.
-The [latest Workbench container](https://github.com/rstudio/rstudio-docker-products/tree/main/workbench#user-provisioning)
-has `sssd` included and running by default (see `userProvisioning` configuration files above).
+Posit Workbench's native user provisioning (SCIM / just-in-time) is the recommended approach and does not require SSSD.
+The legacy approach is `sssd`: the [latest Workbench container](https://github.com/rstudio/rstudio-docker-products/tree/main/workbench#user-provisioning)
+includes `sssd`, but it is started only when `config.sssd.enabled=true` (which requires `pod.runAsRoot: true`; see `config.sssd` above).
The other way that this can be managed is via a lightweight startup service (runs once at startup and then sleeps forever)
or a polling service (checks at regular intervals). Either can be written easily in `bash` or another programming language.
@@ -551,7 +553,7 @@ To activate the use of `SealedSecret` templates instead of `Secret` templates in
- `config.secret`
- `config.sessionSecret`
-- `config.userProvisioning`
+- `config.sssd.conf` (or the deprecated `config.userProvisioning`)
- `launcherPem`
- `secureCookieKey` (or `global.secureCookieKey`)
diff --git a/charts/rstudio-workbench/prestart-launcher.bash b/charts/rstudio-workbench/prestart-launcher.bash
index 0802b81e..a1ee16f5 100644
--- a/charts/rstudio-workbench/prestart-launcher.bash
+++ b/charts/rstudio-workbench/prestart-launcher.bash
@@ -4,24 +4,20 @@ set -o pipefail
main() {
local startup_script="${1:-/usr/lib/rstudio-server/bin/rstudio-launcher}"
- local dyn_dir='/mnt/dynamic/rstudio'
-
- local cacert='/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
- local host="${KUBERNETES_SERVICE_HOST}"
- if [[ "${host}" == *:* ]]; then
- host="[${host}]"
- fi
- local k8s_url="https://${host}:${KUBERNETES_SERVICE_PORT}"
-
- _logf 'Loading service account token'
- local sa_token
- sa_token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
-
- _logf 'Ensuring %s exists' "${dyn_dir}"
- mkdir -p "${dyn_dir}"
# Empty if enabled, set to "disabled" by default
if [[ -z "${RSTUDIO_LAUNCHER_STARTUP_HEALTH_CHECK}" ]]; then
+ local cacert='/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
+ local host="${KUBERNETES_SERVICE_HOST}"
+ if [[ "${host}" == *:* ]]; then
+ host="[${host}]"
+ fi
+ local k8s_url="https://${host}:${KUBERNETES_SERVICE_PORT}"
+
+ _logf 'Loading service account token'
+ local sa_token
+ sa_token="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
+
_logf 'Checking kubernetes health via %s' "${k8s_url}"
# shellcheck disable=SC2086
curl ${RSTUDIO_LAUNCHER_STARTUP_HEALTH_CHECK_ARGS} \
@@ -34,29 +30,10 @@ main() {
printf '\n'
fi
- _logf 'Configuring certs'
- cp -v "${cacert}" ${dyn_dir}/k8s-cert 2>&1 | _indent
- mkdir -p /usr/local/share/ca-certificates/Kubernetes
- cp -v \
- ${dyn_dir}/k8s-cert \
- /usr/local/share/ca-certificates/Kubernetes/cert-Kubernetes.crt 2>&1 | _indent
-
- _logf 'Updating CA certificates'
- PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
- DIST=$(cat /etc/os-release | grep "^ID=" -E -m 1 | cut -c 4-10 | sed 's/"//g')
- if [[ $DIST == "ubuntu" ]]; then
- update-ca-certificates 2>&1 | _indent
- elif [[ $DIST == "rhel" || $DIST == "almalinux" ]]; then
- update-ca-trust 2>&1 | _indent
- fi
-
_logf 'Preparing dirs'
mkdir -p \
/var/lib/rstudio-launcher/Local \
/var/lib/rstudio-launcher/Kubernetes
- chown -v -R \
- rstudio-server:rstudio-server \
- /var/lib/rstudio-launcher/Local 2>&1 | _indent
_logf 'Replacing process with %s' "${startup_script}"
exec "${startup_script}"
diff --git a/charts/rstudio-workbench/prestart-workbench.bash b/charts/rstudio-workbench/prestart-workbench.bash
index aa903d33..d32b141c 100644
--- a/charts/rstudio-workbench/prestart-workbench.bash
+++ b/charts/rstudio-workbench/prestart-workbench.bash
@@ -27,16 +27,6 @@ main() {
echo -e "delete-node-on-exit=1\nwww-host-name=$(hostname -i)" > /mnt/load-balancer/rstudio/load-balancer
fi
- _logf 'Preparing dirs'
- mkdir -p \
- /var/lib/rstudio-server/monitor/log
-
- if [ -d "/var/lib/rstudio-server/Local" ]; then
- chown -v -R \
- rstudio-server:rstudio-server \
- /var/lib/rstudio-server/Local 2>&1 | _indent
- fi
-
_writeEtcRstudioReadme
# TODO: necessary until https://github.com/rstudio/rstudio-pro/issues/3638
@@ -69,7 +59,7 @@ in order to facilitate running in Kubernetes. The directories are specified via
the XDG_CONFIG_DIRS environment variable defined in the Helm chart. The currently
defined directories are:
-$(echo "$XDG_CONFIG_DIRS" | sed 's/:/\n/g')
+$(echo "$XDG_CONFIG_DIRS" | tr ':' '\n')
$HERE$
) > /etc/rstudio/README
}
diff --git a/charts/rstudio-workbench/templates/NOTES.txt b/charts/rstudio-workbench/templates/NOTES.txt
index d8a21ee1..4aa212c0 100644
--- a/charts/rstudio-workbench/templates/NOTES.txt
+++ b/charts/rstudio-workbench/templates/NOTES.txt
@@ -70,3 +70,11 @@ Please consider removing this configuration value.
{{- print "\n\n`config.server.'rserver/.conf'.monitor-graphite-enabled` is overwritten by `prometheus.legacy=false`. Internal Workbench Prometheus will be used instead." }}
{{- end }}
+
+{{- if .Values.config.userProvisioning }}
+{{ print "\n\nWARNING: `config.userProvisioning` is deprecated. Use `config.sssd.conf` instead." }}
+{{- end }}
+
+{{- if and .Values.config.sssd.conf (not (include "rstudio-workbench.sssd.active" .)) }}
+{{ print "\n\nWARNING: `config.sssd.conf` is set but the SSSD daemon will not start. SSSD requires `pod.runAsRoot: true` and `config.sssd.enabled: true`." }}
+{{- end }}
diff --git a/charts/rstudio-workbench/templates/_helpers.tpl b/charts/rstudio-workbench/templates/_helpers.tpl
index 4973da6c..89bc973f 100644
--- a/charts/rstudio-workbench/templates/_helpers.tpl
+++ b/charts/rstudio-workbench/templates/_helpers.tpl
@@ -24,8 +24,42 @@ If release name contains chart name it will be used as a full name.
{{- end }}
{{- end }}
+{{/*
+Returns "true" when the SSSD daemon should actually run: sssd.enabled=true and pod.runAsRoot=true.
+SSSD cannot run as a non-root process, so the flag is silently ignored for non-root deployments.
+*/}}
+{{- define "rstudio-workbench.sssd.active" -}}
+{{- if and .Values.config.sssd.enabled .Values.pod.runAsRoot -}}
+true
+{{- end -}}
+{{- end -}}
+
+{{/*
+supervisord program definition that starts the bundled SSSD daemon.
+Rendered into the user-provisioning startup ConfigMap when sssd is active.
+*/}}
+{{- define "rstudio-workbench.sssd.program" -}}
+[program:sssd]
+command=/usr/sbin/sssd -i -c /etc/sssd/sssd.conf --logger=stderr
+autorestart=false
+numprocs=1
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stdout_logfile_backups=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
+stderr_logfile_backups=0
+{{- end -}}
+
{{- define "rstudio-workbench.containers" -}}
{{- $useNewerOverrides := and (not (hasKey .Values.config.server "launcher.kubernetes.profiles.conf")) (not .Values.launcher.useTemplates) }}
+{{- /* When running as non-root, secret files must be group-readable (0640) because Kubernetes mounts
+ them as root-owned and the non-root process accesses them via fsGroup group membership.
+ Root deployments keep the tighter 0600 default. */ -}}
+{{- $secretMode := $.Values.config.defaultMode.secret -}}
+{{- if not $.Values.pod.runAsRoot -}}
+ {{- $secretMode = 416 -}}{{/* octal 0640 */}}
+{{- end }}
containers:
- name: rstudio
{{- $defaultVersion := .Values.versionOverride | default $.Chart.AppVersion }}
@@ -85,13 +119,13 @@ containers:
{{- if .Values.pod.env }}
{{- toYaml .Values.pod.env | nindent 2 }}
{{- end }}
- {{- if .Values.command }}
+ {{- with .Values.command }}
command:
- {{- toYaml .Values.command | nindent 4 }}
- {{- end }}
- {{- if .Values.args }}
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+ {{- with .Values.args }}
args:
- {{- toYaml .Values.args | nindent 4 }}
+ {{- toYaml . | nindent 4 }}
{{- end }}
imagePullPolicy: "{{ .Values.image.imagePullPolicy }}"
ports:
@@ -139,19 +173,25 @@ containers:
- name: rstudio-secret
mountPath: "/mnt/secret-configmap/rstudio/"
{{- end }}
- {{- if .Values.config.userProvisioning }}
+ {{- if and (include "rstudio-workbench.sssd.active" .) (or .Values.config.sssd.conf .Values.config.userProvisioning) }}
- name: rstudio-user
mountPath: "/etc/sssd/conf.d/"
{{- end }}
- name: etc-rstudio
mountPath: "/etc/rstudio"
+ - name: mnt-dynamic
+ mountPath: "/mnt/dynamic"
+ {{- if or (gt (int .Values.replicas) 1) .Values.loadBalancer.forceEnabled }}
+ - name: mnt-load-balancer
+ mountPath: "/mnt/load-balancer"
+ {{- end }}
- name: rstudio-rsw-startup
mountPath: "/startup/base"
{{- if .Values.launcher.enabled }}
- name: rstudio-launcher-startup
mountPath: "/startup/launcher"
{{- end }}
- {{- if .Values.config.startupUserProvisioning }}
+ {{- if or (include "rstudio-workbench.sssd.active" .) .Values.config.startupUserProvisioning }}
- name: rstudio-user-startup
mountPath: "/startup/user-provisioning"
{{- end }}
@@ -276,6 +316,16 @@ volumes:
{{- end }}
- name: etc-rstudio
emptyDir: {}
+- name: mnt-dynamic
+ emptyDir: {}
+{{- if or (gt (int .Values.replicas) 1) .Values.loadBalancer.forceEnabled }}
+{{- /* prestart-workbench.bash writes the load-balancer config here when RSW_LOAD_BALANCING
+ is set. Without a mounted volume the path is on the read-only-by-default root fs,
+ which a non-root pod (pod.runAsRoot: false) cannot write to. fsGroup makes the
+ emptyDir group-writable for the unprivileged user. */}}
+- name: mnt-load-balancer
+ emptyDir: {}
+{{- end }}
- name: rstudio-config
configMap:
name: {{ include "rstudio-workbench.fullname" . }}-config
@@ -304,7 +354,7 @@ volumes:
name: {{ include "rstudio-workbench.fullname" . }}-start-launcher
defaultMode: {{ .Values.config.defaultMode.startup }}
{{- end }}
-{{- if .Values.config.startupUserProvisioning }}
+{{- if or (include "rstudio-workbench.sssd.active" .) .Values.config.startupUserProvisioning }}
- name: rstudio-user-startup
configMap:
name: {{ include "rstudio-workbench.fullname" . }}-start-user
@@ -335,7 +385,7 @@ volumes:
items:
- key: {{ $key }}
path: {{ $key }}
- mode: {{ $.Values.config.defaultMode.secret }}
+ mode: {{ $secretMode }}
{{- end }}
{{- end }}
{{- if .Values.launcherPem.existingSecret }}
@@ -344,7 +394,7 @@ volumes:
items:
- key: launcher.pem
path: launcher.pem
- mode: {{ .Values.config.defaultMode.secret }}
+ mode: {{ $secretMode }}
{{- end }}
{{- /* Project launcher.pem from chart-managed secret when using .value (not existingSecret). */ -}}
{{- if and .Values.launcherPem.value (not .Values.launcherPem.existingSecret) }}
@@ -353,7 +403,7 @@ volumes:
items:
- key: launcher.pem
path: launcher.pem
- mode: {{ .Values.config.defaultMode.secret }}
+ mode: {{ $secretMode }}
{{- end }}
{{- if and .Values.secureCookieKey.existingSecret (not .Values.global.secureCookieKey.existingSecret) }}
- secret:
@@ -361,7 +411,7 @@ volumes:
items:
- key: secure-cookie-key
path: secure-cookie-key
- mode: {{ .Values.config.defaultMode.secret }}
+ mode: {{ $secretMode }}
{{- end }}
{{- if .Values.global.secureCookieKey.existingSecret }}
- secret:
@@ -369,7 +419,7 @@ volumes:
items:
- key: secure-cookie-key
path: secure-cookie-key
- mode: {{ .Values.config.defaultMode.secret }}
+ mode: {{ $secretMode }}
{{- end }}
{{- /* Project secure-cookie-key from chart-managed secret when using .value (not existingSecret).
This handles the case where secureCookieKey.value or global.secureCookieKey.value is set directly,
@@ -380,7 +430,7 @@ volumes:
items:
- key: secure-cookie-key
path: secure-cookie-key
- mode: {{ .Values.config.defaultMode.secret }}
+ mode: {{ $secretMode }}
{{- end }}
{{- if .Values.config.database.conf.existingSecret }}
- secret:
@@ -388,7 +438,7 @@ volumes:
items:
- key: database.conf
path: database.conf
- mode: {{ .Values.config.defaultMode.secret }}
+ mode: {{ $secretMode }}
{{- end }}
{{- range .Values.config.existingSecrets }}
- secret:
@@ -397,11 +447,11 @@ volumes:
{{- range .items }}
- key: {{ .key }}
path: {{ .path }}
- mode: {{ $.Values.config.defaultMode.secret }}
+ mode: {{ $secretMode }}
{{- end }}
{{- end }}
{{- end }}
-{{- if .Values.config.userProvisioning }}
+{{- if and (include "rstudio-workbench.sssd.active" .) (or .Values.config.sssd.conf .Values.config.userProvisioning) }}
- name: rstudio-user
secret:
secretName: {{ include "rstudio-workbench.fullname" . }}-user
diff --git a/charts/rstudio-workbench/templates/configmap-general.yaml b/charts/rstudio-workbench/templates/configmap-general.yaml
index ef1ef574..b95b0fa0 100644
--- a/charts/rstudio-workbench/templates/configmap-general.yaml
+++ b/charts/rstudio-workbench/templates/configmap-general.yaml
@@ -49,6 +49,9 @@
{{- $defaultIDEServiceName := include "rstudio-workbench.fullname" . }}
{{- $defaultIDEServiceURL := printf "http://%s.%s.svc.cluster.local:80" $defaultIDEServiceName $.Release.Namespace }}
{{- $defaultRServerConfigValues := dict "launcher-sessions-callback-address" ($defaultIDEServiceURL) }}
+{{- if .Values.pod.serviceAccountUser }}
+ {{- $_ := set $defaultRServerConfigValues "server-user" .Values.pod.serviceAccountUser }}
+{{- end }}
{{- if and .Values.launcher.enabled .Values.components.enabled }}
{{- $initTag := .Values.components.sessionInit.image.tag | default $defaultVersion }}
{{- $_ := set $defaultRServerConfigValues "launcher-sessions-auto-update" 1 }}
@@ -109,6 +112,23 @@ data:
{{- end }}
{{- end }}
{{- $overrideDict = mergeOverwrite $defaultLauncherK8sConfig $overrideDict }}
+{{- /* Non-root (rootless) defaults, overridable by config.server:
+ - secure-cookie-key-file: the launcher runs unprivileged and would otherwise generate its own
+ secure-cookie-key, so rserver-signed launcher requests fail auth. Point it at the shared key.
+ - auth-pam-sessions-enabled=0: opening host PAM sessions requires root; launcher session
+ workloads handle session setup. Matches the product default for launcher-sessions deployments.
+ - user-provisioning-enabled=1: native (SCIM) provisioning is the rootless user-resolution path.
+ - launcher-sessions-create-container-user=0: the create-container-user model needs the session
+ container to start as root so it can useradd + setuid into the requesting user; an unprivileged
+ container cannot, so the session fails to launch (EPERM). Disabling it makes rserver stamp the
+ session pod securityContext with the user's UID/GID instead (the runAsUserId branch in
+ ServerJobLauncher), so the session runs directly as the user with no privilege. */}}
+{{- if not .Values.pod.runAsRoot }}
+ {{- $rootlessDefaults := dict "rserver.conf" (dict "auth-pam-sessions-enabled" 0 "user-provisioning-enabled" 1 "launcher-sessions-create-container-user" 0) "launcher.conf" (dict "server" (dict "secure-cookie-key-file" "/mnt/secret-configmap/rstudio/secure-cookie-key")) }}
+ {{- $overrideDict = mergeOverwrite $rootlessDefaults $overrideDict }}
+{{- else }}
+ {{- $overrideDict = mergeOverwrite (dict "rserver.conf" (dict "auth-pam-sessions-enabled" 1)) $overrideDict }}
+{{- end }}
{{ include "rstudio-library.config.ini" $overrideDict | indent 2 }}
{{/* helper variables to make things here a bit more sane */}}
{{- $profilesConfig := .Values.config.profiles }}
diff --git a/charts/rstudio-workbench/templates/configmap-secret.yaml b/charts/rstudio-workbench/templates/configmap-secret.yaml
index 7e9d885b..fe1ae560 100644
--- a/charts/rstudio-workbench/templates/configmap-secret.yaml
+++ b/charts/rstudio-workbench/templates/configmap-secret.yaml
@@ -64,7 +64,9 @@ stringData:
{{- end }}
{{- end }}
---
-{{- if .Values.config.userProvisioning }}
+{{- $sssdConf := .Values.config.sssd.conf }}
+{{- if not $sssdConf }}{{- $sssdConf = (default (dict) .Values.config.userProvisioning) }}{{- end }}
+{{- if and (include "rstudio-workbench.sssd.active" .) $sssdConf }}
{{- if .Values.sealedSecret.enabled -}}
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
@@ -75,7 +77,7 @@ metadata:
namespace: {{ $.Release.Namespace }}
spec:
encryptedData:
- {{- include "rstudio-library.config.ini" .Values.config.userProvisioning | nindent 4 }}
+ {{- include "rstudio-library.config.ini" $sssdConf | nindent 4 }}
{{- else }}
apiVersion: v1
kind: Secret
@@ -83,6 +85,6 @@ metadata:
name: {{ include "rstudio-workbench.fullname" . }}-user
namespace: {{ $.Release.Namespace }}
stringData:
- {{- include "rstudio-library.config.ini" .Values.config.userProvisioning | nindent 2 }}
+ {{- include "rstudio-library.config.ini" $sssdConf | nindent 2 }}
{{- end }}
{{- end }}
diff --git a/charts/rstudio-workbench/templates/configmap-startup.yaml b/charts/rstudio-workbench/templates/configmap-startup.yaml
index 89a2c5f9..46833595 100644
--- a/charts/rstudio-workbench/templates/configmap-startup.yaml
+++ b/charts/rstudio-workbench/templates/configmap-startup.yaml
@@ -31,7 +31,11 @@ data:
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
{{- end }}
-{{- if .Values.config.startupUserProvisioning }}
+{{- $userStartup := deepCopy (default (dict) .Values.config.startupUserProvisioning) }}
+{{- if include "rstudio-workbench.sssd.active" . }}
+{{- $_ := set $userStartup "sssd.conf" (include "rstudio-workbench.sssd.program" .) }}
+{{- end }}
+{{- if $userStartup }}
---
apiVersion: v1
kind: ConfigMap
@@ -39,7 +43,7 @@ metadata:
name: {{ include "rstudio-workbench.fullname" . }}-start-user
namespace: {{ $.Release.Namespace }}
data:
- {{- include "rstudio-library.config.ini" .Values.config.startupUserProvisioning | nindent 2 }}
+ {{- include "rstudio-library.config.ini" $userStartup | nindent 2 }}
{{- end }}
{{- if .Values.config.startupCustom }}
---
diff --git a/charts/rstudio-workbench/templates/deployment.yaml b/charts/rstudio-workbench/templates/deployment.yaml
index 8869e70b..9f67c5f1 100644
--- a/charts/rstudio-workbench/templates/deployment.yaml
+++ b/charts/rstudio-workbench/templates/deployment.yaml
@@ -88,7 +88,21 @@ spec:
topologySpreadConstraints:
{{- toYaml . | nindent 8 }}
{{- end }}
- {{- with .Values.pod.securityContext }}
+ {{- $defaultPodSC := dict }}
+ {{- $userPodSC := default (dict) .Values.pod.securityContext }}
+ {{- if .Values.pod.runAsRoot }}
+ {{- /* Skip runAsUser: 0 when pod.securityContext already pins the user, to avoid emitting
+ a contradictory runAsNonRoot: true + runAsUser: 0 combination that fails admission. */ -}}
+ {{- if not (or (hasKey $userPodSC "runAsUser") (hasKey $userPodSC "runAsNonRoot")) }}
+ {{- $_ := set $defaultPodSC "runAsUser" 0 }}
+ {{- end }}
+ {{- else }}
+ {{- $_ := set $defaultPodSC "runAsNonRoot" true }}
+ {{- $_ := set $defaultPodSC "runAsUser" (int .Values.pod.serviceAccountUserId) }}
+ {{- $_ := set $defaultPodSC "fsGroup" (int .Values.pod.serviceAccountUserId) }}
+ {{- end }}
+ {{- $effectivePodSC := mergeOverwrite $defaultPodSC (deepCopy $userPodSC) }}
+ {{- with $effectivePodSC }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
diff --git a/charts/rstudio-workbench/tests/configmap_test.yaml b/charts/rstudio-workbench/tests/configmap_test.yaml
index bfdc81f4..586f329a 100644
--- a/charts/rstudio-workbench/tests/configmap_test.yaml
+++ b/charts/rstudio-workbench/tests/configmap_test.yaml
@@ -256,3 +256,161 @@ tests:
- notMatchRegex:
path: data["positron.conf"]
pattern: "^exe="
+
+ # -- serviceAccountUser -> rserver.conf server-user
+ - it: should render server-user=rstudio-server by default
+ template: configmap-general.yaml
+ documentIndex: 0
+ asserts:
+ - matchRegex:
+ path: data["rserver.conf"]
+ pattern: "server-user=rstudio-server"
+
+ - it: should render server-user when serviceAccountUser is overridden
+ template: configmap-general.yaml
+ documentIndex: 0
+ set:
+ pod.serviceAccountUser: "workbench"
+ asserts:
+ - matchRegex:
+ path: data["rserver.conf"]
+ pattern: "server-user=workbench"
+
+ - it: should omit server-user when serviceAccountUser is empty
+ template: configmap-general.yaml
+ documentIndex: 0
+ set:
+ pod.serviceAccountUser: ""
+ asserts:
+ - notMatchRegex:
+ path: data["rserver.conf"]
+ pattern: "server-user="
+
+ - it: should let config.server.rserver.conf.server-user override serviceAccountUser
+ template: configmap-general.yaml
+ documentIndex: 0
+ set:
+ pod.serviceAccountUser: "rstudio-server"
+ config:
+ server:
+ rserver.conf:
+ server-user: "explicit-override"
+ asserts:
+ - matchRegex:
+ path: data["rserver.conf"]
+ pattern: "server-user=explicit-override"
+ - notMatchRegex:
+ path: data["rserver.conf"]
+ pattern: "server-user=rstudio-server"
+
+ # -- Rootless: prestart-launcher.bash has no privileged operations
+ - it: should not install CA certs into the system trust store in prestart-launcher.bash
+ template: configmap-prestart.yaml
+ documentIndex: 0
+ asserts:
+ - notMatchRegex:
+ path: data["prestart-launcher.bash"]
+ pattern: "update-ca-certificates"
+ - notMatchRegex:
+ path: data["prestart-launcher.bash"]
+ pattern: "update-ca-trust"
+ - notMatchRegex:
+ path: data["prestart-launcher.bash"]
+ pattern: "/usr/local/share/ca-certificates"
+
+ - it: should not chown launcher dirs in prestart-launcher.bash
+ template: configmap-prestart.yaml
+ documentIndex: 0
+ asserts:
+ - notMatchRegex:
+ path: data["prestart-launcher.bash"]
+ pattern: "chown"
+
+ - it: should keep the launcher health check and scratch dirs in prestart-launcher.bash
+ template: configmap-prestart.yaml
+ documentIndex: 0
+ asserts:
+ - matchRegex:
+ path: data["prestart-launcher.bash"]
+ pattern: "--cacert"
+ - matchRegex:
+ path: data["prestart-launcher.bash"]
+ pattern: "/var/lib/rstudio-launcher/Local"
+
+ # -- Rootless: prestart-workbench.bash has no privileged operations
+ - it: should not chown or create monitor dirs in prestart-workbench.bash
+ template: configmap-prestart.yaml
+ documentIndex: 0
+ asserts:
+ - notMatchRegex:
+ path: data["prestart-workbench.bash"]
+ pattern: "chown"
+ - notMatchRegex:
+ path: data["prestart-workbench.bash"]
+ pattern: "/var/lib/rstudio-server/monitor/log"
+
+ - it: should keep launcher.pub generation and README write in prestart-workbench.bash
+ template: configmap-prestart.yaml
+ documentIndex: 0
+ asserts:
+ - matchRegex:
+ path: data["prestart-workbench.bash"]
+ pattern: "openssl rsa"
+ - matchRegex:
+ path: data["prestart-workbench.bash"]
+ pattern: "/etc/rstudio/README"
+
+ # -- runAsRoot -> rootless config defaults
+ - it: should apply rootless config defaults when runAsRoot is false
+ template: configmap-general.yaml
+ documentIndex: 0
+ set:
+ pod.runAsRoot: false
+ asserts:
+ - matchRegex:
+ path: data["rserver.conf"]
+ pattern: "(?m)^auth-pam-sessions-enabled=0$"
+ - matchRegex:
+ path: data["rserver.conf"]
+ pattern: "(?m)^user-provisioning-enabled=1$"
+ - matchRegex:
+ path: data["rserver.conf"]
+ pattern: "(?m)^launcher-sessions-create-container-user=0$"
+ - matchRegex:
+ path: data["launcher.conf"]
+ pattern: "secure-cookie-key-file=/mnt/secret-configmap/rstudio/secure-cookie-key"
+
+ - it: should keep PAM sessions on and not force provisioning when runAsRoot is true
+ template: configmap-general.yaml
+ documentIndex: 0
+ asserts:
+ - matchRegex:
+ path: data["rserver.conf"]
+ pattern: "(?m)^auth-pam-sessions-enabled=1$"
+ - notMatchRegex:
+ path: data["rserver.conf"]
+ pattern: "user-provisioning-enabled"
+ - notMatchRegex:
+ path: data["rserver.conf"]
+ pattern: "launcher-sessions-create-container-user"
+ - notMatchRegex:
+ path: data["launcher.conf"]
+ pattern: "secure-cookie-key-file"
+
+ - it: should let config.server override the rootless auth defaults
+ template: configmap-general.yaml
+ documentIndex: 0
+ set:
+ pod.serviceAccountUser: "rstudio-server"
+ config:
+ server:
+ rserver.conf:
+ auth-pam-sessions-enabled: 1
+ user-provisioning-enabled: 0
+ asserts:
+ - matchRegex:
+ path: data["rserver.conf"]
+ pattern: "(?m)^auth-pam-sessions-enabled=1$"
+ - matchRegex:
+ path: data["rserver.conf"]
+ pattern: "(?m)^user-provisioning-enabled=0$"
diff --git a/charts/rstudio-workbench/tests/deployment_test.yaml b/charts/rstudio-workbench/tests/deployment_test.yaml
index 876e2b81..d353629a 100644
--- a/charts/rstudio-workbench/tests/deployment_test.yaml
+++ b/charts/rstudio-workbench/tests/deployment_test.yaml
@@ -141,6 +141,61 @@ tests:
- equal:
path: 'spec.template.spec.containers[0].env[?(@.name=="RSW_LOAD_BALANCING")].value'
value: "true"
+ - it: should mount a writable /mnt/load-balancer volume when replicas > 1
+ template: deployment.yaml
+ set:
+ replicas: 2
+ asserts:
+ - contains:
+ path: 'spec.template.spec.containers[0].volumeMounts'
+ content:
+ name: "mnt-load-balancer"
+ mountPath: "/mnt/load-balancer"
+ any: true
+ - contains:
+ path: 'spec.template.spec.volumes'
+ content:
+ name: "mnt-load-balancer"
+ emptyDir: {}
+ any: true
+ - it: should mount a writable /mnt/load-balancer volume when loadBalancer.forceEnabled is true even if replicas = 1
+ template: deployment.yaml
+ set:
+ replicas: 1
+ loadBalancer:
+ forceEnabled: true
+ asserts:
+ - contains:
+ path: 'spec.template.spec.containers[0].volumeMounts'
+ content:
+ name: "mnt-load-balancer"
+ mountPath: "/mnt/load-balancer"
+ any: true
+ - contains:
+ path: 'spec.template.spec.volumes'
+ content:
+ name: "mnt-load-balancer"
+ emptyDir: {}
+ any: true
+ - it: should not mount the /mnt/load-balancer volume when replicas = 1 and forceEnabled is false
+ template: deployment.yaml
+ set:
+ replicas: 1
+ loadBalancer:
+ forceEnabled: false
+ asserts:
+ - notContains:
+ path: 'spec.template.spec.containers[0].volumeMounts'
+ content:
+ name: "mnt-load-balancer"
+ mountPath: "/mnt/load-balancer"
+ any: true
+ - notContains:
+ path: 'spec.template.spec.volumes'
+ content:
+ name: "mnt-load-balancer"
+ emptyDir: {}
+ any: true
- it: should specify a volumeMount and a volume for sharedStorage if sharedStorage.create is true
template: deployment.yaml
set:
@@ -266,12 +321,15 @@ tests:
path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-session-secret")]'
- notExists:
path: 'spec.template.spec.volumes[?(@.name=="rstudio-session-secret")]'
- - it: should specify a volumeMount and a volume for userProvisioning if config.userProvisioning is defined and not empty
+ - it: should specify a volumeMount and a volume for sssd conf when sssd is enabled (deprecated userProvisioning fallback)
template: deployment.yaml
set:
+ pod.serviceAccountUser: root
config:
defaultMode:
userProvisioning: 0600
+ sssd:
+ enabled: true
userProvisioning:
sssd.conf:
dsn: "test"
@@ -353,13 +411,15 @@ tests:
- equal:
path: 'spec.template.spec.volumes[?(@.name=="rstudio-user-startup")].configMap.defaultMode'
value: 0600
- - it: should not specify a volumeMount and a volume for startupUserProvisioning if config.startupUserProvisioning is not defined
+ - it: should not specify a volumeMount and a volume for startupUserProvisioning if config.startupUserProvisioning is not defined and sssd is disabled
template: deployment.yaml
set:
config:
defaultMode:
startup: 0600
startupUserProvisioning: null
+ sssd:
+ enabled: false
asserts:
- notExists:
path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user-startup")]'
@@ -852,3 +912,155 @@ tests:
content:
name: "positron-components"
any: true
+
+ # -- runAsRoot -> pod securityContext
+ - it: should run pod as root by default
+ template: deployment.yaml
+ asserts:
+ - equal:
+ path: spec.template.spec.securityContext.runAsUser
+ value: 0
+ - notExists:
+ path: spec.template.spec.securityContext.runAsNonRoot
+
+ - it: should run pod as non-root when runAsRoot is false
+ template: deployment.yaml
+ set:
+ pod.runAsRoot: false
+ asserts:
+ - equal:
+ path: spec.template.spec.securityContext.runAsNonRoot
+ value: true
+ - equal:
+ path: spec.template.spec.securityContext.runAsUser
+ value: 999
+ - equal:
+ path: spec.template.spec.securityContext.fsGroup
+ value: 999
+
+ - it: should use custom uid when serviceAccountUserId is overridden
+ template: deployment.yaml
+ set:
+ pod.runAsRoot: false
+ pod.serviceAccountUserId: 1500
+ asserts:
+ - equal:
+ path: spec.template.spec.securityContext.runAsNonRoot
+ value: true
+ - equal:
+ path: spec.template.spec.securityContext.runAsUser
+ value: 1500
+
+ - it: should let pod.securityContext override the non-root defaults
+ template: deployment.yaml
+ set:
+ pod.runAsRoot: false
+ pod:
+ securityContext:
+ runAsUser: 4242
+ fsGroup: 4242
+ asserts:
+ - equal:
+ path: spec.template.spec.securityContext.runAsUser
+ value: 4242
+ - equal:
+ path: spec.template.spec.securityContext.fsGroup
+ value: 4242
+ - equal:
+ path: spec.template.spec.securityContext.runAsNonRoot
+ value: true
+
+ - it: should not force runAsUser 0 when root pod.securityContext sets runAsNonRoot
+ template: deployment.yaml
+ set:
+ pod.runAsRoot: true
+ pod:
+ securityContext:
+ runAsNonRoot: true
+ asserts:
+ - equal:
+ path: spec.template.spec.securityContext.runAsNonRoot
+ value: true
+ - notExists:
+ path: spec.template.spec.securityContext.runAsUser
+
+ - it: should respect a root pod.securityContext runAsUser override
+ template: deployment.yaml
+ set:
+ pod.runAsRoot: true
+ pod:
+ securityContext:
+ runAsUser: 1234
+ asserts:
+ - equal:
+ path: spec.template.spec.securityContext.runAsUser
+ value: 1234
+
+ # -- command/args: the image's supervisord.conf supports non-root operation, so the
+ # chart sets no command/args override and relies on the container's default CMD for
+ # every serviceAccountUser value. No supervisor-tmp emptyDir is needed.
+ - it: should not override command/args with default serviceAccountUser
+ template: deployment.yaml
+ asserts:
+ - notExists:
+ path: spec.template.spec.containers[0].command
+ - notExists:
+ path: spec.template.spec.containers[0].args
+ - notContains:
+ path: spec.template.spec.volumes
+ content:
+ name: supervisor-tmp
+ emptyDir: {}
+ - notContains:
+ path: spec.template.spec.containers[0].volumeMounts
+ content:
+ name: supervisor-tmp
+ mountPath: /tmp
+
+ - it: should not override command/args when runAsRoot is true
+ template: deployment.yaml
+ set:
+ pod.runAsRoot: true
+ asserts:
+ - notExists:
+ path: spec.template.spec.containers[0].command
+ - notExists:
+ path: spec.template.spec.containers[0].args
+
+ - it: should not override command/args when runAsRoot is false
+ template: deployment.yaml
+ set:
+ pod.runAsRoot: false
+ asserts:
+ - notExists:
+ path: spec.template.spec.containers[0].command
+ - notExists:
+ path: spec.template.spec.containers[0].args
+
+ - it: should respect an explicitly-set command
+ template: deployment.yaml
+ set:
+ command:
+ - /custom/entrypoint
+ asserts:
+ - equal:
+ path: spec.template.spec.containers[0].command
+ value:
+ - /custom/entrypoint
+ - notExists:
+ path: spec.template.spec.containers[0].args
+
+ - it: should respect explicitly-set args
+ template: deployment.yaml
+ set:
+ args:
+ - --foo
+ - bar
+ asserts:
+ - notExists:
+ path: spec.template.spec.containers[0].command
+ - equal:
+ path: spec.template.spec.containers[0].args
+ value:
+ - --foo
+ - bar
diff --git a/charts/rstudio-workbench/tests/secrets_test.yaml b/charts/rstudio-workbench/tests/secrets_test.yaml
index 9feb395a..f43b45d7 100644
--- a/charts/rstudio-workbench/tests/secrets_test.yaml
+++ b/charts/rstudio-workbench/tests/secrets_test.yaml
@@ -131,6 +131,25 @@ tests:
- equal:
path: 'spec.template.spec.volumes[?(@.name=="rstudio-secret")].projected.sources[0].secret.items[0].path'
value: 'database.conf'
+ - it: should mount secrets group-readable (0640) when running non-root
+ template: deployment.yaml
+ set:
+ pod.runAsRoot: false
+ config:
+ defaultMode:
+ secret: 0600
+ secret:
+ database.conf:
+ provider: 'postgresql'
+ database: 'rsp'
+ port: 5432
+ host: 'db.example.com'
+ username: 'rstudio_app'
+ password: 'securepassword'
+ asserts:
+ - equal:
+ path: 'spec.template.spec.volumes[?(@.name=="rstudio-secret")].projected.sources[0].secret.items[0].mode'
+ value: 0640
- it: should specify a volumeMount and a projected volume for rstudio-secret if launcherPem.existingSecret is set
template: deployment.yaml
set:
diff --git a/charts/rstudio-workbench/tests/sssd_test.yaml b/charts/rstudio-workbench/tests/sssd_test.yaml
new file mode 100644
index 00000000..59ebb8a2
--- /dev/null
+++ b/charts/rstudio-workbench/tests/sssd_test.yaml
@@ -0,0 +1,124 @@
+suite: Workbench SSSD provisioning
+templates:
+ - configmap-startup.yaml
+ - configmap-secret.yaml
+ - configmap-general.yaml
+ - configmap-graphite-exporter.yaml
+ - configmap-prestart.yaml
+ - configmap-session.yaml
+ - deployment.yaml
+ - NOTES.txt
+tests:
+ - it: starts the sssd daemon by default when running as root
+ template: configmap-startup.yaml
+ documentSelector:
+ path: metadata.name
+ value: RELEASE-NAME-rstudio-workbench-start-user
+ asserts:
+ - matchRegex:
+ path: data['sssd.conf']
+ pattern: "\\[program:sssd\\]"
+ - matchRegex:
+ path: data['sssd.conf']
+ pattern: "/usr/sbin/sssd -i -c /etc/sssd/sssd.conf"
+
+ - it: does not start the sssd daemon when runAsRoot is false
+ template: configmap-startup.yaml
+ set:
+ pod.runAsRoot: false
+ asserts:
+ - notMatchRegexRaw:
+ pattern: "program:sssd"
+
+ - it: does not start the sssd daemon when config.sssd.enabled is false
+ template: configmap-startup.yaml
+ set:
+ config.sssd.enabled: false
+ asserts:
+ - notMatchRegexRaw:
+ pattern: "program:sssd"
+
+ - it: does not render the sssd conf secret by default
+ template: configmap-secret.yaml
+ asserts:
+ - notMatchRegexRaw:
+ pattern: "workbench-user"
+
+ - it: renders the sssd conf secret when enabled with conf
+ template: configmap-secret.yaml
+ set:
+ config.sssd.conf:
+ mysssd.conf:
+ sssd:
+ config_file_version: 2
+ domain/example.com:
+ id_provider: ldap
+ documentSelector:
+ path: metadata.name
+ value: RELEASE-NAME-rstudio-workbench-user
+ asserts:
+ - isKind:
+ of: Secret
+ - matchRegex:
+ path: stringData['mysssd.conf']
+ pattern: "id_provider"
+
+ - it: mounts sssd-user-startup by default when running as root
+ template: deployment.yaml
+ asserts:
+ - notExists:
+ path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user")]'
+ - equal:
+ path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user-startup")].mountPath'
+ value: "/startup/user-provisioning"
+
+ - it: does not mount sssd directories when runAsRoot is false
+ template: deployment.yaml
+ set:
+ pod.runAsRoot: false
+ asserts:
+ - notExists:
+ path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user")]'
+ - notExists:
+ path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user-startup")]'
+
+ - it: mounts sssd directories when running as root with sssd.conf set
+ template: deployment.yaml
+ set:
+ config.sssd.conf:
+ mysssd.conf:
+ sssd:
+ config_file_version: 2
+ asserts:
+ - equal:
+ path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user")].mountPath'
+ value: "/etc/sssd/conf.d/"
+ - equal:
+ path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user-startup")].mountPath'
+ value: "/startup/user-provisioning"
+
+ - it: silently skips sssd when runAsRoot is false even if sssd.enabled is true
+ template: NOTES.txt
+ set:
+ pod.runAsRoot: false
+ config.sssd.enabled: true
+ asserts:
+ - matchRegexRaw:
+ pattern: "successfully deployed"
+
+ - it: renders successfully when running as root with sssd enabled
+ template: NOTES.txt
+ asserts:
+ - matchRegexRaw:
+ pattern: "successfully deployed"
+
+ - it: warns when the deprecated config.userProvisioning is set
+ template: NOTES.txt
+ set:
+ config.userProvisioning:
+ mysssd.conf:
+ sssd:
+ config_file_version: 2
+ asserts:
+ - matchRegexRaw:
+ pattern: "config.userProvisioning.*deprecated"
diff --git a/charts/rstudio-workbench/values.yaml b/charts/rstudio-workbench/values.yaml
index c7830ff6..992988dd 100644
--- a/charts/rstudio-workbench/values.yaml
+++ b/charts/rstudio-workbench/values.yaml
@@ -339,9 +339,9 @@ loadBalancer:
# -- whether to force the loadBalancer to be enabled. Otherwise requires replicas > 1. Worth setting if you are HA but may only have one node
forceEnabled: false
-# -- command is the pod container's run command. By default, it uses the container's default. However, the chart expects a container using `supervisord` for startup
+# -- command is the pod container's run command. When unset, the container's default command (`supervisord`) is used, which supports both root and non-root (`serviceAccountUser`) operation.
command: []
-# -- args is the pod container's run arguments.
+# -- args is the pod container's run arguments. When unset, the container's default arguments are used.
args: []
license:
@@ -404,6 +404,18 @@ podDisruptionBudget: {}
revisionHistoryLimit: 3
pod:
+ # -- Whether the pod's containers run as the root OS user. `true` (default) preserves historical
+ # behavior: pod runs as root (`runAsUser: 0`), SSSD starts, secrets mount 0600, PAM sessions on.
+ # `false` runs unprivileged: pod sets `runAsNonRoot: true` and `runAsUser`/`fsGroup` to
+ # `pod.serviceAccountUserId`, SSSD is skipped, secrets mount 0640, and non-root
+ # `rserver.conf`/`launcher.conf` defaults are applied.
+ runAsRoot: true
+ # -- The OS user written to `rserver.conf` as `server-user`. Set to `""` to omit `server-user`
+ # from `rserver.conf` entirely.
+ serviceAccountUser: "rstudio-server"
+ # -- The UID used as `runAsUser`/`fsGroup` in the pod's securityContext when `pod.runAsRoot` is
+ # false. Must match the UID of `pod.serviceAccountUser` baked into the Workbench image.
+ serviceAccountUserId: 999
# -- env is an array of maps that is injected as-is into the "env:" component of the pod.container spec
env: []
# -- volumes is injected as-is into the "volumes:" component of the pod.container spec
@@ -568,14 +580,19 @@ config:
# items:
# - key: custom-file.conf
# path: custom-file.conf
- # -- a map of sssd config files, used for user provisioning. Mounted to `/etc/sssd/conf.d/` with 0600 permissions
+ # -- DEPRECATED: use `config.sssd.conf` instead. A map of sssd config files, mounted to `/etc/sssd/conf.d/` with 0600 permissions. Only applied when `config.sssd.enabled=true`.
userProvisioning: {}
+ # -- Bundled SSSD daemon for legacy LDAP/Active Directory user provisioning. On by default; automatically skipped when the pod runs unprivileged (`pod.runAsRoot: false`), since SSSD requires root. Modern provisioning (SCIM / native) does not require SSSD.
+ sssd:
+ # -- whether to start the bundled SSSD daemon. Automatically skipped (not an error) when `pod.runAsRoot` is false. Set to `false` to disable entirely.
+ enabled: true
+ # -- a map of sssd config files, mounted to `/etc/sssd/conf.d/` with 0600 permissions. Replaces the deprecated `config.userProvisioning`.
+ conf: {}
# -- a map of server config files. Mounted to `/mnt/configmap/rstudio/`
# @default -- [RStudio Workbench Configuration Reference](https://docs.rstudio.com/ide/server-pro/rstudio_server_configuration/rstudio_server_configuration.html). See defaults with `helm show values`
server:
rserver.conf:
server-health-check-enabled: 1
- auth-pam-sessions-enabled: 1
admin-enabled: 1
www-port: 8787
server-project-sharing: 1
@@ -663,19 +680,8 @@ config:
launcher-mounts: []
# -- a map of supervisord .conf files to define custom services. Mounted into the container at /startup/custom/
startupCustom: {}
- # -- a map of supervisord .conf files to define user provisioning services. Mounted into the container at /startup/user-provisioning/
- startupUserProvisioning:
- sssd.conf: |
- [program:sssd]
- command=/usr/sbin/sssd -i -c /etc/sssd/sssd.conf --logger=stderr
- autorestart=false
- numprocs=1
- stdout_logfile=/dev/stdout
- stdout_logfile_maxbytes=0
- stdout_logfile_backups=0
- stderr_logfile=/dev/stderr
- stderr_logfile_maxbytes=0
- stderr_logfile_backups=0
+ # -- a map of supervisord .conf files to define user provisioning services. Mounted into the container at /startup/user-provisioning/. The bundled SSSD service is now controlled by `config.sssd.enabled`; use this only for custom provisioning daemons.
+ startupUserProvisioning: {}
# -- a map of pam config files. Will be mounted into the container directly / per file, in order to avoid overwriting system pam files
pam: {}