Skip to content

Commit 0959353

Browse files
Adjust rate limits (#5)
Signed-off-by: Alexander Piskun <[email protected]> Co-authored-by: Alexander Piskun <[email protected]>
1 parent 905d6c3 commit 0959353

File tree

6 files changed

+118
-74
lines changed

6 files changed

+118
-74
lines changed

Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ FROM haproxy:2.9.2-alpine3.19
33
USER root
44

55
ENV HAPROXY_PORT 2375
6-
ENV EX_APPS_NET "localhost"
6+
ENV BIND_ADDRESS *
7+
ENV EX_APPS_NET_FOR_HTTPS "localhost"
78

89
RUN set -ex; \
910
apk add --no-cache \
@@ -21,4 +22,4 @@ COPY --chmod=664 haproxy_ex_apps.cfg /haproxy_ex_apps.cfg
2122

2223
WORKDIR /
2324
ENTRYPOINT ["/bin/bash", "start.sh"]
24-
HEALTHCHECK --interval=30s --timeout=10s --retries=3 CMD /healthcheck.sh
25+
HEALTHCHECK --interval=10s --timeout=10s --retries=9 CMD /healthcheck.sh

README.md

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,33 +29,43 @@ Instead of `some_secure_password` you put your password that later you should pr
2929

3030
### Docker with TLS
3131

32+
In this case ExApps will only map host's loopback adapter, and will be avalaible to Nextcloud only throw HaProxy.
33+
3234
```shell
3335
docker run -e NC_HAPROXY_PASSWORD="some_secure_password" \
36+
-e BIND_ADDRESS="x.y.z.z"
3437
-v /var/run/docker.sock:/var/run/docker.sock \
3538
-v `pwd`/certs/cert.pem:/certs/cert.pem \
36-
--name aa-docker-socket-proxy -h aa-docker-socket-proxy \
39+
--name aa-docker-socket-proxy -h aa-docker-socket-proxy --net host \
3740
--restart unless-stopped --privileged -d ghcr.io/cloud-py-api/aa-docker-socket-proxy:release
3841
```
3942

40-
Here in addition we map certificate file from host with SSL certificate that will be used by HaProxy.
43+
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.
44+
45+
You should set `BIND_ADDRESS` to the IP on which server with ExApps can accept requests coming from the Nextcloud instance.
46+
47+
*This is necessary when using the “host” network so as not to occupy all interfaces, because ExApp will use loopback adapter.*
4148

4249
> [!WARNING]
4350
> If the certificates are self-signed, your job is to add them to the Nextcloud instance so that AppAPI can recognize them.
4451
4552
### AppAPI
4653

47-
1. Create a daemon from the `Docker Socket Proxy` or `Docker Socket Proxy Remote` template in AppAPI.
54+
1. Create a daemon from the `Docker Socket Proxy` template in AppAPI.
4855
2. Fill the password you used during container creation.
49-
3. If `Docker Socket Proxy Remote` is used you need to specify the IP/DNS of the created HaProxy.
5056

5157
### Additionally supported variables
5258

5359
`HAPROXY_PORT`: using of custom port instead of **2375** which is the default one.
5460

55-
`EX_APPS_NET`: only for custom remote ExApp installs with TLS, determines destination of requests to ExApps for HaProxy.
61+
`BIND_ADDRESS`: the address to use for port binding. (Usually needed only for remote installs, **must be accessible from the Nextcloud**)
62+
63+
`EX_APPS_NET_FOR_HTTPS`: only for custom remote ExApp installs with TLS, determines destination of requests to ExApps for HaProxy.
5664

5765
## Development
5866

67+
### HTTP(local)
68+
5969
To build image locally use:
6070

6171
```shell
@@ -65,16 +75,54 @@ docker build -f ./Dockerfile -t aa-docker-socket-proxy:latest ./
6575
Deploy image(for `nextcloud-docker-dev`):
6676

6777
```shell
68-
docker run -e NC_HAPROXY_PASSWORD="some_secure_password" -v /var/run/docker.sock:/var/run/docker.sock \
69-
--name aa-docker-socket-proxy -h aa-docker-socket-proxy --net master_default --privileged -d aa-docker-socket-proxy:latest
78+
docker run -e NC_HAPROXY_PASSWORD="some_secure_password" \
79+
-v /var/run/docker.sock:/var/run/docker.sock \
80+
--name aa-docker-socket-proxy -h aa-docker-socket-proxy --net master_default \
81+
--privileged -d aa-docker-socket-proxy:latest
7082
```
7183

72-
If you need create Self-Signed cert for tests:
84+
After that create daemon in AppAPI from the Docker Socket Proxy template, specifying:
85+
1. Host: `aa-docker-socket-proxy:2375`
86+
2. Network in Deploy Config equal to `master_default`
87+
3. Deploy Config: HaProxy password: `some_secure_password`
88+
89+
### HTTPS(remote)
90+
91+
We will emulate remote deployment still with `nextcloud-docker-dev` setup.
92+
For this we deploy `aa-docker-socket-proxy` to host network and reach it using `host.docker.internal`.
93+
94+
> [!NOTE]
95+
> Due to current Docker limitations, this setup type is not working on macOS.
96+
> Ref issue: [Support Host Network for macOS](https://github.com/docker/roadmap/issues/238)
97+
98+
First create Self-Signed cert for tests:
7399

74100
```shell
75-
openssl req -nodes -new -x509 -subj '/CN=*' -sha256 -keyout certs/privkey.pem -out certs/fullchain.pem -days 365000 > /dev/null 2>&1
101+
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
76102
```
77103

78104
```shell
79105
cat certs/fullchain.pem certs/privkey.pem | tee certs/cert.pem > /dev/null 2>&1
80106
```
107+
108+
Place `cert.pem` into `data/shared` folder of `nextcloud-docker-dev` and execute inside Nextcloud container:
109+
110+
```shell
111+
sudo -u www-data php occ security:certificates:import /shared/cert.pem
112+
```
113+
114+
Create HaProxy container:
115+
116+
```shell
117+
docker run -e NC_HAPROXY_PASSWORD="some_secure_password" \
118+
-e BIND_ADDRESS="172.17.0.1" \
119+
-v /var/run/docker.sock:/var/run/docker.sock \
120+
-v `pwd`/certs/cert.pem:/certs/cert.pem \
121+
--name aa-docker-socket-proxy -h aa-docker-socket-proxy --net host \
122+
--privileged -d aa-docker-socket-proxy:latest
123+
```
124+
125+
After that create daemon in AppAPI from the Docker Socket Proxy template, with next parameters:
126+
1. Host: `host.docker.internal:2375`
127+
2. Tick `https` checkbox.
128+
3. Deploy Config: HaProxy password: `some_secure_password`

haproxy.cfg

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,24 @@ userlist app_api_credentials
1919

2020
frontend docker_engine
2121
mode http
22-
BIND_DOCKER_PLACEHOLDER
22+
BIND_ADDRESS_PLACEHOLDER
2323

24-
# Rate limiting
25-
stick-table type ip size 1m expire 1440m store http_err_cnt,http_err_rate(60m)
26-
# ACL to restrict rate limited request
27-
acl acl-www-err-rate sc_http_err_rate(0) gt 5
28-
acl acl-www-err-total sc_http_err_cnt(0) gt 10
24+
stick-table type ip size 100k expire 144m store gpc0,http_req_rate(5m)
2925

30-
http-request track-sc0 src
31-
http-request deny if acl-www-err-total
32-
http-request silent-drop if acl-www-err-rate
26+
# Perform Basic Auth
27+
acl valid_credentials http_auth(app_api_credentials)
3328

34-
# Basic Authentication
35-
http-request auth unless { http_auth(app_api_credentials) }
29+
# Increase counter on failed authentication
30+
http-request track-sc0 src if ! valid_credentials
31+
http-request sc-inc-gpc0(0) if ! valid_credentials
32+
33+
# Check if the client IP has more than 5 failed attempts in the last 5 minutes
34+
acl too_many_auth_failures sc0_http_req_rate gt 5
35+
36+
# Use 'silent-drop' to drop the connection without a response
37+
http-request silent-drop if too_many_auth_failures
38+
39+
http-request auth realm AppAPI unless valid_credentials
3640

3741
# docker system _ping
3842
http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/_ping } METH_GET

haproxy_ex_apps.cfg

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
frontend ex_apps
22
mode http
3-
bind *:23000-23999 v4v6 ssl crt /certs/cert.pem
3+
BIND_ADDRESS_PLACEHOLDER
44

5-
# Rate limiting
6-
stick-table type ip size 1m expire 1440m store http_err_cnt,http_err_rate(60m)
7-
# ACL to restrict rate limited request
8-
acl acl-www-err-rate sc_http_err_rate(0) gt 5
9-
acl acl-www-err-total sc_http_err_cnt(0) gt 10
5+
stick-table type ip size 100k expire 144m store gpc0,http_req_rate(5m)
106

11-
http-request track-sc0 src
12-
http-request deny if acl-www-err-total
13-
http-request silent-drop if acl-www-err-rate
7+
# Perform Basic Auth
8+
acl valid_credentials http_auth(app_api_credentials)
149

15-
# Basic Authentication
16-
http-request auth unless { http_auth(app_api_credentials) }
10+
# Increase counter on failed authentication
11+
http-request track-sc0 src if ! valid_credentials
12+
http-request sc-inc-gpc0(0) if ! valid_credentials
13+
14+
# Check if the client IP has more than 5 failed attempts in the last 5 minutes
15+
acl too_many_auth_failures sc0_http_req_rate gt 5
16+
17+
# Use 'silent-drop' to drop the connection without a response
18+
http-request silent-drop if too_many_auth_failures
19+
20+
http-request auth realm AppAPI unless valid_credentials
1721

1822
# We allow anything for ExApps
1923
http-request allow
2024
use_backend bk_ex_apps
2125

2226
backend bk_ex_apps
2327
mode http
24-
server ex_apps EX_APPS_NET_PLACEHOLDER
28+
server ex_apps EX_APPS_NET_FOR_HTTPS_PLACEHOLDER

start.sh

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
#!/bin/sh
22

3-
set -x
4-
HAPROXYFILE="$(sed "s|NC_PASSWORD_PLACEHOLDER|$NC_HAPROXY_PASSWORD|" /haproxy.cfg)"
5-
HAPROXYFILE="$(echo "$HAPROXYFILE" | sed "s|HAPROXY_PORT_PLACEHOLDER|$HAPROXY_PORT|")"
3+
sed -i "s|NC_PASSWORD_PLACEHOLDER|$NC_HAPROXY_PASSWORD|" /haproxy.cfg
64

75
if [ -f "/certs/cert.pem" ]; then
8-
HAPROXYFILE="$(echo "$HAPROXYFILE" | sed "s|BIND_DOCKER_PLACEHOLDER|bind *:$HAPROXY_PORT v4v6 ssl crt /certs/cert.pem|")"
9-
sed -i "s|EX_APPS_NET_PLACEHOLDER|$EX_APPS_NET|" /haproxy_ex_apps.cfg
6+
sed -i "s|BIND_ADDRESS_PLACEHOLDER|bind $BIND_ADDRESS:$HAPROXY_PORT v4v6 ssl crt /certs/cert.pem|" /haproxy.cfg
7+
sed -i "s|BIND_ADDRESS_PLACEHOLDER|bind $BIND_ADDRESS:23000-23999 v4v6 ssl crt /certs/cert.pem|" /haproxy_ex_apps.cfg
8+
sed -i "s|EX_APPS_NET_FOR_HTTPS_PLACEHOLDER|$EX_APPS_NET_FOR_HTTPS|" /haproxy_ex_apps.cfg
109
# Chmod certs to be accessible by haproxy
1110
chmod 644 /certs/cert.pem
1211
else
13-
HAPROXYFILE="$(echo "$HAPROXYFILE" | sed "s|BIND_DOCKER_PLACEHOLDER|bind *:$HAPROXY_PORT v4v6|")"
12+
sed -i "s|BIND_ADDRESS_PLACEHOLDER|bind $BIND_ADDRESS:$HAPROXY_PORT v4v6|" /haproxy.cfg
1413
fi
15-
echo "$HAPROXYFILE" > /haproxy.cfg
1614

17-
set +x
15+
echo "HaProxy config:"
1816

1917
if [ -f "/certs/cert.pem" ]; then
18+
cat /haproxy.cfg
19+
cat /haproxy_ex_apps.cfg
2020
haproxy -f /haproxy.cfg -f /haproxy_ex_apps.cfg -db
2121
else
22+
cat /haproxy.cfg
2223
haproxy -f /haproxy.cfg -db
2324
fi

tests/test_basic.py

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
def test_ping_spam():
77
client = httpx.Client(base_url="http://localhost:2375", auth=("app_api_haproxy_user", "some_secure_password"))
8-
for i in range(60):
8+
for i in range(100):
99
r = client.get("_ping")
1010
assert r.status_code == 200
1111

@@ -23,47 +23,33 @@ def test_volume_creation_removal():
2323

2424
def test_volume_creation_removal_invalid():
2525
volume_name = "app_test_data"
26-
with pytest.raises(httpx.ReadTimeout):
27-
for i in range(9):
28-
r = httpx.post(
29-
"http://localhost:2375/volumes/create",
30-
auth=("app_api_haproxy_user", "some_secure_password"),
31-
json={"name": volume_name},
32-
)
33-
assert r.status_code == 403
34-
r = httpx.delete(f"http://localhost:2375/volumes/{volume_name}",
35-
auth=("app_api_haproxy_user", "some_secure_password"))
36-
assert r.status_code == 403
37-
print("Autoban, invalid volume name:", i + 1)
38-
misc.initialize_container()
39-
40-
41-
def test_invalid_auth():
42-
r = httpx.get("http://localhost:2375/_ping", auth=("app_api_haproxy_user1", "some_secure_password"))
43-
assert r.status_code == 401
44-
r = httpx.get("http://localhost:2375/_ping", auth=("app_api_haproxy_user", "some_secure_password1"))
45-
assert r.status_code == 401
46-
misc.initialize_container()
26+
for i in range(30):
27+
r = httpx.post(
28+
"http://localhost:2375/volumes/create",
29+
auth=("app_api_haproxy_user", "some_secure_password"),
30+
json={"name": volume_name},
31+
)
32+
assert r.status_code == 403
33+
r = httpx.delete(f"http://localhost:2375/volumes/{volume_name}",
34+
auth=("app_api_haproxy_user", "some_secure_password"))
35+
assert r.status_code == 403
36+
37+
38+
def test_invalid_url():
39+
client = httpx.Client(base_url="http://localhost:2375", auth=("app_api_haproxy_user", "some_secure_password"))
40+
for i in range(50):
41+
client.get("_unknown")
4742

4843

4944
def test_autoban():
5045
client = httpx.Client(base_url="http://localhost:2375", auth=("app_api_haproxy_user", "some_secure_password1"))
5146
with pytest.raises(httpx.ReadTimeout):
52-
for i in range(7):
47+
for i in range(10):
5348
client.get("_ping")
5449
print("Autoban, invalid auth:", i + 1)
5550
misc.initialize_container()
5651

5752

58-
def test_autoban_invalid_url():
59-
client = httpx.Client(base_url="http://localhost:2375", auth=("app_api_haproxy_user", "some_secure_password"))
60-
with pytest.raises((httpx.ReadTimeout, httpx.RemoteProtocolError)):
61-
for i in range(11):
62-
client.get("_unknown")
63-
print("Autoban, invalid url:", i + 1)
64-
misc.initialize_container()
65-
66-
6753
# test should be run last
6854
def test_non_standard_port():
6955
misc.remove_haproxy()

0 commit comments

Comments
 (0)