Files
Johnathan Corgan 5119b0f5f3 Add a workspace with an empty no_std fips-core crate
Start the no_std core as a second workspace member. The crate is empty:
it holds the rule that code in it builds for any target with an
allocator (no std, no async runtime, no OS calls, no target cfg), and
the root crate does not depend on it yet, so nothing that builds or
ships changes. The root package stays first in Cargo.toml with its
version line untouched, since the packaging scripts read the release
version from it, and the core is fixed at 0.0.0 with publish = false so
releases keep a single bump site.

default-members names both crates, so the bare cargo clippy and test
commands in local CI and GitHub CI cover the core as well as the root
package.

Add a fence to both runners: clippy and the tests of the core on its
own, so a dependency feature that only the root crate enables cannot
hide one the core needs.

Pin the privileged interface-binding step to the fips package's lib
test binary. With two lib test executables, `tail -1` picked whichever
cargo listed last, and the core's binary passes the ethernet filter
having run nothing. The step now also runs under bash with pipefail and
fails unless at least one test passed.

Widen the comment-reference, portable-atomics and log-string guards to
core/src/, and add core/ to the stale-binary checks of the mdns,
native-api and deb-install harnesses, so code that later moves into the
core stays checked. Copy core/ into the two nostr relay example images,
whose build would otherwise fail to load the workspace manifest, and
give the standalone BLE spike under testing/ble an empty workspace table
of its own, so cargo keeps loading it as its own project rather than
refusing it as an unlisted member of the root workspace.


Add core/README.md for anyone reading or adding code under core/: what the crate is for, the rules its code follows, how to add a dependency without letting workspace feature unification hide a missing feature, how tests work under no_std, a checklist for moving a module in from the root crate, and the checks to run before a change.
2026-10-11 17:08:56 +00:00
..
2026-08-30 10:42:59 +00:00

FIPS Nostr Relay Sidecar

Runs a strfry Nostr relay reachable exclusively over the FIPS mesh. The relay container shares the FIPS sidecar's network namespace and is isolated from the host network by iptables — it can only be reached via the node's .fips name.

How to Run

1. Set your node identity

The relay needs a unique FIPS identity. Generate one with:

fipsctl keygen --stdout

--stdout prints the nsec and npub instead of writing fips.key and fips.pub.

Then set it in .env:

# .env
FIPS_NSEC=nsec1...   # paste your nsec here

FIPS_NSEC is required — the container will refuse to start without it.

2. Start the stack

FIPS is compiled from source inside the Docker build stage — no local Rust toolchain, Zig, or cargo-zigbuild needed.

cd examples/sidecar-nostr-relay
docker compose up -d

This starts two containers that share a network namespace:

  • fips — FIPS daemon + dnsmasq. Owns the namespace, creates fips0.
  • app — strfry relay + nginx. Joins the namespace via network_mode: service:fips.

3. Verify

# FIPS node is up and has a mesh address:
docker exec sidecar-nostr-relay-fips-1 fipsctl show status

# Relay is listening (should show nginx on :80 and strfry on :7777):
docker exec sidecar-nostr-relay-fips-1 ss -tlnp

# Peer link to the public node is established:
docker exec sidecar-nostr-relay-fips-1 fipsctl show peers

# Or check the logs:
docker compose logs -f

4. Connect to the relay

Your node's npub (and therefore its .fips name) is derived from its keypair:

docker exec sidecar-nostr-relay-fips-1 fipsctl show status

Connect from any FIPS-peered client using the node's npub:

ws://npub1xxxx.fips:80

Security Model

The sidecar pattern enforces strict network isolation on the app container:

  • No IPv4 access: iptables blocks all eth0 traffic except the FIPS UDP transport (port 2121), the local FIPS TCP listener (port 8443), and outbound TCP to peers' published endpoints (port 443). The app container cannot reach the Docker bridge, the host network, or any IPv4 address.
  • No IPv6 on eth0: ip6tables blocks all IPv6 traffic on eth0. The app container cannot use link-local or any Docker-assigned IPv6 addresses.
  • FIPS mesh only: The only routable network path is through fips0 (fd00::/8). All application traffic traverses the FIPS mesh with end-to-end encryption.
  • Loopback allowed: lo is unrestricted for inter-process communication within the shared namespace.

This means the app container treats the FIPS mesh as its sole network. Even if the application is compromised, it cannot bypass the mesh or communicate with the transport layer directly.

Architecture

┌───────────────────────────────────────────────────┐
│ Shared network namespace                          │
│                                                   │
│ ┌───────────────┐    ┌──────────────────────────┐ │
│ │ fips-sidecar  │    │ fips-app                 │ │
│ │               │    │                          │ │
│ │ fips daemon   │    │ your workload            │ │
│ │ fipsctl       │    │                          │ │
│ │ dnsmasq       │    │                          │ │
│ └───────────────┘    └──────────────────────────┘ │
│                                                   │
│ Interfaces:                                       │
│   lo    — loopback (unrestricted)                 │
│   eth0  — Docker bridge (iptables: FIPS only)     │
│   fips0 — FIPS TUN (fd00::/8, unrestricted)         │
└───────────────────────────────────────────────────┘

The FIPS sidecar owns the network namespace and creates the fips0 TUN interface. The app container joins via network_mode: service:fips and sees the same interfaces. The entrypoint script applies iptables rules before launching the FIPS daemon:

