skip to Main Content

Situation

The current (07/2024) docker compose documentation states (falsely) that there is a long-syntax when using ‘docker secrets’ that can defines the name, uid, gid and mode of the mounted file

See example and documentaion:

services:
  frontend:
    image: example/webapp
    secrets:
      - source: server-certificate
        target: server.cert
        uid: "103"
        gid: "103"
        mode: 0440
secrets:
  server-certificate:
    file: ./server.cert

This configuration is valid but has no affect at all when using compose (works for docker swarm).
There was an discussion (GitHub Docker Issue 9648) going on about it (showing the different implementations of this specification) but the documentation has not been fixed (GitHub Docker Issue 18907).

Result is a mounted secret (/run/secrets/<foobar>) with root:root (0:0) ownership and permission mode 400.
Sidenote: /run/secrets/ is a read-only mounted filesystem.

Problem

When using docker compose secrets on an image which does come with a non-root user shipped.

services:
    nginx:
        image: nginxinc/nginx-unprivileged:1.27-alpine
        ports:
            - "8080:8080"
        secrets:
            - FOO_BAR_SECRET
secrets:
    FOO_BAR_SECRET:
        file: .foo.bar

Solution idea (question)

Is there another/better solution then creating a custom image (Dockerfile) which switches back to the ‘root‘ user and defines a wrapping docker entrypoint script

FROM nginxinc/nginx-unprivileged:1.27-alpine

## switching to non-root user 'nginx' later in custom 'docker-entrypoint-wrapper.sh'
USER root

## one could argue to just use pre-installed 'runuser' instead, or install 'gosu'. For this example there is no strong argument for either
RUN apk update && apk add su-exec

## enables us to run commands on startup as 'root'
RUN mv /docker-entrypoint.sh /docker-entrypoint-original.sh
COPY docker-entrypoint-wrapper.sh /docker-entrypoint.sh
RUN chmod ug+x /docker-entrypoint.sh

that copies those root-exclusive secrets from the read-only filesystem mount elsewhere, updates the file owner to the desired non-root user and switches back to this user to execute the actual/original docker entrypoint script ?

#!/usr/bin/env sh
set -e

