Skip to content

Commit

Permalink
Adjust rate limits (#5)
Browse files Browse the repository at this point in the history
Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
Co-authored-by: Alexander Piskun <bigcat88@icloud.com>
  • Loading branch information
andrey18106 and bigcat88 committed Jan 24, 2024
1 parent 905d6c3 commit 0959353
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 74 deletions.
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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
66 changes: 57 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`
26 changes: 15 additions & 11 deletions haproxy.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 16 additions & 12 deletions haproxy_ex_apps.cfg
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
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
use_backend bk_ex_apps

backend bk_ex_apps
mode http
server ex_apps EX_APPS_NET_PLACEHOLDER
server ex_apps EX_APPS_NET_FOR_HTTPS_PLACEHOLDER
17 changes: 9 additions & 8 deletions start.sh
Original file line number Diff line number Diff line change
@@ -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
50 changes: 18 additions & 32 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Expand Down

0 comments on commit 0959353

Please sign in to comment.