How do I prevent root access to my docker container

11,701

Solution 1

As David mentions, once someone has access to the docker socket (either via API or with the docker CLI), that typically means they have root access to your host. It's trivial to use that access to run a privileged container with host namespaces and volume mounts that let the attacker do just about anything.

When you need to initialize a container with steps that run as root, I do recommend gosu over something like su since su was not designed for containers and will leave a process running as the root pid. Make sure that you exec the call to gosu and that will eliminate anything running as root. However, the user you start the container as is the same as the user used for docker exec, and since you need to start as root, your exec will run as root unless you override it with a -u flag.

There are additional steps you can take to lock down docker in general:

  1. Use user namespaces. These are defined on the entire daemon, require that you destroy all containers, and pull images again, since the uid mapping affects the storage of image layers. The user namespace offsets the uid's used by docker so that root inside the container is not root on the host, while inside the container you can still bind to low numbered ports and run administrative activities.

  2. Consider authz plugins. Open policy agent and Twistlock are two that I know of, though I don't know if either would allow you to restrict the user of a docker exec command. They likely require that you give users a certificate to connect to docker rather than giving them direct access to the docker socket since the socket doesn't have any user details included in API requests it receives.

  3. Consider rootless docker. This is still experimental, but since docker is not running as root, it has no access back to the host to perform root activities, mitigating many of the issues seen when containers are run as root.

Solution 2

You intrinsically can't prevent root-level access to your container.

Anyone who can run any Docker command at all can always run any of these three commands:

# Get a shell, as root, in a running container
docker exec -it -u 0 container_name /bin/sh

# Launch a new container, running a root shell, on some image
docker run --rm -it -u 0 --entrypoint /bin/sh image_name

# Get an interactive shell with unrestricted root access to the host
# filesystem (cd /host/var/lib/docker)
docker run --rm -it -v /:/host busybox /bin/sh

It is generally considered best practice to run your container as a non-root user, either with a USER directive in the Dockerfile or running something like gosu in an entrypoint script, like what you show. You can't prevent root access, though, in the face of a privileged user who's sufficiently interested in getting it.

Share:
11,701
Sean
Author by

Sean

Updated on June 13, 2022

Comments

  • Sean
    Sean almost 2 years

    I am working on hardening our docker images, which I already have a bit of a weak understanding of. With that being said, the current step I am on is preventing the user from running the container as root. To me, that says "when a user runs 'docker exec -it my-container bash', he shall be an unprivileged user" (correct me if I'm wrong).

    When I start up my container via docker-compose, the start script that is run needs to be as root since it deals with importing certs and mounted files (created externally and seen through a volume mount). After that is done, I would like the user to be 'appuser' for any future access. This question seems to match pretty well what I'm looking for, but I am using docker-compose, not docker run: How to disable the root access of a docker container?

    This seems to be relevant, as the startup command differs from let's say tomcat. We are running a Spring Boot application that we start up with a simple 'java -jar jarFile', and the image is built using maven's dockerfile-maven-plugin. With that being said, should I be changing the user to an unprivileged user before running that, or still after?

    I believe changing the user inside of the Dockerfile instead of the start script will do this... but then it will not run the start script as root, thus blowing up on calls that require root. I had messed with using ENTRYPOINT as well, but could have been doing it wrong there. Similarly, using "user:" in the yml file seemed to make the start.sh script run as that user instead of root, so that wasn't working.

    Dockerfile:

    FROM parent/image:latest
    
    ENV APP_HOME                            /apphome
    ENV APP_USER                            appuser
    ENV APP_GROUP                           appgroup
    
    # Folder containing our application, i.e. jar file, resources, and scripts.
    # This comes from unpacking our maven dependency
    ADD target/classes/app ${APP_HOME}/
    
    # Primarily just our start script, but some others
    ADD target/classes/scripts /scripts/
    
    # Need to create a folder that will be used at runtime
    RUN mkdir -p ${APP_HOME}/data && \
        chmod +x /scripts/*.sh && \
        chmod +x ${APP_HOME}/*.*
    
    # Create unprivileged user
    RUN groupadd -r ${APP_GROUP} && \
        useradd -g ${APP_GROUP} -d ${APP_HOME} -s /sbin/nologin  -c "Unprivileged User" ${APP_USER} && \
        chown -R ${APP_USER}:${APP_GROUP} ${APP_HOME}
    
    WORKDIR $APP_HOME
    
    EXPOSE 8443
    
    CMD /opt/scripts/start.sh
    

    start.sh script:

    #!/bin/bash
    
    # setup SSL, modify java command, etc
    
    # run our java application
    java -jar "boot.jar"
    
    # Switch users to always be unprivileged from here on out? 
    # Whatever "hardening" wants...  Should this be before starting our application?
    exec su -s "/bin/bash" $APP_USER
    

    app.yml file:

    version: '3.3'
    
    services:
      app:
        image: app_image:latest
        labels:
          c2core.docker.compose.display-name: My Application
          c2core.docker.compose.profiles: a_profile
        volumes:
          - "data_mount:/apphome/data"
          - "cert_mount:/certs"
        hostname: some-hostname
        domainname: some-domain
        ports:
        - "8243:8443"
        environment:
          - some_env_vars
        depends_on:
        - another-app
        networks:
          a_network:
            aliases:
              - some-network
    networks:
      a_network:
        driver: bridge
    volumes:
      data_mount:
      cert_mount:
    

    docker-compose shell script:

    docker-compose -f app.yml -f another-app.yml $@
    

    What I would expect is that anyone trying to access the container internally will be doing so as appuser and not root. The goal is to prevent someone from messing with things they shouldn't (i.e. docker itself).

    What is happening is that the script will change users after the app has started (proven via an echo command), but it doesn't seem to be maintained. If I exec into it, I'm still root.

  • Sean
    Sean over 4 years
    well dang. This leads me to another question then. I change the su command to 'exec /usr/local/bin/gosu $appUser "$@"'. What does that actually do for me? if the startup script is still run as root, the file system all has the same ownership and permissions, and you still exec into the container as root anyway, what security does this add? I guess I just don't fully understand what "running as a non-root user" means/does.
  • BMitch
    BMitch over 4 years
    Trying to protect to root account while still giving users access to docker running as root is likely to fail. It's trivial to bypass .bashrc with a non-login command run remotely. And a setuid shell script can be used to run any command by adjusting the path and putting a fake docker command that just gives you a root shell.
  • someonewithpc
    someonewithpc about 3 years
    Generally not a great idea, it's a risky proposition to try to filter safe things
  • jlim
    jlim about 3 years
    @someonewithpc, not sure what is the risk you are referring. The whole idea is to provide sudo with limited execution. This works perfectly in our implementation. Although developers has sudo rights, but they can only run a subset of commands and we wrapped it in a shell script. In this case docker-cli. We can do alot of things in the script to control how developers should run docker command. We tried twistlock and opa policy, those are great authz plugins, but doesn't provide granular permission what we allow for the developer. By wrapping it in docker-cli, we can do alot of things