add docker-socket-proxy as option

Signed-off-by: Simon L <szaimen@e.mail.de>
This commit is contained in:
Simon L 2023-08-24 14:09:21 +02:00
parent 7e5fe5ac8e
commit 6685791427
14 changed files with 236 additions and 7 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"
]
}
]
}

View file

@ -0,0 +1,4 @@
document.addEventListener("DOMContentLoaded", function(event) {
// Docker socket proxy
document.getElementById("docker-socket-proxy").disabled = true;
});

View file

@ -121,6 +121,7 @@ $app->get('/containers', function (Request $request, Response $response, array $
'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) {

View file

@ -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);
});

View file

@ -93,6 +93,10 @@ class ContainerDefinitionFetcher
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 @@ class ContainerDefinitionFetcher
if (!$this->configurationManager->isFulltextsearchEnabled()) {
continue;
}
} elseif ($value === 'nextcloud-aio-docker-socket-proxy') {
if (!$this->configurationManager->isDockerSocketProxyEnabled()) {
continue;
}
}
$dependsOn[] = $value;
}

View file

@ -110,6 +110,11 @@ class ConfigurationController
} 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'])) {

View file

@ -149,6 +149,21 @@ class ConfigurationManager
}
}
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;

View file

@ -346,6 +346,12 @@ class DockerActionManager
} 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 @@ class DockerActionManager
$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 @@ class DockerActionManager
}
}
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 @@ class DockerActionManager
$url,
[
'json' => [
'Name' => 'nextcloud-aio',
'Name' => $network,
'CheckDuplicate' => true,
'Driver' => 'bridge',
'Internal' => false,
@ -815,13 +824,17 @@ class DockerActionManager
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 {

View file

@ -574,6 +574,11 @@
{% else %}
{#<input type="checkbox" id="onlyoffice" name="onlyoffice"><label for="onlyoffice">OnlyOffice</label><br>#}
{% endif %}
{% if is_docker_socket_proxy_enabled == true %}
<input type="checkbox" id="docker-socket-proxy" name="docker-socket-proxy" checked="checked"><label for="docker-socket-proxy">Docker Socket Proxy (needed for app-ecosystem v2)</label><br><br>
{% else %}
<input type="checkbox" id="docker-socket-proxy" name="docker-socket-proxy"><label for="docker-socket-proxy">Docker Socket Proxy (needed for app-ecosystem v2)</label><br><br>
{% endif %}
<input id="options-form-submit" class="button" type="submit" value="Save changes" />
<script type="text/javascript" src="options-form-submit.js"></script>
</form>