Gateway Self-Enrollment
Prerequisites: Network Requirements and Configuration.
Use this path when you run many gateways under one operator. Instead of minting and distributing one ogw_ API key per host, you mint one org-wide join key (ojk_live_...) and let each gateway register its own asymmetric credential on first boot.
The legacy API key path is unchanged. Single-gateway and small deployments should keep using Quick Start.
When to use a join key
API key (ogw_) | Join key (ojk_) | |
|---|---|---|
| Best for | One gateway, or a handful you can map by hand | Fleets — tens or hundreds of hosts |
| Credential | One secret per host | One org key, shared across the fleet |
| Gateway identity | Baked into the key at mint time | Assigned at enroll (client_id in JWT sub) |
| Chain / type / clusters | From the API key | From the join key — gateway cannot self-assign |
Mint a join key
Sign in to the Partner Console.
Select your network in the header picker (Ethereum, Hoodi, or Mock Chain; Ethereum is selected by default). The key inherits it.
In the sidebar open Manage Gateways, then select the Enrollment keys tab.
Click Generate enrollment key and fill in, in this order:
- Network — read-only, inherited from the header picker. Change it there, not here.
- Name — identifies the key in the list, for example
hoodi-dev fleet. A key with no name is listed by its raw ID. - Clusters — tick every cluster the enrolled gateways may join. On any network that has clusters at least one is required, and the selection must include the
gateway_cluster_idyou set on each host. - Valid for (days) — how long the key may enroll new gateways. Default 7, maximum 365. Gateways already enrolled are unaffected when it expires.
The modal confirms that gateways enrolled with this key are provisioned as partner gateways.
Click Generate, then copy the key (
ojk_live_...). It is shown only once. Store it in your secret manager — the same way you would an API key. The same screen shows Enrollment closes with a date; check it, since that is where a shortened validity window would show up.
Each enrollment key admits up to 1000 gateways. That ceiling is fixed and not configurable. Your organization is separately capped at 1000 live gateway credentials in total, counting ogw_ API keys, so a key can stop admitting gateways before its own use count runs out.
The key list shows enrollments used and the expiry date, with a state badge of active, expired, or exhausted. A revoked key is removed from the list rather than badged. Check there first when enrollment starts failing: the enroll endpoint deliberately returns the same opaque 401 for all of unknown, expired, exhausted, and revoked, so the list is the only thing that tells them apart.
Revoking an enrollment key stops new enrollments. Gateways already enrolled keep their own credentials and keep running. To cut one off, revoke it separately under Manage Gateways → Gateway tab, where enrolled credentials are listed alongside ogw_ API keys. The console does not mark which rows came from enrollment, so find yours by the label you set in OPT_GATEWAY_ID.
Configure a gateway
Set exactly one credential mode. api_key and join_key are mutually exclusive — setting both is a startup error.
Create config/app_conf.yml (operational fields only):
log_level: info
gateway_cluster_id: optimum_ethereum_hoodi_v0_1 # must match the join key's cluster scope
agent_lib_p2p_port: 33212
agent_mump2p_port: 33213
telemetry_enable: true
telemetry_port: 48123
identity_libp2p_dir: /data/libp2p
identity_mump2p_dir: /data/mump2pPass the join key and a per-host enrollment label via the environment — not YAML:
export OPT_JOIN_KEY=ojk_live_xxx
export OPT_GATEWAY_ID=hoodi-validator-rack-03 # unique per host — enrollment label onlyJoin key via environment, not YAML. Same rule as
OPT_API_KEY: keep secrets out of config files and image layers.
Config reference (join-key path)
| Key | Env | Default | Description |
|---|---|---|---|
join_key | OPT_JOIN_KEY | (empty) | Org-wide join credential (ojk_live_...). Set via env, not YAML. |
enroll_cred_dir | OPT_ENROLL_CRED_DIR | identity_mump2p_dir | Directory for enrollment.json, plus a transient enrollment.key during first boot. Defaults to the mumP2P identity dir. Must be persistent. |
gateway_id | OPT_GATEWAY_ID | dev-gateway | Under join key: used as the enrollment label at first boot only. Set a unique value per host. Left at the default, the label is empty — see Troubleshooting. After enroll, runtime gateway_id in /health and metrics comes from the JWT sub claim (client_id), not this value. |
gateway_cluster_id | OPT_GATEWAY_CLUSTER_ID | (required) | Must match a cluster ID baked into the join key |
identity_mump2p_dir | OPT_IDENTITY_MUMP2P_DIR | /tmp/mump2p | mumP2P identity — persist as a volume. Holds enrollment credential when enroll_cred_dir is unset |
identity_libp2p_dir | OPT_IDENTITY_LIBP2P_DIR | /tmp/libp2p | libp2p identity — persist as a volume |
remote_auth_url | OPT_REMOTE_AUTH_URL | https://auth.getoptimum.io | Auth service issuer |
All other keys (agent_*_port, telemetry_*, direct_cl_peers, stream_*, etc.) are the same as the API key configuration.
What happens on boot
First boot (no credential on disk):
- Gateway generates a P-256 keypair locally and writes it to
enrollment.keyunderenroll_cred_dirbefore calling out. The private key never leaves the host. - Gateway calls
POST https://auth.getoptimum.io/api/v1/gateways/enrollwith the join key and a proof-of-possession signature. - Auth returns a
client_id(no secret). Gateway writesenrollment.jsonunderenroll_cred_dir(mode0600) and removesenrollment.key. - Gateway mints JWTs by signing a client assertion — same bootstrap and mesh behaviour as the API-key path.
Every restart after that: gateway loads enrollment.json from disk and mints directly. No enroll call, no join-key use consumed.
Persisting the keypair before the POST is what makes an interrupted first boot safe: if the response is lost, the next boot retries with the same key rather than stranding a credential upstream that nothing on the host can use.
docker pull getoptimum/gateway:v1.3.2
docker run -d --name optimum-gateway \
--network host \
-e OPT_JOIN_KEY=ojk_live_xxx \
-e OPT_GATEWAY_ID=hoodi-validator-rack-03 \
-v $(pwd)/config:/app/config \
-v $(pwd)/data/libp2p:/data/libp2p \
-v $(pwd)/data/mump2p:/data/mump2p \
getoptimum/gateway:v1.3.2 \
-config=/app/config/app_conf.ymlPersistent storage
The enrollment credential lives alongside the mumP2P identity:
- Default location:
identity_mump2p_dir, holdingenrollment.json— the credential, including its private key - Override with
enroll_cred_dironly if you need a separate mount — both dirs must survive restarts - Mount the whole directory, not just the credential file. During first boot the gateway also keeps a transient
enrollment.keythere, between generating its keypair and persisting the credential; if the host restarts inside that window, that file is what lets it resume. It is removed onceenrollment.jsonis written, so there is no need to back it up — but restoreenrollment.jsontogether with the mumP2P identity it was enrolled against. The credential records its peer ID, and a credential restored beside a fresh identity fails at startup with a peer ID mismatch
Losing the credential directory means a new keypair, a new enrollment, and a consumed join-key use. With a stable enrollment label (OPT_GATEWAY_ID set per host), re-enrollment under the same label is refused with 409 label_conflict while the old credential is still live — recovery is to revoke the orphan as described under Mint a join key.
Leaving OPT_GATEWAY_ID at its default sends an empty label, which is exempt from that check. A host in that state re-enrolls silently every time it loses its credential directory, consuming a join-key use each time and leaving an unlabelled orphan that is hard to identify in the console. Set a unique label before first boot.
Scope from the join key
A gateway cannot elevate itself:
type— e.g.partner; privileged types are gated at join-key mint timechain_id— Hoodi vs Mainnetcluster_ids— must include the cluster you set ingateway_cluster_id
Mint a join key whose cluster scope matches your deployment. The console requires at least one cluster on any network that has them, so this is normally enforced for you; a key that reaches auth with no cluster_ids produces gateways that authenticate and then fail every mesh handshake.
Verify enrollment
curl -s http://localhost:48123/health | jq '.status, .gateway_id'
curl -s http://localhost:48123/metrics | grep -E 'auth_enrollment_total|auth_token_mint_total'/health reports CL and mesh checks only — there is no auth check. Enrollment is confirmed by the metrics above and by gateway_id switching to the enrolled client_id.
On first boot you should see auth_enrollment_total{result="success"} increment once. On later restarts: result="reused". Repeated success across a fleet usually means credential directories are not persisting.
The gateway_id in /health and metrics is the enrolled client_id (JWT sub), not the enrollment label you set in OPT_GATEWAY_ID.
Migration from API keys
Existing ogw_ gateways are unaffected. There is no forced migration.
To move a host to join-key enrollment:
- Mint a join key with matching chain and cluster scope.
- Remove
OPT_API_KEY, setOPT_JOIN_KEYand a uniqueOPT_GATEWAY_ID. - Restart. The gateway enrolls as a new credential — revoke the old API key in the console when you decommission it.
Next steps
- Configuration — ports, direct CL peers, stream settings
- Kubernetes (Helm) — join-key notes for fleet deployments
- Troubleshooting — enroll failures and credential issues

