Skip to content

Commit 499f3ad

Browse files
committed
update proxy guide
1 parent 7e2d068 commit 499f3ad

2 files changed

Lines changed: 208 additions & 262 deletions

File tree

docs/operate/readme/run-a-node-behind-a-proxy.md

Lines changed: 104 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ Running a publicly accessible node exposes your infrastructure to the open inter
66

77
If you plan to run a Stacks node with publicly accessible RPC endpoints, it is strongly recommended to place the node behind a reverse proxy with rate limiting. Without rate limiting, a public node can be overwhelmed by excessive requests, leading to degraded performance or denial of service.
88

9-
This guide provides minimal, production-tested configurations for [HAProxy](https://www.haproxy.org/) and [Nginx](https://nginx.org/) that you can adapt to your environment.
9+
This guide provides minimal, production-tested configurations for two popular reverse proxies. **Choose one — you do not need both:**
10+
11+
- [**Nginx**](#nginx) — simpler configuration, widely known, good baseline rate limiting.
12+
- [**HAProxy**](#haproxy) — more advanced abuse detection via stick tables, HTTP proxying with automatic IP blocking.
1013

1114
### Ports overview
1215

@@ -15,7 +18,7 @@ A Stacks node deployment typically exposes the following services:
1518
| Service | Default Port | Protocol | Proxy? |
1619
| ----------- | ------------ | -------- | --------------- |
1720
| Stacks RPC | 20443 | HTTP | Yes |
18-
| Stacks P2P | 20444 | TCP | Optional |
21+
| Stacks P2P | 20444 | TCP | No |
1922
| Stacks API | 3999 | HTTP | Yes, if running |
2023
| Bitcoin RPC | 8332 | HTTP | Yes, if exposed |
2124
| Bitcoin P2P | 8333 | TCP | No |
@@ -28,53 +31,112 @@ The **P2P ports** (20444, 8333) use custom binary protocols for peer-to-peer com
2831

2932
Before setting up the proxy, configure your Stacks node so its RPC endpoint is not directly reachable from the public internet. The proxy will be the only public-facing service.
3033

34+
Since the proxy needs to listen on the standard public ports (e.g. `20443`), the node itself must bind to **different** ports to avoid conflicts. The examples below use offset ports (`30443`, `33999`) for the node's RPC and API, while the proxy owns the public-facing ports (`20443`, `3999`). P2P stays on its standard port and is not proxied.
35+
3136
### Bare metal
3237

33-
In your node's configuration file (e.g. `Stacks.toml`), set `rpc_bind` to a localhost address:
38+
In your node's configuration file (e.g. `Stacks.toml`), bind the RPC to a localhost address on an offset port:
3439

3540
{% code title="Stacks.toml" %}
3641

3742
```toml
3843
[node]
39-
rpc_bind = "127.0.0.1:20443" # Only accessible from localhost
40-
p2p_bind = "0.0.0.0:20444" # Open to peers on the network
44+
rpc_bind = "127.0.0.1:30443" # Only accessible from localhost, offset port
45+
p2p_bind = "0.0.0.0:20444" # Standard port, open directly to the network
4146
# data_url = "http://<your-public-ip>:20443" # Uncomment if peers need to reach your RPC
4247
```
4348

4449
{% endcode %}
4550

46-
{% hint style="info" %}
47-
If you change the RPC port (e.g. to `30443`), update the proxy backend to match.
48-
{% endhint %}
51+
The proxy will listen on port `20443` and forward RPC traffic to the offset port. P2P binds directly on the standard port `20444` and does not go through the proxy.
4952

5053
### Docker (stacks-blockchain-docker)
5154

52-
When running with [stacks-blockchain-docker](https://github.com/stacks-network/stacks-blockchain-docker), the node's ports are controlled by the Docker Compose configuration. By default, ports are exposed on all interfaces (`0.0.0.0`). To restrict them to localhost, edit `compose-files/common.yaml` and change the port mappings to bind to `127.0.0.1` with internal host ports:
55+
When running with [stacks-blockchain-docker](https://github.com/stacks-network/stacks-blockchain-docker), the node's ports are controlled by the Docker Compose configuration. By default, ports are exposed on all interfaces (`0.0.0.0`). To restrict the RPC and API to localhost (so only the proxy can reach them), edit `compose-files/common.yaml` and change the port mappings. P2P is published directly on the standard port:
5356

5457
{% code title="compose-files/common.yaml (port changes)" %}
5558

5659
```yaml
5760
services:
5861
stacks-blockchain:
5962
ports:
60-
- 127.0.0.1:30443:20443 # RPC: only localhost, internal port 30443
61-
- 127.0.0.1:30444:20444 # P2P: only localhost, internal port 30444
63+
- 127.0.0.1:30443:20443 # RPC: only localhost, host port 30443
64+
- 0.0.0.0:20444:20444 # P2P: open directly, standard port
6265
- 127.0.0.1:9153:9153 # Metrics: only localhost
6366
stacks-blockchain-api:
6467
ports:
65-
- 127.0.0.1:33999:3999 # API: only localhost, internal port 33999
68+
- 127.0.0.1:33999:3999 # API: only localhost, host port 33999
69+
```
70+
71+
{% endcode %}
72+
73+
The format is `host_ip:host_port:container_port`. The node inside the container keeps its default ports — only the **host** side changes. Offset host ports (`30443`, `33999`) are necessary because the proxy already occupies the standard ports (`20443`, `3999`) on the host. Binding to `127.0.0.1` ensures the container ports are only reachable from the host (where the proxy runs), not from the public internet. P2P is published directly on the standard port `20444`.
74+
75+
{% hint style="info" %}
76+
Inter-container communication (e.g. the API receiving events from the blockchain node) uses Docker's internal network and service names, not published host ports. These port mapping changes do not affect container-to-container traffic.
77+
{% endhint %}
78+
79+
## Nginx
80+
81+
Nginx can serve as a reverse proxy with rate limiting using the `limit_req` module. The configuration below rate-limits the Stacks RPC and Stacks API endpoints.
82+
83+
{% code title="/etc/nginx/sites-available/stacks-node" %}
84+
85+
```nginx
86+
limit_req_zone $binary_remote_addr zone=stacks_rpc:10m rate=5r/s;
87+
limit_req_zone $binary_remote_addr zone=stacks_api:10m rate=10r/s;
88+
89+
server {
90+
listen 20443;
91+
92+
# Stacks RPC
93+
location / {
94+
limit_req zone=stacks_rpc burst=20 nodelay;
95+
proxy_pass http://127.0.0.1:30443;
96+
}
97+
}
98+
99+
server {
100+
listen 3999;
101+
102+
# Stacks API (if running)
103+
location / {
104+
limit_req zone=stacks_api burst=40 nodelay;
105+
proxy_pass http://127.0.0.1:33999;
106+
}
107+
}
108+
```
109+
110+
{% endcode %}
111+
112+
Enable the site and restart Nginx:
113+
114+
{% code title="Enable and start Nginx" %}
115+
116+
```bash
117+
sudo ln -s /etc/nginx/sites-available/stacks-node /etc/nginx/sites-enabled/
118+
sudo nginx -t
119+
sudo systemctl restart nginx
66120
```
67121

68122
{% endcode %}
69123

70-
The node inside the container still listens on its default ports. Docker maps the host-side ports (`30443`, `30444`, `33999`) to the container ports. HAProxy then listens on the standard public ports (`20443`, `20444`, `3999`) and forwards to these internal host ports.
124+
### Verify
125+
126+
{% code title="Test the RPC endpoint through the proxy" %}
127+
128+
```bash
129+
curl -s localhost:20443/v2/info | jq
130+
```
131+
132+
{% endcode %}
71133

72134
## HAProxy
73135

74-
HAProxy provides fine-grained connection tracking and abuse detection via [stick tables](https://www.haproxy.com/blog/introduction-to-haproxy-stick-tables). The configuration below proxies Stacks RPC, P2P, and API traffic, automatically rejecting clients that exceed request rate thresholds.
136+
HAProxy provides fine-grained connection tracking and abuse detection via [stick tables](https://www.haproxy.com/blog/introduction-to-haproxy-stick-tables). The configuration below proxies Stacks RPC and API traffic over HTTP, automatically rejecting clients that exceed request rate thresholds.
75137

76138
{% hint style="info" %}
77-
Adjust `maxconn`, rate thresholds (`ge 25`, `ge 10`), stick-table sizes, and expiry times to suit your traffic patterns. The values below are conservative defaults.
139+
Adjust `maxconn`, rate thresholds (`ge 25`), stick-table sizes, and expiry times to suit your traffic patterns. The values below are conservative defaults.
78140
{% endhint %}
79141

80142
### Linux
@@ -95,12 +157,13 @@ global
95157

96158
defaults
97159
log global
98-
mode tcp
99-
option tcplog
160+
mode http
161+
option httplog
100162
option dontlognull
101163
timeout connect 5000
102164
timeout client 50000
103165
timeout server 50000
166+
timeout http-request 10s
104167

105168
# -------------------------------------------
106169
# Abuse tracking table
@@ -110,66 +173,39 @@ backend Abuse
110173
stick-table type ip size 100K expire 30m store gpc0,http_req_rate(10s)
111174

112175
# -------------------------------------------
113-
# Stacks RPC (public: 20443 -> node: 20443)
114-
# For Docker setups, point to the internal
115-
# host port (e.g. 127.0.0.1:30443)
176+
# Stacks RPC (public: 20443 -> node: 30443)
116177
# -------------------------------------------
117178
frontend stacks_rpc
118179
bind *:20443
119-
maxconn 512
120-
acl is_abuse src_http_req_rate(Abuse) ge 25
121-
acl inc_abuse_cnt src_inc_gpc0(Abuse) gt 0
122-
acl abuse_cnt src_get_gpc0(Abuse) gt 0
123-
tcp-request connection track-sc0 src table Abuse
124-
tcp-request connection reject if abuse_cnt
180+
http-request track-sc0 src table Abuse
181+
http-request deny deny_status 429 if { src_get_gpc0(Abuse) gt 0 }
182+
http-request deny deny_status 429 if { src_http_req_rate(Abuse) ge 25 } { src_inc_gpc0(Abuse) ge 0 }
125183
default_backend stacks_rpc_back
126184

127185
backend stacks_rpc_back
128-
server stacks-node 127.0.0.1:20443 maxconn 100 check inter 10s
129-
130-
# -------------------------------------------
131-
# Stacks P2P (public: 20444 -> node: 20444)
132-
# -------------------------------------------
133-
frontend stacks_p2p
134-
bind *:20444
135-
maxconn 512
136-
acl is_abuse src_http_req_rate(Abuse) ge 10
137-
acl inc_abuse_cnt src_inc_gpc0(Abuse) gt 0
138-
acl abuse_cnt src_get_gpc0(Abuse) gt 0
139-
tcp-request connection track-sc0 src table Abuse
140-
tcp-request connection reject if abuse_cnt
141-
default_backend stacks_p2p_back
142-
143-
backend stacks_p2p_back
144-
server stacks-node 127.0.0.1:20444 maxconn 100 check inter 10s
186+
server stacks-node 127.0.0.1:30443 maxconn 100 check inter 10s
145187

146188
# -------------------------------------------
147-
# Stacks API (public: 3999 -> node: 3999)
189+
# Stacks API (public: 3999 -> node: 33999)
148190
# -------------------------------------------
149191
frontend stacks_api
150192
bind *:3999
151-
maxconn 512
152-
acl is_abuse src_http_req_rate(Abuse) ge 25
153-
acl inc_abuse_cnt src_inc_gpc0(Abuse) gt 0
154-
acl abuse_cnt src_get_gpc0(Abuse) gt 0
155-
tcp-request connection track-sc0 src table Abuse
156-
tcp-request connection reject if abuse_cnt
193+
http-request track-sc0 src table Abuse
194+
http-request deny deny_status 429 if { src_get_gpc0(Abuse) gt 0 }
195+
http-request deny deny_status 429 if { src_http_req_rate(Abuse) ge 25 } { src_inc_gpc0(Abuse) ge 0 }
157196
default_backend stacks_api_back
158197

159198
backend stacks_api_back
160-
server stacks-api 127.0.0.1:3999 maxconn 100 check inter 10s
199+
server stacks-api 127.0.0.1:33999 maxconn 100 check inter 10s
161200

162201
# -------------------------------------------
163202
# Bitcoin RPC (optional, if you expose it)
164203
# -------------------------------------------
165204
frontend btc_rpc
166205
bind *:18332
167-
maxconn 512
168-
acl is_abuse src_http_req_rate(Abuse) ge 25
169-
acl inc_abuse_cnt src_inc_gpc0(Abuse) gt 0
170-
acl abuse_cnt src_get_gpc0(Abuse) gt 0
171-
tcp-request connection track-sc0 src table Abuse
172-
tcp-request connection reject if abuse_cnt
206+
http-request track-sc0 src table Abuse
207+
http-request deny deny_status 429 if { src_get_gpc0(Abuse) gt 0 }
208+
http-request deny deny_status 429 if { src_http_req_rate(Abuse) ge 25 } { src_inc_gpc0(Abuse) ge 0 }
173209
default_backend btc_rpc_back
174210

175211
backend btc_rpc_back
@@ -208,50 +244,32 @@ global
208244
209245
defaults
210246
log global
211-
mode tcp
212-
option tcplog
247+
mode http
248+
option httplog
213249
option dontlognull
214250
timeout connect 5000
215251
timeout client 50000
216252
timeout server 50000
253+
timeout http-request 10s
217254
218255
backend Abuse
219256
stick-table type ip size 100K expire 30m store gpc0,http_req_rate(10s)
220257
221258
frontend stacks_rpc
222259
bind *:20443
223-
maxconn 512
224-
acl is_abuse src_http_req_rate(Abuse) ge 25
225-
acl inc_abuse_cnt src_inc_gpc0(Abuse) gt 0
226-
acl abuse_cnt src_get_gpc0(Abuse) gt 0
227-
tcp-request connection track-sc0 src table Abuse
228-
tcp-request connection reject if abuse_cnt
260+
http-request track-sc0 src table Abuse
261+
http-request deny deny_status 429 if { src_get_gpc0(Abuse) gt 0 }
262+
http-request deny deny_status 429 if { src_http_req_rate(Abuse) ge 25 } { src_inc_gpc0(Abuse) ge 0 }
229263
default_backend stacks_rpc_back
230264
231265
backend stacks_rpc_back
232266
server stacks-node 127.0.0.1:30443 maxconn 100 check inter 10s
233267
234-
frontend stacks_p2p
235-
bind *:20444
236-
maxconn 512
237-
acl is_abuse src_http_req_rate(Abuse) ge 10
238-
acl inc_abuse_cnt src_inc_gpc0(Abuse) gt 0
239-
acl abuse_cnt src_get_gpc0(Abuse) gt 0
240-
tcp-request connection track-sc0 src table Abuse
241-
tcp-request connection reject if abuse_cnt
242-
default_backend stacks_p2p_back
243-
244-
backend stacks_p2p_back
245-
server stacks-node 127.0.0.1:30444 maxconn 100 check inter 10s
246-
247268
frontend stacks_api
248269
bind *:3999
249-
maxconn 512
250-
acl is_abuse src_http_req_rate(Abuse) ge 25
251-
acl inc_abuse_cnt src_inc_gpc0(Abuse) gt 0
252-
acl abuse_cnt src_get_gpc0(Abuse) gt 0
253-
tcp-request connection track-sc0 src table Abuse
254-
tcp-request connection reject if abuse_cnt
270+
http-request track-sc0 src table Abuse
271+
http-request deny deny_status 429 if { src_get_gpc0(Abuse) gt 0 }
272+
http-request deny deny_status 429 if { src_http_req_rate(Abuse) ge 25 } { src_inc_gpc0(Abuse) ge 0 }
255273
default_backend stacks_api_back
256274
257275
backend stacks_api_back
@@ -280,52 +298,7 @@ curl -s localhost:20443/v2/info | jq
280298
{% endcode %}
281299

282300
{% hint style="info" %}
283-
**How the abuse table works:** HAProxy tracks each client IP's request rate. When a client exceeds the threshold (e.g. 25 requests in 10 seconds for RPC), its `gpc0` counter is incremented and all subsequent connections from that IP are rejected. The stick-table entry expires after 30 minutes, lifting the block automatically.
284-
{% endhint %}
285-
286-
## Nginx
287-
288-
Nginx can serve as a reverse proxy with basic rate limiting using the `limit_req` module. The configuration below rate-limits both the Stacks RPC and Stacks API endpoints.
289-
290-
{% code title="/etc/nginx/sites-available/stacks-node" %}
291-
292-
```nginx
293-
limit_req_zone $binary_remote_addr zone=stacks_rpc:10m rate=5r/s;
294-
limit_req_zone $binary_remote_addr zone=stacks_api:10m rate=10r/s;
295-
296-
server {
297-
listen 80;
298-
299-
# Stacks RPC
300-
location /v2/ {
301-
limit_req zone=stacks_rpc burst=20 nodelay;
302-
proxy_pass http://127.0.0.1:20443;
303-
}
304-
305-
# Stacks API (if running)
306-
location / {
307-
limit_req zone=stacks_api burst=40 nodelay;
308-
proxy_pass http://127.0.0.1:3999;
309-
}
310-
}
311-
```
312-
313-
{% endcode %}
314-
315-
Enable the site and restart Nginx:
316-
317-
{% code title="Enable and start Nginx" %}
318-
319-
```bash
320-
sudo ln -s /etc/nginx/sites-available/stacks-node /etc/nginx/sites-enabled/
321-
sudo nginx -t
322-
sudo systemctl restart nginx
323-
```
324-
325-
{% endcode %}
326-
327-
{% hint style="info" %}
328-
HAProxy's stick tables offer more granular abuse detection (tracking multiple dimensions per IP, automatic blocking) compared to Nginx's `limit_req`. If fine-grained rate limiting is your priority, HAProxy is the stronger choice.
301+
**How the abuse table works:** HAProxy tracks each client IP's HTTP request rate. When a client exceeds the threshold (e.g. 25 HTTP requests in 10 seconds), its `gpc0` counter is incremented and all subsequent requests from that IP are denied with HTTP 429. The stick-table entry expires after 30 minutes, lifting the block automatically.
329302
{% endhint %}
330303

331304
## Firewall considerations
@@ -339,7 +312,7 @@ sudo ufw default deny incoming
339312
sudo ufw default allow outgoing
340313
sudo ufw allow 22/tcp # SSH
341314
sudo ufw allow 20443/tcp # Stacks RPC (via proxy)
342-
sudo ufw allow 20444/tcp # Stacks P2P (direct or via proxy)
315+
sudo ufw allow 20444/tcp # Stacks P2P (direct)
343316
sudo ufw allow 8333/tcp # Bitcoin P2P (direct)
344317
sudo ufw enable
345318
```

0 commit comments

Comments
 (0)