From 74fb0153662cf0974b5ddbe8c280b89c853e686d Mon Sep 17 00:00:00 2001 From: bunkerity Date: Wed, 2 Jun 2021 12:12:30 +0200 Subject: [PATCH] web UI - init work on using docker-socket-proxy --- docs/quickstart_guide.md | 5 ++--- docs/security_tuning.md | 31 ++++++++++++++++++++++++++++++ examples/web-ui/docker-compose.yml | 15 +++++++++++++-- ui/Docker.py | 4 ++-- ui/entrypoint.py | 15 +++++++++------ 5 files changed, 57 insertions(+), 13 deletions(-) diff --git a/docs/quickstart_guide.md b/docs/quickstart_guide.md index 61230e1..e104060 100644 --- a/docs/quickstart_guide.md +++ b/docs/quickstart_guide.md @@ -294,8 +294,6 @@ docker service create --name anotherapp \ ## Web UI -**This feature exposes, for now, a security risk because you need to mount the docker socket inside a container exposing a web application. You can test it but you should not use it in servers facing the internet.** - A dedicated image, *bunkerized-nginx-ui*, lets you manage bunkerized-nginx instances and services configurations through a web user interface. This feature is still in beta, feel free to open a new issue if you find a bug and/or you have an idea to improve it. First we need a volume that will store the configurations : @@ -320,6 +318,7 @@ docker run -p 80:8080 \ -e AUTO_LETS_ENCRYPT=yes \ -e REDIRECT_HTTP_TO_HTTPS=yes \ -e DISABLE_DEFAULT_SERVER=yes \ + -e admin.domain.com_USE_MODSECURITY=no \ -e admin.domain.com_SERVE_FILES=no \ -e admin.domain.com_USE_AUTH_BASIC=yes \ -e admin.domain.com_AUTH_BASIC_USER=admin \ @@ -331,7 +330,7 @@ docker run -p 80:8080 \ bunkerity/bunkerized-nginx ``` -The `AUTH_BASIC` environment variables let you define a login/password that must be provided before accessing to the web UI. At the moment, there is no authentication mechanism integrated into bunkerized-nginx-ui. +The `AUTH_BASIC` environment variables let you define a login/password that must be provided before accessing to the web UI. At the moment, there is no authentication mechanism integrated into bunkerized-nginx-ui so **using auth basic with a strong password coupled with a "hard to guess" URI is strongly recommended**. We can now create the bunkerized-nginx-ui container that will host the web UI behind bunkerized-nginx : diff --git a/docs/security_tuning.md b/docs/security_tuning.md index 46cc237..f6d6a9a 100644 --- a/docs/security_tuning.md +++ b/docs/security_tuning.md @@ -211,6 +211,37 @@ Here is the list of related environment variables and their default value : - `USE_BLACKLIST_REVERSE=yes` : enable/disable blacklisting by reverse DNS - `BLACKLIST_REVERSE_LIST=.shodan.io` : the list of reverse DNS suffixes to never trust +## Web UI + +Mounting the docker socket in a container which is facing the network, like we do with the [web UI](https://bunkerized-nginx.readthedocs.io/en/latest/quickstart_guide.html#web-ui), is not a good security practice. In case of a vulnerability inside the application, attackers can freely use the Docker socket and the whole host can be compromised. + +A possible workaround is to use the [tecnativa/docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy) image which acts as a reverse proxy between the application and the Docker socket. It can allow/deny the requests made to the Docker API. + +Before starting the web UI, you need to fire up the docker-socket-proxy (we also need a network because of inter-container communication) : + +```shell +docker network create mynet +``` + +```shell +docker run --name mysocketproxy \ + --network mynet \ + -v /var/run/docker.sock:/var/run/docker.sock:ro \ + -e POST=1 \ + -e CONTAINERS=1 \ + tecnativa/docker-socket-proxy +``` + +You can now start the web UI container and use the `DOCKER_HOST` environment variable to define the Docker API endpoint : + +```shell +docker run --network mynet \ + -v autoconf:/etc/nginx \ + -e ABSOLUTE_URI=https://my.webapp.com/admin/ \ + -e DOCKER_HOST=tcp://mysocketproxy:2375 \ + bunkerity/bunkerized-nginx-ui +``` + ## Container hardening ### Drop capabilities diff --git a/examples/web-ui/docker-compose.yml b/examples/web-ui/docker-compose.yml index 40da258..17e60eb 100644 --- a/examples/web-ui/docker-compose.yml +++ b/examples/web-ui/docker-compose.yml @@ -12,7 +12,6 @@ services: # don't forget to edit the permissions of the files and folders accordingly volumes: - ./letsencrypt:/etc/letsencrypt - - ./web-files:/www:ro - autoconf:/etc/nginx environment: - SERVER_NAME=admin.website.com # replace with your domain @@ -36,11 +35,23 @@ services: myui: image: bunkerity/bunkerized-nginx-ui restart: always + depends_on: + - mywww + - myuiproxy volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - autoconf:/etc/nginx environment: - ABSOLUTE_URI=https://admin.website.com/admin/ # change it to your full URI + - DOCKER_HOST=tcp://myuiproxy:2375 + + myuiproxy: + image: tecnativa/docker-socket-proxy + restart: always + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + environment: + - POST=1 + - CONTAINERS=1 volumes: autoconf: diff --git a/ui/Docker.py b/ui/Docker.py index ad5cfa3..2facce6 100644 --- a/ui/Docker.py +++ b/ui/Docker.py @@ -2,8 +2,8 @@ import docker class Docker : - def __init__(self) : - self.__client = docker.DockerClient(base_url='unix:///var/run/docker.sock') + def __init__(self, docker_host) : + self.__client = docker.DockerClient(base_url=docker_host) def get_instances(self) : return self.__client.containers.list(all=True, filters={"label" : "bunkerized-nginx.UI"}) diff --git a/ui/entrypoint.py b/ui/entrypoint.py index 1d746d8..a56fb5e 100644 --- a/ui/entrypoint.py +++ b/ui/entrypoint.py @@ -11,8 +11,11 @@ app = Flask(__name__, static_url_path="/", static_folder="static", template_fold ABSOLUTE_URI = "" if "ABSOLUTE_URI" in os.environ : ABSOLUTE_URI = os.environ["ABSOLUTE_URI"] +DOCKER_HOST = "unix:///var/run/docker.sock" +if "DOCKER_HOST" in os.environ : + DOCKER_HOST = os.environ["DOCKER_HOST"] app.config["ABSOLUTE_URI"] = ABSOLUTE_URI -app.config["DOCKER"] = Docker() +app.config["DOCKER"] = Docker(DOCKER_HOST) app.config["CONFIG"] = Config() app.jinja_env.globals.update(env_to_summary_class=utils.env_to_summary_class) app.jinja_env.globals.update(form_service_gen=utils.form_service_gen) @@ -46,15 +49,15 @@ def instances() : # Do the operation if request.form["operation"] == "reload" : - operation = app.config["DOCKER"].reload(request_form["INSTANCE_ID"]) + operation = app.config["DOCKER"].reload_instance(request.form["INSTANCE_ID"]) elif request.form["operation"] == "start" : - operation = app.config["DOCKER"].start(request_form["INSTANCE_ID"]) + operation = app.config["DOCKER"].start_instance(request.form["INSTANCE_ID"]) elif request.form["operation"] == "stop" : - operation = app.config["DOCKER"].stop(request_form["INSTANCE_ID"]) + operation = app.config["DOCKER"].stop_instance(request.form["INSTANCE_ID"]) elif request.form["operation"] == "restart" : - operation = app.config["DOCKER"].restart(request_form["INSTANCE_ID"]) + operation = app.config["DOCKER"].restart_instance(request.form["INSTANCE_ID"]) elif request.form["operation"] == "delete" : - operation = app.config["DOCKER"].delete(request_form["INSTANCE_ID"]) + operation = app.config["DOCKER"].delete_instance(request.form["INSTANCE_ID"]) # Display instances instances = app.config["DOCKER"].get_instances()