feat(dock_from_renv): default to non-root user 'rstudio' out of the box#106
Merged
VincentGuyader merged 1 commit intomasterfrom May 6, 2026
Merged
Conversation
dd3c238 to
e06619d
Compare
There was a problem hiding this comment.
Pull request overview
This PR updates dock_from_renv() to generate Dockerfiles that run as a non-root user by default (now user = "rstudio"), fixing the prior mismatch where a non-root USER could be combined with a root-owned RENV_PATHS_CACHE. It also adjusts Dockerfile ordering so root-required steps run before dropping privileges, and expands tests/docs accordingly.
Changes:
- Default
userchanges fromNULLto"rstudio", withuser = NULLpreserving legacy root behavior. renv_paths_cachenow defaults toNULLand is auto-derived fromuser, with a pre-USERmkdir/chown to ensure the cache mount target is writable.- Adds a defensive
useradd(guarded byid -u) and updates tests, fixtures, NEWS, and generated Rd docs.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
R/dock_from_renv.R |
Implements new defaults, cache path derivation, defensive user creation, and moves privilege drop to just before renv::restore(). |
tests/testthat/test-dock_from_renv.R |
Adds/updates tests to cover new user/cache behavior and Dockerfile ordering invariants. |
tests/testthat/renv_Dockerfile |
Regenerates the expected Dockerfile fixture to reflect new instructions/order. |
man/dock_from_renv.Rd |
Updates generated documentation for new parameter defaults and behavior. |
NEWS.md |
Documents the behavior change and opt-out for legacy root behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+157
to
+165
| if (!is.null(user)) { | ||
| dock$RUN( | ||
| sprintf( | ||
| "id -u %s >/dev/null 2>&1 || useradd -m -d /home/%s -s /bin/bash %s", | ||
| user, | ||
| user, | ||
| user | ||
| ) | ||
| ) |
Comment on lines
+328
to
+329
| dock$RUN("mkdir -p ${RENV_PATHS_CACHE}") | ||
| dock$RUN(sprintf("chown -R %s:%s ${RENV_PATHS_CACHE}", user, user)) |
Comment on lines
+89
to
+90
| #' emits a `RUN mkdir -p && chown -R <user>:<user> ${RENV_PATHS_CACHE}` | ||
| #' step right before the `USER <user>` directive so the cache mount |
Comment on lines
+458
to
+469
| test_that("dock_from_renv with user = NULL keeps the RENV_PATHS_CACHE at /root", { | ||
| skip_if(is_rdevel, "skip on R-devel") | ||
| out <- dock_from_renv( | ||
| lockfile = the_lockfile, | ||
| FROM = "rocker/verse", | ||
| user = NULL, | ||
| renv_version = "0.0.0" | ||
| ) | ||
| df <- paste(out$Dockerfile, collapse = "\n") | ||
| expect_match(df, "ARG RENV_PATHS_CACHE=/root/.cache/R/renv", fixed = TRUE) | ||
| }) | ||
|
|
Closes #100. Running the container as root is bad practice; the package's own default `FROM = "rocker/r-base"` ships an `rstudio` user precisely to make non-root operation easy. Up to now `dock_from_renv()` required the caller to pass `user = "rstudio"` explicitly, and even then the cache mount target was wrong (left at `/root/.cache/R/renv` which `rstudio` cannot write to). Three changes, applied as a unit: 1. Default `user` flips from `NULL` to `"rstudio"`. Pass `user = NULL` to opt out and keep the previous root behaviour. 2. The Dockerfile gains a defensive useradd at the top: `RUN id -u <user> >/dev/null 2>&1 || useradd -m -d /home/<user> -s /bin/bash <user>`. This is idempotent: a no-op on rocker/* images where the user already exists, a real `useradd` on `r-base`, `ubuntu:*`, `debian:*`. debian/ubuntu only. 3. `renv_paths_cache` is auto-derived from `user` when not supplied: `/home/<user>/.cache/R/renv` for non-NULL user, `/root/.cache/R/renv` for `user = NULL`. The Dockerfile emits an explicit `RUN mkdir -p && chown -R <user>:<user> ${RENV_PATHS_CACHE}` step right before the `USER <user>` directive, so the `--mount=type=cache,target=${RENV_PATHS_CACHE}` later is writable. The `USER` directive is moved from immediately after the ARG/ENV setup (where it broke `apt-get` because apt-get needs root) to right before the `renv::restore()` cache-mount RUN. All the install steps run as root; only `renv::restore()` and the runtime container run as `<user>`. Test fixture `tests/testthat/renv_Dockerfile` regenerated to match the new default output. Five new tests in `test-dock_from_renv.R` cover the new contract: - user = NULL: no USER, no chown, no useradd, cache at /root. - default (user = "rstudio"): cache at /home/rstudio. - ordering invariant: apt-get -> chown -> USER -> renv::restore. - explicit renv_paths_cache: honored, chown applied to it. - defensive useradd: emitted at the top, before any install RUN. R CMD check --as-cran pending (run after push). devtools::test(): [ FAIL 0 | WARN 3 | SKIP 0 | PASS 253 ].
e06619d to
891b75c
Compare
Comment on lines
+160
to
+166
| if (is.null(renv_paths_cache)) { | ||
| renv_paths_cache <- if (is.null(user)) { | ||
| "/root/.cache/R/renv" | ||
| } else { | ||
| sprintf("/home/%s/.cache/R/renv", user) | ||
| } | ||
| } |
Comment on lines
+356
to
+363
| dock$RUN( | ||
| sprintf( | ||
| 'mkdir -p "${RENV_PATHS_CACHE}" && chown -R %s:%s "${RENV_PATHS_CACHE}"', | ||
| user, | ||
| user | ||
| ) | ||
| ) | ||
| dock$USER(user) |
| with the existing `renv_version = NULL` behaviour. Closes #94. | ||
| - `dock_from_renv()` now defaults to running the runtime container as | ||
| the `rstudio` user (previously root). The generated Dockerfile gains | ||
| a defensive `RUN id -u rstudio || useradd -m -d /home/rstudio -s /bin/bash rstudio` |
3 tasks
VincentGuyader
added a commit
that referenced
this pull request
May 6, 2026
The renv_paths_cache bullet still described the pre-#106 behaviour (default = "/root/.cache/R/renv"). After #106, the signature default is NULL with auto-derivation from `user`: /root for user = NULL, /home/<user> otherwise. Update the bullet to reflect the actual runtime, closing the doc-runtime drift introduced by the merge. No code change. No NAMESPACE / Rd impact.
5 tasks
VincentGuyader
added a commit
that referenced
this pull request
May 6, 2026
…text Closes the post-#106 security audit follow-up. Adds 9 .validate_* helpers in R/utils.R (@nord) that reject inputs which would otherwise be interpolated raw into the generated Dockerfile's shell commands. Each helper raises stop() with a clear message naming the offending parameter. Validators added: - .validate_FROM (image reference: `^[a-zA-Z0-9][a-zA-Z0-9._/-]*(:tag)?(@sha256:hex)?$`) - .validate_AS (build-stage name: `^[a-zA-Z0-9][a-zA-Z0-9._-]*$`) - .validate_repos (https URLs + simple-identifier names; closes the `dput()`-backtick injection vector on names) - .validate_extra_sysreqs (Debian package name: `^[a-z0-9][a-z0-9.+-]+$`) - .validate_renv_version (semver-like, NULL allowed) - .validate_lockfile (basename: alphanumerics + `._-`) - .validate_renv_paths_cache (absolute path, no metacharacters) - .validate_r_version (defensive depth on lockfile-derived value) - .validate_scalar_logical (factored out from strict_install; used for use_pak too, closing a real injection vector via `echo "options(renv.config.pak.enabled = %s, ...)"`) dock_from_desc() and dock_from_renv() call the relevant validators at function entry, just after match.arg(github_pat). The pr-reviewer agent flagged two additional vectors I missed in the first pass: use_pak (B1, blocked) and names(repos) (B2, blocked via the dput() backtick path). Both are closed by this commit. Tests: 9 new expect_error() blocks across test-dock_from_desc.R and test-dock_from_renv.R, each TDD red-against-buggy. One existing test patched: renv_version = "banana" -> "1.2.3" since the new validator rejects non-semver strings (the test was exercising the install_version code path, which "1.2.3" still covers). devtools::test() -> [ FAIL 0 | WARN 3 | SKIP 0 | PASS 302 ]. R CMD check --as-cran: 0 errors / 0 warnings / 1 note (the "unable to verify current time" host note, unrelated). Out of scope for this PR (per the threat model agreed with the maintainer): - R6 helpers in R/add.R (dock$RUN, dock$COPY, ...) are author-controlled API; if the author writes a malicious RUN it is self-foot-shooting, not injection. - distro is deprecated and ignored at runtime. - github_pat is match.arg'd against a fixed enum. NEWS.md gains a `## Security` section above `## New features`.
VincentGuyader
added a commit
that referenced
this pull request
May 6, 2026
…text Closes the post-#106 security audit follow-up. Adds 9 .validate_* helpers in R/utils.R (@nord) that reject inputs which would otherwise be interpolated raw into the generated Dockerfile's shell commands. Each helper raises stop() with a clear message naming the offending parameter. Validators added: - .validate_FROM (image reference: `^[a-zA-Z0-9][a-zA-Z0-9._/-]*(:tag)?(@sha256:hex)?$`) - .validate_AS (build-stage name: `^[a-zA-Z0-9][a-zA-Z0-9._-]*$`) - .validate_repos (https URLs + simple-identifier names; closes the `dput()`-backtick injection vector on names) - .validate_extra_sysreqs (Debian package name: `^[a-z0-9][a-z0-9.+-]+$`) - .validate_renv_version (semver-like, NULL allowed) - .validate_lockfile (basename: alphanumerics + `._-`) - .validate_renv_paths_cache (absolute path, no metacharacters) - .validate_r_version (defensive depth on lockfile-derived value) - .validate_scalar_logical (factored out from strict_install; used for use_pak too, closing a real injection vector via `echo "options(renv.config.pak.enabled = %s, ...)"`) dock_from_desc() and dock_from_renv() call the relevant validators at function entry, just after match.arg(github_pat). The pr-reviewer agent flagged two additional vectors I missed in the first pass: use_pak (B1, blocked) and names(repos) (B2, blocked via the dput() backtick path). Both are closed by this commit. Tests: 9 new expect_error() blocks across test-dock_from_desc.R and test-dock_from_renv.R, each TDD red-against-buggy. One existing test patched: renv_version = "banana" -> "1.2.3" since the new validator rejects non-semver strings (the test was exercising the install_version code path, which "1.2.3" still covers). devtools::test() -> [ FAIL 0 | WARN 3 | SKIP 0 | PASS 302 ]. R CMD check --as-cran: 0 errors / 0 warnings / 1 note (the "unable to verify current time" host note, unrelated). Out of scope for this PR (per the threat model agreed with the maintainer): - R6 helpers in R/add.R (dock$RUN, dock$COPY, ...) are author-controlled API; if the author writes a malicious RUN it is self-foot-shooting, not injection. - distro is deprecated and ignored at runtime. - github_pat is match.arg'd against a fixed enum. NEWS.md gains a `## Security` section above `## New features`.
VincentGuyader
added a commit
that referenced
this pull request
May 6, 2026
…text Closes the post-#106 security audit follow-up. Adds 9 .validate_* helpers in R/utils.R (@nord) that reject inputs which would otherwise be interpolated raw into the generated Dockerfile's shell commands. Each helper raises stop() with a clear message naming the offending parameter. Validators added: - .validate_FROM (image reference: `^[a-zA-Z0-9][a-zA-Z0-9._/-]*(:tag)?(@sha256:hex)?$`) - .validate_AS (build-stage name: `^[a-zA-Z0-9][a-zA-Z0-9._-]*$`) - .validate_repos (https URLs + simple-identifier names; closes the `dput()`-backtick injection vector on names) - .validate_extra_sysreqs (Debian package name: `^[a-z0-9][a-z0-9.+-]+$`) - .validate_renv_version (semver-like, NULL allowed) - .validate_lockfile (basename: alphanumerics + `._-`) - .validate_renv_paths_cache (absolute path, no metacharacters) - .validate_r_version (defensive depth on lockfile-derived value) - .validate_scalar_logical (factored out from strict_install; used for use_pak too, closing a real injection vector via `echo "options(renv.config.pak.enabled = %s, ...)"`) dock_from_desc() and dock_from_renv() call the relevant validators at function entry, just after match.arg(github_pat). The pr-reviewer agent flagged two additional vectors I missed in the first pass: use_pak (B1, blocked) and names(repos) (B2, blocked via the dput() backtick path). Both are closed by this commit. Tests: 9 new expect_error() blocks across test-dock_from_desc.R and test-dock_from_renv.R, each TDD red-against-buggy. One existing test patched: renv_version = "banana" -> "1.2.3" since the new validator rejects non-semver strings (the test was exercising the install_version code path, which "1.2.3" still covers). devtools::test() -> [ FAIL 0 | WARN 3 | SKIP 0 | PASS 302 ]. R CMD check --as-cran: 0 errors / 0 warnings / 1 note (the "unable to verify current time" host note, unrelated). Out of scope for this PR (per the threat model agreed with the maintainer): - R6 helpers in R/add.R (dock$RUN, dock$COPY, ...) are author-controlled API; if the author writes a malicious RUN it is self-foot-shooting, not injection. - distro is deprecated and ignored at runtime. - github_pat is match.arg'd against a fixed enum. NEWS.md gains a `## Security` section above `## New features`.
VincentGuyader
added a commit
that referenced
this pull request
May 6, 2026
…text (#109) * docs(NEWS): align renv_paths_cache bullet with post-#106 default The renv_paths_cache bullet still described the pre-#106 behaviour (default = "/root/.cache/R/renv"). After #106, the signature default is NULL with auto-derivation from `user`: /root for user = NULL, /home/<user> otherwise. Update the bullet to reflect the actual runtime, closing the doc-runtime drift introduced by the merge. No code change. No NAMESPACE / Rd impact. * feat(security): validate user-supplied strings flowing into shell context Closes the post-#106 security audit follow-up. Adds 9 .validate_* helpers in R/utils.R (@nord) that reject inputs which would otherwise be interpolated raw into the generated Dockerfile's shell commands. Each helper raises stop() with a clear message naming the offending parameter. Validators added: - .validate_FROM (image reference: `^[a-zA-Z0-9][a-zA-Z0-9._/-]*(:tag)?(@sha256:hex)?$`) - .validate_AS (build-stage name: `^[a-zA-Z0-9][a-zA-Z0-9._-]*$`) - .validate_repos (https URLs + simple-identifier names; closes the `dput()`-backtick injection vector on names) - .validate_extra_sysreqs (Debian package name: `^[a-z0-9][a-z0-9.+-]+$`) - .validate_renv_version (semver-like, NULL allowed) - .validate_lockfile (basename: alphanumerics + `._-`) - .validate_renv_paths_cache (absolute path, no metacharacters) - .validate_r_version (defensive depth on lockfile-derived value) - .validate_scalar_logical (factored out from strict_install; used for use_pak too, closing a real injection vector via `echo "options(renv.config.pak.enabled = %s, ...)"`) dock_from_desc() and dock_from_renv() call the relevant validators at function entry, just after match.arg(github_pat). The pr-reviewer agent flagged two additional vectors I missed in the first pass: use_pak (B1, blocked) and names(repos) (B2, blocked via the dput() backtick path). Both are closed by this commit. Tests: 9 new expect_error() blocks across test-dock_from_desc.R and test-dock_from_renv.R, each TDD red-against-buggy. One existing test patched: renv_version = "banana" -> "1.2.3" since the new validator rejects non-semver strings (the test was exercising the install_version code path, which "1.2.3" still covers). devtools::test() -> [ FAIL 0 | WARN 3 | SKIP 0 | PASS 302 ]. R CMD check --as-cran: 0 errors / 0 warnings / 1 note (the "unable to verify current time" host note, unrelated). Out of scope for this PR (per the threat model agreed with the maintainer): - R6 helpers in R/add.R (dock$RUN, dock$COPY, ...) are author-controlled API; if the author writes a malicious RUN it is self-foot-shooting, not injection. - distro is deprecated and ignored at runtime. - github_pat is match.arg'd against a fixed enum. NEWS.md gains a `## Security` section above `## New features`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #100.
Running the container as root is bad practice; the package's own
default
FROM = "rocker/r-base"ships anrstudiouser preciselyto make non-root operation easy. Up to now
dock_from_renv()required the caller to pass
user = "rstudio"explicitly, and eventhen the cache mount target was wrong (left at
/root/.cache/R/renvwhich
rstudiocannot write to).This PR makes
user = "rstudio"the default and ensures thegenerated Dockerfile actually works for any non-root user passed.
Three changes, applied as a unit
1. Default user flips from
NULLto"rstudio"The runtime container now drops privilege to
rstudioby default.Pass
user = NULLto opt out and keep the previous root behaviour.2. Defensive
useraddat the top of the DockerfileRUN id -u <user> >/dev/null 2>&1 || useradd -m -d /home/<user> -s /bin/bash <user>Idempotent: a no-op on rocker/* images where the user already
exists, a real
useraddonr-base,ubuntu:*,debian:*. Worksfor any user (
rstudio,kevin,myapp, ...) on debian/ubuntu.For alpine-based images, pass
user = NULLand create the useryourself (alpine uses
adduser, notuseradd).3. Auto-derive
renv_paths_cacheand chown it before USERrenv_paths_cacheis auto-derived fromuserwhen not supplied:user = NULL->/root/.cache/R/renv(legacy).user = "rstudio"->/home/rstudio/.cache/R/renv(new default).user = "kevin"->/home/kevin/.cache/R/renv.The Dockerfile emits an explicit
RUN mkdir -p && chown -R <user>:<user> ${RENV_PATHS_CACHE}step right before the
USER <user>directive, so the--mount=type=cache,target=${RENV_PATHS_CACHE}later is writable.The
USERdirective is moved from immediately after the ARG/ENVsetup (where it broke
apt-getbecause apt-get needs root) toright before the
renv::restore()cache-mount RUN. All installsteps run as root; only
renv::restore()and the runtime containerrun as
<user>.Behaviour change
For users regenerating their Dockerfile without specifying
user:Test plan
dock_from_renv worksupdated; fixturetests/testthat/renv_Dockerfileregenerated.devtools::test()-> [ FAIL 0 | WARN 3 | SKIP 0 | PASS 253 ].R CMD check --as-cran-> 0 errors / 0 warnings / 0 notes(2m 45s).
Context
Tier A item from the post-CRAN-prep roadmap. Companion to #105
(strict_install for dock_from_desc).