symfony-docker
symfony-docker copied to clipboard
Run container as an unprivileged user
Hi,
Thx for this template, very useful ! :pray:
Many resources suggest using an unprivileged user in container in order to prevent privilege escalation attacks(e.g. OWASP https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#rule-2-set-a-user or Docker docs https://docs.docker.com/build/building/best-practices/#user).
It seems it's not the case on this template with FrankenPHP and based on its doc, FrankenPHP can be used with an unprivileged user. Is it on purpose, or is it a feature that can be added to this template ?
Hi, @damienfern
I'm also interested in this question!
For now I'm using the following edits to run FrankenPHP as the built-in user www-data.
frankenphp/docker-entrypoint.sh
@@ -53,8 +53,9 @@
fi
fi
- setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var
- setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var
+ chgrp -R www-data var /data /config
+ setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var /data /config
+ setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var /data /config
fi
-exec docker-php-entrypoint "$@"
+su -c "docker-php-entrypoint $*" -s '/bin/sh' 'www-data'
With this edit the container still runs as root, but FrankenPHP runs as www-data.
I'm not sure if this is the correct way, but in my case it works so far.
I would also be glad if this template contained information about a more correct way to run the container not as an privileged user.
Modifying the Dockerfile like this should help: https://frankenphp.dev/docs/docker/#running-as-a-non-root-user
A doc PR explaining how to change this template to run as a non-root user is very welcome, however, we'll not do that by default, because this causes many issues.
Found a problem when using the method I described above (using su ... instead of exec ...): when using su ... a chain of subprocesses is created (entrypoint→su→sh→frankenphp), while when using exec ... there is no chain (frankenphp).
Due to the chain, signals sent by Docker do not reach the target process. This primarily negatively affects the Symfony Messenger consumer.
Processes when using exec ...:
| PID | USER | Command |
|---|---|---|
| 1 | root | frankenphp run --config /etc/caddy/Caddyfile |
Processes when using su ...:
| PID | USER | Command |
|---|---|---|
| 1 | root | /bin/sh /usr/local/bin/docker-entrypoint frankenphp run --config /etc/caddy/Caddyfile |
| 33 | root | `- su -c docker-php-entrypoint frankenphp run --config /etc/caddy/Caddyfile -s /usr/bin/sh www-data |
| 34 | www‑data | `- sh -c frankenphp run --config /etc/caddy/Caddyfile |
| 35 | www‑data | `- frankenphp run --config /etc/caddy/Caddyfile |
Hi @7-zete-7,
You should give a try to https://github.com/tianon/gosu as it uses the same methods to "impersonate" user as Docker, but it uses an exec instead of a subprocess (which, as you pointed, handles the signal better).
Thanks for such a quick response, @damienfern!
I'll try using gosu for this task.
gosu actually changes the user and does not create any subprocesses. Thanks, @damienfern!
Some update of https://github.com/dunglas/symfony-docker/issues/679#issuecomment-2434587889 to work via gosu.
Dockerfile
@@ -20,6 +20,7 @@
RUN apt-get update && apt-get install -y --no-install-recommends \
+ acl \
file \
git \
+ gosu \
&& rm -rf /var/lib/apt/lists/*
frankenphp/docker-entrypoint.sh
@@ -53,8 +53,9 @@
fi
fi
+ chgrp -R www-data var /data /config
+ setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var /data /config
+ setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var /data /config
fi
-exec docker-php-entrypoint "$@"
+exec /usr/sbin/gosu www-data "$@"
Still not sure if this is the correct way, but it works for now.
Have you tried to use gosu directly ? Based on the gosu's README, it already does an exec.
-exec /usr/sbin/gosu www-data "$@"
+/usr/sbin/gosu www-data "$@"
@damienfern, yes, I did this first. I was surprised that there was a chain of processes and tried with exec. With exec the chain was gone.
| PID | USER | Command |
|---|---|---|
| 1 | www-data | frankenphp run --config /etc/caddy/Caddyfile
|
| PID | USER | Command |
|---|---|---|
| 1 | root | /bin/sh /usr/local/bin/docker-entrypoint frankenphp run --config /etc/caddy/Caddyfile
|
| 20 | www-data | `- frankenphp run --config /etc/caddy/Caddyfile
|
Modifying the
Dockerfilelike this should help: https://frankenphp.dev/docs/docker/#running-as-a-non-root-userA doc PR explaining how to change this template to run as a non-root user is very welcome, however, we'll not do that by default, because this causes many issues.
Hi Dunglas,
can you explain which issues this can cause (or point me to a link where I can find it) ?
We have adopted this template approach for our local env but when running a command inside the container, like creating a migration, file is generated as root and then it creates issues when you want to commit it, switch branch, etc.
Are we missing something in the way we use this template ?
Hi @jeanatpi!
can you explain which issues this can cause (or point me to a link where I can find it) ?
CC @dunglas @maxhelias
IMO the use of superuser in this template is for the purpose of simplification.
Running FrankenPHP in a prod container without superuser will lead to difficulties in setting up permissions. The options proposed in this thread represent only a basic setup. Using these options can lead to errors in the container, project and FrankenPHP.
Using this template for development without superuser will create many difficulties, as some operations inside the container require superuser rights. Setting up user juggling is possible, but it will be more complicated than doing it for a prod environment.
We have adopted this template approach for our local env but when running a command inside the container, like creating a migration, file is generated as root and then it creates issues when you want to commit it, switch branch, etc.
Are we missing something in the way we use this template ?
You can find a solution to this problem (and some other problems) in the file docs/troubleshooting.md.
@7-zete-7 thanks for the clarifications!
So if I understand correctly, mainly due to frankenphp, the container must run as root on prod and dev env. Meaning that we need to accept a potential security risk as mentioned by @damienfern .
For the file permissions I've read the troubleshooting but that would mean to run this each time we have a file created from a command inside the container:
- new migration
- running a test which generate an approval file
1 option for us would be to open a bash on container using something like:
docker compose ... exec --user "$(id -u):$(id -g)" -it <container> bash
Not ideal but that would avoid to fix permissions manually.
So if I understand correctly, mainly due to frankenphp, the container must run as root on prod and dev env.
@jeanatpi Not quite so. Running the container as superuser is necessary for the entrypoint to work correctly. FrankenPHP itself in the container, in my experience, can work under another user.
For FrankenPHP to work as unprivileged user, permissions must be configured at least for the /config /data and /app/var directories. Which either requires superuser rights or configuration of volumes mounted to these directories.
The command described in the docs/troubleshooting.md file can be converted into an automatic handler for convenience. For example (not a ready-made solution):
while inotifywait --quiet --recursive --event create .; do
chown -R $(id -u):$(id -g) .
done
Opening an interactive shell under a local user can solve the problem, but will impose restrictions on the execution of some operations.
Just wanted to share our experience on integrating a root-less docker environment:
The linked FrankenPHP Dockerfile modification is a good start, but covers only the basics as mentioned by others. In our case we got a lot of strange "memory allocation" errors thrown by Caddy / FrankenPHP that were likely related to wrong permission of the docker volumes (we mount a LOT of volumes in our app). Once in a while we had to delete the caddy_data and caddy_config volume manually to get things back working to normal. Strange.
To reduce the number of unexpected difficulties, we found it best to use the same user-id and group-id both inside the host and docker (preferably, 1000:1000). Over on the FrankenPHP repo there are some helpful comments about this regarding Symfony in specific. We found a sweet-spot solution for both, fresh installations and already running installations by adding the following block to services.php in compose.yaml:
build:
args:
HOST_UID: ${HOST_UID:-1000}
HOST_GID: ${HOST_GID:-1000}
Over in the Dockerfile:
ARG HOST_GID
ARG HOST_UID
RUN \
groupadd -g ${HOST_GID} ${HOST_UID}; \
useradd -u ${HOST_UID} -g ${HOST_GID} -m ${HOST_UID}; \
# Add additional capability to bind to port 80 and 443
setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/frankenphp; \
# Give write access to webserver config
chown -R ${HOST_UID}:${HOST_GID} /data/caddy; \
chown -R ${HOST_UID}:${HOST_GID} /config/caddy; \
chown -R ${HOST_UID}:${HOST_GID} /data/; \
# Give write access to composer & symfony directories
mkdir -p var/cache var/log var/translations vendor; \
chown -R ${HOST_UID}:${HOST_GID} var/cache var/log var/translations vendor;
...
USER ${HOST_UID}
CMD [ "frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile", "--watch" ]
Hope this helps anybody.
I stumbled upon this issue while looking for a solution. This is my Dockerfile to run my Symfony project in Openshift as an arbitrary user:
FROM docker.io/dunglas/frankenphp:1.8.0-php8.3.22-bookworm
RUN cp $PHP_INI_DIR/php.ini-production $PHP_INI_DIR/php.ini
COPY .docker/php.ini /usr/local/etc/php/conf.d/zz_project.ini
COPY --from=docker.io/library/composer /usr/bin/composer /usr/bin/composer
ARG USER=appuser
# https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/images/creating-images#use-uid_create-images \
# https://frankenphp.dev/docs/docker/#running-with-no-capabilities
RUN \
useradd ${USER}; \
setcap -r /usr/local/bin/frankenphp; \
chown -R ${USER}:0 /data/caddy /config/caddy /app && \
chmod -R g=u /data/caddy /config/caddy /app
USER ${USER}
COPY --chown=${USER}:0 . /app
RUN composer install --classmap-authoritative --no-dev --no-interaction --no-progress --no-scripts; \
bin/console assets:install --no-interaction; \
rm -rf var/cache/prod var/log/*; \
chmod -R g=u /app/var/cache /app/var/log
I came up with this solution: https://github.com/le-yak/dunglas-symfony-docker/pull/1/files
With this change, container now run as www-data. Optionally, one can change the UID/GID at build time to match the target runtime ID's (your own user in development).
we'll not do that by default, because this causes many issues.
My initial smoke tests did not reveal any obvious problem. If someone could kindly confirm this in a more serious environment, or advise what to watch out for in my tests, I’d be happy to refine this into a real PR. The challenge was tracking all files and directories the runtime needs write access to. I hope I haven't missed too many.