Skip to content

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.

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.

ModeWhat it meansWhere 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.
embeddedThe 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).

The local HTTP server is separate from the peer-to-peer swarm and they have very different exposure defaults:

ListenerConfigmerod init defaultIntended exposure
HTTP server (admin / JSON-RPC / WS / SSE)server.listenloopback only — 127.0.0.1:2528 and [::1]:2528Private. Reach it through a proxy.
libp2p swarm (peer traffic)swarm.listenall interfaces — 0.0.0.0:2428 and [::]:2428Public. 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.

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.

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.

WhatWhereNotes
Node transport identity (private key)config.toml[identity] keypairBase58-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>/blobsThe 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.

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/test
only 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).

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, repository calimero-network/mero-tee, workflow Release mero-kms, trigger push, ref refs/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 varPrecedence
MERO_KMS_RELEASE_TAGhighest
MERO_KMS_VERSIONmiddle
MERO_TEE_VERSIONlowest

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.

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.

  1. Keep the admin surface on loopback. Leave server.listen at its 127.0.0.1 / [::1] default and never widen it to 0.0.0.0. The swarm (swarm.listen) stays public on 0.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"]
  2. Turn auth on. Either run the bundled service (auth_mode = "embedded", above) so the node validates JWTs itself, or keep the default proxy mode and put an authenticating proxy in front. Do not run proxy mode with the bind reachable off-host and nothing enforcing auth.

  3. 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.

  4. 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.

  5. Encrypt state at rest. Provision the datastore encryption key so the node logs Storage encryption enabled at startup. On a TEE deployment, source that key from the KMS and require real attestation — set a [tee.kms.phala.attestation] policy with enabled = true, accept_mock = false, and pin the KMS release version so the release-policy verification above runs.

  6. Keep the secrets private. config.toml holds 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.

  7. No dev switches. Confirm --mock-tee / MEROD_MOCK_TEE, specialized_node.accept_mock_tee, and attestation.accept_mock are all off. A node with a real KMS attestation configured refuses to start with --mock-tee at all.

SurfaceExposure on a hardened internet-facing node
swarm.listen (peer traffic)Public0.0.0.0 / ::, encrypted by the wire protocol.
server.listen (admin / JSON-RPC / WS / SSE / metrics)Loopback only — reached through an authenticating TLS proxy.
/metricsPrivate — scraped over loopback / a private monitoring network, never public.
config.toml, auth store, home directorySecret — service-user only, encrypted backups, never committed.
  1. Do keep server.listen on loopback (127.0.0.1 / ::1) and reach the admin API through a TLS-terminating reverse proxy.

  2. Do put an authenticating proxy in front of the node in the default proxy mode — or switch to auth_mode = "embedded" so the node validates tokens itself.

  3. Do terminate TLS at the edge; the node’s HTTP server has no built-in TLS.

  4. Do back up the whole node home directory, restrict its permissions to the service user, and store backups encrypted. config.toml holds the node’s private key.

  5. Do enable datastore encryption at rest for sensitive deployments.

  6. Don’t bind server.listen to 0.0.0.0 to expose the admin API or dashboard remotely.

  7. Don’t run --mock-tee / MEROD_MOCK_TEE, accept_mock_tee, or attestation.accept_mock outside development.

  8. Don’t enable the auth service’s mock-token endpoint in production (see Auth service & providers).

  9. Don’t commit config.toml or the auth store to version control or share them — they contain private key material.