This week I run into a problem that has nothing to do with blockchain, and everything to do with browsers.
The web wallet works. It signs locally, talks to nodes, and never touches a server for key material. In my lab it feels like the cleanest version of decentralization: one HTML file, one seed phrase, and the network.
Then I try to use it the way normal people will use it: hosted over HTTPS.
And it breaks immediately.
The Mixed Content Wall
When a user opens the wallet at https://wallet.zoobc.net, the browser enforces HTTPS for everything that page does afterward. That is the point of HTTPS.
But ZooBC nodes expose their REST API over plain HTTP, typically on port 8080:
http://node-ip:8080/api/v1/...
Modern browsers refuse that combination. An HTTPS page is not allowed to call an HTTP endpoint. The request is blocked with a “Mixed Content” error. No prompt. No override. Just a hard stop.
So the situation is simple and frustrating:
- The wallet must be HTTPS, or it is unsafe.
- The nodes are HTTP, because that is how they run today.
- The browser will not bridge that gap.
If I want a browser wallet that works in production, I need a gateway.
The Gateway Solution
This week I build the ZooBC Gateway, a minimal HTTPS reverse proxy that sits between the browser and the nodes.
The path looks like this:
Browser (HTTPS) -> Gateway (HTTPS) -> Node (HTTP)
The wallet never tries to call a node directly. Instead it calls a gateway hostname that is HTTPS, and the gateway forwards the request to the target node over HTTP and returns the response.
In practice, the browser sends requests to a hostname like:
The gateway:
- terminates TLS using a wildcard certificate,
- parses the hostname to extract the target IP and port,
- proxies the request to
http://158.247.207.68:8080, - returns the response back to the browser.
The wallet stays clean and simple. The browser stays happy. The nodes stay unchanged.
Hostname Encoding, Not Query Parameters
A key decision is how the gateway knows which node to reach.
I do not pass the target as a query parameter, because that is how you accidentally build an open proxy and invite SSRF problems.
Instead, the target is encoded in the hostname itself:
ip-<A>-<B>-<C>-<D>[-p<PORT>].proofofparticipation.network
Examples:
| Hostname | Target |
|---|---|
ip-158-247-207-68-p8080.proofofparticipation.network | 158.247.207.68:8080 |
ip-128-199-38-140.proofofparticipation.network | 128.199.38.140:8080 (default port) |
This gives the gateway a strict and predictable parsing path, and it avoids “free-form URL forwarding” which is where security goes to die.
Security Hardening: Avoiding an Open Proxy
An open proxy is a security nightmare. If the gateway can be tricked into reaching arbitrary destinations, it becomes a tool for attacking internal networks, cloud metadata endpoints, or anything else an attacker wants.
So I harden it aggressively. The gateway has multiple layers of protection, and it rejects requests early.
Allowlist enforcement is the first layer. The gateway only proxies to known ZooBC nodes. It discovers nodes by querying bootstrap nodes at /api/v1/node/peers and crawling discovered peers. If a request targets an IP that is not in the allowlist, it returns 403 Forbidden.
SSRF protection is the second layer. Even if someone crafts a hostname that points to a private address, the gateway blocks it before proxy logic begins. Private ranges are rejected:
127.0.0.0/8(localhost)10.0.0.0/8,172.16.0.0/12,192.168.0.0/16(RFC1918)169.254.0.0/16(link-local)100.64.0.0/10(carrier-grade NAT)
So even something like ip-127-0-0-1.proofofparticipation.network is dead on arrival.
Path restrictions are another hard boundary. Only /api/v1/* paths are allowed. Any traversal attempts (.., %2e%2e, weird encodings) are blocked. This gateway is not a general-purpose router. It is a narrow pipe.
Rate limiting and connection limits finish the job. A token bucket per client IP allows short bursts, then throttles. Simultaneous connections and upstream requests are capped to prevent resource exhaustion.
All of this is designed to answer one question:
If someone wants to abuse this, how quickly do they hit a wall?
Implementation in C++
The gateway is deliberately small: around 1000 lines of C++ using Boost.Beast for HTTP/HTTPS and Boost.Asio for async I/O.
The flow is straightforward:
// Hostname parsing
auto target = ParseHostname("ip-158-247-207-68-p8080.popo.network");
// Returns: { ip: "158.247.207.68", port: 8080 }
// SSRF check
if (IsPrivateIP(target.ip)) {
return Error(403, "Forbidden: private IP");
}
// Allowlist check
if (!IsAllowedNode(target.ip)) {
return Error(403, "Forbidden: unknown node");
}
// Proxy request
auto response = ProxyRequest(request, target);
AddCORSHeaders(response);
return response;
Nothing exotic. Just strict parsing, strict checks, and predictable proxying.
Zero Logs, By Design
I make a strong choice here: the gateway does not log user activity.
No access logs.
No request or response body logging.
Only fatal startup errors to stderr.
If compiled with -DGATEWAY_NO_LOGGING, it goes completely silent.
This is intentional. The gateway is a transparent pipe, not a surveillance point. Wallet operations should not become someone else’s dataset.
Deployment and Operations
Deployment is conventional and boring, which is what I want. With systemd and Let’s Encrypt, it looks like:
# Build
cd gateway && mkdir build && cd build
cmake .. && make -j$(nproc)
# Configure
sudo cp gateway.conf.example /etc/zoobc/gateway.conf
# Edit: set cert_path, key_path, bootstrap_nodes
# Install and run
sudo make install
sudo systemctl enable --now zoobc-gateway
Certificates can be reloaded via SIGHUP for seamless rotation without restarting the service. Again, not fancy, just practical.
The Result
By the end of this week, the web wallet works from any HTTPS page, in any modern browser, without extensions. Users can query nodes and broadcast transactions while keeping private keys entirely local.
The gateway does not make ZooBC more centralized. It removes a browser limitation that would otherwise force centralization. It is a bridge, not a gatekeeper.
It is also a reminder that sometimes, building decentralized systems means dealing with very centralized platforms.
Browsers included.

