Repository navigation
Introduced redis session handler does not work with non-root images #763
Description
Activity
- added 2 commits that reference this issue
on May 30, 2019 Here is a command to reproduce this behavior:
» docker run --rm -it --user www-data nextcloud:fpm-alpine sh /var/www/html $ NEXTCLOUD_ADMIN_USER=admin NEXTCLOUD_ADMIN_PASSWORD=admin NEXTCLOUD_UPDATE=1 REDIS_HOST=localhost /entrypoint.sh sh -c 'echo $?' Configuring Redis as session handler /entrypoint.sh: line 26: can't create /usr/local/etc/php/conf.d/redis-session.ini: Permission denied Initializing nextcloud 16.0.1.1 ... Initializing finished New nextcloud instance running web-based installer on first connect!- changed the title
[-]Introduced redis session handler breaks entrypoint of non-root images[/-][+]Introduced redis session handler does not work with non-root images[/+]on May 30, 2019 Any workarounds? I have this problem too
Thanks for reporting this. I can reproduce the issue, but I'm not very happy with the proposed permission change in #764.
However, this should not break your NextCloud, you just have no Redis session handler. As a workaround, you can mount aredis-session.inimanually.I know changing the permissions is kind of a security issue. But running the image as root is even more.
What definitely does not work:
- Writing configuration in
.user.inifile. According to PHP memory limit to 128 ? #447 (comment), this does not work for CLI mode.
Here are some options that I figured out:
- Change ownership of
/usr/local/etc/php/conf.d/to userwww-data. The entrypoint createsredis-session.ini. Results in security concerns. - Create placeholder file
/usr/local/etc/php/conf.d/redis-session.iniin the Dockerfile with no content. Change ownership of only/usr/local/etc/php/conf.d/redis-session.inito userwww-data. Content can be written in the entrypoint. More secure than 1. but if redis is not used, there is an empty file. - Create file
/usr/local/etc/php/conf.d/session.iniin the Dockerfile with defaults from default config/usr/local/etc/php/php.ini-production. Change ownership of only/usr/local/etc/php/conf.d/session.inito userwww-data. Content can be replaced withsedin the entrypoint. Security equal to 2. but duplicate config for session. - Only write
/usr/local/etc/php/conf.d/redis-session.iniwhen running as root, otherwise log a warning that the file must be mounted manually instead of throwing the error like described above. No security concerns.
Currently I think version 3 is the best trade-off. @J0WI what do you think?
- Writing configuration in
Why would you prefer 3 over 2?
4 would also be an option for me, this is basically what happens now.@tilosp what is your opinion?
@J0WI I prefer 3 over 2 because if redis host is not set, there is a file called
redis-session.iniwhich might irritate users.
Best practices of writing Docker images saysIf a service can run without privileges, use USER to change to a non-root user.
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#userAll images in this project should run as non-root by default. Having this in mind, 4 is not an option. I will open a new PR that sets non-root users as default for all used images and examples. Not choosing 4 is a precondition for that.
1 and 2 are not really an option because it wouldn't work for users other than root and www-data. Instead we would need to chmod 777 them.
It there any difference between 1, 2 and 3 in terms of security? In each case php can modify it's own config.
All images in this project should run as non-root by default. Having this in mind, 4 is not an option. I will open a new PR that sets non-root users as default for all used images and examples. Not choosing 4 is a precondition for that.
The web server already runs as a non-root user (www-data) by default.
And i don't see how addingUSER www-datato the Dockerfile will improve the security. If you don't trust the container to run apache as www-data you also can't trust the default user to be www-data.It there any difference between 1, 2 and 3 in terms of security?
Not it terms of security, but I'd avoid changing upstream files.
The web server already runs as a non-root user (www-data) by default.
I can't verify that, have a look at the commands:
- FPM
» docker run --rm -it --name nextcloud -d nextcloud:fpm-alpine b24abcf965ff7b7027bfa2d6e7295898e368ea9aba2a56332c301a3b1a7ad267 » docker exec -i -t nextcloud ps aux PID USER TIME COMMAND 1 root 0:00 php-fpm: master process (/usr/local/etc/php-fpm.conf) 35 www-data 0:00 php-fpm: pool www 36 www-data 0:00 php-fpm: pool www 37 root 0:00 ps aux- Apache
» docker run --rm -it --name nextcloud -d nextcloud 7f64940a1f4db036e821432d4a53ba937a32ae5a25093a94ad09073348f29e16 » docker exec -i -t nextcloud ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 3.8 0.4 496432 35640 pts/0 Ss+ 20:07 0:00 apache2 -DFOREG www-data 45 0.0 0.1 496464 9380 pts/0 S+ 20:07 0:00 apache2 -DFOREG www-data 46 0.0 0.1 496464 9380 pts/0 S+ 20:07 0:00 apache2 -DFOREG www-data 47 0.0 0.1 496464 9380 pts/0 S+ 20:07 0:00 apache2 -DFOREG www-data 48 0.0 0.1 496464 9380 pts/0 S+ 20:07 0:00 apache2 -DFOREG www-data 49 0.0 0.1 496464 9380 pts/0 S+ 20:07 0:00 apache2 -DFOREG root 50 22.0 0.0 36632 2804 pts/1 Rs+ 20:07 0:00 ps auxThe PID 1 process always runs as root by using the defaults.
And i don't see how adding USER www-data to the Dockerfile will improve the security. If you don't trust the container to run apache as www-data you also can't trust the default user to be www-data.
Have a look at the following blog post. There is a good explanation, why running images as root is not a good practice:
Containers that run as root frequently have far more permissions than their workload requires which, in case of compromise, could help an attacker further their attack.
https://kubernetes.io/blog/2018/07/18/11-ways-not-to-get-hacked/#8-run-containers-as-a-non-root-user.If the
USERdirective is set towww-datathe attack vector is minimized when running with defaults. And if anyone wants to run the image as root this is still possible by manually changing the user.1 and 2 are not really an option because it wouldn't work for users other than root and www-data. Instead we would need to chmod 777 them.
It makes no sense to use another user than
rootorwww-datato run fpm or apache, other users do not have sufficient permissions.How should we go an with this issue?
I can't verify that, have a look at the commands:
Yes the entrypoint scipt and apache/fpm master process run as root by default. I meant that the processes that handle the requests and run the php code are run by a different user.
If the USER directive is set to www-data the attack vector is minimized when running with defaults. And if anyone wants to run the image as root this is still possible by manually changing the user.
Yes it will reduce the attack surface in case there is a bug in apache/fpm that allows code execution in the master process. But it won't help in the case of a malicious docker image. To protect against a malicious docker image you would need to override to default user for example in the docker-compose file.
Something like this should be done upstream in the php base image, then all php based images get the benefit.
It makes no sense to use another user than root or www-data to run fpm or apache, other users do not have sufficient permissions.
This is valid use case and it is supported by the upstream image. It should work, have you tried it? If there is a permission problem it is a bug.
I was not aware of that in docker-library/php#787 the permissions of folder
/var/www/htmlwere set to777. So yes, every user can be used to run the php-fpm image.I guess the upstream images will not set the user directive in the mid-term because that will break too many existing child images. So in my opinion, this should be treated proactive from the application side.
After the discussions above, how should we follow?
In the meantime, I am just happy if the entrypoint does not throw an exception anymore. And all following changes of this project should work user independent so that updating the image is always possible without manual testing in a sandbox environment.23 remaining items
I work around this by mounting
redis-session.inimanually as a volume, i.e.touch ./nextcloud/redis/redis-session.iniand then add this flag to podman/docker on the nextcloud container:--volume ./nextcloud/redis/redis-session.ini:/usr/local/etc/php/conf.d/redis-session.ini:zHaving the exact same problem. Thank you for the fix. Nextcloud should up their game on how to correctly use docker and providing images. The whole upgrade process with nextcloud is also horrendous. If there would be a viable alternative I would gladly take it.
OT: You may want to check out https://github.com/LorbusChris/nextcloud-quadlet for an alternative way of running a rootless setup. The stack is a bit different, using Valkey instead of Redis, and Envoy as proxy. I've been running this setup for ~4 months with automatic upgrades and without any issues.
Reacted by Jesse Hitch and JL Eulerwill this ever be fixed?
Unlikely, due the reason mentioned in #763 (comment)
Making the server config world writeable would make the image less secure for everyone. Mounting a custom config is the way to go.Hello 👋
For people running a docker Nextcloud instance with a read-only root filesystem and a non-root user, I have found a workaround to this issue.
PHP allows you to change the directories in which it is going to look for ini configurations using the environment
PHP_INI_SCAN_DIRvariable.
So you can launch the container withPHP_INI_SCAN_DIR="/tmp/php_conf_d:"assuming you have mounted a tmpfs in/tmp(note that the trailing:tells PHP to append the specified directory to the existing set of PHP ini scan directories).
Then you can mount a custom script in/docker-entrypoint-hooks.d/before-starting/redis_config_generator.shwhich could look something like this:#!/usr/bin/env bash set -euo pipefail install -D --mode=600 <(cat <<EOF session.save_handler = redis session.save_path = "tcp://${HOOK_REDIS_HOST}:6379?auth=${HOOK_REDIS_HOST_PASSWORD}" redis.session.locking_enabled = 1 redis.session.lock_retries = -1 redis.session.lock_wait_time = 10000 EOF ) "/tmp/php_conf_d/redis-session.ini"
(Note that the redis environment variables are prefixed with
HOOK_to avoid Nextcloud attempting to generate its own configuration on top of yours using theREDIS_environment variables)This should enable you to have a custom Redis configuration readable by your custom
--userand written to a writable location on the filesystem.Reacted by Mohammad Javad NaderiHi, I have the same issue in a Kubernetes environment, using Nextcloud helm chart, in OpenShift with restrictive non root policy.
This is a workaround that works!! I couldn't mount an emptyDir directly to the
/usr/local/etc/php/conf.d, because that would have overriden the existing file. So I created an emptyDir, and initContainer to create en empty file, what a workaround!!!nextcloud: extraInitContainers: # create empty redis file for nextcloud entrypoint to override it - name: init-redis-session-ini image: busybox command: ['touch', '/usr/local/etc/php/conf.d/redis-session.ini'] volumeMounts: - name: nextcloud-redis-session-ini mountPath: "/usr/local/etc/php/conf.d" extraVolumes: - name: nextcloud-redis-session-ini emptyDir: {} extraVolumeMounts: - name: nextcloud-redis-session-ini mountPath: "/usr/local/etc/php/conf.d/redis-session.ini" # fix permission denied error subPath: redis-session.iniReacted by Aetylus, Teo Mrnjavac , Andrzej Kornaszewski, Dennis Zhang and Gábor PichnerThank you, that helped me fix my issue with the Helm chart as well. Just a note for anyone else that finds this issue trying to run as non-root along with the cronjob container, you have to set the
securityContexton theinit-redis-session-iniinitContainer.podSecurityContextwon't work because the cronjob container must run as root to work properly.There is an open pull request (nextcloud/helm#703) to use a k8s CronJob resource instead which would solve this problem, in which case
podSecurityContextshould work if it's accepted.For information, this is the PullRequest I created that fixed this issue: nextcloud/helm#717 . One day it will be merged!
If I'm understanding nextcloud/helm#752 correctly, then this could currently be worked around by adding to the values:
nextcloud: extraVolumes: - name: php-confd emptyDir: {} - name: tmpfs emptyDir: {} extraVolumeMounts: - name: php-confd mountPath: "/usr/local/etc/php/conf.d/redis-session.ini" subPath: redis-session.ini - name: tmpfs mountPath: "/tmp" extraInitContainers: - name: init-redis-session-ini image: mirror.gcr.io/alpine:latest command: ['touch', '/usr/local/etc/php/conf.d/redis-session.ini'] volumeMounts: - name: php-confd mountPath: "/usr/local/etc/php/conf.d"
This seems to work for me, but my testing has been limited so far.
(But my values file is already about 300 lines long and hard to maintain, so I'd prefer to get the above-mentioned fix merged)
Edit: a writable '/tmp' was also needed.
Here's a thought:
- Have the entrypoint script compute the complex Redis session save path (as it basically does currently) but instead of writing it out to an INI file, we export it as an environment variable.
- Have our php.ini (or an included ini file - in our case probably
nextcloud.ini) reference the environment variable(s) directly. **We already do this forPHP_MEMORY_LIMITthough it doesn't have any entrypoint logic since it's much simpler).
Specifically:
- we drop
redis-session.inifile entirely (no write required in the entrypoint) - instead of having the entrypoint's Redis config logic create the file, we have it build the appropriate
session.save_pathvalue just before launching apache/fpm and export it into an environment variable (e.g.PHP_REDIS_SESSION_SAVE_PATH). Also include others likePHP_REDIS_SESSION_HANDLER = "redis"and the locking settings - In
nextcloud.iniwe reference the environment variables just like we do forPHP_MEMORY_LIMIT/etc.
This should work.
Refs:
- Current
REDIS*config handling:Lines 115 to 148 in 0af85f2
if [ -n "${REDIS_HOST+x}" ]; then echo "Configuring Redis as session handler" { file_env REDIS_HOST_PASSWORD echo 'session.save_handler = redis' # check if redis host is an unix socket path if [ "$(echo "$REDIS_HOST" | cut -c1-1)" = "/" ]; then if [ -n "${REDIS_HOST_PASSWORD+x}" ]; then if [ -n "${REDIS_HOST_USER+x}" ]; then echo "session.save_path = \"unix://${REDIS_HOST}?auth[]=${REDIS_HOST_USER}&auth[]=${REDIS_HOST_PASSWORD}\"" else echo "session.save_path = \"unix://${REDIS_HOST}?auth=${REDIS_HOST_PASSWORD}\"" fi else echo "session.save_path = \"unix://${REDIS_HOST}\"" fi # check if redis password has been set elif [ -n "${REDIS_HOST_PASSWORD+x}" ]; then if [ -n "${REDIS_HOST_USER+x}" ]; then echo "session.save_path = \"tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}?auth[]=${REDIS_HOST_USER}&auth[]=${REDIS_HOST_PASSWORD}\"" else echo "session.save_path = \"tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}?auth=${REDIS_HOST_PASSWORD}\"" fi else echo "session.save_path = \"tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}\"" fi echo "redis.session.locking_enabled = 1" echo "redis.session.lock_retries = -1" # redis.session.lock_wait_time is specified in microseconds. # Wait 10ms before retrying the lock rather than the default 2ms. echo "redis.session.lock_wait_time = 10000" } > /usr/local/etc/php/conf.d/redis-session.ini fi - Current PHP.ini handling w/ environment variable substitution (used for
PHP_MEMORY_LIMIT):docker/Dockerfile-debian.template
Lines 99 to 123 in 0af85f2
# set recommended PHP.ini settings # see https://docs.nextcloud.com/server/latest/admin_manual/installation/server_tuning.html#enable-php-opcache RUN { \ echo 'opcache.enable=1'; \ echo 'opcache.interned_strings_buffer=32'; \ echo 'opcache.max_accelerated_files=10000'; \ echo 'opcache.memory_consumption=${PHP_OPCACHE_MEMORY_CONSUMPTION}'; \ echo 'opcache.save_comments=1'; \ echo 'opcache.revalidate_freq=60'; \ echo 'opcache.jit=1255'; \ echo 'opcache.jit_buffer_size=8M'; \ } > "${PHP_INI_DIR}/conf.d/opcache-recommended.ini"; \ \ echo 'apc.enable_cli=1' >> "${PHP_INI_DIR}/conf.d/docker-php-ext-apcu.ini"; \ \ { \ echo 'apc.serializer=igbinary'; \ echo 'session.serialize_handler=igbinary'; \ } >> "${PHP_INI_DIR}/conf.d/docker-php-ext-igbinary.ini"; \ \ { \ echo 'memory_limit=${PHP_MEMORY_LIMIT}'; \ echo 'upload_max_filesize=${PHP_UPLOAD_LIMIT}'; \ echo 'post_max_size=${PHP_UPLOAD_LIMIT}'; \ } > "${PHP_INI_DIR}/conf.d/nextcloud.ini"; \ - Official PHP docs on
php.inienvironment variable support: https://www.php.net/manual/en/configuration.file.php#configuration.file
Proposed (untested):
- Add this logic to our entrypoint before PHP or the webserver runs:
if [ -n "${REDIS_HOST+x}" ]; then # shell function to load REDIS_HOST_PASSWORD from file, if using Docker secrets file_env REDIS_HOST_PASSWORD if [ "$(echo "$REDIS_HOST" | cut -c1-1)" = "/" ]; then # Unix socket path if [ -n "${REDIS_HOST_PASSWORD+x}" ]; then if [ -n "${REDIS_HOST_USER+x}" ]; then FINAL_SAVE_PATH="unix://${REDIS_HOST}?auth[]=${REDIS_HOST_USER}&auth[]=${REDIS_HOST_PASSWORD}" else FINAL_SAVE_PATH="unix://${REDIS_HOST}?auth=${REDIS_HOST_PASSWORD}" fi else FINAL_SAVE_PATH="unix://${REDIS_HOST}" fi elif [ -n "${REDIS_HOST_PASSWORD+x}" ]; then # TCP with password if [ -n "${REDIS_HOST_USER+x}" ]; then FINAL_SAVE_PATH="tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}?auth[]=${REDIS_HOST_USER}&auth[]=${REDIS_HOST_PASSWORD}" else FINAL_SAVE_PATH="tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}?auth=${REDIS_HOST_PASSWORD}" fi else # TCP without password FINAL_SAVE_PATH="tcp://${REDIS_HOST}:${REDIS_HOST_PORT:=6379}" fi # Export as environment variable for PHP-FPM/apache export PHP_REDIS_SESSION_SAVE_PATH="$FINAL_SAVE_PATH" fi # Optionally also set other session handler options export PHP_REDIS_SESSION_HANDLER="redis" export PHP_REDIS_SESSION_LOCKING_ENABLED=1 export PHP_REDIS_SESSION_LOCK_RETRIES=-1 export PHP_REDIS_SESSION_LOCK_WAIT_TIME=10000 # exec the CMD as usual at the end of your entrypoint script: exec "$@"- Make our
nextcloud.inireference the above environment variables (i.e. add these to ourDockerfile-*.templatefiles:
; Use Redis for PHP session handler if configured session.save_handler = "${PHP_REDIS_SESSION_HANDLER}" session.save_path = "${PHP_REDIS_SESSION_SAVE_PATH}" redis.session.locking_enabled = ${PHP_REDIS_SESSION_LOCKING_ENABLED} redis.session.lock_retries = ${PHP_REDIS_SESSION_LOCK_RETRIES} redis.session.lock_wait_time = ${PHP_REDIS_SESSION_LOCK_WAIT_TIME}In PHP 8.3+. we can even add clear fallbacks per PHP docs (though may be unnecessary since defaults are likely fine -- or we could handle it in our entrypoint logic too). Something like this I believe:
session.save_handler = "${PHP_REDIS_SESSION_HANDLER:-'files'}"Reacted by Joda Stößer- added a commit that references this issue
on Mar 23, 2026 Early prototype of approach that eliminates entrypoint needing to modify redis-session.ini file is in draft PR #2550. Needs testing.
Reacted by guillaumedsde, Michael Wadley, Leandro Guedes, Axel Le Bot, Steven, Philipp Grathwohl, amo13 and Hrafn Þorvaldsson
In commit 83ea69d, a redis session handler was intrododuced. But the file
/usr/local/etc/php/conf.d/redis-session.inican not be written when using a non-root image, because the folder is owned by root and the defaultwww-datauser does not have write permission.For running my nextcloud image, I use the default non-privileged user
www-data.