Please report security issues to security@getoptimum.xyz rather than opening a public GitHub issue. We'll acknowledge receipt within 72 hours.
When reporting, include:
- The affected version (
./bin/optimum-gateway --versionor the commit hash). - Steps to reproduce.
- Impact assessment if you have one (read primitive, write primitive, DoS amplification, etc.).
- Whether the issue affects only the gateway, or also the bootstrap server / auth service / fork-digest hub.
We welcome good-faith security research. The LICENSE already
permits you to build, run and modify this software, so no extra permission is
needed for that. What this section does is define what we treat as good-faith
research, so the assurance below is unambiguous.
We will treat security testing as authorized access, and Spice Solutions Inc. ("Optimum") will not pursue legal action for it, provided that you:
- act in good faith and avoid privacy violations, data destruction, and degradation of others' services;
- test only against instances you own or operate, or a dedicated test deployment you control, and do not target Optimum's production systems or other operators' deployments without prior written consent;
- do not access, modify, or exfiltrate data that is not yours, and use the minimum access necessary to demonstrate a finding;
- give us a reasonable opportunity to remediate before any public disclosure (see below), and do not exploit a finding beyond what is needed to prove it.
This assurance concerns security research only. It is not a waiver of any other
right, and Optimum may withdraw it for conduct outside the scope above. Your
rights in the software itself are governed by the LICENSE.
Report findings to security@getoptimum.xyz as described above. We ask for a coordinated disclosure window of 90 days from our acknowledgement, or until a fix ships, whichever comes first; we are glad to agree a different timeline for complex issues. We will not pursue legal action for research conducted in good faith under this safe harbor, and we will credit reporters who wish to be named.
The gateway has three trust anchors, in roughly increasing blast radius:
- The local CL client (the beacon client the gateway peers with via
libp2p on the port configured by
agent_lib_p2p_port/OPT_AGENT_LIB_P2P_PORT, default33212). The operator declares the CL peer IDs indirect_cl_peers(OPT_DIRECT_CL_PEERS), which the gateway hands to libp2p-pubsub as direct peers (WithDirectPeers/WithDirectConnectTicks). When this list is non-empty,onPeerConnectedalso disconnects peers whose ID is not in the list (connect-time allowlist, not aConnectionGater). When the list is empty, no peer-ID filtering is applied; operators must firewall the libp2p port to the intended CL client. - The mump2p fleet the gateway joins via the embedded mump2p node on
the port configured by
agent_mump2p_port/OPT_AGENT_MUMP2P_PORT(default33213). When auth is enabled (OPT_ENABLE_AUTH=truewithOPT_API_KEYset), the handshake is authenticated by the operator's gateway JWT and verified against the upstream JWKS; when auth is disabled,VerifyTokenis a no-op and onlyClusterIDis enforced. - The bootstrap server + fork-digest hub that hand the gateway its initial mump2p peer set on first boot. See "Bootstrap server trust" below — this is the only trust anchor whose compromise can shape the peer-set of every gateway in the fleet.
Anything else reachable on the gateway's listen sockets is treated as hostile by default.
Default ports below are the config defaults; the actual port and (for
pprof/telemetry) bind address come from the env vars in parentheses.
The gateway does not ship its own per-listener auth or loopback
guard for most ports — operators are expected to firewall every port
except the libp2p (OPT_AGENT_LIB_P2P_PORT) and Optimum
(OPT_AGENT_MUMP2P_PORT) listeners to peer-eligible networks.
| Port (default) | Service | Bind | Auth | Notes |
|---|---|---|---|---|
33212 (OPT_AGENT_LIB_P2P_PORT) |
libp2p host (CL gossip) | all interfaces (/ip4/0.0.0.0/tcp/..., /ip6/::/tcp/...); Noise transport |
libp2p-pubsub direct peering with OPT_DIRECT_CL_PEERS; connect-time peer ID allowlist when that list is non-empty (not a ConnectionGater) |
Firewall to the local CL client when OPT_DIRECT_CL_PEERS is empty. |
33213 (OPT_AGENT_MUMP2P_PORT) |
Optimum (mump2p) | all interfaces | ClusterID always enforced in handshake; JWT verified by authMgr.VerifyToken (no-op when OPT_ENABLE_AUTH=false or OPT_API_KEY empty) |
No mainnet/hoodi refusal if auth is disabled — operators are responsible for setting OPT_ENABLE_AUTH=true in production. |
48123 (OPT_TELEMETRY_PORT) |
HTTP telemetry (Fiber) | all interfaces | none — /, /health, /api/v1/self_info are unauthenticated; /metrics is registered only when OPT_ENABLE_TELEMETRY=true and is also unauthenticated |
Put behind a reverse proxy / firewall if exposed. |
6060 (OPT_PPROF_ADDR) |
pprof HTTP | OPT_PPROF_ADDR (default 127.0.0.1:6060) |
none | Only started when OPT_ENABLE_PPROF=true. Remote exposure is controlled by setting OPT_PPROF_ADDR to a non-loopback address. |
On first boot the gateway builds a bootstrap URL via
utils.BootstrapExposeNodesURL(...) and calls
GET <bootstrap>/api/v1/expose-nodes?chain_id=<chain>&cluster_id=<cluster>&expose_amount=<n>
(see pkg/service/gossipsub-gateway/setup_mump2p_host.go) for its
initial mump2p peer set. If that response is non-200 or the returned
list is empty, the gateway falls back to a raw GitHub URL on the
forkdigest-hub repo
(https://raw.githubusercontent.com/getoptimum/forkdigest-hub/refs/heads/main/eth/<chain>/peers.json).
A compromised bootstrap server or forkdigest-hub repo can hand every
gateway in the fleet an attacker-controlled peer set. When auth is
enabled, the mump2p handshake gates final membership: handshakeHandler
checks h.ClusterID == cfg.GatewayClusterID and calls
authMgr.VerifyToken(h.JWTToken), which in turn requires the JWT's
chain_id claim to match the gateway's normalized chain. The impact is
therefore bounded by JWT issuance hygiene — but a holder of a valid JWT
for the same cluster + chain could insert themselves as the bootstrap of
every new gateway. When auth is disabled (OPT_ENABLE_AUTH=false or
OPT_API_KEY empty), VerifyToken is a no-op and only ClusterID is
enforced.
Operator-side mitigations:
- JWKS signing key rotation. Rotate at least every 90 days. The
gateway's JWKS cache refreshes every
JWKSRefreshIntervalSec(default3600seconds / 1h; used as theRefreshinterval of thekeyfunc.Keyfunc), so revocations take up to that long to propagate. Shorten this in incident response. - Branch protection on
forkdigest-hub.mainshould require code review from at least one ops engineer outside the contributor list, and force-push must be disabled. - Bootstrap server access control. The publishing path for
/api/v1/expose-nodesruns under a service account with no shell access; SSH to the bootstrap host is restricted to the on-call rotation.
- A malicious local CL client. The
direct_cl_peersdirect-peering configuration trusts the operator's beacon client. If that client is compromised, the gateway will faithfully forward anything it publishes onto the mump2p mesh. - A leaked or stolen gateway JWT. The JWT binds chain ID but not source IP; a JWT that escapes the operator's infrastructure can be used to peer with the mump2p mesh from anywhere. Revoke via the upstream JWKS rotation rather than waiting for the JWT to expire.
- Compromise of the upstream auth service. A compromised
auth.getoptimum.iocan mint JWTs for arbitrary operators. JWKS rotation only fences this if the key material itself is rotated.
Every image published to Docker Hub carries a BuildKit SBOM and SLSA provenance attestation, and is signed with cosign keyless (Sigstore/Fulcio, GitHub Actions OIDC identity). This applies to all three published images:
getoptimum/gatewaygetoptimum/gateway-benchgetoptimum/hermes-gateway-sidecar
Tagged getoptimum/gateway releases additionally carry the repo's CycloneDX
SBOM (docs/sbom.json, the shipped-binary ./cmd dependency footprint) as a
signed attestation.
Signing is by image-index digest, so the checks below work against any tag pointing at that digest. Set the image once:
IMG=getoptimum/gateway:latest # or :dev-latest, or a specific tag
ISSUER=https://token.actions.githubusercontent.com
IDENTITY='^https://github.com/getoptimum/optimum-gateway/'Verify the signature (fails if the image was not signed by this repo's release workflow):
cosign verify \
--certificate-oidc-issuer "$ISSUER" \
--certificate-identity-regexp "$IDENTITY" \
"$IMG"Inspect the attached SBOM and provenance:
docker buildx imagetools inspect "$IMG" --format '{{json .SBOM}}'
docker buildx imagetools inspect "$IMG" --format '{{json .Provenance}}'Verify the CycloneDX attestation (tagged getoptimum/gateway releases
only):
cosign verify-attestation --type cyclonedx \
--certificate-oidc-issuer "$ISSUER" \
--certificate-identity-regexp "$IDENTITY" \
"$IMG"The committed docs/sbom.json (shipped-binary deps) and docs/sbom-full.json
(all deps) are the source-level SBOMs used for license/dependency governance;
they are regenerated with make sbom and kept current in CI.
Operator-applied mitigations that don't require a rebuild — firewall rules, reverse proxies in front of the telemetry port, JWKS rotation cadence, and pprof exposure — are described inline in the sections above. Release-specific hardening notes, when published, will be linked from this section.