mkdir /run/secrets_ 
&& cp -r /run/secrets/* /run/secrets_ 
&& chown -R nginx:nginx /run/secrets_

# as mentioned in the Docker file: may use 'gosu' or 'runuser' instead
exec su-exec nginx /docker-entrypoint-original.sh "${@}"

Solution alternative (question) when a file is not required or just not an option

Qudos to dcendents (GitHub) pointing out this idea (on GitHub Keycloak Issue 10816, that one could ‘export’ the content of the secret file. Which indeed is not really desired security-wise hence you may not export it but just make it ‘inline available’ for the command.

#!/bin/sh

## find all secret files mounted by docker
for i in $(ls -1 /run/secrets)
do
    ## export secret file name as environment variable
    export "${i}"="$(cat /run/secrets/${i})"
done

## run actual command
exec "$@"

Research

There is one idea of having just an "simple" docker mount declaration with the desired ownership and permission mode but in that case one would be required to do it for every X different services and Y different secrets, which would lead to an "small" bloat of long repeating lines.

Restrictions/requirements on a solution

(A) I need or would like to have a general solution thats works for a Windows and Linux host. Scripting a chown on a Windows host may not be a way.

(B) The FOO_BAR_SECRET will be used by multiple services (it will be a wildcard TLS certificate) which all requires different UIDs.

2

Answers


  1. As mentioned in docker/compose#9648, docker compose now warns about the fact uid, gid, mode fields are not supported:

    services:
      nginx:
        image: nginxinc/nginx-unprivileged:1.27-alpine
        ports:
          - "8080:8080"
        secrets:
          - source: foobar
            target: FOO_BAR_SECRET
            uid: "103"
            gid: "103"
            mode: 0440
        entrypoint: '/bin/sh -c "ls -Rhal /run/secrets/; cat /run/secrets/FOO_BAR_SECRET"'
    secrets:
      foobar:
        file: .foo.bar
    
    $ docker compose up
    

    WARN[0000] secrets `uid`, `gid` and `mode` are not supported, they will be ignored 
    
    Attaching to nginx-1
    nginx-1  | /run/secrets/:
    nginx-1  | total 12K    
    nginx-1  | drwxr-xr-x    2 root     root        4.0K Jul 19 13:31 .
    nginx-1  | drwxr-xr-x    1 root     root        4.0K Jul 19 13:31 ..
    nginx-1  | -rw-r-----    1 1000     1000          18 Jul 19 13:15 FOO_BAR_SECRET
    nginx-1  | cat: can't open '/run/secrets/FOO_BAR_SECRET': Permission denied
    nginx-1 exited with code 1
    

    The secret file is not readable, but note that on my workstation (Debian 12) the file is not owned by root:root, but has the same permissions as the source file:

    $ ls -l .foo.bar
    

    -rw-r----- 1 user user 18 juil. 19 15:15 .foo.bar
    

    So it appears you have at least one way to address your issue (to be tested on your side):

    Check the image’s uid

    Apply the uid locally

    $ sudo chown 101:101 .foo.bar; sudo chmod 440 .foo.bar
    

    Skip unsupported secret fields in docker-compose.yml

    services:
      nginx:
        image: nginxinc/nginx-unprivileged:1.27-alpine
        ports:
          - "8080:8080"
        secrets:
          - source: foobar
            target: FOO_BAR_SECRET
        entrypoint: '/bin/sh -c "ls -Rhal /run/secrets/; cat /run/secrets/FOO_BAR_SECRET"'
    secrets:
      foobar:
        file: .foo.bar
    
    $ docker compose up
    

    Attaching to nginx-1
    nginx-1  | /run/secrets/:
    nginx-1  | total 12K    
    nginx-1  | drwxr-xr-x    2 root     root        4.0K Jul 19 13:44 .
    nginx-1  | drwxr-xr-x    1 root     root        4.0K Jul 19 13:44 ..
    nginx-1  | -r--r-----    1 nginx    nginx         18 Jul 19 13:15 FOO_BAR_SECRET
    nginx-1  | TEST_VAR="VALUE!"
    nginx-1 exited with code 0
    

    Side-notes on Docker Engine for Linux vs. Docker Desktop

    The above behavior may require directly using Docker Engine (and not Docker Desktop), as you confirmed in the comments.

    Indeed, the bind-mount permissions are known to always-default-to-root:root with Docker Desktop (even on Linux), see:

    ⚠️ As another side note, if ever you install Docker Engine for GNU/Linux on a personal workstation, do not add your user account to the docker group as it is suggested in many online tutorials (sudo usermod -aG docker $USER), because this is risky. Just define an alias to avoid typing sudo docker explicitly but still imply sudo to get a a prompt asking for the sudoer password. For details, see this SO answer of mine.

    Login or Signup to reply.
  2. Following the discussion in the comments of my previous answer, here is another solution, which should be cross-platform, and with a better SoC / DRY:

    TL;DR: The OP initially suggested to use a Dockerfile that goes root, adds some layers, and so on, but I’d also suggest to use only one (generic) Dockerfile, and pass values either via build arguments (at build time), or via environment variables (at container run time), all specified in a concise way from the YAML conf file.

    Consider this docker-compose.yml:

    services:
      nginx:
        build:
          context: wrap-secret
          args:
            # original image
            image: nginxinc/nginx-unprivileged:1.27-alpine
            # original entrypoint file
            entrypoint: "/entrypoint.sh"
        environment:
          target_uid: 101
          target_gid: 101
        ports:
          - "8080:8080"
        secrets:
          - source: foobar
            target: FOO_BAR_SECRET
    secrets:
      foobar:
        file: .foo.bar
    

    With this wrap-secret/Dockerfile:

    ARG image
    FROM $image
    USER root
    
    # BEGIN TAKEN FROM https://github.com/tianon/gosu/blob/master/INSTALL.md
    # ASSUMING $image is alpine-based
    ENV GOSU_VERSION 1.17
    RUN set -eux; 
        
        apk add --no-cache --virtual .gosu-deps 
            ca-certificates 
            dpkg 
            gnupg 
        ; 
        
        dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; 
        wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; 
        wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; 
        
    # verify the signature
        export GNUPGHOME="$(mktemp -d)"; 
        gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; 
        gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; 
        gpgconf --kill all; 
        rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; 
        
    # clean up fetch dependencies
        apk del --no-network .gosu-deps; 
        
        chmod +x /usr/local/bin/gosu; 
    # verify that the binary works
        gosu --version; 
        gosu nobody true
    # END TAKEN FROM https://github.com/tianon/gosu/blob/master/INSTALL.md
    
    WORKDIR /app
    COPY wrap-secret-entrypoint.sh /app/wrap-secret-entrypoint.sh
    ARG entrypoint
    RUN chmod a+x /app/wrap-secret-entrypoint.sh 
      && sed -e 's@THE_ENTRYPOINT@'"${entrypoint}"'@' -i /app/wrap-secret-entrypoint.sh
    # we might want to replace sed with a perl oneliner or so
    ENTRYPOINT ["/app/wrap-secret-entrypoint.sh"]
    

    And this script wrap-secret-entrypoint.sh:

    #!/usr/bin/env sh
    set -e
    entrypoint="THE_ENTRYPOINT"
    [ -n "$target_uid" ]
    [ -n "$target_gid" ]
    # we could also pass target_mode…
    mkdir /run/secrets_
    cp -a /run/secrets/* /run/secrets_/
    chown -R "$target_uid:$target_gid" /run/secrets_
    # may use 'su-exec' or 'runuser' instead
    exec gosu "$target_uid:$target_gid" "$entrypoint" "$@"
    

    Then run docker compose up --build.

    What do you think?

    Side-note:

    In your original post, you had written something like:

    set -e
    cmd1 && cmd2
    

    which is buggy, use ( cmd && cmd2 ) instead or cmd1 ; cmd2.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search