Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to explicitly set the uid and gid of the mysql user. #554

Closed
bsutton opened this issue Jan 15, 2024 · 15 comments
Closed

Add option to explicitly set the uid and gid of the mysql user. #554

bsutton opened this issue Jan 15, 2024 · 15 comments

Comments

@bsutton
Copy link

bsutton commented Jan 15, 2024

My project uses host volume mounts for data and log directories.

The problem is that the mysql uid and gid are allocated randomly within the container and as such are 'mapped' to a random user/group on the host ( I guess technically it's not random, just not within my control).

If we could set the uid:gid for the mysql user and group within the container, then we could setup host uid:gid that match and is mapped to a coherent name.

@grooverdan
Copy link
Member

What options does your container runtime provide? Is it a rootless mode?

The mysql id of 999 is fixed in the container.

$ ls -la build-mariadb-server-10.11-datadir/
total 111012
drwxr-xr-x.  6 dan  dan        260 Jan 16 09:53 .
drwxrwxrwt. 24 root root       600 Jan 16 09:53 ..
-rw-rw----.  1 dan  dan     417792 Jan 16 09:53 aria_log.00000001
-rw-rw----.  1 dan  dan         52 Jan 16 09:53 aria_log_control
-rw-rw----.  1 dan  dan        910 Jan 16 09:53 ib_buffer_pool
-rw-rw----.  1 dan  dan   12582912 Jan 16 09:51 ibdata1
-rw-rw----.  1 dan  dan  100663296 Jan 16 09:51 ib_logfile0
-rw-rw----.  1 dan  dan          0 Jan 16 09:51 multi-master.info
drwx------.  2 dan  dan       1800 Jan 16 09:51 mysql
-rw-r--r--.  1 dan  dan         15 Jan 16 09:51 mysql_upgrade_info
drwx------.  2 dan  dan         60 Jan 16 09:51 performance_schema
drwx------.  2 dan  dan       2120 Jan 16 09:51 sys
drwx------.  2 dan  dan         60 Jan 16 09:51 test


/tmp 
$ podman run --userns=keep-id:uid=999,gid=999 -v ./build-mariadb-server-10.11-datadir:/var/lib/mysql --rm   mariadb:10.11
2024-01-15 23:01:23+00:00 [Note] [Entrypoint]: Entrypoint script for MariaDB Server 1:10.11.6+maria~ubu2204 started.
2024-01-15 23:01:23+00:00 [Note] [Entrypoint]: MariaDB upgrade not required
2024-01-15 23:01:23 0 [Note] Starting MariaDB 10.11.6-MariaDB-1:10.11.6+maria~ubu2204 source revision fecd78b83785d5ae96f2c6ff340375be803cd299 as process 1
...

$ podman exec -ti jolly_jepsen ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
mysql          1       0  0 23:01 ?        00:00:00 mariadbd
mysql         33       0  0 23:03 pts/0    00:00:00 ps -ef

I'm also curious why are host volume mounts used instead of named volumes?

@bsutton
Copy link
Author

bsutton commented Jan 15, 2024 via email

@grooverdan
Copy link
Member

Rootless mode is part of some container runtimes that execute containers without (host) root permissions.

In the example provided I used the keep-id feature of the runtime to map the 999 user inside the container, to the current user dan, so that it could access the volume on the local filesystem. Rootful modes provide also options for explicit mappings,

podman run --uidmap=999:0  --user 999   -v ./build-mariadb-server-10.11-datadir:/var/lib/mysql --rm   mariadb:10.11

Also should have worked (containers/common#1802)

User ID maps are supported by the container runtime, which is why I'm not looking to explicitly implement a mariadb container control for it. Essentially doing so would modify the container at runtime (not supported on runtimes like apptainer) and slow the initialization speed. Also named volumes exist for providing exclusive storage to containers.

Without easy documentation, I can see why you describe it as random, or not within your control. After all we only need to set 1 host -> container uid number correctly, so I'm going to try to document this so every container doesn't need to implement the same thing.

@bsutton
Copy link
Author

bsutton commented Jan 16, 2024

If I've understood what you are saying, then the problem is that mariadb (partially) ignores the mapping.

On docker, I've tried mapping the user with docker-compose option user 2002:2002 with 2002 being the uid of the host user mysql.

The problem is that they mysql container creates files using the 'mysql' name rather than the uid the container is running with (as per the above user option).
The result is that I get some files with the correct permissions (those created with the uid) and some not (those that have the owner set to 'mysql').

The only way I can get it to work is to clone the mariadb docker file and modify the mysql uid/gid. When I do this, it doesn't matter if maria uses the id or the name of the user, the permissions will be correct.

By altering https://github.com/MariaDB/mariadb-docker/blob/master/10.11/Dockerfile

to have

RUN groupadd -g 2002 -r mysql && useradd -u 2002 -r -g mysql mysql --home-dir /var/lib/mysql

Note the addition of the -g and -u switches otherwise the file is original.

This combined with the container mapping gives the desired results but without the custom container I can't get it to work.

@bsutton
Copy link
Author

bsutton commented Jan 16, 2024

Here is my docker-compose file:

version: '3.3'

services:
  mysql:
    container_name: mysql
    image: onepub/onepub-mariadb:${ONEPUB_VERSION}
    restart: on-failure
    environment:
      # override from env file to be empty, otherwise it prevents proper  initialization of mysql
      MYSQL_ROOT_PASSWORD: ${MYSQL_ADMIN_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_SCHEMA}
      TZ: ${TIME_ZONE-Australia/Melbourne}
      MARIADB_AUTO_UPGRADE: "true"
    command: >
      --lower-case-table-names=1 --bind-address=127.0.0.1 --datadir=/innodb-data/ --innodb_log_group_home_dir=/innodb-logs/ --max-allowed-packet=512M --innodb_buffer_pool_chunk_size=${MYSQL_INNODB_BUFFER_POOL_CHUNK_SIZE-8M} --innodb_buffer_pool_size=${MYSQL_INNODB_BUFFER_POOL_SIZE-512M} --table_open_cache=${MYSQL_TABLE_OPEN_CACHE-512} --max_connections=${MYSQL_MAX_CONNECTIONS-98} --innodb_flush_neighbors=0 --innodb_fast_shutdown=1 --innodb_flush_log_at_trx_commit=1 --innodb_flush_method=fsync --innodb_doublewrite=0  --innodb_use_native_aio=0 --innodb_read_io_threads=10 --innodb_write_io_threads=10 --slow_query_log_file=/tmp/mysql-slow.log --long-query-time=0.3 --slow_query_log --innodb-print-all-deadlocks
    volumes:
      - /db/mysql-innodb-data:/innodb-data
      - /db/mysql-innodb-logs:/innodb-logs
    network_mode: "host"
    logging:
      driver: "journald"
    user: 2002:2002

The full docker file - note that on the above mentioned line has been changed:

# copied from https://github.com/MariaDB/mariadb-docker/blob/master/10.11/Dockerfile
# The only change is that we explicityly set the mysql uid and gid

# vim:set ft=dockerfile:
FROM ubuntu:jammy

# add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added
#
# ONEPUB - set -g and -u to 2002
#
RUN groupadd -g 2002 -r mysql && useradd -u 2002 -r -g mysql mysql --home-dir /var/lib/mysql

# add gosu for easy step-down from root
# https://github.com/tianon/gosu/releases
# gosu key is B42F6819007F00F88E364FD4036A9C25BF357DD4
ENV GOSU_VERSION 1.17

ARG GPG_KEYS=177F4010FE56CA3336300305F1656F24C74CD1D8
# pub   rsa4096 2016-03-30 [SC]
#         177F 4010 FE56 CA33 3630  0305 F165 6F24 C74C D1D8
# uid           [ unknown] MariaDB Signing Key <signing-key@mariadb.org>
# sub   rsa4096 2016-03-30 [E]
# install "libjemalloc2" as it offers better performance in some cases. Use with LD_PRELOAD
# install "pwgen" for randomizing passwords
# install "tzdata" for /usr/share/zoneinfo/
# install "xz-utils" for .sql.xz docker-entrypoint-initdb.d files
# install "zstd" for .sql.zst docker-entrypoint-initdb.d files
# hadolint ignore=SC2086
RUN set -eux; \
    apt-get update; \
    DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
    ca-certificates \
    gpg \
    gpgv \
    libjemalloc2 \
    pwgen \
    tzdata \
    xz-utils \
    zstd ; \
    savedAptMark="$(apt-mark showmanual)"; \
    apt-get install -y --no-install-recommends \
    dirmngr \
    gpg-agent \
    wget; \
    rm -rf /var/lib/apt/lists/*; \
    dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
    wget -q -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
    wget -q -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
    GNUPGHOME="$(mktemp -d)"; \
    export GNUPGHOME; \
    gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
    for key in $GPG_KEYS; do \
    gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key"; \
    done; \
    gpg --batch --export "$GPG_KEYS" > /etc/apt/trusted.gpg.d/mariadb.gpg; \
    if command -v gpgconf >/dev/null; then \
    gpgconf --kill all; \
    fi; \
    gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
    gpgconf --kill all; \
    rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc; \
    apt-mark auto '.*' > /dev/null; \
    [ -z "$savedAptMark" ] ||	apt-mark manual $savedAptMark >/dev/null; \
    apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
    chmod +x /usr/local/bin/gosu; \
    gosu --version; \
    gosu nobody true

RUN mkdir /docker-entrypoint-initdb.d

# Ensure the container exec commands handle range of utf8 characters based of
# default locales in base image (https://github.com/docker-library/docs/blob/135b79cc8093ab02e55debb61fdb079ab2dbce87/ubuntu/README.md#locales)
ENV LANG C.UTF-8

# OCI annotations to image
LABEL org.opencontainers.image.authors="MariaDB Community" \
    org.opencontainers.image.title="MariaDB Database" \
    org.opencontainers.image.description="MariaDB Database for relational SQL" \
    org.opencontainers.image.documentation="https://hub.docker.com/_/mariadb/" \
    org.opencontainers.image.base.name="docker.io/library/ubuntu:jammy" \
    org.opencontainers.image.licenses="GPL-2.0" \
    org.opencontainers.image.source="https://github.com/MariaDB/mariadb-docker" \
    org.opencontainers.image.vendor="MariaDB Community" \
    org.opencontainers.image.version="10.11.6" \
    org.opencontainers.image.url="https://github.com/MariaDB/mariadb-docker"

# bashbrew-architectures: amd64 arm64v8 ppc64le s390x
ARG MARIADB_VERSION=1:10.11.6+maria~ubu2204
ENV MARIADB_VERSION $MARIADB_VERSION
# release-status:Stable
# release-support-type:Long Term Support
# (https://downloads.mariadb.org/rest-api/mariadb/)

# Allowing overriding of REPOSITORY, a URL that includes suite and component for testing and Enterprise Versions
ARG REPOSITORY="http://archive.mariadb.org/mariadb-10.11.6/repo/ubuntu/ jammy main main/debug"

RUN set -e;\
    echo "deb ${REPOSITORY}" > /etc/apt/sources.list.d/mariadb.list; \
    { \
    echo 'Package: *'; \
    echo 'Pin: release o=MariaDB'; \
    echo 'Pin-Priority: 999'; \
    } > /etc/apt/preferences.d/mariadb
# add repository pinning to make sure dependencies from this MariaDB repo are preferred over Debian dependencies
#  libmariadbclient18 : Depends: libmysqlclient18 (= 5.5.42+maria-1~wheezy) but 5.5.43-0+deb7u1 is to be installed

# the "/var/lib/mysql" stuff here is because the mysql-server postinst doesn't have an explicit way to disable the mysql_install_db codepath besides having a database already "configured" (ie, stuff in /var/lib/mysql/mysql)
# also, we set debconf keys to make APT a little quieter
# hadolint ignore=DL3015
RUN set -ex; \
    { \
    echo "mariadb-server" mysql-server/root_password password 'unused'; \
    echo "mariadb-server" mysql-server/root_password_again password 'unused'; \
    } | debconf-set-selections; \
    apt-get update; \
    # postinst script creates a datadir, so avoid creating it by faking its existance.
    mkdir -p /var/lib/mysql/mysql ; touch /var/lib/mysql/mysql/user.frm ; \
    # mariadb-backup is installed at the same time so that `mysql-common` is only installed once from just mariadb repos
    apt-get install -y --no-install-recommends mariadb-server="$MARIADB_VERSION" mariadb-backup socat \
    ; \
    rm -rf /var/lib/apt/lists/*; \
    # purge and re-create /var/lib/mysql with appropriate ownership
    rm -rf /var/lib/mysql /etc/mysql/mariadb.conf.d/50-mysqld_safe.cnf; \
    mkdir -p /var/lib/mysql /run/mysqld; \
    chown -R mysql:mysql /var/lib/mysql /run/mysqld; \
    # ensure that /run/mysqld (used for socket and lock files) is writable regardless of the UID our mysqld instance ends up having at runtime
    chmod 1777 /run/mysqld; \
    # comment out a few problematic configuration values
    find /etc/mysql/ -name '*.cnf' -print0 \
    | xargs -0 grep -lZE '^(bind-address|log|user\s)' \
    | xargs -rt -0 sed -Ei 's/^(bind-address|log|user\s)/#&/'; \
    # don't reverse lookup hostnames, they are usually another container
    printf "[mariadb]\nhost-cache-size=0\nskip-name-resolve\n" > /etc/mysql/mariadb.conf.d/05-skipcache.cnf; \
    # Issue #327 Correct order of reading directories /etc/mysql/mariadb.conf.d before /etc/mysql/conf.d (mount-point per documentation)
    if [ -L /etc/mysql/my.cnf ]; then \
    # 10.5+
    sed -i -e '/includedir/ {N;s/\(.*\)\n\(.*\)/\n\2\n\1/}' /etc/mysql/mariadb.cnf; \
    fi


VOLUME /var/lib/mysql

COPY healthcheck.sh /usr/local/bin/healthcheck.sh
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 3306
CMD ["mariadbd"]

@grooverdan
Copy link
Member

A slightly simpler form is:

FROM mariadb:10.11

RUN useradd -u 2002 -r localuser --home-dir /var/lib/mysql

As this is all build time, to do this for a prebuild image of a dynamic uid is more complex.

And then in compose for the service user: localuser. The container started as a non-root user will not switch to mysql.

There's also usermode_ns: host, which comes down to dockerd user namespace and userns-remap with the notable text:

This re-mapping is transparent to the container, but introduces some configuration complexity in situations where the container needs access to resources on the Docker host, such as bind mounts into areas of the filesystem that the system user cannot write to. From a security standpoint, it is best to avoid these situations.

@bsutton
Copy link
Author

bsutton commented Jan 17, 2024 via email

@grooverdan
Copy link
Member

The issue here is that it requires customisation of the container - which I have successfully done - but it would seem preferable that this is a supported feature which doesn't require a container mod.

Actually user: 2002 in compose might be sufficient on an unmod container.

I can't imagine that I'm the only person trying to mount the db from the host.

Note, I'm not saying "no", just understanding the domain a bit more in the complexity of multiple runtime implementations, root(full|less) modes, an standard behaviour, OCI, runtime and other containers.

ref: https://github.com/opencontainers/runtime-spec/blob/main/config-linux.md#user-namespace-mappings
ref: https://github.com/opencontainers/runtime-spec/blob/main/config.md#configLinuxMountOptions idMap SHOULD be implemented by runtimes

@bsutton
Copy link
Author

bsutton commented Jan 17, 2024 via email

@martadinata666
Copy link

Chim in because out of curiousity, so I tried the compose

version: '3.3'

services:
  mysql:
    container_name: mysql
    image: mariadb:11.0
#    restart: on-failure
    environment:
      # override from env file to be empty, otherwise it prevents proper  initialization of mysql
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: somedbname
      MARIADB_AUTO_UPGRADE: "true"
    command: >
      --lower-case-table-names=1 --bind-address=127.0.0.1 --datadir=/innodb-data/ --innodb_log_group_home_dir=/innodb-logs/ --max-allowed-packet=512M --innodb_buffer_pool_chunk_size=${MYSQL_INNODB_BUFFER_POOL_CHUNK_SIZE-8M} --innodb_buffer_pool_size=${MYSQL_INNODB_BUFFER_POOL_SIZE-512M} --table_open_cache=${MYSQL_TABLE_OPEN_CACHE-512} --max_connections=${MYSQL_MAX_CONNECTIONS-98} --innodb_flush_neighbors=0 --innodb_fast_shutdown=1 --innodb_flush_log_at_trx_commit=1 --innodb_flush_method=fsync --innodb_doublewrite=0  --innodb_use_native_aio=0 --innodb_read_io_threads=10 --innodb_write_io_threads=10 --slow_query_log_file=/tmp/mysql-slow.log --long-query-time=0.3 --slow_query_log --innodb-print-all-deadlocks
    volumes:
      - ./data:/innodb-data
      - ./logs:/innodb-logs
#    network_mode: "host"
    logging:
      driver: "journald"
    user: 2002:2002
❯ docker exec -it mysql ps -ef
UID          PID    PPID  C STIME TTY          TIME CMD
2002           1       0  0 05:34 ?        00:00:00 mariadbd --lower-case-table-names=1 --bind-address=127.0.0.1 --datadir=/innodb-data/ --innodb_log_group_home_dir=/innodb-logs/ --max-allowed-packet=512M --innodb_buffer_pool_chunk_size
2002         117       0  0 05:40 pts/0    00:00:00 ps -ef
❯ docker exec -it mysql ls -aln /innodb-data
total 71812
drwxrwxr-x 1 2002 2002      378 Jan 17 05:36 .
drwxr-xr-x 1    0    0     4096 Jan 17 05:34 ..
-rw------- 1 2002 2002      131 Jan 17 05:36 .my-healthcheck.cnf
-rw-rw---- 1 2002 2002 16875520 Jan 17 05:36 aria_log.00000001
-rw-rw---- 1 2002 2002       52 Jan 17 05:36 aria_log_control
-rw-rw---- 1 2002 2002        9 Jan 17 05:36 ddl_recovery.log
-rw-rw---- 1 2002 2002      690 Jan 17 05:36 ib_buffer_pool
-rw-rw---- 1 2002 2002 12582912 Jan 17 05:36 ibdata1
-rw-rw---- 1 2002 2002 12582912 Jan 17 05:36 ibtmp1
-rw-r--r-- 1 2002 2002       14 Jan 17 05:36 mariadb_upgrade_info
-rw-rw---- 1 2002 2002        0 Jan 17 05:36 multi-master.info
drwx------ 1 2002 2002     2718 Jan 17 05:36 mysql
drwx------ 1 2002 2002       12 Jan 17 05:35 performance_schema
drwx------ 1 2002 2002       12 Jan 17 05:36 somedbname
drwx------ 1 2002 2002     6536 Jan 17 05:35 sys
-rw-rw---- 1 2002 2002 10485760 Jan 17 05:36 undo001
-rw-rw---- 1 2002 2002 10485760 Jan 17 05:36 undo002
-rw-rw---- 1 2002 2002 10485760 Jan 17 05:36 undo003

It run as 2002 as expected, I just need to ensure that host path the data and logs writable by 2002. Need more explicit which part that doesn't work?

@AmirHosseinKarimi
Copy link

The problem is that the mysql process will run from a user other than root which does not follow the docker patterns.
This container is a single process and that process should run from the root user.
So the uid and gid mapping will work correctly and the file ownership in the host will be fixed.

@martadinata666
Copy link

No no, docker pattern and security should drop root as soon as possible or even run with specified user that why many rootless docker image mostly run as 1000 or some other user specified.

@Jip-Hop
Copy link

Jip-Hop commented Feb 3, 2024

Seems to work fine over here with uid and gid explicitly set (besides dropping all capabilities and running the image readonly).

version: "3"

services:
  mariadb:
    container_name: mariadb
    image: mariadb:10.6.16
    restart: unless-stopped
    command: --transaction-isolation=READ-COMMITTED --log-bin=binlog --binlog-format=ROW
    user: 2002:2002
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp:noexec
      - /run:noexec
      - /run/mysqld:noexec
    volumes:
      - ./mariadb:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD
      - MYSQL_PASSWORD
      - MYSQL_DATABASE=mariadb
      - MYSQL_USER=mariadb
      - MARIADB_AUTO_UPGRADE=1
      - MARIADB_DISABLE_UPGRADE_BACKUP=0

Running find . ! -user 2002 inside the bind mounted mariadb directory returns 0 files.

@grooverdan
Copy link
Member

Thanks @Jip-Hop, for highlighting the read_only mode. tmpfs also highlighting how things like pidfiles and sockets add to the inflexibility of the image (more to fix).

Highlighting cap_drop: all I think is an important aspect as it highlights exactly what capabilities the root user (checked with grep ^Cap /proc/self/status and it seems only root - CapBnd remains when --user specified meaning it needs a filesystem cap label to gain capability. (readable list)

I just read though a number of best practices guides to be sure, and none mention going to a non-root user.

So if we go back to security principles of risk/convenience tradeoff lets look at exactly what's is/needed.

  • changing to mysql user inside container was following what MariaDB did outside the container, and running as root for mariadb-install-db would error (though --user root would allow this).
  • changing user implicitly drops effective capabilities leaving what's normally required to run MariaDB, nothing special, just write access to database files.
  • To facilitate the above, FOWNER/ CHOWN capabilities are used by the entrypoint to ensure the /var/lib/mysql (DATADIR) volume has the right ownership for mariadbd to write database files. There isn't the ability of OCI container to specify to the container runtime that volumes need to be owned by a particular user, so by default attract a root user.

So options:

  1. keep as is, like shown above user: specified by the user can set arbitary uid that map to the host storage (though it need to be documented clearer)
  2. Like Improve security permissions in docker images #461, a USER mysql is the Dockerfile cannot cope with arbitrary volumes
  3. Any modification of a user -> uid within the container to solve an external volume is an option at build time, but with so many user ids possible we can't make a container for every user.
  4. change mariadbd to capset to drop capabilities, not complain about an euid of 0, mask off the filesystem that it doesn't need somehowe, and then it can run as the default USER root.

@grooverdan
Copy link
Member

Are the FAQs sufficient?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

5 participants