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
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
As mentioned in docker/compose#9648,
docker compose
now warns about the factuid
,gid
,mode
fields are not supported:↓
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:↓
So it appears you have at least one way to address your issue (to be tested on your side):
Check the image’s uid
Browsing https://explore.ggcr.dev/?image=nginxinc/nginx-unprivileged
Then following the top digest links lead to:
Apply the uid locally
Skip unsupported secret fields in docker-compose.yml
↓
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 typingsudo docker
explicitly but still implysudo
to get a a prompt asking for the sudoer password. For details, see this SO answer of mine.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:
With this wrap-secret/Dockerfile:
And this script wrap-secret-entrypoint.sh:
Then run
docker compose up --build
.What do you think?
Side-note:
In your original post, you had written something like:
which is buggy, use
( cmd && cmd2 )
instead orcmd1 ; cmd2
.