Hardening & Security
This page collects the security-relevant decisions for running a merod node:
how authentication is enforced, what must never face the public internet, where
the node’s keys live, and which development-only switches are unsafe in
production. Configuration keys and CLI flags referenced here are documented in
full on the config reference and the
merod reference; the edge/TLS setup is covered end-to-end on
the deployment guide.
Authentication modes
Section titled “Authentication modes”The HTTP server (admin API, JSON-RPC, WebSocket, SSE) is gated by a single
server.auth_mode setting. There are two modes, and the default is proxy.
| Mode | What it means | Where auth is enforced |
|---|---|---|
proxy (default) | The node does not authenticate requests itself. It expects an external authenticating proxy in front of it. | At your reverse proxy / auth proxy. |
embedded | The node runs the bundled auth service (mero-auth) in-process and validates JWTs itself. | Inside the node (see Auth service & providers). |
You can set the mode at init (merod init --auth-mode embedded), in
config.toml (server.auth_mode), or override it for a single run
(merod run --auth-mode embedded).
Keep the admin API off public interfaces
Section titled “Keep the admin API off public interfaces”The local HTTP server is separate from the peer-to-peer swarm and they have very different exposure defaults:
| Listener | Config | merod init default | Intended exposure |
|---|---|---|---|
| HTTP server (admin / JSON-RPC / WS / SSE) | server.listen | loopback only — 127.0.0.1:2528 and [::1]:2528 | Private. Reach it through a proxy. |
| libp2p swarm (peer traffic) | swarm.listen | all interfaces — 0.0.0.0:2428 and [::]:2428 | Public. Peers dial it directly. |
merod init --server-host defaults to 127.0.0.1,::1, so the admin surface is
not reachable off-host until you deliberately widen it. The swarm host defaults
to 0.0.0.0,:: because peers must reach it.
TLS termination
Section titled “TLS termination”The node’s HTTP server speaks plain HTTP and has no built-in TLS. For any
access beyond loopback, terminate TLS at a reverse proxy (nginx, Caddy, Traefik,
…) and forward to the loopback bind. That proxy is also where you enforce
authentication when running the default proxy mode. The
deployment guide
has a working nginx sketch, including the WebSocket/SSE upgrade and buffering
settings those streaming endpoints need.
The libp2p swarm is encrypted independently (TLS / Noise over TCP, and QUIC), so swarm traffic does not need a TLS proxy — see Networking.
Key & identity storage
Section titled “Key & identity storage”Everything a node needs to prove who it is and to read its encrypted state lives
under the node home directory (<home>/<node-name>). Treat that directory as
secret material.
| What | Where | Notes |
|---|---|---|
| Node transport identity (private key) | config.toml → [identity] keypair | Base58-encoded libp2p keypair. This is the node’s private key, stored in plaintext in the config file. The matching peer_id is public. |
| Application & group state | <home>/data (RocksDB) | The datastore.path directory. Can be encrypted at rest (see below). |
| Blobs | <home>/blobs | The blobstore.path directory. |
| Embedded-auth store (JWT signing secret, root/client keys) | <home>/auth (RocksDB) | Present when auth_mode = "embedded" with persistent storage; the auth service’s JWT secret is generated and held here. |
The node’s protocol-level identities (the per-group signing identities used for governance) are derived material managed by the node and stored in the datastore; see Identities for the protocol model.
The --mock-tee switch
Section titled “The --mock-tee switch”merod run --mock-tee (env MEROD_MOCK_TEE=true) makes the node produce and
accept mock TEE attestation quotes instead of real TDX attestation. It exists
for development and testing only.
The node has a built-in guard: --mock-tee is refused at startup when the
config has a real KMS attestation configured (a tee.kms.phala block with
attestation enabled and not accepting mock), with an error like:
--mock-tee refused: a real KMS/attestation is configured. Mock TEE is dev/testonly and cannot coexist with real attestation.When mock is allowed, the node logs a loud MOCK TEE ENABLED — INSECURE, DEV/TEST ONLY warning, and warns again if a Phala KMS provider is configured at
all (likely a misconfiguration).
KMS release-policy verification
Section titled “KMS release-policy verification”A TEE node pulls its storage-encryption keys from the mero-kms KMS only after it
is satisfied the KMS is running the expected enclave. That decision is governed
by an attestation policy — the allow-listed TDX measurements (MRTD, RTMR0–3)
and TCB statuses the KMS quote must match. So that the policy itself can’t be
silently swapped, merod verifies the policy with Sigstore before fetching any
keys.
When you pin a KMS release version, merod downloads three assets from the
official mero-tee GitHub release (mero-kms-v{version}) — the policy JSON, a
detached signature, and a Sigstore bundle — and verifies, fail-closed:
- the Rekor signed entry timestamp (the transparency-log inclusion proof),
- the detached signature over the exact policy bytes, and
- the Fulcio certificate chain, pinned to a specific GitHub Actions identity:
OIDC issuer
https://token.actions.githubusercontent.com, repositorycalimero-network/mero-tee, workflowRelease mero-kms, triggerpush, refrefs/heads/master.
If any check fails, startup errors out rather than continuing with an unverified policy.
You select the release version through one of three environment variables, in precedence order — the first one set wins:
| Env var | Precedence |
|---|---|
MERO_KMS_RELEASE_TAG | highest |
MERO_KMS_VERSION | middle |
MERO_TEE_VERSION | lowest |
The value may be a plain version (2.1.14) or the prefixed tag
(mero-kms-v2.1.14). With none of these set and USE_ENV_POLICY unset, no
release policy is fetched.
Hardening pass: an internet-facing node
Section titled “Hardening pass: an internet-facing node”A concrete, end-to-end pass for a node that must be reachable from the public internet (its swarm port is public; its admin surface must not be). Adapt the hostnames and key handling to your environment.
-
Keep the admin surface on loopback. Leave
server.listenat its127.0.0.1/[::1]default and never widen it to0.0.0.0. The swarm (swarm.listen) stays public on0.0.0.0/::— peers must dial it.[server]listen = ["/ip4/127.0.0.1/tcp/2528", "/ip6/::1/tcp/2528"]auth_mode = "embedded"[swarm]listen = ["/ip4/0.0.0.0/tcp/2428", "/ip4/0.0.0.0/udp/2428/quic-v1"] -
Turn auth on. Either run the bundled service (
auth_mode = "embedded", above) so the node validates JWTs itself, or keep the defaultproxymode and put an authenticating proxy in front. Do not runproxymode with the bind reachable off-host and nothing enforcing auth. -
Terminate TLS at the edge and forward to the loopback bind. The node’s HTTP server speaks plain HTTP; the reverse proxy is also where
proxy-mode auth is enforced. See the reverse-proxy setup. -
Firewall
/metrics. The metrics endpoint is part of the same loopback HTTP server — do not expose it publicly. Scrape it over loopback (or a private network only your monitoring reaches), and let the host firewall drop off-host traffic to the server port. Reach the dashboard and metrics through the same authenticated proxy, never by widening the bind. -
Encrypt state at rest. Provision the datastore encryption key so the node logs
Storage encryption enabledat startup. On a TEE deployment, source that key from the KMS and require real attestation — set a[tee.kms.phala.attestation]policy withenabled = true,accept_mock = false, and pin the KMS release version so the release-policy verification above runs. -
Keep the secrets private.
config.tomlholds the[identity] keypair(the node’s private key) in plaintext, and the embedded-auth store holds the JWT signing secret. Restrict the home directory to the service user, store backups encrypted, and never commit either to version control. -
No dev switches. Confirm
--mock-tee/MEROD_MOCK_TEE,specialized_node.accept_mock_tee, andattestation.accept_mockare all off. A node with a real KMS attestation configured refuses to start with--mock-teeat all.
| Surface | Exposure on a hardened internet-facing node |
|---|---|
swarm.listen (peer traffic) | Public — 0.0.0.0 / ::, encrypted by the wire protocol. |
server.listen (admin / JSON-RPC / WS / SSE / metrics) | Loopback only — reached through an authenticating TLS proxy. |
/metrics | Private — scraped over loopback / a private monitoring network, never public. |
config.toml, auth store, home directory | Secret — service-user only, encrypted backups, never committed. |
Do / don’t checklist
Section titled “Do / don’t checklist”-
Do keep
server.listenon loopback (127.0.0.1/::1) and reach the admin API through a TLS-terminating reverse proxy. -
Do put an authenticating proxy in front of the node in the default
proxymode — or switch toauth_mode = "embedded"so the node validates tokens itself. -
Do terminate TLS at the edge; the node’s HTTP server has no built-in TLS.
-
Do back up the whole node home directory, restrict its permissions to the service user, and store backups encrypted.
config.tomlholds the node’s private key. -
Do enable datastore encryption at rest for sensitive deployments.
-
Don’t bind
server.listento0.0.0.0to expose the admin API or dashboard remotely. -
Don’t run
--mock-tee/MEROD_MOCK_TEE,accept_mock_tee, orattestation.accept_mockoutside development. -
Don’t enable the auth service’s mock-token endpoint in production (see Auth service & providers).
-
Don’t commit
config.tomlor theauthstore to version control or share them — they contain private key material.