IPv4 rules (iptables):

  • ACCEPT on lo (both directions)
  • ACCEPT UDP sport/dport 2121 on eth0 (FIPS UDP transport)
  • ACCEPT TCP dport 443 / sport 443 on eth0 (outbound to peers' TCP endpoints)
  • ACCEPT TCP dport/sport 8443 on eth0 (local FIPS TCP listener, FIPS_TCP_BIND)
  • DROP everything else on eth0

IPv6 rules (ip6tables):

  • ACCEPT on lo (both directions)
  • ACCEPT on fips0 (both directions)
  • DROP everything on eth0

DNS Resolution

DNS inside the container is handled by dnsmasq (127.0.0.1:53):

  • .fips queries are forwarded to the FIPS daemon's built-in DNS resolver (127.0.0.1:5354), which resolves npub-based names to fd00::/8 addresses
  • All other queries are forwarded to Docker's embedded DNS (127.0.0.11)

The resolv.conf mount points the container's resolver at 127.0.0.1, where dnsmasq handles the routing.

Run with Peers

To connect the sidecar to an existing mesh, provide the peer's npub and transport address:

FIPS_PEER_NPUB=npub1... \
FIPS_PEER_ADDR=203.0.113.10:2121 \
FIPS_PEER_ALIAS=gateway \
docker compose up -d

Verify the peer link:

docker exec sidecar-nostr-relay-fips-1 fipsctl show peers
docker exec sidecar-nostr-relay-fips-1 fipsctl show links

Verify Connectivity and Isolation

From the app container:

# Ping a mesh node by npub (resolves via .fips DNS):
docker exec sidecar-nostr-relay-app-1 ping6 -c3 npub1xxxx.fips

# Fetch a web page from some other mesh node over FIPS
# (:8000 is a stand-in for that node's own service; this relay serves :80):
docker exec sidecar-nostr-relay-app-1 curl -6 "http://npub1xxxx.fips:8000/"

# Docker bridge is blocked — this should fail:
docker exec sidecar-nostr-relay-app-1 ping -c1 -W2 172.20.0.13

# Loopback is allowed:
docker exec sidecar-nostr-relay-app-1 ping -c1 127.0.0.1

Environment Variables

Variable Default Description
FIPS_NSEC (required) Node secret key (hex or nsec1 bech32)
FIPS_PEER_NPUB (empty) Peer's npub to connect to
FIPS_PEER_ADDR (empty) Peer's transport address (e.g. 203.0.113.10:2121)
FIPS_PEER_ALIAS peer Human-readable peer name
FIPS_UDP_BIND 0.0.0.0:2121 UDP transport bind address
FIPS_TCP_BIND 0.0.0.0:8443 TCP transport bind address
FIPS_PEER_TRANSPORT udp Peer transport type (udp or tcp)
FIPS_TUN_MTU 1280 TUN interface MTU
FIPS_UDP_MTU 1472 UDP transport MTU (default is Docker bridge IPv4 max; set to 1280 for IPv6-min-safe deploys)
FIPS_NETWORK fips-sidecar-net Docker network name (set to join external network)
FIPS_SUBNET 172.20.1.0/24 Docker network subnet
FIPS_IPV4 172.20.1.20 Sidecar's IPv4 address on the Docker network
RUST_LOG info FIPS log level

Troubleshooting

FIPS_NSEC is required — The FIPS_NSEC environment variable is not set. Either add it to .env or pass it on the command line. Generate a random key with: openssl rand -hex 32

fips0 interface not appearing — The FIPS daemon needs /dev/net/tun and NET_ADMIN capability. Check that the compose file includes both:

cap_add:
  - NET_ADMIN
devices:
  - /dev/net/tun:/dev/net/tun

No peer connection established — Verify the peer address is reachable from the sidecar container (docker exec sidecar-nostr-relay-fips-1 ping -c1 <peer-ip>). If joining an external Docker network, ensure FIPS_NETWORK, FIPS_SUBNET, and FIPS_IPV4 match the target network. Check logs with docker logs sidecar-nostr-relay-fips-1.

DNS not resolving .fips names — Verify dnsmasq is running: docker exec sidecar-nostr-relay-fips-1 pgrep dnsmasq. Check that resolv.conf is mounted (should contain nameserver 127.0.0.1). Verify the FIPS DNS resolver is listening: docker exec sidecar-nostr-relay-fips-1 dig @127.0.0.1 -p 5354 <npub>.fips AAAA.

iptables errors in entrypoint — The sidecar container requires NET_ADMIN capability for iptables. Without it, the isolation rules cannot be applied and the entrypoint will fail.

Production Considerations

Secrets management: The shipped .env leaves FIPS_NSEC= empty, so the operator supplies the key. In production, use Docker secrets, a vault, or inject the key via a secure CI/CD pipeline. Never commit production keys to version control.

Logging: Set RUST_LOG to control log verbosity (debug, info, warn, error). For production, configure the Docker logging driver with size limits:

logging:
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

Resource limits: Add memory and CPU constraints in the compose file:

deploy:
  resources:
    limits:
      memory: 256M
      cpus: "0.5"

Multiple peers: The entrypoint supports a single peer via environment variables. For multiple peers, mount a custom fips.yaml directly:

volumes:
  - ./my-fips.yaml:/etc/fips/fips.yaml:ro

Health checks: Add a Docker health check using fipsctl:

healthcheck:
  test: ["CMD", "fipsctl", "show", "status"]
  interval: 30s
  timeout: 5s
  retries: 3