symfony-docker icon indicating copy to clipboard operation
symfony-docker copied to clipboard

Run container as an unprivileged user

Open damienfern opened this issue 1 year ago • 9 comments

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 ?

damienfern avatar Oct 22 '24 13:10 damienfern

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.

7-zete-7 avatar Oct 24 '24 08:10 7-zete-7

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.

dunglas avatar Oct 24 '24 14:10 dunglas

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

7-zete-7 avatar Nov 26 '24 15:11 7-zete-7

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).

damienfern avatar Nov 26 '24 16:11 damienfern

Thanks for such a quick response, @damienfern!

I'll try using gosu for this task.

7-zete-7 avatar Nov 26 '24 16:11 7-zete-7

gosu actually changes the user and does not create any subprocesses. Thanks, @damienfern!

7-zete-7 avatar Nov 26 '24 16:11 7-zete-7

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.

7-zete-7 avatar Nov 26 '24 16:11 7-zete-7

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 avatar Nov 29 '24 10:11 damienfern

@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.

Using exec /usr/sbin/gosu www-data "$@"
PIDUSERCommand
1www-datafrankenphp run --config /etc/caddy/Caddyfile
Using /usr/sbin/gosu www-data "$@" or gosu www-data "$@"
PIDUSERCommand
1root/bin/sh /usr/local/bin/docker-entrypoint frankenphp run --config /etc/caddy/Caddyfile
20www-data`- frankenphp run --config /etc/caddy/Caddyfile

7-zete-7 avatar Nov 29 '24 12:11 7-zete-7

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.

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 ?

jeanatpi avatar Jun 30 '25 06:06 jeanatpi

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 avatar Jun 30 '25 08:06 7-zete-7

@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.

jeanatpi avatar Jun 30 '25 11:06 jeanatpi

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.

7-zete-7 avatar Jun 30 '25 13:06 7-zete-7

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.

FlxRobole avatar Jul 02 '25 07:07 FlxRobole

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

mvhirsch avatar Jul 07 '25 13:07 mvhirsch

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.

le-yak avatar Aug 10 '25 19:08 le-yak