1

My Docker wrapper script works as intended when the current working directory does not contain spaces, however there is a bug when it does.

I have simplified an example to make use of the smallest official Docker image I could find and a well known GNU core utility. Of course this example is not very useful. In my real world use case, a much more complicated environment is packaged.

Docker Wrapper Script:

#!/usr/bin/env bash
##
## Dockerized ls
##

set -eux

# Only allocate tty if one is detected
# See https://stackoverflow.com/questions/911168/how-to-detect-if-my-shell-script-is-running-through-a-pipe
if [[ -t 0 ]]; then
    DOCKER_RUN_OPTIONS+="-i "
fi
if [[ -t 1 ]]; then
    DOCKER_RUN_OPTIONS+="-t "
fi

WORK_DIR="$(realpath .)"

DOCKER_RUN_OPTIONS+="--rm --user=$(id -u $(logname)):$(id -g $(logname)) --workdir=${WORK_DIR} --mount type=bind,source=${WORK_DIR},target=${WORK_DIR}"

exec docker run ${DOCKER_RUN_OPTIONS} busybox:latest ls "$@"

You can save this somewhere as /tmp/docker_ls for example. Remember to chmod +x /tmp/docker_ls

Now you are able to use this Dockerized ls in any path which contains no spaces as follows:

/tmp/docker_ls -lah
/tmp/docker_ls -lah | grep 'r'

Note that /tmp/docker_ls -lah /path/to/something is not implemented. The wrapper script would have to be adapted to parse parameters and mount the path argument into the container.

Can you see why this would not work when current working directory path contains spaces? What can be done to rectify it?

Solution:

@david-maze's answer solved the problem. Please see: https://stackoverflow.com/a/55763212/1782641

Using his advice I refactored my script as follows:

#!/usr/bin/env bash
##
## Dockerized ls
##

set -eux

# Only allocate tty if one is detected. See - https://stackoverflow.com/questions/911168
if [[ -t 0 ]]; then IT+=(-i); fi
if [[ -t 1 ]]; then IT+=(-t); fi

USER="$(id -u $(logname)):$(id -g $(logname))"
WORKDIR="$(realpath .)"
MOUNT="type=bind,source=${WORKDIR},target=${WORKDIR}"

exec docker run --rm "${IT[@]}" --user "${USER}" --workdir "${WORKDIR}" --mount "${MOUNT}" busybox:latest ls "$@"
dnk8n
  • 675
  • 8
  • 21

2 Answers2

1

If your goal is to run a process on the current host directory as the current host user, you will find it vastly easier and safer to use a host process, and not an isolation layer like Docker that intentionally tries to hide these things from you. For what you’re showing I would just skip Docker and run

#!/bin/sh
ls "$@"

Most software is fairly straightforward to install without Docker, either using a package manager like APT or filesystem-level isolation like Python’s virtual environments and Node’s node_modules directory. If you’re writing this script then Docker is just getting in your way.


In a portable shell script there’s no way to make “a list of words” in a way that keeps their individual wordiness. If you know you’ll always want to pass some troublesome options then this is still fairly straightforward: include them directly in the docker run command and don’t try to create a variable of options.

#!/bin/sh
RM_IT="--rm"
if [[ -t 0 ]]; then RM_IT="$RM_IT -i"; fi
if [[ -t 1 ]]; then RM_IT="$RM_IT -t"; fi
UID=$(id -u $(logname))
GID=$(id -g $(logname))

# We want the --rm -it options to be expanded into separate
# words; we want the volume options to stay as a single word
docker run $RM_IT "-u$UID:$GID" "-w$PWD" "-v$PWD:$PWD" \
  busybox \
  ls "$@"

Some shells like ksh, bash, and zsh have array types, but these shells may not be present on every system or environment (your busybox image doesn’t have any of these for example). You also might consider picking a higher-level scripting language that can more explicitly pass words into an exec type call.

