diff --git a/README.md b/README.md
index 5460ffe..19a8056 100644
--- a/README.md
+++ b/README.md
@@ -5,13 +5,13 @@
Flashbots proxy to allow redundant execution client (EL) state sync post merge.
-* Runs a proxy server that proxies requests from a beacon node (BN) to multiple other execution clients
-* Can drive EL sync from multiple BNs for redundancy
+- Runs a proxy server that proxies requests from a beacon node (BN) to multiple other execution clients
+- Can drive EL sync from multiple BNs for redundancy
## Getting Started
-* Run a BN with the execution endpoint pointing to the proxy (default is `localhost:25590`).
-* Start the proxy with a flag specifying one or multiple EL endpoints (make sure to point to the authenticated port).
+- Run a BN with the execution endpoint pointing to the proxy (default is `localhost:25590`).
+- Start the proxy with a flag specifying one or multiple EL endpoints (make sure to point to the authenticated port).
```bash
git clone https://github.com/flashbots/sync-proxy.git
@@ -35,6 +35,7 @@ The sync proxy can also be used with nginx, with requests proxied from the beaco

An example nginx config like this can be run with the sync proxy:
+
/etc/nginx/conf.d/sync_proxy.conf
@@ -48,7 +49,6 @@ server {
location / {
mirror /sync_proxy_1;
mirror /sync_proxy_2;
- mirror /sync_proxy_3;
proxy_pass http://localhost:8551;
proxy_set_header X-Real-IP $remote_addr;
@@ -73,14 +73,103 @@ server {
proxy_connect_timeout 100ms;
proxy_read_timeout 100ms;
}
+}
+```
+
+
+
+And if you'd like to use different JWT secrets for different ELs:
+
+
+Example
+First, install jwt-tokens-service: `go install github.com/flashbots/sync-proxy/cmd/jwt-tokens-service@latest`
+
+Set up the service, e.g. for systemd:
+
+```
+[Unit]
+Description=JWT tokens service
+After=network.target
+Wants=network.target
+
+[Service]
+Type=simple
+
+ExecStart=/.../jwt-tokens-service \
+ -config /.../jwt-secrets.json \
+ -client-id some-cl-name
+
+[Install]
+WantedBy=default.target
+```
- location = /sync_proxy_3 {
+Generate a secret for each EL with `openssl rand -hex 32` and put them in a JSON file:
+
+```json
+{
+ "sync-proxy-1": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdee",
+ "sync-proxy-2": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+}
+```
+
+Then, set up nginx:
+
+```
+# /etc/nginx/conf.d/sync_proxy.conf
+server {
+ listen 8552;
+ listen [::]:8552;
+
+ server_name _;
+
+ location / {
+ mirror /sync_proxy_1;
+ mirror /sync_proxy_2;
+
+ auth_request /_tokens;
+ # make sure to lowercase and replace dashes with underscores from names in json config
+ auth_request_set $auth_header_sync_proxy_1 $upstream_http_authorization_sync_proxy_1;
+ auth_request_set $auth_header_sync_proxy_2 $upstream_http_authorization_sync_proxy_2;
+
+ proxy_pass http://localhost:8551;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header Host $host;
+ proxy_set_header Referer $http_referer;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location = /_tokens {
internal;
- proxy_pass http://sync-proxy-3.local:8552$request_uri;
+ proxy_pass http://127.0.0.1:1337/tokens/;
+ proxy_pass_request_body off;
+ proxy_set_header Content-Length "";
+ }
+
+ #
+ # execution nodes
+ #
+ location = /sync_proxy_1 {
+ internal;
+ proxy_pass http://sync-proxy-1.local:8552$request_uri;
proxy_connect_timeout 100ms;
proxy_read_timeout 100ms;
+
+ proxy_hide_header Authorization;
+ proxy_set_header Authorization $auth_header_sync_proxy_1;
}
+
+ location = /sync_proxy_2 {
+ internal;
+ proxy_pass http://sync-proxy-2.local:8552$request_uri;
+ proxy_connect_timeout 100ms;
+ proxy_read_timeout 100ms;
+
+ proxy_hide_header Authorization;
+ proxy_set_header Authorization $auth_header_sync_proxy_2;
+ }
+}
```
+
## Caveats
diff --git a/cmd/jwt-tokens-service/.gitignore b/cmd/jwt-tokens-service/.gitignore
new file mode 100644
index 0000000..d344ba6
--- /dev/null
+++ b/cmd/jwt-tokens-service/.gitignore
@@ -0,0 +1 @@
+config.json
diff --git a/cmd/jwt-tokens-service/README.md b/cmd/jwt-tokens-service/README.md
new file mode 100644
index 0000000..7f6d5ca
--- /dev/null
+++ b/cmd/jwt-tokens-service/README.md
@@ -0,0 +1,3 @@
+# jwt-tokens-service
+
+Small service to generate JWT tokens for multiple EL hosts. Intended to be used with nginx auth_request module, see root readme for example.
diff --git a/cmd/jwt-tokens-service/main.go b/cmd/jwt-tokens-service/main.go
new file mode 100644
index 0000000..049cbd3
--- /dev/null
+++ b/cmd/jwt-tokens-service/main.go
@@ -0,0 +1,73 @@
+package main
+
+import (
+ "encoding/hex"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+
+ "github.com/golang-jwt/jwt"
+)
+
+var (
+ listenAddr = flag.String("addr", "localhost:1337", "listen address")
+ configFile = flag.String("config", "config.json", "path to the config file")
+ clientID = flag.String("client-id", "", "CL client id, optional")
+)
+
+func main() {
+ flag.Parse()
+
+ f, err := os.Open(*configFile)
+ if err != nil {
+ log.Fatalf("failed to open config file: %v", err)
+ }
+ defer f.Close()
+
+ // host name => hex jwt secret
+ var secrets map[string]string
+ if err := json.NewDecoder(f).Decode(&secrets); err != nil {
+ log.Fatalf("failed to read config file: %v", err)
+ }
+
+ http.HandleFunc("/tokens/", func(w http.ResponseWriter, r *http.Request) {
+ log.Printf("requested tokens from %s", r.RemoteAddr)
+
+ for host, secret := range secrets {
+ token, err := generateJWT(secret)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Failed to generate token for %s: %v", host, err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Authorization-"+host, "Bearer "+token)
+ }
+
+ w.WriteHeader(http.StatusOK)
+ })
+
+ log.Printf("Starting server on %s", *listenAddr)
+ if err := http.ListenAndServe(*listenAddr, nil); err != nil {
+ log.Fatalf("Server failed: %v", err)
+ }
+}
+
+func generateJWT(secretHex string) (string, error) {
+ secret, err := hex.DecodeString(secretHex)
+ if err != nil {
+ return "", fmt.Errorf("invalid hex secret: %v", err)
+ }
+
+ token := jwt.New(jwt.SigningMethodHS256)
+ claims := token.Claims.(jwt.MapClaims)
+
+ claims["iat"] = jwt.TimeFunc().Unix()
+ if *clientID != "" {
+ claims["id"] = *clientID
+ }
+
+ return token.SignedString(secret)
+}
diff --git a/go.mod b/go.mod
index f793c3a..cf57031 100644
--- a/go.mod
+++ b/go.mod
@@ -41,6 +41,7 @@ require (
require (
github.com/ethereum/go-ethereum v1.15.2
+ github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/gorilla/mux v1.8.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
diff --git a/go.sum b/go.sum
index a6f4400..d270d35 100644
--- a/go.sum
+++ b/go.sum
@@ -34,6 +34,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
+github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
+github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=