You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -6,7 +6,10 @@ Running a publicly accessible node exposes your infrastructure to the open inter
6
6
7
7
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.
8
8
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.
10
13
11
14
### Ports overview
12
15
@@ -15,7 +18,7 @@ A Stacks node deployment typically exposes the following services:
@@ -28,53 +31,112 @@ The **P2P ports** (20444, 8333) use custom binary protocols for peer-to-peer com
28
31
29
32
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.
30
33
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
+
31
36
### Bare metal
32
37
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:
34
39
35
40
{% code title="Stacks.toml" %}
36
41
37
42
```toml
38
43
[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
41
46
# data_url = "http://<your-public-ip>:20443" # Uncomment if peers need to reach your RPC
42
47
```
43
48
44
49
{% endcode %}
45
50
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.
49
52
50
53
### Docker (stacks-blockchain-docker)
51
54
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:
- 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
62
65
- 127.0.0.1:9153:9153 # Metrics: only localhost
63
66
stacks-blockchain-api:
64
67
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.
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 %}
71
133
72
134
## HAProxy
73
135
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 RPCand API traffic over HTTP, automatically rejecting clients that exceed request rate thresholds.
75
137
76
138
{% 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.
78
140
{% endhint %}
79
141
80
142
### Linux
@@ -95,12 +157,13 @@ global
95
157
96
158
defaults
97
159
log global
98
-
mode tcp
99
-
option tcplog
160
+
mode http
161
+
option httplog
100
162
option dontlognull
101
163
timeout connect 5000
102
164
timeout client 50000
103
165
timeout server 50000
166
+
timeout http-request 10s
104
167
105
168
# -------------------------------------------
106
169
# Abuse tracking table
@@ -110,66 +173,39 @@ backend Abuse
110
173
stick-table type ip size 100K expire 30m store gpc0,http_req_rate(10s)
111
174
112
175
# -------------------------------------------
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)
116
177
# -------------------------------------------
117
178
frontend stacks_rpc
118
179
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 }
125
183
default_backend stacks_rpc_back
126
184
127
185
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
145
187
146
188
# -------------------------------------------
147
-
# Stacks API (public: 3999 -> node: 3999)
189
+
# Stacks API (public: 3999 -> node: 33999)
148
190
# -------------------------------------------
149
191
frontend stacks_api
150
192
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 }
157
196
default_backend stacks_api_back
158
197
159
198
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
161
200
162
201
# -------------------------------------------
163
202
# Bitcoin RPC (optional, if you expose it)
164
203
# -------------------------------------------
165
204
frontend btc_rpc
166
205
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 }
173
209
default_backend btc_rpc_back
174
210
175
211
backend btc_rpc_back
@@ -208,50 +244,32 @@ global
208
244
209
245
defaults
210
246
log global
211
-
mode tcp
212
-
option tcplog
247
+
mode http
248
+
option httplog
213
249
option dontlognull
214
250
timeout connect 5000
215
251
timeout client 50000
216
252
timeout server 50000
253
+
timeout http-request 10s
217
254
218
255
backend Abuse
219
256
stick-table type ip size 100K expire 30m store gpc0,http_req_rate(10s)
220
257
221
258
frontend stacks_rpc
222
259
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 }
229
263
default_backend stacks_rpc_back
230
264
231
265
backend stacks_rpc_back
232
266
server stacks-node 127.0.0.1:30443 maxconn 100 check inter 10s
233
267
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
-
247
268
frontend stacks_api
248
269
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 }
**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.
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.
0 commit comments