David Maze
  • 130,717
  • 29
  • 175
  • 215
  • My real life example is a lot more complicated and Docker is a requirement to ensure reproducability for a scientific journal. Nearly there, so it would be more work to change everything now, but I see your point. – dnk8n Apr 19 '19 at 14:25
  • your approach is very helpful thank you. I will approve this as the answer (and update my final implementation) because it solves my problem and makes my script more readable. – dnk8n Apr 19 '19 at 14:38
  • @DeanKayton if you want a "generic" docker "wrapper script" that lets users issue arbitrary commands without being able to escalate privileges, check out https://gist.github.com/eqhmcow/60b7f6413036c89187727f07bce97c41 – Daniel S. Sterling Apr 20 '19 at 16:51
  • @DanielS.Sterling I don't know about allowing users to issue arbitrary commands but I do allow them to use Docker in an explicit way by setting things up like this: https://gitlab.com/snippets/1777768 . I white-list users that I want to be able to run a specific docker command by adding them to an unprivileged group. I then use `sudo visudo` to allow users part of that unprivileged group to be allowed to run the Docker command as a user belonging to the docker group. They can't do anything else with Docker other than run that specific Docker command. Would be interested in what you think. – dnk8n Apr 22 '19 at 07:51
  • Had a deeper look at the gist recommended... and I don't feel comfortable using it. Attack surface is very large and I see no tests to attempt to prove no vulnerabilities introduced. My method uses native Docker (which is tried and tested) but only allows whitelisted images, whitelisted commands. Mine is also not tried and tested but I don't incorporate much extra logic into the system. – dnk8n Apr 22 '19 at 13:18
  • @DeanKayton aye the script is just a more complex version of the whitelisting you're doing. I've made every effort to ensure it's secure but it certainly hasn't been vetted by anyone outside my work group. I would welcome any feedback or vulnerability information. obviously trusting sudo is a big part of having the script be secure; one doesn't just give suid root to a script without using sudo :) – Daniel S. Sterling May 16 '19 at 04:45
0

I'm taking a stab at this to give you something to try: Change this:

DOCKER_RUN_OPTIONS+="--rm --user=$(id -u $(logname)):$(id -g $(logname)) --workdir=${WORK_DIR} --mount type=bind,source=${WORK_DIR},target=${WORK_DIR}"

To this:

DOCKER_RUN_OPTIONS+="--rm --user=$(id -u $(logname)):$(id -g $(logname)) --workdir=${WORK_DIR} --mount type=bind,source='${WORK_DIR}',target='${WORK_DIR}'"

Essentially, we are putting the ' in there to escape the space when the $DOCKER_RUN_OPTIONS variable is evaluated by bash on the 'exec docker' command.

I haven't tried this - it's just a hunch / first shot.

Mark
  • 4,249
  • 1
  • 18
  • 27
  • I tried that, should have included that... but I have tried so many things that I have lost track. It does not work... the exec command that is returned is now: `exec docker run -i -t --rm --user=1000:1000 --workdir=/home/dean/Downloads/test_script/a folder with spaces --mount 'type=bind,source='\''/home/dean/Downloads/test_script/a' folder with 'spaces'\'',target='\''/home/dean/Downloads/test_script/a' folder with 'spaces'\''' busybox:latest ls -lah`. This makes Docker start looking for a `folder:latest` image! – dnk8n Apr 19 '19 at 14:13
  • Just noticed in your answer, you probably meant to also: `--workdir='${WORK_DIR}'` – dnk8n Apr 19 '19 at 14:15
  • the exec command that is returned is now: `exec docker run -i -t --rm --user=1000:1000 '--workdir='\''/home/dean/Downloads/test_script/a' folder with 'spaces'\''' --mount 'type=bind,source='\''/home/dean/Downloads/test_script/a' folder with 'spaces'\'',target='\''/home/dean/Downloads/test_script/a' folder with 'spaces'\''' busybox:latest ls -lah`. Docker error: `Error response from daemon: the working directory ''/home/dean/Downloads/test_script/a' is invalid, it needs to be an absolute path.` – dnk8n Apr 19 '19 at 14:17
  • same problem like in your last question. the $MOUNT string is broken by spaces from $WORK_DIR. solution is to eliminate spaces in $WORK_DIR - replace spaces with `[[:space:]]` – alecxs Apr 19 '19 at 20:11