From 66857914277b1187e861f661e716f280973b5e80 Mon Sep 17 00:00:00 2001 From: Simon L Date: Thu, 24 Aug 2023 14:09:21 +0200 Subject: [PATCH] add docker-socket-proxy as option Signed-off-by: Simon L --- .github/dependabot.yml | 9 +++ Containers/docker-socket-proxy/Dockerfile | 39 ++++++++++ Containers/docker-socket-proxy/haproxy.cfg | 72 +++++++++++++++++++ Containers/nextcloud/entrypoint.sh | 19 +++++ manual-install/update-yaml.sh | 2 + php/containers.json | 29 +++++++- php/public/disable-docker-socket-proxy.js | 4 ++ php/public/index.php | 1 + php/public/options-form-submit.js | 12 ++++ php/src/ContainerDefinitionFetcher.php | 8 +++ .../Controller/ConfigurationController.php | 5 ++ php/src/Data/ConfigurationManager.php | 15 ++++ php/src/Docker/DockerActionManager.php | 23 ++++-- php/templates/containers.twig | 5 ++ 14 files changed, 236 insertions(+), 7 deletions(-) create mode 100644 Containers/docker-socket-proxy/Dockerfile create mode 100644 Containers/docker-socket-proxy/haproxy.cfg create mode 100644 php/public/disable-docker-socket-proxy.js diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f39c749ac4a..5beec9d06f0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -165,3 +165,12 @@ updates: labels: - 3. to review - dependencies +- package-ecosystem: "docker" + directory: "/Containers/docker-socket-proxy" + schedule: + interval: "daily" + time: "12:00" + open-pull-requests-limit: 10 + labels: + - 3. to review + - dependencies diff --git a/Containers/docker-socket-proxy/Dockerfile b/Containers/docker-socket-proxy/Dockerfile new file mode 100644 index 00000000000..b9b5ef87739 --- /dev/null +++ b/Containers/docker-socket-proxy/Dockerfile @@ -0,0 +1,39 @@ +# Inspiration: https://github.com/Tecnativa/docker-socket-proxy/blob/master/Dockerfile +FROM haproxy:2.4.24-alpine3.18 + +RUN set -ex; \ + apk add --no-cache date; \ + chmod 777 -R /run/; \ + chmod 777 -R /var/lib/haproxy + +EXPOSE 2375 +ENV ALLOW_RESTARTS=1 \ + AUTH=1 \ + BUILD=0 \ + COMMIT=0 \ + CONFIGS=0 \ + CONTAINERS=1 \ + DISTRIBUTION=0 \ + EVENTS=1 \ + EXEC=0 \ + GRPC=0 \ + IMAGES=1 \ + INFO=1 \ + LOG_LEVEL=info \ + NETWORKS=1 \ + NODES=0 \ + PING=1 \ + PLUGINS=0 \ + POST=0 \ + SECRETS=0 \ + SERVICES=1 \ + SESSION=0 \ + SOCKET_PATH=/var/run/docker.sock \ + SWARM=0 \ + SYSTEM=0 \ + TASKS=0 \ + VERSION=1 \ + VOLUMES=1 +COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg + +USER haproxy:root diff --git a/Containers/docker-socket-proxy/haproxy.cfg b/Containers/docker-socket-proxy/haproxy.cfg new file mode 100644 index 00000000000..4eae253def8 --- /dev/null +++ b/Containers/docker-socket-proxy/haproxy.cfg @@ -0,0 +1,72 @@ +# Inspiration: https://github.com/Tecnativa/docker-socket-proxy/blob/master/haproxy.cfg + +global + log stdout format raw daemon "${LOG_LEVEL}" + + pidfile /run/haproxy.pid + maxconn 4000 + + # Turn on stats unix socket + server-state-file /var/lib/haproxy/server-state + +defaults + mode http + log global + option httplog + option dontlognull + option http-server-close + option redispatch + retries 3 + timeout http-request 10s + timeout queue 1m + timeout connect 10s + timeout client 10m + timeout server 10m + timeout http-keep-alive 10s + timeout check 10s + maxconn 3000 + + # Allow seamless reloads + load-server-state-from-file global + + # Use provided example error pages + errorfile 400 /usr/local/etc/haproxy/errors/400.http + errorfile 403 /usr/local/etc/haproxy/errors/403.http + errorfile 408 /usr/local/etc/haproxy/errors/408.http + errorfile 500 /usr/local/etc/haproxy/errors/500.http + errorfile 502 /usr/local/etc/haproxy/errors/502.http + errorfile 503 /usr/local/etc/haproxy/errors/503.http + errorfile 504 /usr/local/etc/haproxy/errors/504.http + +backend dockerbackend + server dockersocket $SOCKET_PATH + +frontend dockerfrontend + bind :2375 + http-request deny unless METH_GET || { env(POST) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/((stop)|(restart)|(kill)) } { env(ALLOW_RESTARTS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/auth } { env(AUTH) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/build } { env(BUILD) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/commit } { env(COMMIT) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/configs } { env(CONFIGS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers } { env(CONTAINERS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/distribution } { env(DISTRIBUTION) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/events } { env(EVENTS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/exec } { env(EXEC) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/grpc } { env(GRPC) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/images } { env(IMAGES) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/info } { env(INFO) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/networks } { env(NETWORKS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/nodes } { env(NODES) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/_ping } { env(PING) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/plugins } { env(PLUGINS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/secrets } { env(SECRETS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/services } { env(SERVICES) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/session } { env(SESSION) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/swarm } { env(SWARM) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/system } { env(SYSTEM) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/tasks } { env(TASKS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/version } { env(VERSION) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/volumes } { env(VOLUMES) -m bool } + http-request deny + default_backend dockerbackend diff --git a/Containers/nextcloud/entrypoint.sh b/Containers/nextcloud/entrypoint.sh index 5db7d152738..980f8a569b2 100644 --- a/Containers/nextcloud/entrypoint.sh +++ b/Containers/nextcloud/entrypoint.sh @@ -282,6 +282,8 @@ DATADIR_PERMISSION_CONF touch "$NEXTCLOUD_DATA_DIR/install.failed" exit 1 fi + # shellcheck disable=SC2016 + installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" fi php /var/www/html/occ app:disable updatenotification rm -rf /var/www/html/apps/updatenotification @@ -732,5 +734,22 @@ else fi fi +# Docker socket proxy +if version_greater "$installed_version" "28.0.0.0"; then + if [ "$DOCKER_SOCKET_PROXY_ENABLED" = 'yes' ]; then + if ! [ -d "/var/www/html/custom_apps/app_ecosystem_v2" ]; then + php /var/www/html/occ app:install app_ecosystem_v2 + elif [ "$(php /var/www/html/occ config:app:get app_ecosystem_v2 enabled)" != "yes" ]; then + php /var/www/html/occ app:enable app_ecosystem_v2 + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update app_ecosystem_v2 + fi + else + if [ -d "/var/www/html/custom_apps/app_ecosystem_v2" ]; then + php /var/www/html/occ app:remove app_ecosystem_v2 + fi + fi +fi + # Remove the update skip file always rm -f "$NEXTCLOUD_DATA_DIR"/skip.update diff --git a/manual-install/update-yaml.sh b/manual-install/update-yaml.sh index 39ac7c370ae..055466e04fb 100644 --- a/manual-install/update-yaml.sh +++ b/manual-install/update-yaml.sh @@ -20,6 +20,7 @@ OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].nextcloud_exec_commands)')" OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-watchtower"))')" OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-domaincheck"))')" OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-borgbackup"))')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-docker-socket-proxy"))')" OUTPUT="$(echo "$OUTPUT" | jq '.services[] |= if has("depends_on") then .depends_on |= map({ (.): { "condition": "service_started", "required": false } }) else . end' | jq '.services[] |= if has("depends_on") then .depends_on |= reduce .[] as $item ({}; . + $item) else . end')" snap install yq @@ -66,6 +67,7 @@ do sed -i "s|$variable|\${$sole_variable}|g" containers.yml done +sed -i '/DOCKER_SOCKET_PROXY_ENABLED/d' sample.conf sed -i 's|_ENABLED=|_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically.|' sample.conf sed -i 's|CLAMAV_ENABLED=no.*|CLAMAV_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. Note: arm64 has no clamav support|' sample.conf sed -i 's|TALK_ENABLED=no|TALK_ENABLED="yes"|' sample.conf diff --git a/php/containers.json b/php/containers.json index c3689f6fdb2..a380e355849 100644 --- a/php/containers.json +++ b/php/containers.json @@ -120,7 +120,8 @@ "nextcloud-aio-clamav", "nextcloud-aio-fulltextsearch", "nextcloud-aio-talk-recording", - "nextcloud-aio-imaginary" + "nextcloud-aio-imaginary", + "nextcloud-aio-docker-socket-proxy" ], "display_name": "Nextcloud", "image": "nextcloud/aio-nextcloud", @@ -203,7 +204,8 @@ "TALK_RECORDING_ENABLED=%TALK_RECORDING_ENABLED%", "RECORDING_SECRET=%RECORDING_SECRET%", "TALK_RECORDING_HOST=nextcloud-aio-talk-recording", - "FULLTEXTSEARCH_PASSWORD=%FULLTEXTSEARCH_PASSWORD%" + "FULLTEXTSEARCH_PASSWORD=%FULLTEXTSEARCH_PASSWORD%", + "DOCKER_SOCKET_PROXY_ENABLED=%DOCKER_SOCKET_PROXY_ENABLED%" ], "restart": "unless-stopped", "devices": [ @@ -639,6 +641,29 @@ "secrets": [ "FULLTEXTSEARCH_PASSWORD" ] + }, + { + "container_name": "nextcloud-aio-docker-socket-proxy", + "display_name": "Docker Socket Proxy", + "image": "nextcloud/aio-docker-socket-proxy", + "init": true, + "internal_port": "2375", + "environment": [ + "TZ=%TIMEZONE%" + ], + "volumes": [ + { + "source": "%WATCHTOWER_DOCKER_SOCKET_PATH%", + "destination": "/var/run/docker.sock", + "writeable": false + } + ], + "restart": "unless-stopped", + "read_only": true, + "tmpfs": [ + "/run/", + "/var/lib/haproxy" + ] } ] } diff --git a/php/public/disable-docker-socket-proxy.js b/php/public/disable-docker-socket-proxy.js new file mode 100644 index 00000000000..79099423348 --- /dev/null +++ b/php/public/disable-docker-socket-proxy.js @@ -0,0 +1,4 @@ +document.addEventListener("DOMContentLoaded", function(event) { + // Docker socket proxy + document.getElementById("docker-socket-proxy").disabled = true; +}); diff --git a/php/public/index.php b/php/public/index.php index de46eb411dc..7397782e341 100644 --- a/php/public/index.php +++ b/php/public/index.php @@ -121,6 +121,7 @@ 'nextcloud_memory_limit' => $configurationManager->GetNextcloudMemoryLimit(), 'is_dri_device_enabled' => $configurationManager->isDriDeviceEnabled(), 'is_talk_recording_enabled' => $configurationManager->isTalkRecordingEnabled(), + 'is_docker_socket_proxy_enabled' => $configurationManager->isDockerSocketProxyEnabled(), ]); })->setName('profile'); $app->get('/login', function (Request $request, Response $response, array $args) use ($container) { diff --git a/php/public/options-form-submit.js b/php/public/options-form-submit.js index cbd55fbabf4..a8a4411ddef 100644 --- a/php/public/options-form-submit.js +++ b/php/public/options-form-submit.js @@ -14,6 +14,13 @@ function handleTalkVisibility() { } } +function handleDockerSocketProxyWarning() { + let dockerSocketProxy = document.getElementById("docker-socket-proxy"); + if (dockerSocketProxy.checked) { + alert('⚠️ Warning! Enabling this container comes with possible Security problems since you are exposing the docker socket and all its privileges to the Nextcloud container. Enable this only if you are sure what you are doing!') + } +} + document.addEventListener("DOMContentLoaded", function(event) { // handle submit button for options form let optionsFormSubmit = document.getElementById("options-form-submit"); @@ -52,4 +59,9 @@ document.addEventListener("DOMContentLoaded", function(event) { // Fulltextsearch let fulltextsearch = document.getElementById("fulltextsearch"); fulltextsearch.addEventListener('change', makeOptionsFormSubmitVisible); + + // Docker socket proxy + let dockerSocketProxy = document.getElementById("docker-socket-proxy"); + dockerSocketProxy.addEventListener('change', makeOptionsFormSubmitVisible); + dockerSocketProxy.addEventListener('change', handleDockerSocketProxyWarning); }); diff --git a/php/src/ContainerDefinitionFetcher.php b/php/src/ContainerDefinitionFetcher.php index 14cd9c252a7..3438b889057 100644 --- a/php/src/ContainerDefinitionFetcher.php +++ b/php/src/ContainerDefinitionFetcher.php @@ -93,6 +93,10 @@ private function GetDefinition(bool $latest): array if (!$this->configurationManager->isFulltextsearchEnabled()) { continue; } + } elseif ($entry['container_name'] === 'nextcloud-aio-docker-socket-proxy') { + if (!$this->configurationManager->isDockerSocketProxyEnabled()) { + continue; + } } $ports = new ContainerPorts(); @@ -195,6 +199,10 @@ private function GetDefinition(bool $latest): array if (!$this->configurationManager->isFulltextsearchEnabled()) { continue; } + } elseif ($value === 'nextcloud-aio-docker-socket-proxy') { + if (!$this->configurationManager->isDockerSocketProxyEnabled()) { + continue; + } } $dependsOn[] = $value; } diff --git a/php/src/Controller/ConfigurationController.php b/php/src/Controller/ConfigurationController.php index 1dbb20f5005..b7271398fa0 100644 --- a/php/src/Controller/ConfigurationController.php +++ b/php/src/Controller/ConfigurationController.php @@ -110,6 +110,11 @@ public function SetConfig(Request $request, Response $response, array $args) : R } else { $this->configurationManager->SetFulltextsearchEnabledState(0); } + if (isset($request->getParsedBody()['docker-socket-proxy'])) { + $this->configurationManager->SetDockerSocketProxyEnabledState(1); + } else { + $this->configurationManager->SetDockerSocketProxyEnabledState(0); + } } if (isset($request->getParsedBody()['delete_collabora_dictionaries'])) { diff --git a/php/src/Data/ConfigurationManager.php b/php/src/Data/ConfigurationManager.php index 9bb6fbc9f30..86a04d5e808 100644 --- a/php/src/Data/ConfigurationManager.php +++ b/php/src/Data/ConfigurationManager.php @@ -149,6 +149,21 @@ public function isClamavEnabled() : bool { } } + public function isDockerSocketProxyEnabled() : bool { + $config = $this->GetConfig(); + if (isset($config['isDockerSocketProxyEnabled']) && $config['isDockerSocketProxyEnabled'] === 1) { + return true; + } else { + return false; + } + } + + public function SetDockerSocketProxyEnabledState(int $value) : void { + $config = $this->GetConfig(); + $config['isDockerSocketProxyEnabled'] = $value; + $this->WriteConfig($config); + } + public function SetClamavEnabledState(int $value) : void { $config = $this->GetConfig(); $config['isClamavEnabled'] = $value; diff --git a/php/src/Docker/DockerActionManager.php b/php/src/Docker/DockerActionManager.php index 3a3d9c61de2..0e9228d69d2 100644 --- a/php/src/Docker/DockerActionManager.php +++ b/php/src/Docker/DockerActionManager.php @@ -346,6 +346,12 @@ public function CreateContainer(Container $container) : void { } else { $replacements[1] = ''; } + } elseif ($out[1] === 'DOCKER_SOCKET_PROXY_ENABLED') { + if ($this->configurationManager->isDockerSocketProxyEnabled()) { + $replacements[1] = 'yes'; + } else { + $replacements[1] = ''; + } } elseif ($out[1] === 'NEXTCLOUD_UPLOAD_LIMIT') { $replacements[1] = $this->configurationManager->GetNextcloudUploadLimit(); } elseif ($out[1] === 'NEXTCLOUD_MEMORY_LIMIT') { @@ -406,7 +412,11 @@ public function CreateContainer(Container $container) : void { $portWithProtocol = $value->port . '/' . $value->protocol; $exposedPorts[$portWithProtocol] = null; } - $requestBody['HostConfig']['NetworkMode'] = 'nextcloud-aio'; + if ($container->GetIdentifier() !== 'nextcloud-aio-docker-socket-proxy') { + $requestBody['HostConfig']['NetworkMode'] = 'nextcloud-aio'; + } else { + $requestBody['HostConfig']['NetworkMode'] = 'nextcloud-aio-docker-socket-proxy-network'; + } } else { $requestBody['HostConfig']['NetworkMode'] = 'host'; } @@ -763,13 +773,12 @@ private function DisconnectContainerFromBridgeNetwork(string $id) : void } } - private function ConnectContainerIdToNetwork(string $id, string $internalPort) : void + private function ConnectContainerIdToNetwork(string $id, string $internalPort, string $network = 'nextcloud-aio') : void { if ($internalPort === 'host') { return; } - $network = 'nextcloud-aio'; $url = $this->BuildApiUrl('networks/create'); try { $this->guzzleClient->request( @@ -777,7 +786,7 @@ private function ConnectContainerIdToNetwork(string $id, string $internalPort) : $url, [ 'json' => [ - 'Name' => 'nextcloud-aio', + 'Name' => $network, 'CheckDuplicate' => true, 'Driver' => 'bridge', 'Internal' => false, @@ -815,13 +824,17 @@ private function ConnectContainerIdToNetwork(string $id, string $internalPort) : public function ConnectMasterContainerToNetwork() : void { $this->ConnectContainerIdToNetwork('nextcloud-aio-mastercontainer', ''); + $this->ConnectContainerIdToNetwork('nextcloud-aio-mastercontainer', '', 'nextcloud-aio-docker-socket-proxy-network'); // Don't disconnect here since it slows down the initial login by a lot. Is getting done during cron.sh instead. // $this->DisconnectContainerFromBridgeNetwork('nextcloud-aio-mastercontainer'); } public function ConnectContainerToNetwork(Container $container) : void { - $this->ConnectContainerIdToNetwork($container->GetIdentifier(), $container->GetInternalPort()); + $this->ConnectContainerIdToNetwork($container->GetIdentifier(), $container->GetInternalPort()); + if ($container->GetIdentifier() === 'nextcloud-aio-nextcloud' || $container->GetIdentifier() === 'nextcloud-aio-docker-socket-proxy') { + $this->ConnectContainerIdToNetwork($container->GetIdentifier(), $container->GetInternalPort(), 'nextcloud-aio-docker-socket-proxy-network'); + } } public function StopContainer(Container $container) : void { diff --git a/php/templates/containers.twig b/php/templates/containers.twig index e47baff498a..0eab3f36e0f 100644 --- a/php/templates/containers.twig +++ b/php/templates/containers.twig @@ -574,6 +574,11 @@ {% else %} {#
#} {% endif %} + {% if is_docker_socket_proxy_enabled == true %} +

+ {% else %} +

+ {% endif %}