Wearing your docker.sock on your sleeve? Use a proxy instead

Secure your docker socket by proxying it and protecting it from unlimited access
Photo by Markus Winkler / Unsplash

Certain containers need access to your docker socket, which allows them to view and have access to all docker containers and settings. Portainer is a great example of this.

One way of allowing this is to map the following volume to the container:

volumes:
  - /var/run/docker.sock:/var/run/docker.sock:ro

But doing this, especially in a publicly accessible container, is a security risk.

Instead we use a socket proxy to limit access and keep our system protected, and for that, you guessed it, we need to create another container. Luckily this works pretty easily with minimal fuss.


Prerequisites

  • Docker and docker-compose installed on your machine
  • sudo privileges to be able to run docker commands as root, or you've added your user to the docker group to bypass that
  • The ability to SSH / use CLI/terminal on your machine, or use Portainer to spin up your stacks

Creating the container

  • Create your docker-compose.yml file, and then copy the below into it:
networks:
  socket_proxy:
    name: socket_proxy
    ipam:
      config:
        - subnet: 172.100.0.0/24 #change subnet as necessary

services:
  socket-proxy:
    container_name: socket-proxy
    image: tecnativa/docker-socket-proxy
    restart: always
    networks:
      - socket_proxy
    # privileged: true # true for VM. False for unprivileged LXC container.
    ports:
    #  - "127.0.0.1:2375:2375" # Port 2375 should only ever get exposed to the internal network. When possible use this line.
    # I use the next line instead, as I want portainer to manage multiple docker endpoints within my home network.
     - "2375:2375"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
    environment:
      - LOG_LEVEL=info # debug,info,notice,warning,err,crit,alert,emerg
      ## Variables match the URL prefix (i.e. AUTH blocks access to /auth/* parts of the API, etc.).
      # 0 to revoke access.
      # 1 to grant access.
      ## Granted by Default
      - EVENTS=1
      - PING=1
      - VERSION=1
      ## Revoked by Default
      # Security critical
      - AUTH=0
      - SECRETS=0
      - POST=1 # Watchtower
      # Not always needed
      - BUILD=0
      - COMMIT=0
      - CONFIGS=0
      - CONTAINERS=1 # Traefik, portainer, etc.
      - DISTRIBUTION=0
      - EXEC=0
      - IMAGES=1 # Portainer
      - INFO=1 # Portainer
      - NETWORKS=1 # Portainer
      - NODES=0
      - PLUGINS=0
      - SERVICES=1 # Portainer
      - SESSION=0
      - SWARM=0
      - SYSTEM=0
      - TASKS=1 # Portainer
      - VOLUMES=1 # Portainer

What are we doing here?

  1. We're creating a docker bridge network called socket_proxy
  2. We're creating a container called socket-proxy and connecting it to our docker network
  3. We're making this container accessible at port 2375
  4. I want it to always restart whenever docker or the system restarts, as other containers depend on it
  5. We are giving it access to and specifying which parts of the docker.sock it can read (0 = no access, 1 = access)
  • Spin up the container using Portainer or your terminal (docker-compose up -d)

Connecting a container to the socket proxy

Now that the container is up and running, we can use it in place of the volume mapping mentioned above for those containers needing docker socket access.

To do this, we need to add one env variable, and connect the container in question to the socket_proxy docker network. Let's take Watchtower as an example, and the following docker-compose.yml for it:

networks:
  default:
    name: socket_proxy
    external: true

services:
  watchtower: #automatic container version monitoring and updating
    container_name: watchtower
    image: containrrr/watchtower:latest-dev
    environment: 
      - TZ=$TZ
      - DEBUG=true
      - WATCHTOWER_LABEL_ENABLE=true
      - WATCHTOWER_CLEANUP=true
      - WATCHTOWER_INCLUDE_RESTARTING=true
      - WATCHTOWER_INCLUDE_STOPPED=true
      - WATCHTOWER_NOTIFICATIONS=shoutrrr
      - WATCHTOWER_NOTIFICATION_URL=pushover://shoutrrr:$PUSHAPI@$PUSHKEY/?devices=$PUSHDEVICE
      - DOCKER_HOST=tcp://socket-proxy:2375
    command: --interval 21600
    restart: unless-stopped
    volumes:
      - $LOCALTIME
    networks:
      - socket_proxy
  • Note the DOCKER_HOST in the environment block, and the networks
  • This assumes the socket-proxy container is in a different docker-compose file to Watchtower. If they were in the same compose file, you could simply write DOCKER_HOST=[servicename], so in our case, DOCKER_HOST=socket-proxy

You now have a functioning proxy to protect your docker.sock and you can make the relevant changes to your associated compose files as necessary.


Not all containers are the same

Not all containers are created equal. This means that while the above should work for most of your containers, not all will necessarily be able to read the - DOCKER_HOST line. Some may require a command, so read the documenation for them or on the socket proxy github repo.

Portainer

Something like Portainer for instance requires a bit of a change to the compose file.

Just above the environment variable, add the following:

command: -H tcp://socket-proxy:2375

Then go into the gui and make a change to the environment endpoint:

  • Either modify your existing environment or add a new one and delete the old one. In any case, your final page should look something like this:
  • Name can be anything
  • The URL is your socket-proxy container name and the container IP
  • Your public IP is your host machine's IP
  • Don't forget to ensure your Portainer container is on the same docker network as your socket proxy container

Hopefully the above has given you a good start on protecting your docker socket from prying eyes. It's not a catch-all, but for those containers with which it doesn't work directly out the box you can normally find documentation to help you out.


Swag, Authelia and Reverse Proxies
A step-by-step walkthrough to self-host your Reverse Proxy with SWAG, and providing SSO and 2FA security using Authelia, all in docker
Portainer - Easy Container Management for Docker
A step-by-step docker walkthrough to installing and configuring Portainer, your one-stop container-management resource