From 026a9388bdf46f6cf2d05e91d6a188ce72bd61e8 Mon Sep 17 00:00:00 2001 From: Sean Sinnott Date: Mon, 1 Jun 2026 15:27:15 -0400 Subject: [PATCH 1/9] Run Workbench as non-root by default via serviceAccountUser - Run Workbench as non-root by default via serviceAccountUser - Invoke supervisord with -u "" when running Workbench non-root - Fix supervisord non-root workaround: pass -u instead of -u "" - Fix non-root supervisord startup: emptyDir, path rewriting, fsGroup - Simplify prestart-launcher.bash for non-root operation - Simplify prestart-workbench.bash for non-root operation - Update secrets test mode assertions for non-root default (0640) - Document rootless prestart script simplification - Drop supervisord non-root workaround, use image default command - Add explicit config.sssd toggle, off by default - Add non-root config defaults for launcher auth, PAM, provisioning - Default serviceAccountUser to root, keep non-root opt-in - Avoid contradictory root securityContext on user override --- charts/rstudio-workbench/Chart.yaml | 2 +- charts/rstudio-workbench/NEWS.md | 17 ++ charts/rstudio-workbench/README.md | 58 +++--- charts/rstudio-workbench/README.md.gotmpl | 39 +++-- .../rstudio-workbench/prestart-launcher.bash | 45 ++--- .../rstudio-workbench/prestart-workbench.bash | 12 +- charts/rstudio-workbench/templates/NOTES.txt | 12 ++ .../rstudio-workbench/templates/_helpers.tpl | 62 +++++-- .../templates/configmap-general.yaml | 15 ++ .../templates/configmap-secret.yaml | 8 +- .../templates/configmap-startup.yaml | 8 +- .../templates/deployment.yaml | 16 +- .../tests/configmap_test.yaml | 154 ++++++++++++++++ .../tests/deployment_test.yaml | 165 +++++++++++++++++- .../rstudio-workbench/tests/secrets_test.yaml | 19 ++ charts/rstudio-workbench/tests/sssd_test.yaml | 122 +++++++++++++ charts/rstudio-workbench/values.yaml | 34 ++-- 17 files changed, 658 insertions(+), 130 deletions(-) create mode 100644 charts/rstudio-workbench/tests/sssd_test.yaml diff --git a/charts/rstudio-workbench/Chart.yaml b/charts/rstudio-workbench/Chart.yaml index 6c0175ba2..b076f9ef5 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.0 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 0e027dfe2..6ce9a827a 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -1,5 +1,22 @@ # Changelog +## 0.21.0 + +- Workbench pods can now run as a non-root user. New top-level values `serviceAccountUser` (default `root`) and `serviceAccountUserId` (default `999`) control the OS user and UID. The user is written to `rserver.conf` as `server-user`, and a non-root value sets the pod `securityContext` to `runAsNonRoot: true` with `runAsUser`/`fsGroup` of `serviceAccountUserId`. The default remains `root`, so existing deployments are unchanged. Set `serviceAccountUser` to a non-root user (e.g. `rstudio-server`) to run unprivileged; this requires a Workbench image whose `/var/lib/rstudio-server`, `/var/lib/rstudio-launcher`, and `/var/log/rstudio` directories are owned by that user. +- When running non-root (`serviceAccountUser` other than `root`), the chart applies defaults so Workbench comes up cleanly without root, all overridable via `config.server`: + - The Job Launcher's `secure-cookie-key-file` points at the shared key (`/mnt/secret-configmap/rstudio/secure-cookie-key`). An unprivileged launcher otherwise generates its own key, which breaks authentication of rserver-signed launcher requests (`401 Unauthorized`). + - `auth-pam-sessions-enabled` defaults to `0`. Opening host PAM sessions requires root; launcher session workloads handle session setup. This matches Workbench's own default for launcher-sessions deployments. Root deployments are unchanged (`1`). + - `user-provisioning-enabled` defaults to `1`. Native (SCIM) provisioning is the supported user-resolution path when not running as root. + - Mounted secret files are made group-readable (`0640`) so the non-root process can read them via `fsGroup`. Root deployments keep the `0600` default. +- **BREAKING**: The bundled SSSD daemon is no longer started by default. SSSD is a legacy LDAP/Active Directory user-provisioning path that must run as root; modern provisioning (SCIM / native) does not require it. A new `config.sssd` block controls it: + - `config.sssd.enabled` (default `false`) starts the bundled SSSD daemon. It requires `serviceAccountUser: root` and the chart will fail to render if SSSD is enabled while running as a non-root user. + - `config.sssd.conf` replaces `config.userProvisioning` for the files mounted to `/etc/sssd/conf.d/`. + - To restore the previous behavior, set `config.sssd.enabled: true` (the default `serviceAccountUser: root` already satisfies the root requirement) and move any `config.userProvisioning` files to `config.sssd.conf`. +- `config.userProvisioning` is deprecated. It is still honored as a fallback for `config.sssd.conf` when SSSD is enabled, and the chart emits a warning when it is set. +- `config.startupUserProvisioning` no longer ships a default SSSD program. It remains available for custom provisioning daemons; the bundled SSSD service is now controlled by `config.sssd.enabled`. +- The chart no longer overrides the container `command`/`args`; the image's default `supervisord` startup is used for both root and non-root operation. Non-root operation requires a Workbench image whose `supervisord.conf` writes its socket and pidfile to a non-root-writable location (e.g. `/var/run/supervisor`, owned by `rstudio-server`). +- The Workbench and launcher prestart scripts (`prestart-workbench.bash`, `prestart-launcher.bash`) no longer perform root-only operations. The redundant Kubernetes CA trust-store install was removed (the launcher reads `ca.crt` directly), and the `chown`/`mkdir` calls for `/var/lib/rstudio-server` and `/var/lib/rstudio-launcher` were dropped. + ## 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 784078580..72a7fced5 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -1,6 +1,6 @@ # Posit Workbench -![Version: 0.20.0](https://img.shields.io/badge/Version-0.20.0-informational?style=flat-square) ![AppVersion: 2026.04.0](https://img.shields.io/badge/AppVersion-2026.04.0-informational?style=flat-square) +![Version: 0.21.0](https://img.shields.io/badge/Version-0.21.0-informational?style=flat-square) ![AppVersion: 2026.04.0](https://img.shields.io/badge/AppVersion-2026.04.0-informational?style=flat-square) #### _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.0: ```{.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.0 ``` To explore other chart versions, look at: @@ -62,20 +62,23 @@ 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, but it is **not** started by default. SSSD must run as root, so it can only be enabled when `serviceAccountUser: root`. Modern provisioning (SCIM / native) does not require SSSD. + To start the bundled SSSD daemon, set `config.sssd.enabled: true` and provide its configuration in `config.sssd.conf` like so: ```yaml + serviceAccountUser: root 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 +305,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 +358,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 `serviceAccountUser: root`; 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 +608,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 +619,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 +637,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 +665,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":false}` | Bundled SSSD daemon for legacy LDAP/Active Directory user provisioning. SSSD must run as root, so this cannot be enabled when the pod runs as a non-root user (`serviceAccountUser` other than `root`). Off by default in all modes. 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 | `false` | whether to start the bundled SSSD daemon. Requires `serviceAccountUser: root`. | | 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 | @@ -764,6 +770,8 @@ Use of [Sealed secrets](https://github.com/bitnami-labs/sealed-secrets) disables | service.port | int | `80` | The Service port. This is the port your service will run under. | | service.targetPort | int | `8787` | The port to forward to on the Workbench pod. Also see pod.port | | service.type | string | `"ClusterIP"` | The service type, usually ClusterIP (in-cluster only) or LoadBalancer (to expose the service using your cloud provider's load balancer) | +| serviceAccountUser | string | `"root"` | The OS user that the Workbench server runs as. Written to `rserver.conf` as `server-user` and used to derive the pod's `securityContext`. Defaults to `"root"` (sets `runAsUser: 0` and leaves `runAsNonRoot` unset), preserving the historical behavior. Set to a non-root user (e.g. `"rstudio-server"`) to run unprivileged, which sets `runAsNonRoot: true` and `runAsUser: serviceAccountUserId`, applies non-root `rserver.conf`/`launcher.conf` defaults, and mounts secrets group-readable (0640). Set to `""` to omit `server-user` from `rserver.conf` and skip the runAsUser/runAsNonRoot defaults entirely. | +| serviceAccountUserId | int | `999` | The UID matching `serviceAccountUser`, used as `runAsUser`/`fsGroup` in the pod's securityContext when `serviceAccountUser` is not `"root"` and not empty. Must match the UID baked into the Workbench image for the named user. | | serviceMonitor.additionalLabels | object | `{}` | additionalLabels normally includes the release name of the Prometheus Operator | | serviceMonitor.enabled | bool | `false` | Whether to create a ServiceMonitor CRD for use with a Prometheus Operator | | serviceMonitor.namespace | string | `""` | Namespace to create the ServiceMonitor in (usually the same as the one in which the Prometheus Operator is running). Defaults to the release namespace | diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl index df7239fb9..7e44634aa 100644 --- a/charts/rstudio-workbench/README.md.gotmpl +++ b/charts/rstudio-workbench/README.md.gotmpl @@ -33,20 +33,23 @@ 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, but it is **not** started by default. SSSD must run as root, so it can only be enabled when `serviceAccountUser: root`. Modern provisioning (SCIM / native) does not require SSSD. + To start the bundled SSSD daemon, set `config.sssd.enabled: true` and provide its configuration in `config.sssd.conf` like so: ```yaml + serviceAccountUser: root 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 +251,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 +304,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 `serviceAccountUser: root`; 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 +554,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 0802b81e7..a1ee16f50 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 aa903d33c..d32b141c2 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 d8a21ee1a..3f21e572c 100644 --- a/charts/rstudio-workbench/templates/NOTES.txt +++ b/charts/rstudio-workbench/templates/NOTES.txt @@ -70,3 +70,15 @@ 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 and .Values.config.sssd.enabled (ne .Values.serviceAccountUser "root") }} + {{- fail (printf "\n\n`config.sssd.enabled=true` requires running as root, but `serviceAccountUser` is `%s`. SSSD (legacy LDAP/AD provisioning) cannot run rootless.\n\nEither set `serviceAccountUser: root`, or disable SSSD with `config.sssd.enabled=false` and use SCIM/native provisioning instead." .Values.serviceAccountUser) }} +{{- end }} + +{{- if .Values.config.userProvisioning }} +{{ print "\n\nWARNING: `config.userProvisioning` is deprecated. Use `config.sssd.conf` instead, and set `config.sssd.enabled=true` to start the bundled SSSD daemon." }} +{{- end }} + +{{- if and .Values.config.sssd.conf (not .Values.config.sssd.enabled) }} +{{ print "\n\nWARNING: `config.sssd.conf` is set but `config.sssd.enabled=false`, so the SSSD daemon will not start and the config has no effect. Set `config.sssd.enabled=true` to use it." }} +{{- end }} diff --git a/charts/rstudio-workbench/templates/_helpers.tpl b/charts/rstudio-workbench/templates/_helpers.tpl index 4973da6c4..b6d60e48e 100644 --- a/charts/rstudio-workbench/templates/_helpers.tpl +++ b/charts/rstudio-workbench/templates/_helpers.tpl @@ -24,8 +24,32 @@ If release name contains chart name it will be used as a full name. {{- end }} {{- end }} +{{/* +supervisord program definition that starts the bundled SSSD daemon. +Rendered into the user-provisioning startup ConfigMap when config.sssd.enabled is true. +*/}} +{{- 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 and $.Values.serviceAccountUser (ne $.Values.serviceAccountUser "root") -}} + {{- $secretMode = 416 -}}{{/* octal 0640 */}} +{{- end }} containers: - name: rstudio {{- $defaultVersion := .Values.versionOverride | default $.Chart.AppVersion }} @@ -85,13 +109,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 +163,21 @@ containers: - name: rstudio-secret mountPath: "/mnt/secret-configmap/rstudio/" {{- end }} - {{- if .Values.config.userProvisioning }} + {{- if and .Values.config.sssd.enabled (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" - 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 .Values.config.sssd.enabled .Values.config.startupUserProvisioning }} - name: rstudio-user-startup mountPath: "/startup/user-provisioning" {{- end }} @@ -276,6 +302,8 @@ volumes: {{- end }} - name: etc-rstudio emptyDir: {} +- name: mnt-dynamic + emptyDir: {} - name: rstudio-config configMap: name: {{ include "rstudio-workbench.fullname" . }}-config @@ -304,7 +332,7 @@ volumes: name: {{ include "rstudio-workbench.fullname" . }}-start-launcher defaultMode: {{ .Values.config.defaultMode.startup }} {{- end }} -{{- if .Values.config.startupUserProvisioning }} +{{- if or .Values.config.sssd.enabled .Values.config.startupUserProvisioning }} - name: rstudio-user-startup configMap: name: {{ include "rstudio-workbench.fullname" . }}-start-user @@ -335,7 +363,7 @@ volumes: items: - key: {{ $key }} path: {{ $key }} - mode: {{ $.Values.config.defaultMode.secret }} + mode: {{ $secretMode }} {{- end }} {{- end }} {{- if .Values.launcherPem.existingSecret }} @@ -344,7 +372,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 +381,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 +389,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 +397,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 +408,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 +416,7 @@ volumes: items: - key: database.conf path: database.conf - mode: {{ .Values.config.defaultMode.secret }} + mode: {{ $secretMode }} {{- end }} {{- range .Values.config.existingSecrets }} - secret: @@ -397,11 +425,11 @@ volumes: {{- range .items }} - key: {{ .key }} path: {{ .path }} - mode: {{ $.Values.config.defaultMode.secret }} + mode: {{ $secretMode }} {{- end }} {{- end }} {{- end }} -{{- if .Values.config.userProvisioning }} +{{- if and .Values.config.sssd.enabled (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 ef1ef5743..fa7f42b11 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.serviceAccountUser }} + {{- $_ := set $defaultRServerConfigValues "server-user" .Values.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,18 @@ 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. */}} +{{- if and .Values.serviceAccountUser (ne .Values.serviceAccountUser "root") }} + {{- $rootlessDefaults := dict "rserver.conf" (dict "auth-pam-sessions-enabled" 0 "user-provisioning-enabled" 1) "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 7e9d885b3..b00ae8f8f 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 .Values.config.sssd.enabled $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 89a2c5f96..a76239cf0 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 .Values.config.sssd.enabled }} +{{- $_ := 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 8869e70b2..732a21e4b 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 }} + {{- if eq .Values.serviceAccountUser "root" }} + {{- /* Skip the runAsUser: 0 default when pod.securityContext already pins the user + (runAsUser or runAsNonRoot), so we never emit a contradictory + runAsNonRoot: true + runAsUser: 0 combination that fails admission. */ -}} + {{- if not (or (hasKey .Values.pod.securityContext "runAsUser") (hasKey .Values.pod.securityContext "runAsNonRoot")) }} + {{- $_ := set $defaultPodSC "runAsUser" 0 }} + {{- end }} + {{- else if .Values.serviceAccountUser }} + {{- $_ := set $defaultPodSC "runAsNonRoot" true }} + {{- $_ := set $defaultPodSC "runAsUser" (int .Values.serviceAccountUserId) }} + {{- $_ := set $defaultPodSC "fsGroup" (int .Values.serviceAccountUserId) }} + {{- end }} + {{- $effectivePodSC := mergeOverwrite $defaultPodSC (deepCopy .Values.pod.securityContext) }} + {{- 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 bfdc81f48..0ab5fad91 100644 --- a/charts/rstudio-workbench/tests/configmap_test.yaml +++ b/charts/rstudio-workbench/tests/configmap_test.yaml @@ -256,3 +256,157 @@ tests: - notMatchRegex: path: data["positron.conf"] pattern: "^exe=" + + # -- serviceAccountUser -> rserver.conf server-user + - it: should render server-user=root by default + template: configmap-general.yaml + documentIndex: 0 + asserts: + - matchRegex: + path: data["rserver.conf"] + pattern: "server-user=root" + + - it: should render server-user when serviceAccountUser is overridden + template: configmap-general.yaml + documentIndex: 0 + set: + 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: + 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: + 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" + + # -- Rootless config defaults (serviceAccountUser != root) + - it: should apply rootless config defaults when running non-root + template: configmap-general.yaml + documentIndex: 0 + set: + serviceAccountUser: "rstudio-server" + 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["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 root + template: configmap-general.yaml + documentIndex: 0 + set: + serviceAccountUser: "root" + 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["launcher.conf"] + pattern: "secure-cookie-key-file" + + - it: should let config.server override the rootless auth defaults + template: configmap-general.yaml + documentIndex: 0 + set: + 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 876e2b817..1c2bde26a 100644 --- a/charts/rstudio-workbench/tests/deployment_test.yaml +++ b/charts/rstudio-workbench/tests/deployment_test.yaml @@ -266,12 +266,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: + serviceAccountUser: root config: defaultMode: userProvisioning: 0600 + sssd: + enabled: true userProvisioning: sssd.conf: dsn: "test" @@ -852,3 +855,163 @@ tests: content: name: "positron-components" any: true + + # -- serviceAccountUser -> 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 serviceAccountUser is non-root + template: deployment.yaml + set: + serviceAccountUser: "rstudio-server" + 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: + serviceAccountUser: "workbench" + serviceAccountUserId: 1500 + asserts: + - equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + - equal: + path: spec.template.spec.securityContext.runAsUser + value: 1500 + + - it: should not set securityContext defaults when serviceAccountUser is empty + template: deployment.yaml + set: + serviceAccountUser: "" + asserts: + - notExists: + path: spec.template.spec.securityContext + + - it: should let pod.securityContext override the non-root defaults + template: deployment.yaml + set: + serviceAccountUser: "rstudio-server" + 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: + serviceAccountUser: "root" + 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: + serviceAccountUser: "root" + 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 serviceAccountUser is root + template: deployment.yaml + set: + serviceAccountUser: "root" + asserts: + - notExists: + path: spec.template.spec.containers[0].command + - notExists: + path: spec.template.spec.containers[0].args + + - it: should not override command/args when serviceAccountUser is empty + template: deployment.yaml + set: + serviceAccountUser: "" + 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 9feb395af..299b381f1 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: + serviceAccountUser: "rstudio-server" + 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 000000000..0d8fd0626 --- /dev/null +++ b/charts/rstudio-workbench/tests/sssd_test.yaml @@ -0,0 +1,122 @@ +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: does not start the sssd daemon by default + template: configmap-startup.yaml + asserts: + - notMatchRegexRaw: + pattern: "program:sssd" + + - it: starts the sssd daemon when config.sssd.enabled is true + template: configmap-startup.yaml + set: + serviceAccountUser: root + config.sssd.enabled: true + 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 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: + serviceAccountUser: root + config.sssd.enabled: true + 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: does not mount sssd directories by default + template: deployment.yaml + 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 enabled as root + template: deployment.yaml + set: + serviceAccountUser: root + config.sssd.enabled: true + 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: fails when sssd is enabled while running rootless + template: NOTES.txt + set: + serviceAccountUser: rstudio-server + config.sssd.enabled: true + asserts: + - failedTemplate: + errorPattern: "config.sssd.enabled.*requires.*root" + + - it: fails when sssd is enabled with an empty serviceAccountUser + template: NOTES.txt + set: + serviceAccountUser: "" + config.sssd.enabled: true + asserts: + - failedTemplate: + errorPattern: "config.sssd.enabled.*requires.*root" + + - it: allows sssd when running as root + template: NOTES.txt + set: + serviceAccountUser: root + config.sssd.enabled: true + 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 c7830ff6d..6dbc71ce9 100644 --- a/charts/rstudio-workbench/values.yaml +++ b/charts/rstudio-workbench/values.yaml @@ -6,6 +6,12 @@ fullnameOverride: "" # -- A Workbench version to override the "tag" for the server image and session-init container. Does not affect the session image tag, which is controlled by session.image.rVersion, session.image.pythonVersion, and session.image.os. Necessary until https://github.com/helm/helm/issues/8194 versionOverride: "" +# -- The OS user that the Workbench server runs as. Written to `rserver.conf` as `server-user` and used to derive the pod's `securityContext`. Defaults to `"root"` (sets `runAsUser: 0` and leaves `runAsNonRoot` unset), preserving the historical behavior. Set to a non-root user (e.g. `"rstudio-server"`) to run unprivileged, which sets `runAsNonRoot: true` and `runAsUser: serviceAccountUserId`, applies non-root `rserver.conf`/`launcher.conf` defaults, and mounts secrets group-readable (0640). Set to `""` to omit `server-user` from `rserver.conf` and skip the runAsUser/runAsNonRoot defaults entirely. +serviceAccountUser: "root" + +# -- The UID matching `serviceAccountUser`, used as `runAsUser`/`fsGroup` in the pod's securityContext when `serviceAccountUser` is not `"root"` and not empty. Must match the UID baked into the Workbench image for the named user. +serviceAccountUserId: 999 + # -- Settings for enabling server diagnostics diagnostics: enabled: false @@ -339,9 +345,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: @@ -568,14 +574,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. SSSD must run as root, so this cannot be enabled when the pod runs as a non-root user (`serviceAccountUser` other than `root`). Off by default in all modes. Modern provisioning (SCIM / native) does not require SSSD. + sssd: + # -- whether to start the bundled SSSD daemon. Requires `serviceAccountUser: root`. + enabled: false + # -- 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 +674,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: {} From 7b0c64b0749aacbf8e90dd8427ee583ef35df820 Mon Sep 17 00:00:00 2001 From: Sean Sinnott Date: Mon, 1 Jun 2026 16:35:06 -0400 Subject: [PATCH 2/9] Enable SSSD by default, auto-disable for non-root deployments Previously sssd.enabled defaulted to false with a hard render failure if set to true while running non-root. Now sssd.enabled defaults to true and is silently skipped when serviceAccountUser is not root, preserving existing root behavior with no BREAKING change. Introduce rstudio-workbench.sssd.active helper to centralize the effective-SSSD gate (enabled AND root) used across helpers, configmaps, and NOTES.txt warnings. --- charts/rstudio-workbench/NEWS.md | 17 +++--- charts/rstudio-workbench/README.md | 4 +- charts/rstudio-workbench/templates/NOTES.txt | 10 +--- .../rstudio-workbench/templates/_helpers.tpl | 20 +++++-- .../templates/configmap-secret.yaml | 2 +- .../templates/configmap-startup.yaml | 2 +- .../tests/deployment_test.yaml | 4 +- charts/rstudio-workbench/tests/sssd_test.yaml | 57 ++++++++++++------- charts/rstudio-workbench/values.yaml | 6 +- 9 files changed, 72 insertions(+), 50 deletions(-) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index 6ce9a827a..1632d175d 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -4,16 +4,13 @@ - Workbench pods can now run as a non-root user. New top-level values `serviceAccountUser` (default `root`) and `serviceAccountUserId` (default `999`) control the OS user and UID. The user is written to `rserver.conf` as `server-user`, and a non-root value sets the pod `securityContext` to `runAsNonRoot: true` with `runAsUser`/`fsGroup` of `serviceAccountUserId`. The default remains `root`, so existing deployments are unchanged. Set `serviceAccountUser` to a non-root user (e.g. `rstudio-server`) to run unprivileged; this requires a Workbench image whose `/var/lib/rstudio-server`, `/var/lib/rstudio-launcher`, and `/var/log/rstudio` directories are owned by that user. - When running non-root (`serviceAccountUser` other than `root`), the chart applies defaults so Workbench comes up cleanly without root, all overridable via `config.server`: - - The Job Launcher's `secure-cookie-key-file` points at the shared key (`/mnt/secret-configmap/rstudio/secure-cookie-key`). An unprivileged launcher otherwise generates its own key, which breaks authentication of rserver-signed launcher requests (`401 Unauthorized`). - - `auth-pam-sessions-enabled` defaults to `0`. Opening host PAM sessions requires root; launcher session workloads handle session setup. This matches Workbench's own default for launcher-sessions deployments. Root deployments are unchanged (`1`). - - `user-provisioning-enabled` defaults to `1`. Native (SCIM) provisioning is the supported user-resolution path when not running as root. - - Mounted secret files are made group-readable (`0640`) so the non-root process can read them via `fsGroup`. Root deployments keep the `0600` default. -- **BREAKING**: The bundled SSSD daemon is no longer started by default. SSSD is a legacy LDAP/Active Directory user-provisioning path that must run as root; modern provisioning (SCIM / native) does not require it. A new `config.sssd` block controls it: - - `config.sssd.enabled` (default `false`) starts the bundled SSSD daemon. It requires `serviceAccountUser: root` and the chart will fail to render if SSSD is enabled while running as a non-root user. - - `config.sssd.conf` replaces `config.userProvisioning` for the files mounted to `/etc/sssd/conf.d/`. - - To restore the previous behavior, set `config.sssd.enabled: true` (the default `serviceAccountUser: root` already satisfies the root requirement) and move any `config.userProvisioning` files to `config.sssd.conf`. -- `config.userProvisioning` is deprecated. It is still honored as a fallback for `config.sssd.conf` when SSSD is enabled, and the chart emits a warning when it is set. -- `config.startupUserProvisioning` no longer ships a default SSSD program. It remains available for custom provisioning daemons; the bundled SSSD service is now controlled by `config.sssd.enabled`. + - `secure-cookie-key-file` points at the shared key so the launcher does not generate its own (which breaks authentication of rserver-signed requests). + - `auth-pam-sessions-enabled: 0` — opening host PAM sessions requires root. + - `user-provisioning-enabled: 1` — enables native (SCIM) provisioning. + - Mounted secret files are made group-readable (`0640`) so the non-root process can read them via `fsGroup`. + - The bundled SSSD daemon is automatically skipped. SSSD requires root; use native (SCIM) provisioning for non-root deployments. +- A new `config.sssd` block controls the bundled SSSD daemon: `config.sssd.enabled` (default `true`, effective only when `serviceAccountUser: root`) and `config.sssd.conf` for SSSD config files (replaces the deprecated `config.userProvisioning`). +- `config.userProvisioning` is deprecated; use `config.sssd.conf`. A warning is emitted when it is set. - The chart no longer overrides the container `command`/`args`; the image's default `supervisord` startup is used for both root and non-root operation. Non-root operation requires a Workbench image whose `supervisord.conf` writes its socket and pidfile to a non-root-writable location (e.g. `/var/run/supervisor`, owned by `rstudio-server`). - The Workbench and launcher prestart scripts (`prestart-workbench.bash`, `prestart-launcher.bash`) no longer perform root-only operations. The redundant Kubernetes CA trust-store install was removed (the launcher reads `ca.crt` directly), and the `chown`/`mkdir` calls for `/var/lib/rstudio-server` and `/var/lib/rstudio-launcher` were dropped. diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index 72a7fced5..1f0afbaf4 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -665,9 +665,9 @@ 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":false}` | Bundled SSSD daemon for legacy LDAP/Active Directory user provisioning. SSSD must run as root, so this cannot be enabled when the pod runs as a non-root user (`serviceAccountUser` other than `root`). Off by default in all modes. Modern provisioning (SCIM / native) does not require SSSD. | +| 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 as a non-root user (`serviceAccountUser` other than `root`), 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 | `false` | whether to start the bundled SSSD daemon. Requires `serviceAccountUser: root`. | +| config.sssd.enabled | bool | `true` | whether to start the bundled SSSD daemon. Automatically skipped (not an error) when `serviceAccountUser` is not `root`. 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 | `{}` | 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`. | diff --git a/charts/rstudio-workbench/templates/NOTES.txt b/charts/rstudio-workbench/templates/NOTES.txt index 3f21e572c..eff8e66c1 100644 --- a/charts/rstudio-workbench/templates/NOTES.txt +++ b/charts/rstudio-workbench/templates/NOTES.txt @@ -71,14 +71,10 @@ Please consider removing this configuration value. {{- end }} -{{- if and .Values.config.sssd.enabled (ne .Values.serviceAccountUser "root") }} - {{- fail (printf "\n\n`config.sssd.enabled=true` requires running as root, but `serviceAccountUser` is `%s`. SSSD (legacy LDAP/AD provisioning) cannot run rootless.\n\nEither set `serviceAccountUser: root`, or disable SSSD with `config.sssd.enabled=false` and use SCIM/native provisioning instead." .Values.serviceAccountUser) }} -{{- end }} - {{- if .Values.config.userProvisioning }} -{{ print "\n\nWARNING: `config.userProvisioning` is deprecated. Use `config.sssd.conf` instead, and set `config.sssd.enabled=true` to start the bundled SSSD daemon." }} +{{ print "\n\nWARNING: `config.userProvisioning` is deprecated. Use `config.sssd.conf` instead." }} {{- end }} -{{- if and .Values.config.sssd.conf (not .Values.config.sssd.enabled) }} -{{ print "\n\nWARNING: `config.sssd.conf` is set but `config.sssd.enabled=false`, so the SSSD daemon will not start and the config has no effect. Set `config.sssd.enabled=true` to use it." }} +{{- 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 `serviceAccountUser: root` and `config.sssd.enabled: true`." }} {{- end }} diff --git a/charts/rstudio-workbench/templates/_helpers.tpl b/charts/rstudio-workbench/templates/_helpers.tpl index b6d60e48e..412559dad 100644 --- a/charts/rstudio-workbench/templates/_helpers.tpl +++ b/charts/rstudio-workbench/templates/_helpers.tpl @@ -24,9 +24,19 @@ 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 serviceAccountUser=root. +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 (eq .Values.serviceAccountUser "root") -}} +true +{{- end -}} +{{- end -}} + {{/* supervisord program definition that starts the bundled SSSD daemon. -Rendered into the user-provisioning startup ConfigMap when config.sssd.enabled is true. +Rendered into the user-provisioning startup ConfigMap when sssd is active. */}} {{- define "rstudio-workbench.sssd.program" -}} [program:sssd] @@ -163,7 +173,7 @@ containers: - name: rstudio-secret mountPath: "/mnt/secret-configmap/rstudio/" {{- end }} - {{- if and .Values.config.sssd.enabled (or .Values.config.sssd.conf .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 }} @@ -177,7 +187,7 @@ containers: - name: rstudio-launcher-startup mountPath: "/startup/launcher" {{- end }} - {{- if or .Values.config.sssd.enabled .Values.config.startupUserProvisioning }} + {{- if or (include "rstudio-workbench.sssd.active" .) .Values.config.startupUserProvisioning }} - name: rstudio-user-startup mountPath: "/startup/user-provisioning" {{- end }} @@ -332,7 +342,7 @@ volumes: name: {{ include "rstudio-workbench.fullname" . }}-start-launcher defaultMode: {{ .Values.config.defaultMode.startup }} {{- end }} -{{- if or .Values.config.sssd.enabled .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 @@ -429,7 +439,7 @@ volumes: {{- end }} {{- end }} {{- end }} -{{- if and .Values.config.sssd.enabled (or .Values.config.sssd.conf .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-secret.yaml b/charts/rstudio-workbench/templates/configmap-secret.yaml index b00ae8f8f..fe1ae560b 100644 --- a/charts/rstudio-workbench/templates/configmap-secret.yaml +++ b/charts/rstudio-workbench/templates/configmap-secret.yaml @@ -66,7 +66,7 @@ stringData: --- {{- $sssdConf := .Values.config.sssd.conf }} {{- if not $sssdConf }}{{- $sssdConf = (default (dict) .Values.config.userProvisioning) }}{{- end }} -{{- if and .Values.config.sssd.enabled $sssdConf }} +{{- if and (include "rstudio-workbench.sssd.active" .) $sssdConf }} {{- if .Values.sealedSecret.enabled -}} apiVersion: bitnami.com/v1alpha1 kind: SealedSecret diff --git a/charts/rstudio-workbench/templates/configmap-startup.yaml b/charts/rstudio-workbench/templates/configmap-startup.yaml index a76239cf0..46833595a 100644 --- a/charts/rstudio-workbench/templates/configmap-startup.yaml +++ b/charts/rstudio-workbench/templates/configmap-startup.yaml @@ -32,7 +32,7 @@ data: stderr_logfile_maxbytes=0 {{- end }} {{- $userStartup := deepCopy (default (dict) .Values.config.startupUserProvisioning) }} -{{- if .Values.config.sssd.enabled }} +{{- if include "rstudio-workbench.sssd.active" . }} {{- $_ := set $userStartup "sssd.conf" (include "rstudio-workbench.sssd.program" .) }} {{- end }} {{- if $userStartup }} diff --git a/charts/rstudio-workbench/tests/deployment_test.yaml b/charts/rstudio-workbench/tests/deployment_test.yaml index 1c2bde26a..c81f040b2 100644 --- a/charts/rstudio-workbench/tests/deployment_test.yaml +++ b/charts/rstudio-workbench/tests/deployment_test.yaml @@ -356,13 +356,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")]' diff --git a/charts/rstudio-workbench/tests/sssd_test.yaml b/charts/rstudio-workbench/tests/sssd_test.yaml index 0d8fd0626..2cb66e952 100644 --- a/charts/rstudio-workbench/tests/sssd_test.yaml +++ b/charts/rstudio-workbench/tests/sssd_test.yaml @@ -9,17 +9,8 @@ templates: - deployment.yaml - NOTES.txt tests: - - it: does not start the sssd daemon by default + - it: starts the sssd daemon by default when running as root template: configmap-startup.yaml - asserts: - - notMatchRegexRaw: - pattern: "program:sssd" - - - it: starts the sssd daemon when config.sssd.enabled is true - template: configmap-startup.yaml - set: - serviceAccountUser: root - config.sssd.enabled: true documentSelector: path: metadata.name value: RELEASE-NAME-rstudio-workbench-start-user @@ -31,6 +22,22 @@ tests: path: data['sssd.conf'] pattern: "/usr/sbin/sssd -i -c /etc/sssd/sssd.conf" + - it: does not start the sssd daemon when running non-root + template: configmap-startup.yaml + set: + serviceAccountUser: rstudio-server + 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: @@ -58,19 +65,29 @@ tests: path: stringData['mysssd.conf'] pattern: "id_provider" - - it: does not mount sssd directories by default + - 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 running non-root template: deployment.yaml + set: + serviceAccountUser: rstudio-server 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 enabled as root + - it: mounts sssd directories when running as root with sssd.conf set template: deployment.yaml set: serviceAccountUser: root - config.sssd.enabled: true config.sssd.conf: mysssd.conf: sssd: @@ -83,25 +100,25 @@ tests: path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user-startup")].mountPath' value: "/startup/user-provisioning" - - it: fails when sssd is enabled while running rootless + - it: silently skips sssd when running non-root even if sssd.enabled is true template: NOTES.txt set: serviceAccountUser: rstudio-server config.sssd.enabled: true asserts: - - failedTemplate: - errorPattern: "config.sssd.enabled.*requires.*root" + - matchRegexRaw: + pattern: "successfully deployed" - - it: fails when sssd is enabled with an empty serviceAccountUser + - it: silently skips sssd with empty serviceAccountUser even if sssd.enabled is true template: NOTES.txt set: serviceAccountUser: "" config.sssd.enabled: true asserts: - - failedTemplate: - errorPattern: "config.sssd.enabled.*requires.*root" + - matchRegexRaw: + pattern: "successfully deployed" - - it: allows sssd when running as root + - it: renders successfully when running as root with sssd enabled template: NOTES.txt set: serviceAccountUser: root diff --git a/charts/rstudio-workbench/values.yaml b/charts/rstudio-workbench/values.yaml index 6dbc71ce9..09f0efa37 100644 --- a/charts/rstudio-workbench/values.yaml +++ b/charts/rstudio-workbench/values.yaml @@ -576,10 +576,10 @@ config: # path: custom-file.conf # -- 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. SSSD must run as root, so this cannot be enabled when the pod runs as a non-root user (`serviceAccountUser` other than `root`). Off by default in all modes. Modern provisioning (SCIM / native) does not require SSSD. + # -- Bundled SSSD daemon for legacy LDAP/Active Directory user provisioning. On by default; automatically skipped when the pod runs as a non-root user (`serviceAccountUser` other than `root`), since SSSD requires root. Modern provisioning (SCIM / native) does not require SSSD. sssd: - # -- whether to start the bundled SSSD daemon. Requires `serviceAccountUser: root`. - enabled: false + # -- whether to start the bundled SSSD daemon. Automatically skipped (not an error) when `serviceAccountUser` is not `root`. 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/` From 63771fa204902157ee0fc8f31b721301f9178823 Mon Sep 17 00:00:00 2001 From: Sean Sinnott Date: Mon, 1 Jun 2026 17:30:03 -0400 Subject: [PATCH 3/9] Introduce runAsRoot flag to decouple pod privilege from serviceAccountUser Add top-level runAsRoot: true (default) as the single binary control for pod privilege. Previously, serviceAccountUser == "root" was the implicit trigger for all root-vs-non-root behavior (securityContext, SSSD, secret file modes, rserver.conf defaults), conflating the OS application user with the Kubernetes security concept. serviceAccountUser now solely controls server-user in rserver.conf. All privilege-gating logic switches from eq serviceAccountUser "root" to .Values.runAsRoot / not .Values.runAsRoot. --- charts/rstudio-workbench/NEWS.md | 7 +++-- charts/rstudio-workbench/README.md | 5 ++-- charts/rstudio-workbench/templates/NOTES.txt | 2 +- .../rstudio-workbench/templates/_helpers.tpl | 4 +-- .../templates/configmap-general.yaml | 2 +- .../templates/deployment.yaml | 9 +++--- .../tests/configmap_test.yaml | 10 +++---- .../tests/deployment_test.yaml | 30 +++++++------------ .../rstudio-workbench/tests/secrets_test.yaml | 2 +- charts/rstudio-workbench/tests/sssd_test.yaml | 27 ++++------------- charts/rstudio-workbench/values.yaml | 13 ++++++-- 11 files changed, 48 insertions(+), 63 deletions(-) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index 1632d175d..8eed09d0d 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -2,14 +2,15 @@ ## 0.21.0 -- Workbench pods can now run as a non-root user. New top-level values `serviceAccountUser` (default `root`) and `serviceAccountUserId` (default `999`) control the OS user and UID. The user is written to `rserver.conf` as `server-user`, and a non-root value sets the pod `securityContext` to `runAsNonRoot: true` with `runAsUser`/`fsGroup` of `serviceAccountUserId`. The default remains `root`, so existing deployments are unchanged. Set `serviceAccountUser` to a non-root user (e.g. `rstudio-server`) to run unprivileged; this requires a Workbench image whose `/var/lib/rstudio-server`, `/var/lib/rstudio-launcher`, and `/var/log/rstudio` directories are owned by that user. -- When running non-root (`serviceAccountUser` other than `root`), the chart applies defaults so Workbench comes up cleanly without root, all overridable via `config.server`: +- Workbench pods can now run as a non-root user. Set `runAsRoot: false` (new top-level value, default `true`) to run unprivileged; the pod `securityContext` is set to `runAsNonRoot: true` with `runAsUser`/`fsGroup` of `serviceAccountUserId` (default `999`). Existing deployments are unchanged. +- `serviceAccountUser` is now solely the `server-user` written to `rserver.conf`; pod privilege is controlled by `runAsRoot`, not by the value of `serviceAccountUser`. +- When `runAsRoot: false`, the chart applies non-root defaults (all overridable via `config.server`): - `secure-cookie-key-file` points at the shared key so the launcher does not generate its own (which breaks authentication of rserver-signed requests). - `auth-pam-sessions-enabled: 0` — opening host PAM sessions requires root. - `user-provisioning-enabled: 1` — enables native (SCIM) provisioning. - Mounted secret files are made group-readable (`0640`) so the non-root process can read them via `fsGroup`. - The bundled SSSD daemon is automatically skipped. SSSD requires root; use native (SCIM) provisioning for non-root deployments. -- A new `config.sssd` block controls the bundled SSSD daemon: `config.sssd.enabled` (default `true`, effective only when `serviceAccountUser: root`) and `config.sssd.conf` for SSSD config files (replaces the deprecated `config.userProvisioning`). +- A new `config.sssd` block controls the bundled SSSD daemon: `config.sssd.enabled` (default `true`, effective only when `runAsRoot: true`) and `config.sssd.conf` for SSSD config files (replaces the deprecated `config.userProvisioning`). - `config.userProvisioning` is deprecated; use `config.sssd.conf`. A warning is emitted when it is set. - The chart no longer overrides the container `command`/`args`; the image's default `supervisord` startup is used for both root and non-root operation. Non-root operation requires a Workbench image whose `supervisord.conf` writes its socket and pidfile to a non-root-writable location (e.g. `/var/run/supervisor`, owned by `rstudio-server`). - The Workbench and launcher prestart scripts (`prestart-workbench.bash`, `prestart-launcher.bash`) no longer perform root-only operations. The redundant Kubernetes CA trust-store install was removed (the launcher reads `ca.crt` directly), and the `chown`/`mkdir` calls for `/var/lib/rstudio-server` and `/var/lib/rstudio-launcher` were dropped. diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index 1f0afbaf4..c5875c06e 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -757,6 +757,7 @@ Use of [Sealed secrets](https://github.com/bitnami-labs/sealed-secrets) disables | replicas | int | `1` | replicas is the number of replica pods to maintain for this service. Use 2 or more to enable HA | | resources | object | `{"limits":{"cpu":"2000m","enabled":false,"ephemeralStorage":"200Mi","memory":"4Gi"},"requests":{"cpu":"100m","enabled":false,"ephemeralStorage":"100Mi","memory":"2Gi"}}` | resources define requests and limits for the rstudio-server pod | | revisionHistoryLimit | int | `3` | The revisionHistoryLimit to use for the pod deployment. Do not set to 0 | +| 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 `serviceAccountUserId`, SSSD is skipped, secrets mount 0640, and non-root `rserver.conf`/`launcher.conf` defaults are applied. | | sealedSecret.annotations | object | `{}` | annotations for SealedSecret resources | | sealedSecret.enabled | bool | `false` | use SealedSecret instead of Secret to deploy secrets | | secureCookieKey | object | `{"existingSecret":"","value":""}` | global.secureCookieKey takes precedence over secureCookieKey | @@ -770,8 +771,8 @@ Use of [Sealed secrets](https://github.com/bitnami-labs/sealed-secrets) disables | service.port | int | `80` | The Service port. This is the port your service will run under. | | service.targetPort | int | `8787` | The port to forward to on the Workbench pod. Also see pod.port | | service.type | string | `"ClusterIP"` | The service type, usually ClusterIP (in-cluster only) or LoadBalancer (to expose the service using your cloud provider's load balancer) | -| serviceAccountUser | string | `"root"` | The OS user that the Workbench server runs as. Written to `rserver.conf` as `server-user` and used to derive the pod's `securityContext`. Defaults to `"root"` (sets `runAsUser: 0` and leaves `runAsNonRoot` unset), preserving the historical behavior. Set to a non-root user (e.g. `"rstudio-server"`) to run unprivileged, which sets `runAsNonRoot: true` and `runAsUser: serviceAccountUserId`, applies non-root `rserver.conf`/`launcher.conf` defaults, and mounts secrets group-readable (0640). Set to `""` to omit `server-user` from `rserver.conf` and skip the runAsUser/runAsNonRoot defaults entirely. | -| serviceAccountUserId | int | `999` | The UID matching `serviceAccountUser`, used as `runAsUser`/`fsGroup` in the pod's securityContext when `serviceAccountUser` is not `"root"` and not empty. Must match the UID baked into the Workbench image for the named user. | +| serviceAccountUser | string | `"root"` | The OS user written to `rserver.conf` as `server-user`. Set to `""` to omit `server-user` from `rserver.conf` entirely. | +| serviceAccountUserId | int | `999` | The UID used as `runAsUser`/`fsGroup` in the pod's securityContext when `runAsRoot` is false. Must match the UID of `serviceAccountUser` baked into the Workbench image. | | serviceMonitor.additionalLabels | object | `{}` | additionalLabels normally includes the release name of the Prometheus Operator | | serviceMonitor.enabled | bool | `false` | Whether to create a ServiceMonitor CRD for use with a Prometheus Operator | | serviceMonitor.namespace | string | `""` | Namespace to create the ServiceMonitor in (usually the same as the one in which the Prometheus Operator is running). Defaults to the release namespace | diff --git a/charts/rstudio-workbench/templates/NOTES.txt b/charts/rstudio-workbench/templates/NOTES.txt index eff8e66c1..50cbef256 100644 --- a/charts/rstudio-workbench/templates/NOTES.txt +++ b/charts/rstudio-workbench/templates/NOTES.txt @@ -76,5 +76,5 @@ Please consider removing this configuration value. {{- 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 `serviceAccountUser: root` and `config.sssd.enabled: true`." }} +{{ print "\n\nWARNING: `config.sssd.conf` is set but the SSSD daemon will not start. SSSD requires `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 412559dad..c071342ba 100644 --- a/charts/rstudio-workbench/templates/_helpers.tpl +++ b/charts/rstudio-workbench/templates/_helpers.tpl @@ -29,7 +29,7 @@ Returns "true" when the SSSD daemon should actually run: sssd.enabled=true and s 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 (eq .Values.serviceAccountUser "root") -}} +{{- if and .Values.config.sssd.enabled .Values.runAsRoot -}} true {{- end -}} {{- end -}} @@ -57,7 +57,7 @@ stderr_logfile_backups=0 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 and $.Values.serviceAccountUser (ne $.Values.serviceAccountUser "root") -}} +{{- if not $.Values.runAsRoot -}} {{- $secretMode = 416 -}}{{/* octal 0640 */}} {{- end }} containers: diff --git a/charts/rstudio-workbench/templates/configmap-general.yaml b/charts/rstudio-workbench/templates/configmap-general.yaml index fa7f42b11..54ceb23f3 100644 --- a/charts/rstudio-workbench/templates/configmap-general.yaml +++ b/charts/rstudio-workbench/templates/configmap-general.yaml @@ -118,7 +118,7 @@ data: - 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. */}} -{{- if and .Values.serviceAccountUser (ne .Values.serviceAccountUser "root") }} +{{- if not .Values.runAsRoot }} {{- $rootlessDefaults := dict "rserver.conf" (dict "auth-pam-sessions-enabled" 0 "user-provisioning-enabled" 1) "launcher.conf" (dict "server" (dict "secure-cookie-key-file" "/mnt/secret-configmap/rstudio/secure-cookie-key")) }} {{- $overrideDict = mergeOverwrite $rootlessDefaults $overrideDict }} {{- else }} diff --git a/charts/rstudio-workbench/templates/deployment.yaml b/charts/rstudio-workbench/templates/deployment.yaml index 732a21e4b..709567f55 100644 --- a/charts/rstudio-workbench/templates/deployment.yaml +++ b/charts/rstudio-workbench/templates/deployment.yaml @@ -89,14 +89,13 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- $defaultPodSC := dict }} - {{- if eq .Values.serviceAccountUser "root" }} - {{- /* Skip the runAsUser: 0 default when pod.securityContext already pins the user - (runAsUser or runAsNonRoot), so we never emit a contradictory - runAsNonRoot: true + runAsUser: 0 combination that fails admission. */ -}} + {{- if .Values.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 .Values.pod.securityContext "runAsUser") (hasKey .Values.pod.securityContext "runAsNonRoot")) }} {{- $_ := set $defaultPodSC "runAsUser" 0 }} {{- end }} - {{- else if .Values.serviceAccountUser }} + {{- else }} {{- $_ := set $defaultPodSC "runAsNonRoot" true }} {{- $_ := set $defaultPodSC "runAsUser" (int .Values.serviceAccountUserId) }} {{- $_ := set $defaultPodSC "fsGroup" (int .Values.serviceAccountUserId) }} diff --git a/charts/rstudio-workbench/tests/configmap_test.yaml b/charts/rstudio-workbench/tests/configmap_test.yaml index 0ab5fad91..4c479fde8 100644 --- a/charts/rstudio-workbench/tests/configmap_test.yaml +++ b/charts/rstudio-workbench/tests/configmap_test.yaml @@ -360,12 +360,12 @@ tests: path: data["prestart-workbench.bash"] pattern: "/etc/rstudio/README" - # -- Rootless config defaults (serviceAccountUser != root) - - it: should apply rootless config defaults when running non-root + # -- runAsRoot -> rootless config defaults + - it: should apply rootless config defaults when runAsRoot is false template: configmap-general.yaml documentIndex: 0 set: - serviceAccountUser: "rstudio-server" + runAsRoot: false asserts: - matchRegex: path: data["rserver.conf"] @@ -377,11 +377,9 @@ tests: 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 root + - it: should keep PAM sessions on and not force provisioning when runAsRoot is true template: configmap-general.yaml documentIndex: 0 - set: - serviceAccountUser: "root" asserts: - matchRegex: path: data["rserver.conf"] diff --git a/charts/rstudio-workbench/tests/deployment_test.yaml b/charts/rstudio-workbench/tests/deployment_test.yaml index c81f040b2..e7046b7d7 100644 --- a/charts/rstudio-workbench/tests/deployment_test.yaml +++ b/charts/rstudio-workbench/tests/deployment_test.yaml @@ -858,7 +858,7 @@ tests: name: "positron-components" any: true - # -- serviceAccountUser -> pod securityContext + # -- runAsRoot -> pod securityContext - it: should run pod as root by default template: deployment.yaml asserts: @@ -868,10 +868,10 @@ tests: - notExists: path: spec.template.spec.securityContext.runAsNonRoot - - it: should run pod as non-root when serviceAccountUser is non-root + - it: should run pod as non-root when runAsRoot is false template: deployment.yaml set: - serviceAccountUser: "rstudio-server" + runAsRoot: false asserts: - equal: path: spec.template.spec.securityContext.runAsNonRoot @@ -886,7 +886,7 @@ tests: - it: should use custom uid when serviceAccountUserId is overridden template: deployment.yaml set: - serviceAccountUser: "workbench" + runAsRoot: false serviceAccountUserId: 1500 asserts: - equal: @@ -896,18 +896,10 @@ tests: path: spec.template.spec.securityContext.runAsUser value: 1500 - - it: should not set securityContext defaults when serviceAccountUser is empty - template: deployment.yaml - set: - serviceAccountUser: "" - asserts: - - notExists: - path: spec.template.spec.securityContext - - it: should let pod.securityContext override the non-root defaults template: deployment.yaml set: - serviceAccountUser: "rstudio-server" + runAsRoot: false pod: securityContext: runAsUser: 4242 @@ -926,7 +918,7 @@ tests: - it: should not force runAsUser 0 when root pod.securityContext sets runAsNonRoot template: deployment.yaml set: - serviceAccountUser: "root" + runAsRoot: true pod: securityContext: runAsNonRoot: true @@ -940,7 +932,7 @@ tests: - it: should respect a root pod.securityContext runAsUser override template: deployment.yaml set: - serviceAccountUser: "root" + runAsRoot: true pod: securityContext: runAsUser: 1234 @@ -970,20 +962,20 @@ tests: name: supervisor-tmp mountPath: /tmp - - it: should not override command/args when serviceAccountUser is root + - it: should not override command/args when runAsRoot is true template: deployment.yaml set: - serviceAccountUser: "root" + 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 serviceAccountUser is empty + - it: should not override command/args when runAsRoot is false template: deployment.yaml set: - serviceAccountUser: "" + runAsRoot: false asserts: - notExists: path: spec.template.spec.containers[0].command diff --git a/charts/rstudio-workbench/tests/secrets_test.yaml b/charts/rstudio-workbench/tests/secrets_test.yaml index 299b381f1..2ea95aabe 100644 --- a/charts/rstudio-workbench/tests/secrets_test.yaml +++ b/charts/rstudio-workbench/tests/secrets_test.yaml @@ -134,7 +134,7 @@ tests: - it: should mount secrets group-readable (0640) when running non-root template: deployment.yaml set: - serviceAccountUser: "rstudio-server" + runAsRoot: false config: defaultMode: secret: 0600 diff --git a/charts/rstudio-workbench/tests/sssd_test.yaml b/charts/rstudio-workbench/tests/sssd_test.yaml index 2cb66e952..10a4d6509 100644 --- a/charts/rstudio-workbench/tests/sssd_test.yaml +++ b/charts/rstudio-workbench/tests/sssd_test.yaml @@ -22,10 +22,10 @@ tests: path: data['sssd.conf'] pattern: "/usr/sbin/sssd -i -c /etc/sssd/sssd.conf" - - it: does not start the sssd daemon when running non-root + - it: does not start the sssd daemon when runAsRoot is false template: configmap-startup.yaml set: - serviceAccountUser: rstudio-server + runAsRoot: false asserts: - notMatchRegexRaw: pattern: "program:sssd" @@ -47,8 +47,6 @@ tests: - it: renders the sssd conf secret when enabled with conf template: configmap-secret.yaml set: - serviceAccountUser: root - config.sssd.enabled: true config.sssd.conf: mysssd.conf: sssd: @@ -74,10 +72,10 @@ tests: path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user-startup")].mountPath' value: "/startup/user-provisioning" - - it: does not mount sssd directories when running non-root + - it: does not mount sssd directories when runAsRoot is false template: deployment.yaml set: - serviceAccountUser: rstudio-server + runAsRoot: false asserts: - notExists: path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user")]' @@ -87,7 +85,6 @@ tests: - it: mounts sssd directories when running as root with sssd.conf set template: deployment.yaml set: - serviceAccountUser: root config.sssd.conf: mysssd.conf: sssd: @@ -100,19 +97,10 @@ tests: path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user-startup")].mountPath' value: "/startup/user-provisioning" - - it: silently skips sssd when running non-root even if sssd.enabled is true - template: NOTES.txt - set: - serviceAccountUser: rstudio-server - config.sssd.enabled: true - asserts: - - matchRegexRaw: - pattern: "successfully deployed" - - - it: silently skips sssd with empty serviceAccountUser even if sssd.enabled is true + - it: silently skips sssd when runAsRoot is false even if sssd.enabled is true template: NOTES.txt set: - serviceAccountUser: "" + runAsRoot: false config.sssd.enabled: true asserts: - matchRegexRaw: @@ -120,9 +108,6 @@ tests: - it: renders successfully when running as root with sssd enabled template: NOTES.txt - set: - serviceAccountUser: root - config.sssd.enabled: true asserts: - matchRegexRaw: pattern: "successfully deployed" diff --git a/charts/rstudio-workbench/values.yaml b/charts/rstudio-workbench/values.yaml index 09f0efa37..1c359998f 100644 --- a/charts/rstudio-workbench/values.yaml +++ b/charts/rstudio-workbench/values.yaml @@ -6,10 +6,19 @@ fullnameOverride: "" # -- A Workbench version to override the "tag" for the server image and session-init container. Does not affect the session image tag, which is controlled by session.image.rVersion, session.image.pythonVersion, and session.image.os. Necessary until https://github.com/helm/helm/issues/8194 versionOverride: "" -# -- The OS user that the Workbench server runs as. Written to `rserver.conf` as `server-user` and used to derive the pod's `securityContext`. Defaults to `"root"` (sets `runAsUser: 0` and leaves `runAsNonRoot` unset), preserving the historical behavior. Set to a non-root user (e.g. `"rstudio-server"`) to run unprivileged, which sets `runAsNonRoot: true` and `runAsUser: serviceAccountUserId`, applies non-root `rserver.conf`/`launcher.conf` defaults, and mounts secrets group-readable (0640). Set to `""` to omit `server-user` from `rserver.conf` and skip the runAsUser/runAsNonRoot defaults entirely. +# -- 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 +# `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: "root" -# -- The UID matching `serviceAccountUser`, used as `runAsUser`/`fsGroup` in the pod's securityContext when `serviceAccountUser` is not `"root"` and not empty. Must match the UID baked into the Workbench image for the named user. +# -- The UID used as `runAsUser`/`fsGroup` in the pod's securityContext when `runAsRoot` is false. +# Must match the UID of `serviceAccountUser` baked into the Workbench image. serviceAccountUserId: 999 # -- Settings for enabling server diagnostics From b0f822983a5841abd32437fe3c7d2e72fa215a5b Mon Sep 17 00:00:00 2001 From: Sean Sinnott Date: Mon, 1 Jun 2026 17:37:16 -0400 Subject: [PATCH 4/9] Add news. --- charts/rstudio-workbench/NEWS.md | 1 - 1 file changed, 1 deletion(-) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index 8eed09d0d..29e212e83 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -3,7 +3,6 @@ ## 0.21.0 - Workbench pods can now run as a non-root user. Set `runAsRoot: false` (new top-level value, default `true`) to run unprivileged; the pod `securityContext` is set to `runAsNonRoot: true` with `runAsUser`/`fsGroup` of `serviceAccountUserId` (default `999`). Existing deployments are unchanged. -- `serviceAccountUser` is now solely the `server-user` written to `rserver.conf`; pod privilege is controlled by `runAsRoot`, not by the value of `serviceAccountUser`. - When `runAsRoot: false`, the chart applies non-root defaults (all overridable via `config.server`): - `secure-cookie-key-file` points at the shared key so the launcher does not generate its own (which breaks authentication of rserver-signed requests). - `auth-pam-sessions-enabled: 0` — opening host PAM sessions requires root. From 922e9090e905e7b393246dfffceb0cb9cf77bc75 Mon Sep 17 00:00:00 2001 From: Sean Sinnott Date: Mon, 1 Jun 2026 21:07:25 -0400 Subject: [PATCH 5/9] Servier account and news tweaks. --- charts/rstudio-workbench/Chart.yaml | 2 +- charts/rstudio-workbench/NEWS.md | 14 ++++---------- charts/rstudio-workbench/tests/configmap_test.yaml | 4 ++-- charts/rstudio-workbench/values.yaml | 2 +- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/charts/rstudio-workbench/Chart.yaml b/charts/rstudio-workbench/Chart.yaml index b076f9ef5..36d2cf72d 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.21.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 29e212e83..887f19a1b 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -2,17 +2,11 @@ ## 0.21.0 -- Workbench pods can now run as a non-root user. Set `runAsRoot: false` (new top-level value, default `true`) to run unprivileged; the pod `securityContext` is set to `runAsNonRoot: true` with `runAsUser`/`fsGroup` of `serviceAccountUserId` (default `999`). Existing deployments are unchanged. -- When `runAsRoot: false`, the chart applies non-root defaults (all overridable via `config.server`): - - `secure-cookie-key-file` points at the shared key so the launcher does not generate its own (which breaks authentication of rserver-signed requests). - - `auth-pam-sessions-enabled: 0` — opening host PAM sessions requires root. - - `user-provisioning-enabled: 1` — enables native (SCIM) provisioning. - - Mounted secret files are made group-readable (`0640`) so the non-root process can read them via `fsGroup`. - - The bundled SSSD daemon is automatically skipped. SSSD requires root; use native (SCIM) provisioning for non-root deployments. -- A new `config.sssd` block controls the bundled SSSD daemon: `config.sssd.enabled` (default `true`, effective only when `runAsRoot: true`) and `config.sssd.conf` for SSSD config files (replaces the deprecated `config.userProvisioning`). +- Workbench pods can now run as a non-root user. Set `runAsRoot: false` (new top-level 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 chart no longer overrides the container `command`/`args`; the image's default `supervisord` startup is used for both root and non-root operation. Non-root operation requires a Workbench image whose `supervisord.conf` writes its socket and pidfile to a non-root-writable location (e.g. `/var/run/supervisor`, owned by `rstudio-server`). -- The Workbench and launcher prestart scripts (`prestart-workbench.bash`, `prestart-launcher.bash`) no longer perform root-only operations. The redundant Kubernetes CA trust-store install was removed (the launcher reads `ca.crt` directly), and the `chown`/`mkdir` calls for `/var/lib/rstudio-server` and `/var/lib/rstudio-launcher` were dropped. +- The Workbench and launcher prestart scripts (`prestart-workbench.bash`, `prestart-launcher.bash`) no longer perform root-only operations. ## 0.20.0 diff --git a/charts/rstudio-workbench/tests/configmap_test.yaml b/charts/rstudio-workbench/tests/configmap_test.yaml index 4c479fde8..d36c1f88f 100644 --- a/charts/rstudio-workbench/tests/configmap_test.yaml +++ b/charts/rstudio-workbench/tests/configmap_test.yaml @@ -258,13 +258,13 @@ tests: pattern: "^exe=" # -- serviceAccountUser -> rserver.conf server-user - - it: should render server-user=root by default + - it: should render server-user=rstudio-server by default template: configmap-general.yaml documentIndex: 0 asserts: - matchRegex: path: data["rserver.conf"] - pattern: "server-user=root" + pattern: "server-user=rstudio-server" - it: should render server-user when serviceAccountUser is overridden template: configmap-general.yaml diff --git a/charts/rstudio-workbench/values.yaml b/charts/rstudio-workbench/values.yaml index 1c359998f..94e421571 100644 --- a/charts/rstudio-workbench/values.yaml +++ b/charts/rstudio-workbench/values.yaml @@ -15,7 +15,7 @@ runAsRoot: true # -- The OS user written to `rserver.conf` as `server-user`. Set to `""` to omit `server-user` # from `rserver.conf` entirely. -serviceAccountUser: "root" +serviceAccountUser: "rstudio-server" # -- The UID used as `runAsUser`/`fsGroup` in the pod's securityContext when `runAsRoot` is false. # Must match the UID of `serviceAccountUser` baked into the Workbench image. From 1dbc0beeb5b435751f1466e588afe816eb66c573 Mon Sep 17 00:00:00 2001 From: Sean Sinnott Date: Tue, 2 Jun 2026 07:50:18 -0400 Subject: [PATCH 6/9] Move rootless settings under pod: in workbench chart --- charts/rstudio-workbench/NEWS.md | 2 +- charts/rstudio-workbench/README.md | 21 ++++++------- charts/rstudio-workbench/README.md.gotmpl | 5 ++- charts/rstudio-workbench/templates/NOTES.txt | 2 +- .../rstudio-workbench/templates/_helpers.tpl | 6 ++-- .../templates/configmap-general.yaml | 6 ++-- .../templates/deployment.yaml | 6 ++-- .../tests/configmap_test.yaml | 10 +++--- .../tests/deployment_test.yaml | 18 +++++------ .../rstudio-workbench/tests/secrets_test.yaml | 2 +- charts/rstudio-workbench/tests/sssd_test.yaml | 6 ++-- charts/rstudio-workbench/values.yaml | 31 +++++++++---------- 12 files changed, 55 insertions(+), 60 deletions(-) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index 887f19a1b..acbbb4180 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -2,7 +2,7 @@ ## 0.21.0 -- Workbench pods can now run as a non-root user. Set `runAsRoot: false` (new top-level value, default `true`) to run unprivileged. +- 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. diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index c5875c06e..3005e1952 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -1,6 +1,6 @@ # Posit Workbench -![Version: 0.21.0](https://img.shields.io/badge/Version-0.21.0-informational?style=flat-square) ![AppVersion: 2026.04.0](https://img.shields.io/badge/AppVersion-2026.04.0-informational?style=flat-square) +![Version: 0.21.1](https://img.shields.io/badge/Version-0.21.1-informational?style=flat-square) ![AppVersion: 2026.04.0](https://img.shields.io/badge/AppVersion-2026.04.0-informational?style=flat-square) #### _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.21.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.21.0 +helm upgrade --install my-release rstudio/rstudio-workbench --version=0.21.1 ``` To explore other chart versions, look at: @@ -62,11 +62,10 @@ 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 `posit/workbench` image ships `sssd` for legacy LDAP/Active Directory provisioning, but it is **not** started by default. SSSD must run as root, so it can only be enabled when `serviceAccountUser: root`. Modern provisioning (SCIM / native) does not require SSSD. +* 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, but it is **not** started by default. SSSD must run as root, so it can only be enabled when `pod.runAsRoot: true`. Modern provisioning (SCIM / native) does not require SSSD. To start the bundled SSSD daemon, set `config.sssd.enabled: true` and provide its configuration in `config.sssd.conf` like so: ```yaml - serviceAccountUser: root config: sssd: enabled: true @@ -360,7 +359,7 @@ administrator. 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 `serviceAccountUser: root`; see `config.sssd` above). +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. @@ -665,9 +664,9 @@ 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 as a non-root user (`serviceAccountUser` other than `root`), since SSSD requires root. Modern provisioning (SCIM / native) does not require SSSD. | +| 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 `serviceAccountUser` is not `root`. Set to `false` to disable entirely. | +| 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 | `{}` | 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`. | @@ -733,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 | @@ -757,7 +759,6 @@ Use of [Sealed secrets](https://github.com/bitnami-labs/sealed-secrets) disables | replicas | int | `1` | replicas is the number of replica pods to maintain for this service. Use 2 or more to enable HA | | resources | object | `{"limits":{"cpu":"2000m","enabled":false,"ephemeralStorage":"200Mi","memory":"4Gi"},"requests":{"cpu":"100m","enabled":false,"ephemeralStorage":"100Mi","memory":"2Gi"}}` | resources define requests and limits for the rstudio-server pod | | revisionHistoryLimit | int | `3` | The revisionHistoryLimit to use for the pod deployment. Do not set to 0 | -| 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 `serviceAccountUserId`, SSSD is skipped, secrets mount 0640, and non-root `rserver.conf`/`launcher.conf` defaults are applied. | | sealedSecret.annotations | object | `{}` | annotations for SealedSecret resources | | sealedSecret.enabled | bool | `false` | use SealedSecret instead of Secret to deploy secrets | | secureCookieKey | object | `{"existingSecret":"","value":""}` | global.secureCookieKey takes precedence over secureCookieKey | @@ -771,8 +772,6 @@ Use of [Sealed secrets](https://github.com/bitnami-labs/sealed-secrets) disables | service.port | int | `80` | The Service port. This is the port your service will run under. | | service.targetPort | int | `8787` | The port to forward to on the Workbench pod. Also see pod.port | | service.type | string | `"ClusterIP"` | The service type, usually ClusterIP (in-cluster only) or LoadBalancer (to expose the service using your cloud provider's load balancer) | -| serviceAccountUser | string | `"root"` | The OS user written to `rserver.conf` as `server-user`. Set to `""` to omit `server-user` from `rserver.conf` entirely. | -| serviceAccountUserId | int | `999` | The UID used as `runAsUser`/`fsGroup` in the pod's securityContext when `runAsRoot` is false. Must match the UID of `serviceAccountUser` baked into the Workbench image. | | serviceMonitor.additionalLabels | object | `{}` | additionalLabels normally includes the release name of the Prometheus Operator | | serviceMonitor.enabled | bool | `false` | Whether to create a ServiceMonitor CRD for use with a Prometheus Operator | | serviceMonitor.namespace | string | `""` | Namespace to create the ServiceMonitor in (usually the same as the one in which the Prometheus Operator is running). Defaults to the release namespace | diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl index 7e44634aa..c119fbae1 100644 --- a/charts/rstudio-workbench/README.md.gotmpl +++ b/charts/rstudio-workbench/README.md.gotmpl @@ -33,11 +33,10 @@ 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 `posit/workbench` image ships `sssd` for legacy LDAP/Active Directory provisioning, but it is **not** started by default. SSSD must run as root, so it can only be enabled when `serviceAccountUser: root`. Modern provisioning (SCIM / native) does not require SSSD. +* 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, but it is **not** started by default. SSSD must run as root, so it can only be enabled when `pod.runAsRoot: true`. Modern provisioning (SCIM / native) does not require SSSD. To start the bundled SSSD daemon, set `config.sssd.enabled: true` and provide its configuration in `config.sssd.conf` like so: ```yaml - serviceAccountUser: root config: sssd: enabled: true @@ -306,7 +305,7 @@ administrator. 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 `serviceAccountUser: root`; see `config.sssd` above). +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. diff --git a/charts/rstudio-workbench/templates/NOTES.txt b/charts/rstudio-workbench/templates/NOTES.txt index 50cbef256..4aa212c02 100644 --- a/charts/rstudio-workbench/templates/NOTES.txt +++ b/charts/rstudio-workbench/templates/NOTES.txt @@ -76,5 +76,5 @@ Please consider removing this configuration value. {{- 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 `runAsRoot: true` and `config.sssd.enabled: true`." }} +{{ 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 c071342ba..109ad38d2 100644 --- a/charts/rstudio-workbench/templates/_helpers.tpl +++ b/charts/rstudio-workbench/templates/_helpers.tpl @@ -25,11 +25,11 @@ If release name contains chart name it will be used as a full name. {{- end }} {{/* -Returns "true" when the SSSD daemon should actually run: sssd.enabled=true and serviceAccountUser=root. +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.runAsRoot -}} +{{- if and .Values.config.sssd.enabled .Values.pod.runAsRoot -}} true {{- end -}} {{- end -}} @@ -57,7 +57,7 @@ stderr_logfile_backups=0 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.runAsRoot -}} +{{- if not $.Values.pod.runAsRoot -}} {{- $secretMode = 416 -}}{{/* octal 0640 */}} {{- end }} containers: diff --git a/charts/rstudio-workbench/templates/configmap-general.yaml b/charts/rstudio-workbench/templates/configmap-general.yaml index 54ceb23f3..e3901305f 100644 --- a/charts/rstudio-workbench/templates/configmap-general.yaml +++ b/charts/rstudio-workbench/templates/configmap-general.yaml @@ -49,8 +49,8 @@ {{- $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.serviceAccountUser }} - {{- $_ := set $defaultRServerConfigValues "server-user" .Values.serviceAccountUser }} +{{- 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 }} @@ -118,7 +118,7 @@ data: - 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. */}} -{{- if not .Values.runAsRoot }} +{{- if not .Values.pod.runAsRoot }} {{- $rootlessDefaults := dict "rserver.conf" (dict "auth-pam-sessions-enabled" 0 "user-provisioning-enabled" 1) "launcher.conf" (dict "server" (dict "secure-cookie-key-file" "/mnt/secret-configmap/rstudio/secure-cookie-key")) }} {{- $overrideDict = mergeOverwrite $rootlessDefaults $overrideDict }} {{- else }} diff --git a/charts/rstudio-workbench/templates/deployment.yaml b/charts/rstudio-workbench/templates/deployment.yaml index 709567f55..ebb7ad218 100644 --- a/charts/rstudio-workbench/templates/deployment.yaml +++ b/charts/rstudio-workbench/templates/deployment.yaml @@ -89,7 +89,7 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- $defaultPodSC := dict }} - {{- if .Values.runAsRoot }} + {{- 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 .Values.pod.securityContext "runAsUser") (hasKey .Values.pod.securityContext "runAsNonRoot")) }} @@ -97,8 +97,8 @@ spec: {{- end }} {{- else }} {{- $_ := set $defaultPodSC "runAsNonRoot" true }} - {{- $_ := set $defaultPodSC "runAsUser" (int .Values.serviceAccountUserId) }} - {{- $_ := set $defaultPodSC "fsGroup" (int .Values.serviceAccountUserId) }} + {{- $_ := set $defaultPodSC "runAsUser" (int .Values.pod.serviceAccountUserId) }} + {{- $_ := set $defaultPodSC "fsGroup" (int .Values.pod.serviceAccountUserId) }} {{- end }} {{- $effectivePodSC := mergeOverwrite $defaultPodSC (deepCopy .Values.pod.securityContext) }} {{- with $effectivePodSC }} diff --git a/charts/rstudio-workbench/tests/configmap_test.yaml b/charts/rstudio-workbench/tests/configmap_test.yaml index d36c1f88f..0a7cb312a 100644 --- a/charts/rstudio-workbench/tests/configmap_test.yaml +++ b/charts/rstudio-workbench/tests/configmap_test.yaml @@ -270,7 +270,7 @@ tests: template: configmap-general.yaml documentIndex: 0 set: - serviceAccountUser: "workbench" + pod.serviceAccountUser: "workbench" asserts: - matchRegex: path: data["rserver.conf"] @@ -280,7 +280,7 @@ tests: template: configmap-general.yaml documentIndex: 0 set: - serviceAccountUser: "" + pod.serviceAccountUser: "" asserts: - notMatchRegex: path: data["rserver.conf"] @@ -290,7 +290,7 @@ tests: template: configmap-general.yaml documentIndex: 0 set: - serviceAccountUser: "rstudio-server" + pod.serviceAccountUser: "rstudio-server" config: server: rserver.conf: @@ -365,7 +365,7 @@ tests: template: configmap-general.yaml documentIndex: 0 set: - runAsRoot: false + pod.runAsRoot: false asserts: - matchRegex: path: data["rserver.conf"] @@ -395,7 +395,7 @@ tests: template: configmap-general.yaml documentIndex: 0 set: - serviceAccountUser: "rstudio-server" + pod.serviceAccountUser: "rstudio-server" config: server: rserver.conf: diff --git a/charts/rstudio-workbench/tests/deployment_test.yaml b/charts/rstudio-workbench/tests/deployment_test.yaml index e7046b7d7..e9eb4dda5 100644 --- a/charts/rstudio-workbench/tests/deployment_test.yaml +++ b/charts/rstudio-workbench/tests/deployment_test.yaml @@ -269,7 +269,7 @@ tests: - it: should specify a volumeMount and a volume for sssd conf when sssd is enabled (deprecated userProvisioning fallback) template: deployment.yaml set: - serviceAccountUser: root + pod.serviceAccountUser: root config: defaultMode: userProvisioning: 0600 @@ -871,7 +871,7 @@ tests: - it: should run pod as non-root when runAsRoot is false template: deployment.yaml set: - runAsRoot: false + pod.runAsRoot: false asserts: - equal: path: spec.template.spec.securityContext.runAsNonRoot @@ -886,8 +886,8 @@ tests: - it: should use custom uid when serviceAccountUserId is overridden template: deployment.yaml set: - runAsRoot: false - serviceAccountUserId: 1500 + pod.runAsRoot: false + pod.serviceAccountUserId: 1500 asserts: - equal: path: spec.template.spec.securityContext.runAsNonRoot @@ -899,7 +899,7 @@ tests: - it: should let pod.securityContext override the non-root defaults template: deployment.yaml set: - runAsRoot: false + pod.runAsRoot: false pod: securityContext: runAsUser: 4242 @@ -918,7 +918,7 @@ tests: - it: should not force runAsUser 0 when root pod.securityContext sets runAsNonRoot template: deployment.yaml set: - runAsRoot: true + pod.runAsRoot: true pod: securityContext: runAsNonRoot: true @@ -932,7 +932,7 @@ tests: - it: should respect a root pod.securityContext runAsUser override template: deployment.yaml set: - runAsRoot: true + pod.runAsRoot: true pod: securityContext: runAsUser: 1234 @@ -965,7 +965,7 @@ tests: - it: should not override command/args when runAsRoot is true template: deployment.yaml set: - runAsRoot: true + pod.runAsRoot: true asserts: - notExists: path: spec.template.spec.containers[0].command @@ -975,7 +975,7 @@ tests: - it: should not override command/args when runAsRoot is false template: deployment.yaml set: - runAsRoot: false + pod.runAsRoot: false asserts: - notExists: path: spec.template.spec.containers[0].command diff --git a/charts/rstudio-workbench/tests/secrets_test.yaml b/charts/rstudio-workbench/tests/secrets_test.yaml index 2ea95aabe..f43b45d78 100644 --- a/charts/rstudio-workbench/tests/secrets_test.yaml +++ b/charts/rstudio-workbench/tests/secrets_test.yaml @@ -134,7 +134,7 @@ tests: - it: should mount secrets group-readable (0640) when running non-root template: deployment.yaml set: - runAsRoot: false + pod.runAsRoot: false config: defaultMode: secret: 0600 diff --git a/charts/rstudio-workbench/tests/sssd_test.yaml b/charts/rstudio-workbench/tests/sssd_test.yaml index 10a4d6509..59ebb8a2c 100644 --- a/charts/rstudio-workbench/tests/sssd_test.yaml +++ b/charts/rstudio-workbench/tests/sssd_test.yaml @@ -25,7 +25,7 @@ tests: - it: does not start the sssd daemon when runAsRoot is false template: configmap-startup.yaml set: - runAsRoot: false + pod.runAsRoot: false asserts: - notMatchRegexRaw: pattern: "program:sssd" @@ -75,7 +75,7 @@ tests: - it: does not mount sssd directories when runAsRoot is false template: deployment.yaml set: - runAsRoot: false + pod.runAsRoot: false asserts: - notExists: path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user")]' @@ -100,7 +100,7 @@ tests: - it: silently skips sssd when runAsRoot is false even if sssd.enabled is true template: NOTES.txt set: - runAsRoot: false + pod.runAsRoot: false config.sssd.enabled: true asserts: - matchRegexRaw: diff --git a/charts/rstudio-workbench/values.yaml b/charts/rstudio-workbench/values.yaml index 94e421571..992988dd7 100644 --- a/charts/rstudio-workbench/values.yaml +++ b/charts/rstudio-workbench/values.yaml @@ -6,21 +6,6 @@ fullnameOverride: "" # -- A Workbench version to override the "tag" for the server image and session-init container. Does not affect the session image tag, which is controlled by session.image.rVersion, session.image.pythonVersion, and session.image.os. Necessary until https://github.com/helm/helm/issues/8194 versionOverride: "" -# -- 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 -# `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 `runAsRoot` is false. -# Must match the UID of `serviceAccountUser` baked into the Workbench image. -serviceAccountUserId: 999 - # -- Settings for enabling server diagnostics diagnostics: enabled: false @@ -419,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 @@ -585,9 +582,9 @@ config: # path: custom-file.conf # -- 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 as a non-root user (`serviceAccountUser` other than `root`), since SSSD requires root. Modern provisioning (SCIM / native) does not require SSSD. + # -- 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 `serviceAccountUser` is not `root`. Set to `false` to disable entirely. + # -- 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: {} From e118a9b214f36815a7d79afbb99bdef704bea797 Mon Sep 17 00:00:00 2001 From: Sean Sinnott Date: Tue, 2 Jun 2026 07:57:01 -0400 Subject: [PATCH 7/9] Harden pod.securityContext handling and fix SSSD docs --- charts/rstudio-workbench/README.md | 4 ++-- charts/rstudio-workbench/README.md.gotmpl | 4 ++-- charts/rstudio-workbench/templates/deployment.yaml | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index 3005e1952..a56a62b06 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -62,8 +62,8 @@ 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 `posit/workbench` image ships `sssd` for legacy LDAP/Active Directory provisioning, but it is **not** started by default. SSSD must run as root, so it can only be enabled when `pod.runAsRoot: true`. Modern provisioning (SCIM / native) does not require SSSD. - To start the bundled SSSD daemon, set `config.sssd.enabled: true` and provide its configuration in `config.sssd.conf` 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: diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl index c119fbae1..7302e18cf 100644 --- a/charts/rstudio-workbench/README.md.gotmpl +++ b/charts/rstudio-workbench/README.md.gotmpl @@ -33,8 +33,8 @@ 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 `posit/workbench` image ships `sssd` for legacy LDAP/Active Directory provisioning, but it is **not** started by default. SSSD must run as root, so it can only be enabled when `pod.runAsRoot: true`. Modern provisioning (SCIM / native) does not require SSSD. - To start the bundled SSSD daemon, set `config.sssd.enabled: true` and provide its configuration in `config.sssd.conf` 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: diff --git a/charts/rstudio-workbench/templates/deployment.yaml b/charts/rstudio-workbench/templates/deployment.yaml index ebb7ad218..9f67c5f12 100644 --- a/charts/rstudio-workbench/templates/deployment.yaml +++ b/charts/rstudio-workbench/templates/deployment.yaml @@ -89,10 +89,11 @@ spec: {{- toYaml . | nindent 8 }} {{- end }} {{- $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 .Values.pod.securityContext "runAsUser") (hasKey .Values.pod.securityContext "runAsNonRoot")) }} + {{- if not (or (hasKey $userPodSC "runAsUser") (hasKey $userPodSC "runAsNonRoot")) }} {{- $_ := set $defaultPodSC "runAsUser" 0 }} {{- end }} {{- else }} @@ -100,7 +101,7 @@ spec: {{- $_ := set $defaultPodSC "runAsUser" (int .Values.pod.serviceAccountUserId) }} {{- $_ := set $defaultPodSC "fsGroup" (int .Values.pod.serviceAccountUserId) }} {{- end }} - {{- $effectivePodSC := mergeOverwrite $defaultPodSC (deepCopy .Values.pod.securityContext) }} + {{- $effectivePodSC := mergeOverwrite $defaultPodSC (deepCopy $userPodSC) }} {{- with $effectivePodSC }} securityContext: {{- toYaml . | nindent 8 }} From a2e9596cf6590cfb0206fefee78c1cf1b2ffb266 Mon Sep 17 00:00:00 2001 From: Sean Sinnott Date: Tue, 2 Jun 2026 07:59:33 -0400 Subject: [PATCH 8/9] Align NEWS heading with chart version 0.21.1 --- charts/rstudio-workbench/NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index acbbb4180..4833db519 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -1,6 +1,6 @@ # Changelog -## 0.21.0 +## 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. From 061ca5fce1cfc54b4605d2e9f2d7da2b3e9f93d6 Mon Sep 17 00:00:00 2001 From: Sean Sinnott Date: Fri, 5 Jun 2026 15:04:24 -0400 Subject: [PATCH 9/9] Mount writable /mnt/load-balancer volume for rootless load balancing When load balancing is active (replicas > 1 or loadBalancer.forceEnabled), prestart-workbench.bash writes the load-balancer config under /mnt/load-balancer. With no volume mounted there the path lives on the container root fs, which a non-root pod (pod.runAsRoot: false) cannot write to, causing CrashLoopBackOff on mkdir. Mount an emptyDir at /mnt/load-balancer under the same condition that sets RSW_LOAD_BALANCING, mirroring the /mnt/dynamic pattern. fsGroup makes it writable for the unprivileged user. --- charts/rstudio-workbench/NEWS.md | 1 + .../rstudio-workbench/templates/_helpers.tpl | 12 ++++ .../tests/deployment_test.yaml | 55 +++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index 4833db519..36db29709 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -7,6 +7,7 @@ - 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 diff --git a/charts/rstudio-workbench/templates/_helpers.tpl b/charts/rstudio-workbench/templates/_helpers.tpl index 109ad38d2..89bc973f3 100644 --- a/charts/rstudio-workbench/templates/_helpers.tpl +++ b/charts/rstudio-workbench/templates/_helpers.tpl @@ -181,6 +181,10 @@ containers: 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 }} @@ -314,6 +318,14 @@ volumes: 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 diff --git a/charts/rstudio-workbench/tests/deployment_test.yaml b/charts/rstudio-workbench/tests/deployment_test.yaml index e9eb4dda5..d353629a7 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: