From 0959353134699f98733cf7a5ead6c231f1850d13 Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Wed, 24 Jan 2024 18:34:34 +0300 Subject: [PATCH] Adjust rate limits (#5) Signed-off-by: Alexander Piskun Co-authored-by: Alexander Piskun --- Dockerfile | 5 ++-- README.md | 66 ++++++++++++++++++++++++++++++++++++++------- haproxy.cfg | 26 ++++++++++-------- haproxy_ex_apps.cfg | 28 ++++++++++--------- start.sh | 17 ++++++------ tests/test_basic.py | 50 +++++++++++++--------------------- 6 files changed, 118 insertions(+), 74 deletions(-) diff --git a/Dockerfile b/Dockerfile index 00aaf73..9dd28ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,8 @@ FROM haproxy:2.9.2-alpine3.19 USER root ENV HAPROXY_PORT 2375 -ENV EX_APPS_NET "localhost" +ENV BIND_ADDRESS * +ENV EX_APPS_NET_FOR_HTTPS "localhost" RUN set -ex; \ apk add --no-cache \ @@ -21,4 +22,4 @@ COPY --chmod=664 haproxy_ex_apps.cfg /haproxy_ex_apps.cfg WORKDIR / ENTRYPOINT ["/bin/bash", "start.sh"] -HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD /healthcheck.sh +HEALTHCHECK --interval=10s --timeout=10s --retries=9 CMD /healthcheck.sh diff --git a/README.md b/README.md index 8ae86f4..208b410 100644 --- a/README.md +++ b/README.md @@ -29,33 +29,43 @@ Instead of `some_secure_password` you put your password that later you should pr ### Docker with TLS +In this case ExApps will only map host's loopback adapter, and will be avalaible to Nextcloud only throw HaProxy. + ```shell docker run -e NC_HAPROXY_PASSWORD="some_secure_password" \ + -e BIND_ADDRESS="x.y.z.z" -v /var/run/docker.sock:/var/run/docker.sock \ -v `pwd`/certs/cert.pem:/certs/cert.pem \ - --name aa-docker-socket-proxy -h aa-docker-socket-proxy \ + --name aa-docker-socket-proxy -h aa-docker-socket-proxy --net host \ --restart unless-stopped --privileged -d ghcr.io/cloud-py-api/aa-docker-socket-proxy:release ``` -Here in addition we map certificate file from host with SSL certificate that will be used by HaProxy. +Here in addition we map certificate file from host with SSL certificate that will be used by HaProxy and specify to use the `host` network. + +You should set `BIND_ADDRESS` to the IP on which server with ExApps can accept requests coming from the Nextcloud instance. + +*This is necessary when using the “host” network so as not to occupy all interfaces, because ExApp will use loopback adapter.* > [!WARNING] > If the certificates are self-signed, your job is to add them to the Nextcloud instance so that AppAPI can recognize them. ### AppAPI -1. Create a daemon from the `Docker Socket Proxy` or `Docker Socket Proxy Remote` template in AppAPI. +1. Create a daemon from the `Docker Socket Proxy` template in AppAPI. 2. Fill the password you used during container creation. -3. If `Docker Socket Proxy Remote` is used you need to specify the IP/DNS of the created HaProxy. ### Additionally supported variables `HAPROXY_PORT`: using of custom port instead of **2375** which is the default one. -`EX_APPS_NET`: only for custom remote ExApp installs with TLS, determines destination of requests to ExApps for HaProxy. +`BIND_ADDRESS`: the address to use for port binding. (Usually needed only for remote installs, **must be accessible from the Nextcloud**) + +`EX_APPS_NET_FOR_HTTPS`: only for custom remote ExApp installs with TLS, determines destination of requests to ExApps for HaProxy. ## Development +### HTTP(local) + To build image locally use: ```shell @@ -65,16 +75,54 @@ docker build -f ./Dockerfile -t aa-docker-socket-proxy:latest ./ Deploy image(for `nextcloud-docker-dev`): ```shell -docker run -e NC_HAPROXY_PASSWORD="some_secure_password" -v /var/run/docker.sock:/var/run/docker.sock \ ---name aa-docker-socket-proxy -h aa-docker-socket-proxy --net master_default --privileged -d aa-docker-socket-proxy:latest +docker run -e NC_HAPROXY_PASSWORD="some_secure_password" \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --name aa-docker-socket-proxy -h aa-docker-socket-proxy --net master_default \ + --privileged -d aa-docker-socket-proxy:latest ``` -If you need create Self-Signed cert for tests: +After that create daemon in AppAPI from the Docker Socket Proxy template, specifying: +1. Host: `aa-docker-socket-proxy:2375` +2. Network in Deploy Config equal to `master_default` +3. Deploy Config: HaProxy password: `some_secure_password` + +### HTTPS(remote) + +We will emulate remote deployment still with `nextcloud-docker-dev` setup. +For this we deploy `aa-docker-socket-proxy` to host network and reach it using `host.docker.internal`. + +> [!NOTE] +> Due to current Docker limitations, this setup type is not working on macOS. +> Ref issue: [Support Host Network for macOS](https://github.com/docker/roadmap/issues/238) + +First create Self-Signed cert for tests: ```shell -openssl req -nodes -new -x509 -subj '/CN=*' -sha256 -keyout certs/privkey.pem -out certs/fullchain.pem -days 365000 > /dev/null 2>&1 +openssl req -nodes -new -x509 -subj '/CN=host.docker.internal' -sha256 -keyout certs/privkey.pem -out certs/fullchain.pem -days 365000 > /dev/null 2>&1 ``` ```shell cat certs/fullchain.pem certs/privkey.pem | tee certs/cert.pem > /dev/null 2>&1 ``` + +Place `cert.pem` into `data/shared` folder of `nextcloud-docker-dev` and execute inside Nextcloud container: + +```shell +sudo -u www-data php occ security:certificates:import /shared/cert.pem +``` + +Create HaProxy container: + +```shell +docker run -e NC_HAPROXY_PASSWORD="some_secure_password" \ + -e BIND_ADDRESS="172.17.0.1" \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v `pwd`/certs/cert.pem:/certs/cert.pem \ + --name aa-docker-socket-proxy -h aa-docker-socket-proxy --net host \ + --privileged -d aa-docker-socket-proxy:latest +``` + +After that create daemon in AppAPI from the Docker Socket Proxy template, with next parameters: +1. Host: `host.docker.internal:2375` +2. Tick `https` checkbox. +3. Deploy Config: HaProxy password: `some_secure_password` diff --git a/haproxy.cfg b/haproxy.cfg index bbbddf2..be89f8c 100644 --- a/haproxy.cfg +++ b/haproxy.cfg @@ -19,20 +19,24 @@ userlist app_api_credentials frontend docker_engine mode http - BIND_DOCKER_PLACEHOLDER + BIND_ADDRESS_PLACEHOLDER - # Rate limiting - stick-table type ip size 1m expire 1440m store http_err_cnt,http_err_rate(60m) - # ACL to restrict rate limited request - acl acl-www-err-rate sc_http_err_rate(0) gt 5 - acl acl-www-err-total sc_http_err_cnt(0) gt 10 + stick-table type ip size 100k expire 144m store gpc0,http_req_rate(5m) - http-request track-sc0 src - http-request deny if acl-www-err-total - http-request silent-drop if acl-www-err-rate + # Perform Basic Auth + acl valid_credentials http_auth(app_api_credentials) - # Basic Authentication - http-request auth unless { http_auth(app_api_credentials) } + # Increase counter on failed authentication + http-request track-sc0 src if ! valid_credentials + http-request sc-inc-gpc0(0) if ! valid_credentials + + # Check if the client IP has more than 5 failed attempts in the last 5 minutes + acl too_many_auth_failures sc0_http_req_rate gt 5 + + # Use 'silent-drop' to drop the connection without a response + http-request silent-drop if too_many_auth_failures + + http-request auth realm AppAPI unless valid_credentials # docker system _ping http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/_ping } METH_GET diff --git a/haproxy_ex_apps.cfg b/haproxy_ex_apps.cfg index 0af2572..42a7e9d 100644 --- a/haproxy_ex_apps.cfg +++ b/haproxy_ex_apps.cfg @@ -1,19 +1,23 @@ frontend ex_apps mode http - bind *:23000-23999 v4v6 ssl crt /certs/cert.pem + BIND_ADDRESS_PLACEHOLDER - # Rate limiting - stick-table type ip size 1m expire 1440m store http_err_cnt,http_err_rate(60m) - # ACL to restrict rate limited request - acl acl-www-err-rate sc_http_err_rate(0) gt 5 - acl acl-www-err-total sc_http_err_cnt(0) gt 10 + stick-table type ip size 100k expire 144m store gpc0,http_req_rate(5m) - http-request track-sc0 src - http-request deny if acl-www-err-total - http-request silent-drop if acl-www-err-rate + # Perform Basic Auth + acl valid_credentials http_auth(app_api_credentials) - # Basic Authentication - http-request auth unless { http_auth(app_api_credentials) } + # Increase counter on failed authentication + http-request track-sc0 src if ! valid_credentials + http-request sc-inc-gpc0(0) if ! valid_credentials + + # Check if the client IP has more than 5 failed attempts in the last 5 minutes + acl too_many_auth_failures sc0_http_req_rate gt 5 + + # Use 'silent-drop' to drop the connection without a response + http-request silent-drop if too_many_auth_failures + + http-request auth realm AppAPI unless valid_credentials # We allow anything for ExApps http-request allow @@ -21,4 +25,4 @@ frontend ex_apps backend bk_ex_apps mode http - server ex_apps EX_APPS_NET_PLACEHOLDER + server ex_apps EX_APPS_NET_FOR_HTTPS_PLACEHOLDER diff --git a/start.sh b/start.sh index 3e62cd1..10f2c94 100644 --- a/start.sh +++ b/start.sh @@ -1,23 +1,24 @@ #!/bin/sh -set -x -HAPROXYFILE="$(sed "s|NC_PASSWORD_PLACEHOLDER|$NC_HAPROXY_PASSWORD|" /haproxy.cfg)" -HAPROXYFILE="$(echo "$HAPROXYFILE" | sed "s|HAPROXY_PORT_PLACEHOLDER|$HAPROXY_PORT|")" +sed -i "s|NC_PASSWORD_PLACEHOLDER|$NC_HAPROXY_PASSWORD|" /haproxy.cfg if [ -f "/certs/cert.pem" ]; then - HAPROXYFILE="$(echo "$HAPROXYFILE" | sed "s|BIND_DOCKER_PLACEHOLDER|bind *:$HAPROXY_PORT v4v6 ssl crt /certs/cert.pem|")" - sed -i "s|EX_APPS_NET_PLACEHOLDER|$EX_APPS_NET|" /haproxy_ex_apps.cfg + sed -i "s|BIND_ADDRESS_PLACEHOLDER|bind $BIND_ADDRESS:$HAPROXY_PORT v4v6 ssl crt /certs/cert.pem|" /haproxy.cfg + sed -i "s|BIND_ADDRESS_PLACEHOLDER|bind $BIND_ADDRESS:23000-23999 v4v6 ssl crt /certs/cert.pem|" /haproxy_ex_apps.cfg + sed -i "s|EX_APPS_NET_FOR_HTTPS_PLACEHOLDER|$EX_APPS_NET_FOR_HTTPS|" /haproxy_ex_apps.cfg # Chmod certs to be accessible by haproxy chmod 644 /certs/cert.pem else - HAPROXYFILE="$(echo "$HAPROXYFILE" | sed "s|BIND_DOCKER_PLACEHOLDER|bind *:$HAPROXY_PORT v4v6|")" + sed -i "s|BIND_ADDRESS_PLACEHOLDER|bind $BIND_ADDRESS:$HAPROXY_PORT v4v6|" /haproxy.cfg fi -echo "$HAPROXYFILE" > /haproxy.cfg -set +x +echo "HaProxy config:" if [ -f "/certs/cert.pem" ]; then + cat /haproxy.cfg + cat /haproxy_ex_apps.cfg haproxy -f /haproxy.cfg -f /haproxy_ex_apps.cfg -db else + cat /haproxy.cfg haproxy -f /haproxy.cfg -db fi diff --git a/tests/test_basic.py b/tests/test_basic.py index 826df6c..e336269 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -5,7 +5,7 @@ def test_ping_spam(): client = httpx.Client(base_url="http://localhost:2375", auth=("app_api_haproxy_user", "some_secure_password")) - for i in range(60): + for i in range(100): r = client.get("_ping") assert r.status_code == 200 @@ -23,47 +23,33 @@ def test_volume_creation_removal(): def test_volume_creation_removal_invalid(): volume_name = "app_test_data" - with pytest.raises(httpx.ReadTimeout): - for i in range(9): - r = httpx.post( - "http://localhost:2375/volumes/create", - auth=("app_api_haproxy_user", "some_secure_password"), - json={"name": volume_name}, - ) - assert r.status_code == 403 - r = httpx.delete(f"http://localhost:2375/volumes/{volume_name}", - auth=("app_api_haproxy_user", "some_secure_password")) - assert r.status_code == 403 - print("Autoban, invalid volume name:", i + 1) - misc.initialize_container() - - -def test_invalid_auth(): - r = httpx.get("http://localhost:2375/_ping", auth=("app_api_haproxy_user1", "some_secure_password")) - assert r.status_code == 401 - r = httpx.get("http://localhost:2375/_ping", auth=("app_api_haproxy_user", "some_secure_password1")) - assert r.status_code == 401 - misc.initialize_container() + for i in range(30): + r = httpx.post( + "http://localhost:2375/volumes/create", + auth=("app_api_haproxy_user", "some_secure_password"), + json={"name": volume_name}, + ) + assert r.status_code == 403 + r = httpx.delete(f"http://localhost:2375/volumes/{volume_name}", + auth=("app_api_haproxy_user", "some_secure_password")) + assert r.status_code == 403 + + +def test_invalid_url(): + client = httpx.Client(base_url="http://localhost:2375", auth=("app_api_haproxy_user", "some_secure_password")) + for i in range(50): + client.get("_unknown") def test_autoban(): client = httpx.Client(base_url="http://localhost:2375", auth=("app_api_haproxy_user", "some_secure_password1")) with pytest.raises(httpx.ReadTimeout): - for i in range(7): + for i in range(10): client.get("_ping") print("Autoban, invalid auth:", i + 1) misc.initialize_container() -def test_autoban_invalid_url(): - client = httpx.Client(base_url="http://localhost:2375", auth=("app_api_haproxy_user", "some_secure_password")) - with pytest.raises((httpx.ReadTimeout, httpx.RemoteProtocolError)): - for i in range(11): - client.get("_unknown") - print("Autoban, invalid url:", i + 1) - misc.initialize_container() - - # test should be run last def test_non_standard_port(): misc.remove_haproxy()