diff --git a/charts/rstudio-workbench/Chart.yaml b/charts/rstudio-workbench/Chart.yaml index 62007070..39f21694 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.4 +version: 0.21.0 apiVersion: v2 appVersion: 2026.06.0 icon: diff --git a/charts/rstudio-workbench/NEWS.md b/charts/rstudio-workbench/NEWS.md index 61d3ddd3..5643b728 100644 --- a/charts/rstudio-workbench/NEWS.md +++ b/charts/rstudio-workbench/NEWS.md @@ -1,5 +1,11 @@ # Changelog +## 0.21.0 + +- New `pod.runAsRoot` value (default `true`); set it to `false` to run the Workbench pod as a non-root user. Rootless mode requires Workbench's native user provisioning (SCIM or just-in-time) with an external auth provider (e.g. OpenID/SAML) — local/PAM authentication is not supported — and adds init containers. +- The bundled SSSD daemon is now controlled by the new `config.sssd` block; `config.userProvisioning` is deprecated. +- Supporting changes for unprivileged pods and the non-root launcher: prestart scripts no longer perform root-only operations, the launcher's ServiceAccount token is made group-readable via `fsGroup`, the `secure-cookie-key` is shared with the launcher, the Kubernetes launcher scratch dir (`/var/lib/rstudio-launcher/Kubernetes`) is backed by an `fsGroup`-owned `emptyDir` so it is group-writable for the non-root launcher, and writable scratch and `/mnt/load-balancer` volumes are mounted as needed. + ## 0.20.4 - finally fix the last bit for suppressStdinAnnotation (cf. PR #842) @@ -8,12 +14,10 @@ - Bump Workbench version to 2026.06.0 - ## 0.20.2 - Bump Workbench version to 2026.05.1 - ## 0.20.1 - Bump Workbench version to 2026.05.0 diff --git a/charts/rstudio-workbench/README.md b/charts/rstudio-workbench/README.md index 7ec0c088..211c11cb 100644 --- a/charts/rstudio-workbench/README.md +++ b/charts/rstudio-workbench/README.md @@ -1,6 +1,6 @@ # Posit Workbench -![Version: 0.20.4](https://img.shields.io/badge/Version-0.20.4-informational?style=flat-square) ![AppVersion: 2026.06.0](https://img.shields.io/badge/AppVersion-2026.06.0-informational?style=flat-square) +![Version: 0.21.0](https://img.shields.io/badge/Version-0.21.0-informational?style=flat-square) ![AppVersion: 2026.06.0](https://img.shields.io/badge/AppVersion-2026.06.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.4: +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.4 +helm upgrade --install my-release rstudio/rstudio-workbench --version=0.21.0 ``` To explore other chart versions, look at: @@ -62,20 +62,22 @@ To function, this chart requires the following: * If using load balancing (by setting `replicas > 1`), you need similar storage defined for `sharedStorage` to store shared project configuration. However, you can also configure the product to store its shared data underneath `/home` by setting `config.server.rserver\.conf.server-shared-storage-path=/home/some-shared-dir`. -* A method to join the deployed `rstudio-workbench` container to your auth domain. The default `posit/workbench` image has `sssd` installed and started by default. - You can include `sssd` configuration in `config.userProvisioning` like so: +* A method to join the deployed `rstudio-workbench` container to your auth domain. The `posit/workbench` image ships `sssd` for legacy LDAP/Active Directory provisioning; because it must run as root, the bundled daemon starts only when `config.sssd.enabled: true` and `pod.runAsRoot: true`. Modern provisioning (SCIM / native) does not require `sssd`. See [User provisioning](#user-provisioning) for details. + Provide `sssd` configuration in `config.sssd.conf` like so: ```yaml config: - userProvisioning: - mysssd.conf: - sssd: - config_file_version: 2 - services: nss, pam - domains: rstudio.com - domain/rstudio.com: - id_provider: ldap - auth_provider: ldap + sssd: + enabled: true + conf: + mysssd.conf: + sssd: + config_file_version: 2 + services: nss, pam + domains: rstudio.com + domain/rstudio.com: + id_provider: ldap + auth_provider: ldap ``` ## Licensing @@ -302,9 +304,9 @@ the `XDG_CONFIG_DIRS` environment variable. - It is mounted into the pod at `/scripts/`. - `prestart-workbench.bash` is used to start workbench. - `prestart-launcher.bash` is used to start launcher. -- User Provisioning Configuration: - - These configuration files are used for configuring user provisioning (i.e., `sssd`). - - Located at:
`config.userProvisioning.<< name of file >>` Helm values +- SSSD Configuration: + - These configuration files configure the bundled `sssd` daemon (legacy LDAP/AD provisioning), which is started only when `config.sssd.enabled=true`. + - Located at:
`config.sssd.conf.<< name of file >>` Helm values. The deprecated `config.userProvisioning` is still read when `config.sssd.conf` is unset. - Mounted onto:
`/etc/sssd/conf.d/` with `0600` permissions by default. - Custom Startup Configuration: - `supervisord` service / unit definition `.conf` files. @@ -355,9 +357,9 @@ Provisioning users in Workbench containers is challenging. Session images create consistent UIDs / GIDs). However, creating users in the Workbench containers is a responsibility that falls to the administrator. -The most common way to provision users is via `sssd`. -The [latest Workbench container](https://github.com/rstudio/rstudio-docker-products/tree/main/workbench#user-provisioning) -has `sssd` included and running by default (see `userProvisioning` configuration files above). +Posit Workbench's native user provisioning (SCIM / just-in-time) is the recommended approach and does not require SSSD. +`sssd` is the legacy approach: the [latest Workbench container](https://github.com/rstudio/rstudio-docker-products/tree/main/workbench#user-provisioning) +includes `sssd`, but it is started only when `config.sssd.enabled=true` (which requires `pod.runAsRoot: true`; see `config.sssd` above). The other way that this can be managed is via a lightweight startup service (runs once at startup and then sleeps forever) or a polling service (checks at regular intervals). Either can be written easily in `bash` or another programming language. @@ -605,18 +607,20 @@ 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`) Use of [Sealed secrets](https://github.com/bitnami-labs/sealed-secrets) disables the chart's auto-generation and reuse capabilities for `launcherPem` and `secureCookieKey`. `launcherPem` is an RSA private key, which can be generated via an RSA tool such as Helm's [`genPrivateKey`](https://helm.sh/docs/chart_template_guide/function_list/#genprivatekey) function. `secureCookieKey` is typically a UUID, which can be generated via a UUID generator such as Helm's [`uuidv4`](https://helm.sh/docs/chart_template_guide/function_list/#uuid-functions) function. +When combining `sealedSecret.enabled=true` with rootless mode (`pod.runAsRoot=false`), you **must** explicitly provide `secureCookieKey` (value or existingSecret, local or global). In rootless mode the launcher reads `/mnt/secret-configmap/rstudio/secure-cookie-key`, so the key cannot be omitted. + ## Values | 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 | the image's built-in arguments | 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 +638,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 +666,12 @@ Use of [Sealed secrets](https://github.com/bitnami-labs/sealed-secrets) disables | config.serverDcf | object | `{"launcher-mounts":[]}` | a map of server-scoped config files (akin to `config.server`), but with .dcf file formatting (i.e. `launcher-mounts`, `launcher-env`, etc.) | | config.session | object | `{"notifications.conf":{},"repos.conf":{"CRAN":"https://packagemanager.posit.co/cran/__linux__/jammy/latest"},"rsession.conf":{},"rstudio-prefs.json":"{}\n"}` | a map of session-scoped config files. Mounted to `/mnt/session-configmap/rstudio/` on both server and session, by default. | | config.sessionSecret | object | `{}` | a map of secret, session-scoped config files (odbc.ini, etc.). Mounted to `/mnt/session-secret/` on both server and session, by default | +| config.sssd | object | `{"conf":{},"enabled":true}` | Bundled SSSD daemon for legacy LDAP/Active Directory user provisioning. On by default; automatically skipped when the pod runs unprivileged (`pod.runAsRoot: false`), since SSSD requires root. Modern provisioning (SCIM / native) does not require SSSD. | +| config.sssd.conf | object | `{}` | a map of sssd config files, mounted to `/etc/sssd/conf.d/` with 0600 permissions. Replaces the deprecated `config.userProvisioning`. | +| config.sssd.enabled | bool | `true` | whether to start the bundled SSSD daemon. Automatically skipped (not an error) when `pod.runAsRoot` is false. Set to `false` to disable entirely. | | config.startupCustom | object | `{}` | a map of supervisord .conf files to define custom services. Mounted into the container at /startup/custom/ | -| config.startupUserProvisioning | object | `{"sssd.conf":"[program:sssd]\ncommand=/usr/sbin/sssd -i -c /etc/sssd/sssd.conf --logger=stderr\nautorestart=false\nnumprocs=1\nstdout_logfile=/dev/stdout\nstdout_logfile_maxbytes=0\nstdout_logfile_backups=0\nstderr_logfile=/dev/stderr\nstderr_logfile_maxbytes=0\nstderr_logfile_backups=0\n"}` | a map of supervisord .conf files to define user provisioning services. Mounted into the container at /startup/user-provisioning/ | -| config.userProvisioning | object | `{}` | a map of sssd config files, used for user provisioning. Mounted to `/etc/sssd/conf.d/` with 0600 permissions | +| config.startupUserProvisioning | object | `{}` | a map of supervisord .conf files to define user provisioning services. Mounted into the container at /startup/user-provisioning/. The bundled SSSD service is now controlled by `config.sssd.enabled`; use this only for custom provisioning daemons. | +| config.userProvisioning | object | `{}` | DEPRECATED: use `config.sssd.conf` instead. A map of sssd config files, mounted to `/etc/sssd/conf.d/` with 0600 permissions. Only applied when `config.sssd.enabled=true`. | | dangerRegenerateAutomatedValues | bool | `false` | | | deployment.annotations | object | `{}` | Additional annotations to add to the rstudio-workbench deployment | | diagnostics | object | `{"directory":"/var/log/rstudio","enabled":false}` | Settings for enabling server diagnostics | @@ -727,7 +734,10 @@ Use of [Sealed secrets](https://github.com/bitnami-labs/sealed-secrets) disables | pod.labels | object | `{}` | Additional labels to add to the rstudio-workbench pods | | pod.lifecycle | object | `{}` | container lifecycle hooks | | pod.port | int | `8787` | The containerPort used by the main pod container | +| pod.runAsRoot | bool | `true` | Whether the pod's containers run as the root OS user. `true` (default) preserves historical behavior: pod runs as root (`runAsUser: 0`), SSSD starts, secrets mount 0600, PAM sessions on. `false` runs unprivileged: pod sets `runAsNonRoot: true` and `runAsUser`/`fsGroup` to `pod.serviceAccountUserId`, SSSD is skipped, secrets mount 0640, and non-root `rserver.conf`/`launcher.conf` defaults are applied. | | pod.securityContext | object | `{}` | Values to set the `securityContext` for the service pod | +| pod.serviceAccountUser | string | `"rstudio-server"` | The OS user written to `rserver.conf` as `server-user`. Set to `""` to omit `server-user` from `rserver.conf` entirely. | +| pod.serviceAccountUserId | int | `999` | The UID used as `runAsUser`/`fsGroup` in the pod's securityContext when `pod.runAsRoot` is false. Must match the UID of `pod.serviceAccountUser` baked into the Workbench image. | | pod.sidecar | list | `[]` | sidecar is an array of containers that will be run alongside the main container | | pod.terminationGracePeriodSeconds | int | `120` | The termination grace period seconds allowed for the pod before shutdown | | pod.volumeMounts | list | `[]` | volumeMounts is injected as-is into the "volumeMounts:" component of the pod.container spec | diff --git a/charts/rstudio-workbench/README.md.gotmpl b/charts/rstudio-workbench/README.md.gotmpl index df7239fb..50c78e6b 100644 --- a/charts/rstudio-workbench/README.md.gotmpl +++ b/charts/rstudio-workbench/README.md.gotmpl @@ -33,20 +33,22 @@ To function, this chart requires the following: * If using load balancing (by setting `replicas > 1`), you need similar storage defined for `sharedStorage` to store shared project configuration. However, you can also configure the product to store its shared data underneath `/home` by setting `config.server.rserver\.conf.server-shared-storage-path=/home/some-shared-dir`. -* A method to join the deployed `rstudio-workbench` container to your auth domain. The default `posit/workbench` image has `sssd` installed and started by default. - You can include `sssd` configuration in `config.userProvisioning` like so: +* A method to join the deployed `rstudio-workbench` container to your auth domain. The `posit/workbench` image ships `sssd` for legacy LDAP/Active Directory provisioning; because it must run as root, the bundled daemon starts only when `config.sssd.enabled: true` and `pod.runAsRoot: true`. Modern provisioning (SCIM / native) does not require `sssd`. See [User provisioning](#user-provisioning) for details. + Provide `sssd` configuration in `config.sssd.conf` like so: ```yaml config: - userProvisioning: - mysssd.conf: - sssd: - config_file_version: 2 - services: nss, pam - domains: rstudio.com - domain/rstudio.com: - id_provider: ldap - auth_provider: ldap + sssd: + enabled: true + conf: + mysssd.conf: + sssd: + config_file_version: 2 + services: nss, pam + domains: rstudio.com + domain/rstudio.com: + id_provider: ldap + auth_provider: ldap ``` {{ template "rstudio.licensing" . }} @@ -248,9 +250,9 @@ the `XDG_CONFIG_DIRS` environment variable. - It is mounted into the pod at `/scripts/`. - `prestart-workbench.bash` is used to start workbench. - `prestart-launcher.bash` is used to start launcher. -- User Provisioning Configuration: - - These configuration files are used for configuring user provisioning (i.e., `sssd`). - - Located at:
`config.userProvisioning.<< name of file >>` Helm values +- SSSD Configuration: + - These configuration files configure the bundled `sssd` daemon (legacy LDAP/AD provisioning), which is started only when `config.sssd.enabled=true`. + - Located at:
`config.sssd.conf.<< name of file >>` Helm values. The deprecated `config.userProvisioning` is still read when `config.sssd.conf` is unset. - Mounted onto:
`/etc/sssd/conf.d/` with `0600` permissions by default. - Custom Startup Configuration: - `supervisord` service / unit definition `.conf` files. @@ -301,9 +303,9 @@ Provisioning users in Workbench containers is challenging. Session images create consistent UIDs / GIDs). However, creating users in the Workbench containers is a responsibility that falls to the administrator. -The most common way to provision users is via `sssd`. -The [latest Workbench container](https://github.com/rstudio/rstudio-docker-products/tree/main/workbench#user-provisioning) -has `sssd` included and running by default (see `userProvisioning` configuration files above). +Posit Workbench's native user provisioning (SCIM / just-in-time) is the recommended approach and does not require SSSD. +`sssd` is the legacy approach: the [latest Workbench container](https://github.com/rstudio/rstudio-docker-products/tree/main/workbench#user-provisioning) +includes `sssd`, but it is started only when `config.sssd.enabled=true` (which requires `pod.runAsRoot: true`; see `config.sssd` above). The other way that this can be managed is via a lightweight startup service (runs once at startup and then sleeps forever) or a polling service (checks at regular intervals). Either can be written easily in `bash` or another programming language. @@ -551,12 +553,14 @@ 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`) Use of [Sealed secrets](https://github.com/bitnami-labs/sealed-secrets) disables the chart's auto-generation and reuse capabilities for `launcherPem` and `secureCookieKey`. `launcherPem` is an RSA private key, which can be generated via an RSA tool such as Helm's [`genPrivateKey`](https://helm.sh/docs/chart_template_guide/function_list/#genprivatekey) function. `secureCookieKey` is typically a UUID, which can be generated via a UUID generator such as Helm's [`uuidv4`](https://helm.sh/docs/chart_template_guide/function_list/#uuid-functions) function. +When combining `sealedSecret.enabled=true` with rootless mode (`pod.runAsRoot=false`), you **must** explicitly provide `secureCookieKey` (value or existingSecret, local or global). In rootless mode the launcher reads `/mnt/secret-configmap/rstudio/secure-cookie-key`, so the key cannot be omitted. + {{ template "chart.valuesSection" . }} {{ template "helm-docs.versionFooter" . }} diff --git a/charts/rstudio-workbench/prestart-launcher.bash b/charts/rstudio-workbench/prestart-launcher.bash index 0802b81e..2f8243d3 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,14 @@ 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 - + # Create the Local scratch dir setgid + group-writable (2775). In root mode this prestart runs as + # root but the launcher drops to the non-root serviceAccountUser; the dir inherits the + # rstudio-server group from the setgid parent, so the launcher can write to it via group + # permission without any root-only ownership change. In rootless mode the process already + # is serviceAccountUser. The Kubernetes scratch dir is mounted as an fsGroup-owned emptyDir by + # the chart, so it arrives group-writable and must not be chmod-ed here (a non-owner chmod EPERMs). _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 + install -d -m 2775 /var/lib/rstudio-launcher/Local _logf 'Replacing process with %s' "${startup_script}" exec "${startup_script}" diff --git a/charts/rstudio-workbench/prestart-workbench.bash b/charts/rstudio-workbench/prestart-workbench.bash index aa903d33..163c020e 100644 --- a/charts/rstudio-workbench/prestart-workbench.bash +++ b/charts/rstudio-workbench/prestart-workbench.bash @@ -12,6 +12,14 @@ main() { _logf 'Ensuring %s exists' "${dyn_dir}" mkdir -p "${dyn_dir}" + if [[ -f "${launcher_pem}" ]]; then + # fsGroup adds group-read (0640) to the secret mount, which rserver rejects for + # launcher.pem. Read the key from a private 0600 copy instead; dyn_dir precedes + # the secret mount in XDG_CONFIG_DIRS. + _logf 'Copying %s to %s with mode 0600' "${launcher_pem}" "${dyn_dir}/launcher.pem" + install -m 0600 "${launcher_pem}" "${dyn_dir}/launcher.pem" + fi + if [[ ! -s "${launcher_pub}" ]] && [[ -f "${launcher_pem}" ]]; then _logf 'Generating %s from %s' "${launcher_pub}" "${launcher_pem}" openssl rsa -in "${launcher_pem}" -outform PEM -pubout -out "${launcher_pub}" 2>&1 | _indent @@ -27,16 +35,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 +67,7 @@ in order to facilitate running in Kubernetes. The directories are specified via the XDG_CONFIG_DIRS environment variable defined in the Helm chart. The currently defined directories are: -$(echo "$XDG_CONFIG_DIRS" | sed 's/:/\n/g') +$(echo "$XDG_CONFIG_DIRS" | tr ':' '\n') $HERE$ ) > /etc/rstudio/README } diff --git a/charts/rstudio-workbench/templates/NOTES.txt b/charts/rstudio-workbench/templates/NOTES.txt index d8a21ee1..b26b72ca 100644 --- a/charts/rstudio-workbench/templates/NOTES.txt +++ b/charts/rstudio-workbench/templates/NOTES.txt @@ -70,3 +70,12 @@ Please consider removing this configuration value. {{- print "\n\n`config.server.'rserver/.conf'.monitor-graphite-enabled` is overwritten by `prometheus.legacy=false`. Internal Workbench Prometheus will be used instead." }} {{- end }} + +{{- if .Values.config.userProvisioning }} +{{ print "\n\nWARNING: `config.userProvisioning` is deprecated. Use `config.sssd.conf` instead." }} +{{- end }} + +{{- $sssd := mergeOverwrite (dict "enabled" true "conf" (dict)) (deepCopy (default (dict) .Values.config.sssd)) }} +{{- if and $sssd.conf (not (include "rstudio-workbench.sssd.active" .)) }} +{{ print "\n\nWARNING: `config.sssd.conf` is set but the SSSD daemon will not start. SSSD requires `pod.runAsRoot: true` and `config.sssd.enabled: true`." }} +{{- end }} diff --git a/charts/rstudio-workbench/templates/_helpers.tpl b/charts/rstudio-workbench/templates/_helpers.tpl index 4973da6c..93f1ebd4 100644 --- a/charts/rstudio-workbench/templates/_helpers.tpl +++ b/charts/rstudio-workbench/templates/_helpers.tpl @@ -24,8 +24,44 @@ If release name contains chart name it will be used as a full name. {{- end }} {{- end }} +{{/* +Returns "true" when the SSSD daemon should actually run: sssd.enabled=true and pod.runAsRoot=true. +SSSD cannot run as a non-root process, so the flag is silently ignored for non-root deployments. +*/}} +{{- define "rstudio-workbench.sssd.active" -}} +{{- $sssd := mergeOverwrite (dict "enabled" true) (deepCopy (default (dict) .Values.config.sssd)) -}} +{{- if and $sssd.enabled .Values.pod.runAsRoot -}} +true +{{- end -}} +{{- end -}} + +{{/* +supervisord program definition that starts the bundled SSSD daemon. +Rendered into the user-provisioning startup ConfigMap when sssd is active. +*/}} +{{- define "rstudio-workbench.sssd.program" -}} +[program:sssd] +command=/usr/sbin/sssd -i -c /etc/sssd/sssd.conf --logger=stderr +autorestart=false +numprocs=1 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stdout_logfile_backups=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +stderr_logfile_backups=0 +{{- end -}} + {{- define "rstudio-workbench.containers" -}} {{- $useNewerOverrides := and (not (hasKey .Values.config.server "launcher.kubernetes.profiles.conf")) (not .Values.launcher.useTemplates) }} +{{- $sssd := mergeOverwrite (dict "enabled" true "conf" (dict)) (deepCopy (default (dict) .Values.config.sssd)) }} +{{- /* When running as non-root, secret files must be group-readable (0640) because Kubernetes mounts + them as root-owned and the non-root process accesses them via fsGroup group membership. + Root deployments keep the tighter 0600 default. */ -}} +{{- $secretMode := $.Values.config.defaultMode.secret -}} +{{- if not $.Values.pod.runAsRoot -}} + {{- $secretMode = 416 -}}{{/* octal 0640 */}} +{{- end }} containers: - name: rstudio {{- $defaultVersion := .Values.versionOverride | default $.Chart.AppVersion }} @@ -85,13 +121,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: @@ -135,23 +171,32 @@ containers: {{- end }} {{- /* Mount rstudio-secret volume when any secret content needs to be available in the pod. This includes both existingSecret references AND direct .value settings for launcherPem and secureCookieKey. */ -}} - {{- if or .Values.launcherPem.existingSecret .Values.launcherPem.value .Values.secureCookieKey.existingSecret .Values.secureCookieKey.value .Values.global.secureCookieKey.existingSecret .Values.global.secureCookieKey.value .Values.config.database.conf.existingSecret .Values.config.secret .Values.config.existingSecrets }} + {{- /* Also mount in rootless mode: the rootless launcher.conf default points + secure-cookie-key-file at /mnt/secret-configmap/rstudio/secure-cookie-key, which is + projected from the chart-managed secret even when the key is autogenerated. */ -}} + {{- if or .Values.launcherPem.existingSecret .Values.launcherPem.value .Values.secureCookieKey.existingSecret .Values.secureCookieKey.value .Values.global.secureCookieKey.existingSecret .Values.global.secureCookieKey.value .Values.config.database.conf.existingSecret .Values.config.secret .Values.config.existingSecrets (not .Values.pod.runAsRoot) }} - name: rstudio-secret mountPath: "/mnt/secret-configmap/rstudio/" {{- end }} - {{- if .Values.config.userProvisioning }} + {{- if and (include "rstudio-workbench.sssd.active" .) (or $sssd.conf .Values.config.userProvisioning) }} - name: rstudio-user mountPath: "/etc/sssd/conf.d/" {{- end }} - name: etc-rstudio mountPath: "/etc/rstudio" + - name: mnt-dynamic + mountPath: "/mnt/dynamic" + {{- if or (gt (int .Values.replicas) 1) .Values.loadBalancer.forceEnabled }} + - name: mnt-load-balancer + mountPath: "/mnt/load-balancer" + {{- end }} - name: rstudio-rsw-startup mountPath: "/startup/base" {{- if .Values.launcher.enabled }} - name: rstudio-launcher-startup mountPath: "/startup/launcher" {{- end }} - {{- if .Values.config.startupUserProvisioning }} + {{- if or (include "rstudio-workbench.sssd.active" .) .Values.config.startupUserProvisioning }} - name: rstudio-user-startup mountPath: "/startup/user-provisioning" {{- end }} @@ -176,6 +221,14 @@ containers: - name: rstudio-job-overrides-new mountPath: "/mnt/job-json-overrides-new" {{- end }} + {{- if .Values.launcher.enabled }} + {{- /* Back the Kubernetes launcher scratch-path with an emptyDir so it is governed by fsGroup + and comes up group-writable for the non-root launcher. Without this, the useTemplates + subPath mounts below force kubelet to pre-create this dir on the container rootfs as + root-owned, which an unprivileged prestart cannot chmod. */}} + - name: launcher-kubernetes-scratch + mountPath: "/var/lib/rstudio-launcher/Kubernetes" + {{- end }} {{- if .Values.launcher.useTemplates }} # mount into the default scratch-path... what if it gets changed? - name: session-templates @@ -276,6 +329,16 @@ volumes: {{- end }} - name: etc-rstudio emptyDir: {} +- name: mnt-dynamic + emptyDir: {} +{{- if or (gt (int .Values.replicas) 1) .Values.loadBalancer.forceEnabled }} +{{- /* prestart-workbench.bash writes the load-balancer config here when RSW_LOAD_BALANCING + is set. Without a mounted volume the path is on the read-only-by-default root fs, + which a non-root pod (pod.runAsRoot: false) cannot write to. fsGroup makes the + emptyDir group-writable for the unprivileged user. */}} +- name: mnt-load-balancer + emptyDir: {} +{{- end }} - name: rstudio-config configMap: name: {{ include "rstudio-workbench.fullname" . }}-config @@ -303,8 +366,10 @@ volumes: configMap: name: {{ include "rstudio-workbench.fullname" . }}-start-launcher defaultMode: {{ .Values.config.defaultMode.startup }} +- name: launcher-kubernetes-scratch + emptyDir: {} {{- end }} -{{- if .Values.config.startupUserProvisioning }} +{{- if or (include "rstudio-workbench.sssd.active" .) .Values.config.startupUserProvisioning }} - name: rstudio-user-startup configMap: name: {{ include "rstudio-workbench.fullname" . }}-start-user @@ -324,7 +389,7 @@ volumes: {{- end }} {{- /* Create rstudio-secret volume when any secret content needs to be projected into the pod. This includes both existingSecret references AND direct .value settings for launcherPem and secureCookieKey. */ -}} -{{- if or .Values.launcherPem.existingSecret .Values.launcherPem.value .Values.secureCookieKey.existingSecret .Values.secureCookieKey.value .Values.global.secureCookieKey.existingSecret .Values.global.secureCookieKey.value .Values.config.database.conf.existingSecret .Values.config.secret .Values.config.existingSecrets }} +{{- if or .Values.launcherPem.existingSecret .Values.launcherPem.value .Values.secureCookieKey.existingSecret .Values.secureCookieKey.value .Values.global.secureCookieKey.existingSecret .Values.global.secureCookieKey.value .Values.config.database.conf.existingSecret .Values.config.secret .Values.config.existingSecrets (not .Values.pod.runAsRoot) }} - name: rstudio-secret projected: sources: @@ -335,7 +400,7 @@ volumes: items: - key: {{ $key }} path: {{ $key }} - mode: {{ $.Values.config.defaultMode.secret }} + mode: {{ $secretMode }} {{- end }} {{- end }} {{- if .Values.launcherPem.existingSecret }} @@ -344,7 +409,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 +418,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 +426,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,18 +434,19 @@ 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, - or when the key is auto-generated. */ -}} -{{- if and (or .Values.secureCookieKey.value .Values.global.secureCookieKey.value) (not (or .Values.secureCookieKey.existingSecret .Values.global.secureCookieKey.existingSecret)) }} +{{- /* Project secure-cookie-key from the chart-managed secret when not using an existingSecret. + Fires for an explicit .value, and also for the autogenerated key in rootless mode, where the + launcher.conf default reads the shared key from /mnt/secret-configmap/rstudio/secure-cookie-key + (so the launcher does not self-generate a key that fails to verify rserver-signed requests). */ -}} +{{- if and (or .Values.secureCookieKey.value .Values.global.secureCookieKey.value (not .Values.pod.runAsRoot)) (not (or .Values.secureCookieKey.existingSecret .Values.global.secureCookieKey.existingSecret)) }} - secret: name: {{ include "rstudio-workbench.fullname" . }}-secret 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 +454,7 @@ volumes: items: - key: database.conf path: database.conf - mode: {{ .Values.config.defaultMode.secret }} + mode: {{ $secretMode }} {{- end }} {{- range .Values.config.existingSecrets }} - secret: @@ -397,11 +463,11 @@ volumes: {{- range .items }} - key: {{ .key }} path: {{ .path }} - mode: {{ $.Values.config.defaultMode.secret }} + mode: {{ $secretMode }} {{- end }} {{- end }} {{- end }} -{{- if .Values.config.userProvisioning }} +{{- if and (include "rstudio-workbench.sssd.active" .) (or $sssd.conf .Values.config.userProvisioning) }} - name: rstudio-user secret: secretName: {{ include "rstudio-workbench.fullname" . }}-user diff --git a/charts/rstudio-workbench/templates/configmap-general.yaml b/charts/rstudio-workbench/templates/configmap-general.yaml index ef1ef574..8eaa3bed 100644 --- a/charts/rstudio-workbench/templates/configmap-general.yaml +++ b/charts/rstudio-workbench/templates/configmap-general.yaml @@ -49,6 +49,9 @@ {{- $defaultIDEServiceName := include "rstudio-workbench.fullname" . }} {{- $defaultIDEServiceURL := printf "http://%s.%s.svc.cluster.local:80" $defaultIDEServiceName $.Release.Namespace }} {{- $defaultRServerConfigValues := dict "launcher-sessions-callback-address" ($defaultIDEServiceURL) }} +{{- if .Values.pod.serviceAccountUser }} + {{- $_ := set $defaultRServerConfigValues "server-user" .Values.pod.serviceAccountUser }} +{{- end }} {{- if and .Values.launcher.enabled .Values.components.enabled }} {{- $initTag := .Values.components.sessionInit.image.tag | default $defaultVersion }} {{- $_ := set $defaultRServerConfigValues "launcher-sessions-auto-update" 1 }} @@ -109,6 +112,36 @@ 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. + Rootless session user provisioning: when the session-init container is available + (launcher.enabled + components.enabled, which also sets launcher-sessions-auto-update=1), + default to nss mode. The session container starts directly as the requesting user and + resolves identity in-session via the pwb NSS module (backed by SCIM), so no root/useradd + step is needed. This requires create-container-user=1, create-container-user-mode=nss, and + init-container-run-user=server-user (the init container projects the NSS module + config + into the shared volume as the server user). The session image must ship libnss_pwb plus the + `pwb` nsswitch entries. Without the init container we fall back to create-container-user=0, + which stamps the pod's UID/GID directly (the runAsUserId branch in ServerJobLauncher) but + does not wire NSS. */}} +{{- if not .Values.pod.runAsRoot }} + {{- $rootlessRserver := dict "auth-pam-sessions-enabled" 0 "user-provisioning-enabled" 1 }} + {{- if and .Values.launcher.enabled .Values.components.enabled }} + {{- $_ := set $rootlessRserver "launcher-sessions-create-container-user" 1 }} + {{- $_ := set $rootlessRserver "launcher-sessions-create-container-user-mode" "nss" }} + {{- $_ := set $rootlessRserver "launcher-sessions-init-container-run-user" "server-user" }} + {{- else }} + {{- $_ := set $rootlessRserver "launcher-sessions-create-container-user" 0 }} + {{- end }} + {{- $rootlessDefaults := dict "rserver.conf" $rootlessRserver "launcher.conf" (dict "server" (dict "secure-cookie-key-file" "/mnt/secret-configmap/rstudio/secure-cookie-key")) }} + {{- $overrideDict = mergeOverwrite $rootlessDefaults $overrideDict }} +{{- else }} + {{- $overrideDict = mergeOverwrite (dict "rserver.conf" (dict "auth-pam-sessions-enabled" 1)) $overrideDict }} +{{- end }} {{ include "rstudio-library.config.ini" $overrideDict | indent 2 }} {{/* helper variables to make things here a bit more sane */}} {{- $profilesConfig := .Values.config.profiles }} diff --git a/charts/rstudio-workbench/templates/configmap-secret.yaml b/charts/rstudio-workbench/templates/configmap-secret.yaml index 7e9d885b..bdadab08 100644 --- a/charts/rstudio-workbench/templates/configmap-secret.yaml +++ b/charts/rstudio-workbench/templates/configmap-secret.yaml @@ -1,4 +1,9 @@ --- +{{- $secureCookieKeyValue := default .Values.secureCookieKey.value .Values.global.secureCookieKey.value }} +{{- $secureCookieKeyExistingSecret := default .Values.secureCookieKey.existingSecret .Values.global.secureCookieKey.existingSecret }} +{{- if and .Values.sealedSecret.enabled (not .Values.pod.runAsRoot) (empty $secureCookieKeyValue) (empty $secureCookieKeyExistingSecret) }} +{{- fail "\n\n`sealedSecret.enabled=true` with `pod.runAsRoot=false` requires an explicit secure cookie key.\n\nSet one of:\n - `global.secureCookieKey.value` (encrypted)\n - `global.secureCookieKey.existingSecret`\n - `secureCookieKey.value` (encrypted)\n - `secureCookieKey.existingSecret`\n\nThe chart cannot auto-generate `secure-cookie-key` into a SealedSecret." }} +{{- end }} {{- if .Values.sealedSecret.enabled -}} apiVersion: bitnami.com/v1alpha1 kind: SealedSecret @@ -30,9 +35,9 @@ spec: {{- $secureCookieKey := index $secureCookieKeySecret.data "secure-cookie-key" | b64dec }} {{- $secureCookieKey | nindent 6 }} {{- end }} - {{- else if or .Values.secureCookieKey.value .Values.global.secureCookieKey.value }} + {{- else if not (empty $secureCookieKeyValue) }} secure-cookie-key: | - {{- default .Values.secureCookieKey.value .Values.global.secureCookieKey.value | nindent 6 }} + {{- $secureCookieKeyValue | nindent 6 }} {{- end }} template: data: @@ -64,7 +69,10 @@ stringData: {{- end }} {{- end }} --- -{{- if .Values.config.userProvisioning }} +{{- $sssd := mergeOverwrite (dict "enabled" true "conf" (dict)) (deepCopy (default (dict) .Values.config.sssd)) }} +{{- $sssdConf := $sssd.conf }} +{{- if not $sssdConf }}{{- $sssdConf = (default (dict) .Values.config.userProvisioning) }}{{- end }} +{{- if and (include "rstudio-workbench.sssd.active" .) $sssdConf }} {{- if .Values.sealedSecret.enabled -}} apiVersion: bitnami.com/v1alpha1 kind: SealedSecret @@ -75,7 +83,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 +91,6 @@ metadata: name: {{ include "rstudio-workbench.fullname" . }}-user namespace: {{ $.Release.Namespace }} stringData: - {{- include "rstudio-library.config.ini" .Values.config.userProvisioning | nindent 2 }} + {{- include "rstudio-library.config.ini" $sssdConf | nindent 2 }} {{- end }} {{- end }} diff --git a/charts/rstudio-workbench/templates/configmap-startup.yaml b/charts/rstudio-workbench/templates/configmap-startup.yaml index 89a2c5f9..46833595 100644 --- a/charts/rstudio-workbench/templates/configmap-startup.yaml +++ b/charts/rstudio-workbench/templates/configmap-startup.yaml @@ -31,7 +31,11 @@ data: stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 {{- end }} -{{- if .Values.config.startupUserProvisioning }} +{{- $userStartup := deepCopy (default (dict) .Values.config.startupUserProvisioning) }} +{{- if include "rstudio-workbench.sssd.active" . }} +{{- $_ := set $userStartup "sssd.conf" (include "rstudio-workbench.sssd.program" .) }} +{{- end }} +{{- if $userStartup }} --- apiVersion: v1 kind: ConfigMap @@ -39,7 +43,7 @@ metadata: name: {{ include "rstudio-workbench.fullname" . }}-start-user namespace: {{ $.Release.Namespace }} data: - {{- include "rstudio-library.config.ini" .Values.config.startupUserProvisioning | nindent 2 }} + {{- include "rstudio-library.config.ini" $userStartup | nindent 2 }} {{- end }} {{- if .Values.config.startupCustom }} --- diff --git a/charts/rstudio-workbench/templates/deployment.yaml b/charts/rstudio-workbench/templates/deployment.yaml index 8869e70b..8d8bce4d 100644 --- a/charts/rstudio-workbench/templates/deployment.yaml +++ b/charts/rstudio-workbench/templates/deployment.yaml @@ -88,7 +88,31 @@ spec: topologySpreadConstraints: {{- toYaml . | nindent 8 }} {{- end }} - {{- with .Values.pod.securityContext }} + {{- $defaultPodSC := dict }} + {{- $userPodSC := default (dict) .Values.pod.securityContext }} + {{- if .Values.pod.runAsRoot }} + {{- /* Skip runAsUser: 0 when pod.securityContext already pins the user, to avoid emitting + a contradictory runAsNonRoot: true + runAsUser: 0 combination that fails admission. */ -}} + {{- if not (or (hasKey $userPodSC "runAsUser") (hasKey $userPodSC "runAsNonRoot")) }} + {{- $_ := set $defaultPodSC "runAsUser" 0 }} + {{- end }} + {{- /* The Kubernetes launcher daemon drops to serviceAccountUser (non-root) even in a root + pod, so its projected ServiceAccount token (root:root 0600) is otherwise unreadable. + fsGroup makes the token group-readable (root: 0640) to the launcher without + exposing it world-readable to end-user sessions on the pod. OnRootMismatch bounds the + one-time recursive chown of mounted volumes. Only needed when the launcher runs. */ -}} + {{- if .Values.launcher.enabled }} + {{- $_ := set $defaultPodSC "fsGroup" (int .Values.pod.serviceAccountUserId) }} + {{- $_ := set $defaultPodSC "fsGroupChangePolicy" "OnRootMismatch" }} + {{- end }} + {{- else }} + {{- $_ := set $defaultPodSC "runAsNonRoot" true }} + {{- $_ := set $defaultPodSC "runAsUser" (int .Values.pod.serviceAccountUserId) }} + {{- $_ := set $defaultPodSC "fsGroup" (int .Values.pod.serviceAccountUserId) }} + {{- $_ := set $defaultPodSC "fsGroupChangePolicy" "OnRootMismatch" }} + {{- end }} + {{- $effectivePodSC := mergeOverwrite $defaultPodSC (deepCopy $userPodSC) }} + {{- with $effectivePodSC }} securityContext: {{- toYaml . | nindent 8 }} {{- end }} diff --git a/charts/rstudio-workbench/tests/configmap_test.yaml b/charts/rstudio-workbench/tests/configmap_test.yaml index bfdc81f4..f338e5e9 100644 --- a/charts/rstudio-workbench/tests/configmap_test.yaml +++ b/charts/rstudio-workbench/tests/configmap_test.yaml @@ -256,3 +256,205 @@ tests: - notMatchRegex: path: data["positron.conf"] pattern: "^exe=" + + # -- serviceAccountUser -> rserver.conf server-user + - it: should render server-user=rstudio-server by default + template: configmap-general.yaml + documentIndex: 0 + asserts: + - matchRegex: + path: data["rserver.conf"] + pattern: "server-user=rstudio-server" + + - it: should render server-user when serviceAccountUser is overridden + template: configmap-general.yaml + documentIndex: 0 + set: + pod.serviceAccountUser: "workbench" + asserts: + - matchRegex: + path: data["rserver.conf"] + pattern: "server-user=workbench" + + - it: should omit server-user when serviceAccountUser is empty + template: configmap-general.yaml + documentIndex: 0 + set: + pod.serviceAccountUser: "" + asserts: + - notMatchRegex: + path: data["rserver.conf"] + pattern: "server-user=" + + - it: should let config.server.rserver.conf.server-user override serviceAccountUser + template: configmap-general.yaml + documentIndex: 0 + set: + pod.serviceAccountUser: "rstudio-server" + config: + server: + rserver.conf: + server-user: "explicit-override" + asserts: + - matchRegex: + path: data["rserver.conf"] + pattern: "server-user=explicit-override" + - notMatchRegex: + path: data["rserver.conf"] + pattern: "server-user=rstudio-server" + + # -- Rootless: prestart-launcher.bash has no privileged operations + - it: should not install CA certs into the system trust store in prestart-launcher.bash + template: configmap-prestart.yaml + documentIndex: 0 + asserts: + - notMatchRegex: + path: data["prestart-launcher.bash"] + pattern: "update-ca-certificates" + - notMatchRegex: + path: data["prestart-launcher.bash"] + pattern: "update-ca-trust" + - notMatchRegex: + path: data["prestart-launcher.bash"] + pattern: "/usr/local/share/ca-certificates" + + - it: should not chown launcher dirs in prestart-launcher.bash + template: configmap-prestart.yaml + documentIndex: 0 + asserts: + - notMatchRegex: + path: data["prestart-launcher.bash"] + pattern: "chown" + + - it: should keep the launcher health check and scratch dirs in prestart-launcher.bash + template: configmap-prestart.yaml + documentIndex: 0 + asserts: + - matchRegex: + path: data["prestart-launcher.bash"] + pattern: "--cacert" + - matchRegex: + path: data["prestart-launcher.bash"] + pattern: "/var/lib/rstudio-launcher/Local" + + - it: should create the Local scratch dir group-writable in prestart-launcher.bash + template: configmap-prestart.yaml + documentIndex: 0 + asserts: + # In root mode prestart runs as root but the launcher drops to serviceAccountUser; + # creating the scratch dir setgid + group-writable lets the launcher write to it + # via the rstudio-server group inherited from the parent, with no root-only chown. + - matchRegex: + path: data["prestart-launcher.bash"] + pattern: "install -d -m 2775 /var/lib/rstudio-launcher/Local" + - notMatchRegex: + path: data["prestart-launcher.bash"] + pattern: "mkdir -p" + + - it: should not create the Kubernetes scratch dir in prestart-launcher.bash + template: configmap-prestart.yaml + documentIndex: 0 + asserts: + # The Kubernetes scratch dir is an fsGroup-owned emptyDir; a non-owner chmod would EPERM, + # so prestart must not try to create or chmod it. + - notMatchRegex: + path: data["prestart-launcher.bash"] + pattern: "/var/lib/rstudio-launcher/Kubernetes" + + # -- Rootless: prestart-workbench.bash has no privileged operations + - it: should not chown or create monitor dirs in prestart-workbench.bash + template: configmap-prestart.yaml + documentIndex: 0 + asserts: + - notMatchRegex: + path: data["prestart-workbench.bash"] + pattern: "chown" + - notMatchRegex: + path: data["prestart-workbench.bash"] + pattern: "/var/lib/rstudio-server/monitor/log" + + - it: should keep launcher.pub generation and README write in prestart-workbench.bash + template: configmap-prestart.yaml + documentIndex: 0 + asserts: + - matchRegex: + path: data["prestart-workbench.bash"] + pattern: "openssl rsa" + - matchRegex: + path: data["prestart-workbench.bash"] + pattern: "/etc/rstudio/README" + + # -- runAsRoot -> rootless config defaults (nss-mode when the init container is available) + - it: should default to nss-mode session provisioning when runAsRoot is false and components are enabled + template: configmap-general.yaml + documentIndex: 0 + set: + pod.runAsRoot: false + asserts: + - matchRegex: + path: data["rserver.conf"] + pattern: "(?m)^auth-pam-sessions-enabled=0$" + - matchRegex: + path: data["rserver.conf"] + pattern: "(?m)^user-provisioning-enabled=1$" + - matchRegex: + path: data["rserver.conf"] + pattern: "(?m)^launcher-sessions-create-container-user=1$" + - matchRegex: + path: data["rserver.conf"] + pattern: "(?m)^launcher-sessions-create-container-user-mode=nss$" + - matchRegex: + path: data["rserver.conf"] + pattern: "(?m)^launcher-sessions-init-container-run-user=server-user$" + - matchRegex: + path: data["launcher.conf"] + pattern: "secure-cookie-key-file=/mnt/secret-configmap/rstudio/secure-cookie-key" + + - it: should fall back to create-container-user=0 when runAsRoot is false but components are disabled + template: configmap-general.yaml + documentIndex: 0 + set: + pod.runAsRoot: false + components.enabled: false + asserts: + - matchRegex: + path: data["rserver.conf"] + pattern: "(?m)^launcher-sessions-create-container-user=0$" + - notMatchRegex: + path: data["rserver.conf"] + pattern: "launcher-sessions-create-container-user-mode" + + - it: should keep PAM sessions on and not force provisioning when runAsRoot is true + template: configmap-general.yaml + documentIndex: 0 + asserts: + - matchRegex: + path: data["rserver.conf"] + pattern: "(?m)^auth-pam-sessions-enabled=1$" + - notMatchRegex: + path: data["rserver.conf"] + pattern: "user-provisioning-enabled" + - notMatchRegex: + path: data["rserver.conf"] + pattern: "launcher-sessions-create-container-user" + - notMatchRegex: + path: data["launcher.conf"] + pattern: "secure-cookie-key-file" + + - it: should let config.server override the rootless auth defaults + template: configmap-general.yaml + documentIndex: 0 + set: + pod.serviceAccountUser: "rstudio-server" + config: + server: + rserver.conf: + auth-pam-sessions-enabled: 1 + user-provisioning-enabled: 0 + asserts: + - matchRegex: + path: data["rserver.conf"] + pattern: "(?m)^auth-pam-sessions-enabled=1$" + - matchRegex: + path: data["rserver.conf"] + pattern: "(?m)^user-provisioning-enabled=0$" diff --git a/charts/rstudio-workbench/tests/deployment_test.yaml b/charts/rstudio-workbench/tests/deployment_test.yaml index 876e2b81..2550c2b9 100644 --- a/charts/rstudio-workbench/tests/deployment_test.yaml +++ b/charts/rstudio-workbench/tests/deployment_test.yaml @@ -141,6 +141,61 @@ tests: - equal: path: 'spec.template.spec.containers[0].env[?(@.name=="RSW_LOAD_BALANCING")].value' value: "true" + - it: should mount a writable /mnt/load-balancer volume when replicas > 1 + template: deployment.yaml + set: + replicas: 2 + asserts: + - contains: + path: 'spec.template.spec.containers[0].volumeMounts' + content: + name: "mnt-load-balancer" + mountPath: "/mnt/load-balancer" + any: true + - contains: + path: 'spec.template.spec.volumes' + content: + name: "mnt-load-balancer" + emptyDir: {} + any: true + - it: should mount a writable /mnt/load-balancer volume when loadBalancer.forceEnabled is true even if replicas = 1 + template: deployment.yaml + set: + replicas: 1 + loadBalancer: + forceEnabled: true + asserts: + - contains: + path: 'spec.template.spec.containers[0].volumeMounts' + content: + name: "mnt-load-balancer" + mountPath: "/mnt/load-balancer" + any: true + - contains: + path: 'spec.template.spec.volumes' + content: + name: "mnt-load-balancer" + emptyDir: {} + any: true + - it: should not mount the /mnt/load-balancer volume when replicas = 1 and forceEnabled is false + template: deployment.yaml + set: + replicas: 1 + loadBalancer: + forceEnabled: false + asserts: + - notContains: + path: 'spec.template.spec.containers[0].volumeMounts' + content: + name: "mnt-load-balancer" + mountPath: "/mnt/load-balancer" + any: true + - notContains: + path: 'spec.template.spec.volumes' + content: + name: "mnt-load-balancer" + emptyDir: {} + any: true - it: should specify a volumeMount and a volume for sharedStorage if sharedStorage.create is true template: deployment.yaml set: @@ -266,12 +321,15 @@ tests: path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-session-secret")]' - notExists: path: 'spec.template.spec.volumes[?(@.name=="rstudio-session-secret")]' - - it: should specify a volumeMount and a volume for userProvisioning if config.userProvisioning is defined and not empty + - it: should specify a volumeMount and a volume for sssd conf when sssd is enabled (deprecated userProvisioning fallback) template: deployment.yaml set: + pod.serviceAccountUser: root config: defaultMode: userProvisioning: 0600 + sssd: + enabled: true userProvisioning: sssd.conf: dsn: "test" @@ -317,6 +375,11 @@ tests: - equal: path: 'spec.template.spec.volumes[?(@.name=="rstudio-launcher-startup")].configMap.defaultMode' value: 0755 + - equal: + path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="launcher-kubernetes-scratch")].mountPath' + value: "/var/lib/rstudio-launcher/Kubernetes" + - exists: + path: 'spec.template.spec.volumes[?(@.name=="launcher-kubernetes-scratch")].emptyDir' - it: should not specify a volumeMount and a volume for launcher if launcher.enabled is false template: deployment.yaml set: @@ -327,6 +390,10 @@ tests: path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-launcher-startup")]' - notExists: path: 'spec.template.spec.volumes[?(@.name=="rstudio-launcher-startup")]' + - notExists: + path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="launcher-kubernetes-scratch")]' + - notExists: + path: 'spec.template.spec.volumes[?(@.name=="launcher-kubernetes-scratch")]' - it: should specify a volumeMount and a volume for startupUserProvisioning if config.startupUserProvisioning is defined and not empty template: deployment.yaml set: @@ -353,13 +420,15 @@ tests: - equal: path: 'spec.template.spec.volumes[?(@.name=="rstudio-user-startup")].configMap.defaultMode' value: 0600 - - it: should not specify a volumeMount and a volume for startupUserProvisioning if config.startupUserProvisioning is not defined + - it: should not specify a volumeMount and a volume for startupUserProvisioning if config.startupUserProvisioning is not defined and sssd is disabled template: deployment.yaml set: config: defaultMode: startup: 0600 startupUserProvisioning: null + sssd: + enabled: false asserts: - notExists: path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user-startup")]' @@ -852,3 +921,221 @@ tests: content: name: "positron-components" any: true + + # -- runAsRoot -> pod securityContext + - it: should run pod as root by default + template: deployment.yaml + asserts: + - equal: + path: spec.template.spec.securityContext.runAsUser + value: 0 + - notExists: + path: spec.template.spec.securityContext.runAsNonRoot + + - it: should run pod as non-root when runAsRoot is false + template: deployment.yaml + set: + pod.runAsRoot: false + asserts: + - equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + - equal: + path: spec.template.spec.securityContext.runAsUser + value: 999 + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 999 + + - it: should set fsGroup in root mode when launcher is enabled + template: deployment.yaml + set: + pod.runAsRoot: true + launcher.enabled: true + asserts: + - equal: + path: spec.template.spec.securityContext.runAsUser + value: 0 + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 999 + - equal: + path: spec.template.spec.securityContext.fsGroupChangePolicy + value: OnRootMismatch + + - it: should not set fsGroup in root mode when launcher is disabled + template: deployment.yaml + set: + pod.runAsRoot: true + launcher.enabled: false + asserts: + - equal: + path: spec.template.spec.securityContext.runAsUser + value: 0 + - notExists: + path: spec.template.spec.securityContext.fsGroup + - notExists: + path: spec.template.spec.securityContext.fsGroupChangePolicy + + - it: should use custom serviceAccountUserId for root-mode fsGroup + template: deployment.yaml + set: + pod.runAsRoot: true + launcher.enabled: true + pod.serviceAccountUserId: 1500 + asserts: + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 1500 + + - it: should let pod.securityContext fsGroup override the root-mode default + template: deployment.yaml + set: + pod.runAsRoot: true + launcher.enabled: true + pod: + securityContext: + fsGroup: 4242 + asserts: + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 4242 + + - it: should set fsGroupChangePolicy alongside fsGroup in rootless mode + template: deployment.yaml + set: + pod.runAsRoot: false + asserts: + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 999 + - equal: + path: spec.template.spec.securityContext.fsGroupChangePolicy + value: OnRootMismatch + + - it: should use custom uid when serviceAccountUserId is overridden + template: deployment.yaml + set: + pod.runAsRoot: false + pod.serviceAccountUserId: 1500 + asserts: + - equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + - equal: + path: spec.template.spec.securityContext.runAsUser + value: 1500 + + - it: should let pod.securityContext override the non-root defaults + template: deployment.yaml + set: + pod.runAsRoot: false + pod: + securityContext: + runAsUser: 4242 + fsGroup: 4243 + asserts: + - equal: + path: spec.template.spec.securityContext.runAsUser + value: 4242 + - equal: + path: spec.template.spec.securityContext.fsGroup + value: 4243 + - equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + + - it: should not force runAsUser 0 when root pod.securityContext sets runAsNonRoot + template: deployment.yaml + set: + pod.runAsRoot: true + pod: + securityContext: + runAsNonRoot: true + asserts: + - equal: + path: spec.template.spec.securityContext.runAsNonRoot + value: true + - notExists: + path: spec.template.spec.securityContext.runAsUser + + - it: should respect a root pod.securityContext runAsUser override + template: deployment.yaml + set: + pod.runAsRoot: true + pod: + securityContext: + runAsUser: 1234 + asserts: + - equal: + path: spec.template.spec.securityContext.runAsUser + value: 1234 + + # -- command/args: the image's supervisord.conf supports non-root operation, so the + # chart sets no command/args override and relies on the container's default CMD for + # every serviceAccountUser value. No supervisor-tmp emptyDir is needed. + - it: should not override command/args with default serviceAccountUser + template: deployment.yaml + asserts: + - notExists: + path: spec.template.spec.containers[0].command + - notExists: + path: spec.template.spec.containers[0].args + - notContains: + path: spec.template.spec.volumes + content: + name: supervisor-tmp + emptyDir: {} + - notContains: + path: spec.template.spec.containers[0].volumeMounts + content: + name: supervisor-tmp + mountPath: /tmp + + - it: should not override command/args when runAsRoot is true + template: deployment.yaml + set: + pod.runAsRoot: true + asserts: + - notExists: + path: spec.template.spec.containers[0].command + - notExists: + path: spec.template.spec.containers[0].args + + - it: should not override command/args when runAsRoot is false + template: deployment.yaml + set: + pod.runAsRoot: false + asserts: + - notExists: + path: spec.template.spec.containers[0].command + - notExists: + path: spec.template.spec.containers[0].args + + - it: should respect an explicitly-set command + template: deployment.yaml + set: + command: + - /custom/entrypoint + asserts: + - equal: + path: spec.template.spec.containers[0].command + value: + - /custom/entrypoint + - notExists: + path: spec.template.spec.containers[0].args + + - it: should respect explicitly-set args + template: deployment.yaml + set: + args: + - --foo + - bar + asserts: + - notExists: + path: spec.template.spec.containers[0].command + - equal: + path: spec.template.spec.containers[0].args + value: + - --foo + - bar diff --git a/charts/rstudio-workbench/tests/secrets_test.yaml b/charts/rstudio-workbench/tests/secrets_test.yaml index 9feb395a..d617c4db 100644 --- a/charts/rstudio-workbench/tests/secrets_test.yaml +++ b/charts/rstudio-workbench/tests/secrets_test.yaml @@ -131,6 +131,25 @@ tests: - equal: path: 'spec.template.spec.volumes[?(@.name=="rstudio-secret")].projected.sources[0].secret.items[0].path' value: 'database.conf' + - it: should mount secrets group-readable (0640) when running non-root + template: deployment.yaml + set: + pod.runAsRoot: false + config: + defaultMode: + secret: 0600 + secret: + database.conf: + provider: 'postgresql' + database: 'rsp' + port: 5432 + host: 'db.example.com' + username: 'rstudio_app' + password: 'securepassword' + asserts: + - equal: + path: 'spec.template.spec.volumes[?(@.name=="rstudio-secret")].projected.sources[0].secret.items[0].mode' + value: 0640 - it: should specify a volumeMount and a projected volume for rstudio-secret if launcherPem.existingSecret is set template: deployment.yaml set: @@ -494,3 +513,45 @@ tests: content: launcher.pem: | my-launcher-pem-value + + - it: should project the autogenerated secure-cookie-key in rootless mode by default + template: deployment.yaml + set: + pod.runAsRoot: false + asserts: + - equal: + path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-secret")].mountPath' + value: "/mnt/secret-configmap/rstudio/" + - equal: + path: 'spec.template.spec.volumes[?(@.name=="rstudio-secret")].projected.sources[0].secret.name' + value: 'RELEASE-NAME-rstudio-workbench-secret' + - equal: + path: 'spec.template.spec.volumes[?(@.name=="rstudio-secret")].projected.sources[0].secret.items[0].key' + value: 'secure-cookie-key' + - equal: + path: 'spec.template.spec.volumes[?(@.name=="rstudio-secret")].projected.sources[0].secret.items[0].path' + value: 'secure-cookie-key' + + - it: should not duplicate the secure-cookie-key source when an existing secret is set in rootless mode + template: deployment.yaml + set: + pod.runAsRoot: false + secureCookieKey: + existingSecret: "my-cookie-secret" + asserts: + - equal: + path: 'spec.template.spec.volumes[?(@.name=="rstudio-secret")].projected.sources[0].secret.name' + value: 'my-cookie-secret' + - lengthEqual: + path: 'spec.template.spec.volumes[?(@.name=="rstudio-secret")].projected.sources' + count: 1 + + - it: should not mount rstudio-secret in root mode with no configured secrets + template: deployment.yaml + set: + pod.runAsRoot: true + asserts: + - notExists: + path: 'spec.template.spec.volumes[?(@.name=="rstudio-secret")]' + - notExists: + path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-secret")]' diff --git a/charts/rstudio-workbench/tests/sssd_test.yaml b/charts/rstudio-workbench/tests/sssd_test.yaml new file mode 100644 index 00000000..328c9e6d --- /dev/null +++ b/charts/rstudio-workbench/tests/sssd_test.yaml @@ -0,0 +1,150 @@ +suite: Workbench SSSD provisioning +templates: + - configmap-startup.yaml + - configmap-secret.yaml + - configmap-general.yaml + - configmap-graphite-exporter.yaml + - configmap-prestart.yaml + - configmap-session.yaml + - deployment.yaml + - NOTES.txt +tests: + - it: starts the sssd daemon by default when running as root + template: configmap-startup.yaml + documentSelector: + path: metadata.name + value: RELEASE-NAME-rstudio-workbench-start-user + asserts: + - matchRegex: + path: data['sssd.conf'] + pattern: "\\[program:sssd\\]" + - matchRegex: + path: data['sssd.conf'] + pattern: "/usr/sbin/sssd -i -c /etc/sssd/sssd.conf" + + - it: does not start the sssd daemon when runAsRoot is false + template: configmap-startup.yaml + set: + pod.runAsRoot: false + asserts: + - notMatchRegexRaw: + pattern: "program:sssd" + + - it: does not start the sssd daemon when config.sssd.enabled is false + template: configmap-startup.yaml + set: + config.sssd.enabled: false + asserts: + - notMatchRegexRaw: + pattern: "program:sssd" + + - it: does not render the sssd conf secret by default + template: configmap-secret.yaml + asserts: + - notMatchRegexRaw: + pattern: "workbench-user" + + - it: renders successfully when config.sssd is explicitly null + template: configmap-secret.yaml + set: + config.sssd: null + asserts: + - notMatchRegexRaw: + pattern: "workbench-user" + + - it: renders the sssd conf secret when enabled with conf + template: configmap-secret.yaml + set: + config.sssd.conf: + mysssd.conf: + sssd: + config_file_version: 2 + domain/example.com: + id_provider: ldap + documentSelector: + path: metadata.name + value: RELEASE-NAME-rstudio-workbench-user + asserts: + - isKind: + of: Secret + - matchRegex: + path: stringData['mysssd.conf'] + pattern: "id_provider" + + - it: mounts sssd-user-startup by default when running as root + template: deployment.yaml + asserts: + - notExists: + path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user")]' + - equal: + path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user-startup")].mountPath' + value: "/startup/user-provisioning" + + - it: does not mount sssd directories when runAsRoot is false + template: deployment.yaml + set: + pod.runAsRoot: false + asserts: + - notExists: + path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user")]' + - notExists: + path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user-startup")]' + + - it: mounts sssd directories when running as root with sssd.conf set + template: deployment.yaml + set: + config.sssd.conf: + mysssd.conf: + sssd: + config_file_version: 2 + asserts: + - equal: + path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user")].mountPath' + value: "/etc/sssd/conf.d/" + - equal: + path: 'spec.template.spec.containers[0].volumeMounts[?(@.name=="rstudio-user-startup")].mountPath' + value: "/startup/user-provisioning" + + - it: silently skips sssd when runAsRoot is false even if sssd.enabled is true + template: NOTES.txt + set: + pod.runAsRoot: false + config.sssd.enabled: true + asserts: + - matchRegexRaw: + pattern: "successfully deployed" + + - it: renders successfully when running as root with sssd enabled + template: NOTES.txt + asserts: + - matchRegexRaw: + pattern: "successfully deployed" + + - it: warns when the deprecated config.userProvisioning is set + template: NOTES.txt + set: + config.userProvisioning: + mysssd.conf: + sssd: + config_file_version: 2 + asserts: + - matchRegexRaw: + pattern: "config.userProvisioning.*deprecated" + + - it: fails rootless + sealedSecret when secureCookieKey is not explicitly set + template: deployment.yaml + set: + sealedSecret: + enabled: true + pod: + runAsRoot: false + secureCookieKey: + value: "" + existingSecret: "" + global: + secureCookieKey: + value: "" + existingSecret: "" + asserts: + - failedTemplate: + errorPattern: "sealedSecret.enabled=true.*pod.runAsRoot=false.*requires an explicit secure cookie key" diff --git a/charts/rstudio-workbench/values.yaml b/charts/rstudio-workbench/values.yaml index c7830ff6..bccae27a 100644 --- a/charts/rstudio-workbench/values.yaml +++ b/charts/rstudio-workbench/values.yaml @@ -339,9 +339,10 @@ 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. +# @default -- the image's built-in arguments args: [] license: @@ -404,6 +405,18 @@ podDisruptionBudget: {} revisionHistoryLimit: 3 pod: + # -- Whether the pod's containers run as the root OS user. `true` (default) preserves historical + # behavior: pod runs as root (`runAsUser: 0`), SSSD starts, secrets mount 0600, PAM sessions on. + # `false` runs unprivileged: pod sets `runAsNonRoot: true` and `runAsUser`/`fsGroup` to + # `pod.serviceAccountUserId`, SSSD is skipped, secrets mount 0640, and non-root + # `rserver.conf`/`launcher.conf` defaults are applied. + runAsRoot: true + # -- The OS user written to `rserver.conf` as `server-user`. Set to `""` to omit `server-user` + # from `rserver.conf` entirely. + serviceAccountUser: "rstudio-server" + # -- The UID used as `runAsUser`/`fsGroup` in the pod's securityContext when `pod.runAsRoot` is + # false. Must match the UID of `pod.serviceAccountUser` baked into the Workbench image. + serviceAccountUserId: 999 # -- env is an array of maps that is injected as-is into the "env:" component of the pod.container spec env: [] # -- volumes is injected as-is into the "volumes:" component of the pod.container spec @@ -568,14 +581,19 @@ config: # items: # - key: custom-file.conf # path: custom-file.conf - # -- a map of sssd config files, used for user provisioning. Mounted to `/etc/sssd/conf.d/` with 0600 permissions + # -- DEPRECATED: use `config.sssd.conf` instead. A map of sssd config files, mounted to `/etc/sssd/conf.d/` with 0600 permissions. Only applied when `config.sssd.enabled=true`. userProvisioning: {} + # -- Bundled SSSD daemon for legacy LDAP/Active Directory user provisioning. On by default; automatically skipped when the pod runs unprivileged (`pod.runAsRoot: false`), since SSSD requires root. Modern provisioning (SCIM / native) does not require SSSD. + sssd: + # -- whether to start the bundled SSSD daemon. Automatically skipped (not an error) when `pod.runAsRoot` is false. Set to `false` to disable entirely. + enabled: true + # -- a map of sssd config files, mounted to `/etc/sssd/conf.d/` with 0600 permissions. Replaces the deprecated `config.userProvisioning`. + conf: {} # -- a map of server config files. Mounted to `/mnt/configmap/rstudio/` # @default -- [RStudio Workbench Configuration Reference](https://docs.rstudio.com/ide/server-pro/rstudio_server_configuration/rstudio_server_configuration.html). See defaults with `helm show values` server: rserver.conf: server-health-check-enabled: 1 - auth-pam-sessions-enabled: 1 admin-enabled: 1 www-port: 8787 server-project-sharing: 1 @@ -663,19 +681,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: {}