Secure your docker socket by proxying it and protecting it from unlimited access

Photo by Markus Winkler / Unsplash

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

Containers Dec 5, 2022

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:[email protected]$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 fully functioning proxy to protect your docker.sock and you can make the above changes to your associated compose files as necessary.


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

PTS

With very limited knowledge, PTS fell down the selfhosted rabbit hole after buying his first NAS in October 2020. You can find him on the Synology discord server (click the icon below)

Have some feedback or something to add? Comments are welcome!

Please note comments should be respectful, and may be moderated