Skip to content

Commit a4cb188

Browse files
authored
fix(caddy): block the unauthenticated beacon validator namespace from public (VANA-BC-001) (#27)
## What The public Caddy vhost forwards all of `/eth/*` to the beacon REST gateway. That prefix includes Prysm's validator namespace, which is unauthenticated by design (it is meant to be reached only by the validator client on a trusted network). As shipped it is reachable by anyone on the internet. An unauthenticated remote `POST /eth/v1/validator/prepare_beacon_proposer` writes into Prysm's `TrackedValidatorsCache`, which is the source of `SuggestedFeeRecipient` in the execution payload. The validator client signs the produced block without comparing the payload's fee recipient against its configured one. So on a node that is both publicly exposed and actively proposing, an attacker can set the fee recipient for a validator index and redirect that block's execution-layer priority fees to an address they control. The proposer schedule needed for timing is published by the same namespace (`/eth/v1/validator/duties/proposer/{epoch}`), and block production (`/eth/v{2,3}/validator/blocks/{slot}`) is exposed too. Reference: VANA-BC-001. Verified live against `rpc.vana.org` (the write returns 200 and reaches the real Prysm handler; a dummy recipient was used, nothing moved) and reproduced locally against this exact config. ## Fix - Deny `/eth/v1|v2|v3/validator` and everything under it on the public path with a 403, ahead of the public consensus handler. - Move the trusted-IP handler ahead of the public handlers. It was dead code before, because `@public_el` / `@public_cl` matched first, which also meant `RPC_TRUSTED_IP_RANGES` had no effect. Trusted ranges now keep full access to the execution and consensus layers, including the validator namespace the validator client needs. - Public reads are unchanged. ## Testing Reproduced the deployment locally (this Caddyfile in front of a stub beacon) and confirmed: - public client: every `/eth/v{1,2,3}/validator/*` path, including `prepare_beacon_proposer`, returns 403 - public client: `/eth/v1/node/syncing`, `/eth/v1/beacon/genesis` and other reads still reach the beacon (200) - public client: EL JSON-RPC on `/` still routes to geth - trusted-IP client: full access preserved, including the validator namespace `caddy validate` passes. ## Follow-ups (not in this PR) - Narrow the public consensus handler to an explicit read-only allowlist (the paths are sketched in comments) instead of forwarding the whole read namespace. Kept broad here so the security fix does not risk breaking existing public reads. - Set `CORS_ALLOWED_ORIGINS` explicitly and pass it to the caddy service in `docker-compose.yml` instead of defaulting to `*`. - `archive.vana.org:3500` reportedly exposes the REST API directly on a public port; apply the same policy there.
1 parent f48c9f3 commit a4cb188

1 file changed

Lines changed: 61 additions & 30 deletions

File tree

Caddyfile

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@
3131
remote_ip {$RPC_TRUSTED_IP_RANGES}
3232
}
3333

34+
# WebSocket upgrade detection
35+
@websocket {
36+
header Connection *Upgrade*
37+
header Upgrade websocket
38+
}
39+
40+
# All consensus layer paths (used by the trusted handler below)
41+
@eth_all {
42+
path /eth/*
43+
}
44+
3445
# Execution layer public endpoints
3546
@public_el {
3647
method GET POST # GET is required for WebSocket connections
@@ -39,23 +50,60 @@
3950
path /
4051
}
4152

42-
# Consensus layer public endpoints
53+
# The beacon validator namespace is UNAUTHENTICATED in Prysm and must never
54+
# be reachable from the public internet (VANA-BC-001). An unauthenticated
55+
# POST to /eth/v1/validator/prepare_beacon_proposer overwrites a proposer's
56+
# execution-layer fee recipient, and /eth/v{2,3}/validator/blocks plus
57+
# /eth/v1/validator/duties/proposer widen the surface. These are meant to be
58+
# called only by the validator client over the internal network, never here.
59+
@validator_private {
60+
path /eth/v1/validator /eth/v1/validator/*
61+
path /eth/v2/validator /eth/v2/validator/*
62+
path /eth/v3/validator /eth/v3/validator/*
63+
}
64+
65+
# Consensus layer public endpoints.
66+
# NOTE: this still forwards the read namespace broadly. The intended
67+
# hardening is to narrow this to an explicit read-only allowlist, e.g.:
68+
# path /eth/v1/beacon/genesis*
69+
# path /eth/v1/beacon/headers*
70+
# path /eth/v1/beacon/states/*/validators
71+
# path /eth/v1/beacon/states/*/finality_checkpoints
72+
# path /eth/v1/node/health
73+
# path /eth/v1/node/syncing
74+
# path /eth/v1/node/identity
75+
# path /eth/v1/config/*
76+
# Kept broad here so this security fix does not break existing public reads;
77+
# the validator namespace is denied above regardless.
4378
@public_cl {
44-
# path /eth/v1/beacon/genesis*
45-
# path /eth/v1/beacon/headers*
46-
# path /eth/v1/beacon/states/*/validators
47-
# path /eth/v1/beacon/states/*/finality_checkpoints
48-
# path /eth/v1/node/health
49-
# path /eth/v1/node/syncing
50-
# path /eth/v1/node/identity
51-
# Forward all CL requests until we have a final list of public endpoints
5279
path /eth/*
5380
}
5481

55-
# WebSocket upgrade detection
56-
@websocket {
57-
header Connection *Upgrade*
58-
header Upgrade websocket
82+
# Trusted IP access. Must be evaluated BEFORE the public handlers, otherwise
83+
# @public_el / @public_cl match first and this block is dead code (which also
84+
# means RPC_TRUSTED_IP_RANGES currently has no effect). Trusted ranges keep
85+
# full access to the execution and consensus layers, including the validator
86+
# namespace the validator client needs.
87+
handle @trusted {
88+
handle @websocket {
89+
reverse_proxy geth:{$GETH_WS_PORT:8546}
90+
}
91+
92+
handle / {
93+
reverse_proxy geth:{$HTTP_PORT:8545}
94+
}
95+
96+
handle @eth_all {
97+
reverse_proxy beacon:{$GRPC_GATEWAY_PORT:3500}
98+
}
99+
100+
# Trusted but unhandled path
101+
respond 404
102+
}
103+
104+
# Deny the validator namespace for everyone who is not a trusted IP.
105+
handle @validator_private {
106+
respond 403
59107
}
60108

61109
# Public execution layer endpoints
@@ -74,23 +122,6 @@
74122
reverse_proxy beacon:{$GRPC_GATEWAY_PORT:3500}
75123
}
76124

77-
# Trusted IP access
78-
handle @trusted {
79-
handle / {
80-
# Handle WebSocket connections for trusted IPs
81-
handle @websocket {
82-
reverse_proxy geth:{$GETH_WS_PORT:8546}
83-
}
84-
85-
# Handle regular HTTP connections for trusted IPs
86-
reverse_proxy geth:{$HTTP_PORT:8545}
87-
}
88-
89-
handle /eth* {
90-
reverse_proxy beacon:{$GRPC_GATEWAY_PORT:3500}
91-
}
92-
}
93-
94125
# Default: block unhandled requests
95126
respond 404
96127
}

0 commit comments

Comments
 (0)