Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@

- Reject invalid APNs `environment` configuration values at startup instead of silently routing pushes to the sandbox gateway; only `production` and `sandbox` are accepted ([#143](https://github.com/marmot-protocol/transponder/pull/143)).
- Limit each notification event to at most 100 encrypted tokens before base64 decoding, preventing oversized events from forcing unbounded token blob allocation and rate-limit work ([#38](https://github.com/marmot-protocol/transponder/pull/38)).
- Validate configuration at load time so misconfiguration fails loudly at startup with a named-field error instead of silently coercing or detonating later: `server.max_dedup_cache_size`, `server.max_rate_limit_cache_size`, and `server.max_tokens_per_event` now reject `0`; `health.bind_address` must parse as a socket address; `logging.format` accepts only `json`/`pretty` (unknown values, including `off`, are rejected — silence console logs with `logging.level = "off"`); and `relays.onion` entries must be `ws://`/`wss://` URLs with a `.onion` host. Change-detection for the kind-10050 inbox relay list now normalizes URLs through `RelayUrl` and selects the newest event by `created_at`, avoiding spurious republishes.

### Changed

- Updated MIP-05 token handling for the expanded 1084-byte encrypted token format and variable-length APNs/FCM device tokens introduced in [marmot-protocol/mdk#254](https://github.com/marmot-protocol/mdk/pull/254) ([#40](https://github.com/marmot-protocol/transponder/pull/40)).
- Centralized configuration defaults on the serde `default_*` functions as the single source of truth, removing the parallel `set_default` ladder that could silently drift out of sync.

### Security

Expand All @@ -23,3 +25,5 @@
- Changed the default health server bind address to localhost and documented internal-only exposure for the unauthenticated health, readiness, and metrics endpoints ([#39](https://github.com/marmot-protocol/transponder/pull/39)).
- Redacted FCM service account private keys from debug output and zeroized the service account JSON buffer after loading [#35](https://github.com/marmot-protocol/transponder/pull/35)
- Zeroized the server Nostr private key in config state and shortened the lifetime of resolved key material during startup ([#37](https://github.com/marmot-protocol/transponder/pull/37)).
- Refused to start when the server `private_key_file` grants group or other read access (mode `& 0o077`), mirroring the `0600` the `generate-keys` write path enforces, so an accidentally world-readable key file is rejected instead of loaded silently.
- Kept the inline server private key (`server.private_key` / `TRANSPONDER_SERVER_PRIVATE_KEY`) out of the `config` crate's un-zeroized `Value` tree by resolving it through a dedicated `Zeroizing` path before the rest of the configuration is parsed.
42 changes: 29 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,20 +49,25 @@ Transponder uses TOML configuration files with environment variable overrides. T

```toml
[server]
# Server's Nostr private key in hex format (64 hex characters)
# Prefer private_key_file for production secret handling.
# Server's Nostr private key in hex format (64 hex characters).
# Prefer private_key_file for production: it keeps the secret out of the config
# file and lets the server enforce file permissions on it. An inline value here
# (or in TRANSPONDER_SERVER_PRIVATE_KEY) is read through a dedicated zeroizing
# path that never stores it in the config parser's un-zeroized buffers.
private_key = ""

# Alternative: path to a file containing the private key.
# The file must be mode 0600 (not group/world readable) or the server refuses
# to start, mirroring the 0600 that generate-keys writes.
# Generate with: transponder generate-keys --output /path/to/server.key
# private_key_file = "/run/secrets/transponder_private_key"

# Graceful shutdown timeout in seconds (must be >= 1; 0 skips the drain)
shutdown_timeout_secs = 10

# Volatile event deduplication cache size when durable replay state is disabled
# (default: 100000). With dedup_state_path set, all terminal event IDs inside
# dedup_retention_secs are retained for the full NIP-59 subscription lookback.
# (default: 100000; must be >= 1). With dedup_state_path set, all terminal event
# IDs inside dedup_retention_secs are retained for the full NIP-59 lookback.
# max_dedup_cache_size = 100000

# Optional durable replay state for processed gift-wrap event IDs.
Expand All @@ -75,7 +80,9 @@ shutdown_timeout_secs = 10
# max_notification_age_secs = 3600
# max_notification_future_skew_secs = 300

# Rate limiting to prevent spam and replay attacks
# Rate limiting to prevent spam and replay attacks. Size fields must be >= 1;
# a value of 0 is rejected at startup (it previously either silently swapped in
# the default or rejected every event).
# max_rate_limit_cache_size = 100000 # Tracked keys per limiter, not total timestamps
# max_tokens_per_event = 100 # Per notification event
# encrypted_token_rate_limit_per_minute = 240 # Per encrypted token (replay protection)
Expand All @@ -100,7 +107,10 @@ clearnet = [
allow_unencrypted_clearnet_relays = false

# Tor/onion relays (optional)
# Requires a build with `--features tor` and a host that can support Tor traffic
# Requires a build with `--features tor` and a host that can support Tor traffic.
# Each entry must be a ws:// or wss:// URL with a .onion host; a clearnet or
# malformed entry here is rejected at startup instead of silently degrading to
# clearnet.
onion = []

# Reconnection settings
Expand Down Expand Up @@ -147,23 +157,29 @@ service_account_path = ""
project_id = ""

[health]
# Enable the health check HTTP server
# Enable the /health and /ready endpoints
enabled = true

# Address and port to bind the health server to
# Keep this on localhost unless an internal proxy, VPN, or load balancer needs it
# Address and port to bind the health/metrics listener to. Must be a valid
# IP:port socket address (a hostname like "localhost:8080" or an out-of-range
# port is rejected at startup). Keep this on localhost unless an internal proxy,
# VPN, or load balancer needs it.
bind_address = "127.0.0.1:8080"

[metrics]
# Whether Prometheus metrics are enabled
# Metrics are exposed at /metrics on the health server port
# Whether Prometheus metrics are enabled.
# /metrics is served on health.bind_address whenever metrics are enabled, even
# if the health endpoints (health.enabled) are disabled.
enabled = true

[logging]
# Log level: "trace", "debug", "info", "warn", "error", "off"
# Log level: "trace", "debug", "info", "warn", "error", "off".
# level = "off" silences all console output while keeping the subscriber active.
level = "info"

# Log format: "json" (structured, for production) or "pretty" (human-readable)
# Log format: "json" (structured, for production) or "pretty" (human-readable).
# Only "json" or "pretty" are accepted; any other value (including "off") is
# rejected at startup. To silence logs, set level = "off".
format = "json"

[glitchtip]
Expand Down
32 changes: 28 additions & 4 deletions config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@
# MIP-05 Push Notification Server

[server]
# Server's Nostr private key (hex format)
# Prefer private_key_file for production secret handling.
# Server's Nostr private key (hex format).
#
# Prefer private_key_file for production. An inline private_key here (or in
# TRANSPONDER_SERVER_PRIVATE_KEY) is read through a dedicated zeroizing path so
# it never lands in the config parser's un-zeroized buffers, but a file keeps
# the secret out of the config file entirely and lets the loader enforce
# permissions on it.
private_key = ""

# Alternative: path to a file containing the private key.
# Useful with Docker secrets or other mounted secret files.
# The file must NOT be group- or world-readable (mode 0600): the server refuses
# to start if it grants group/other access, mirroring the 0600 that
# generate-keys writes.
# Generate with: transponder generate-keys --output /run/secrets/transponder_private_key
# private_key_file = "/run/secrets/transponder_private_key"

Expand All @@ -20,6 +28,8 @@ shutdown_timeout_secs = 10
# Applies only when dedup_state_path is unset. Durable replay state retains all
# terminal event IDs inside dedup_retention_secs so restart/reconnect replay
# suppression covers the full NIP-59 subscription lookback window.
# Must be >= 1; a value of 0 is rejected at startup (it was previously swapped
# for the default silently).
# Default: 100000
# max_dedup_cache_size = 100000

Expand Down Expand Up @@ -47,10 +57,14 @@ shutdown_timeout_secs = 10
# Transponder has two such limiters.
# At capacity, new keys evict stale or below-limit entries when possible.
# If no safe eviction candidate exists, the new key is rate limited.
# Must be >= 1; a value of 0 is rejected at startup (it was previously swapped
# for the 100000 default silently, per limiter).
# Default: 100000 entries per cache
# max_rate_limit_cache_size = 100000

# Maximum encrypted tokens accepted in a single notification event.
# Must be >= 1; a value of 0 is rejected at startup (it would reject every
# notification event — a total, silent push outage).
# Default: 100
# max_tokens_per_event = 100

Expand Down Expand Up @@ -105,6 +119,9 @@ allow_unencrypted_clearnet_relays = false

# Tor/onion relays (optional)
# Requires a binary built with: cargo build --features tor
# Each entry must be a ws:// or wss:// URL with a .onion host; a plaintext
# clearnet URL or a malformed .onion entry here is rejected at startup rather
# than silently degrading to clearnet.
onion = []

# Reconnection settings. reconnect_interval_secs must be between 1 and 300 seconds.
Expand Down Expand Up @@ -156,6 +173,9 @@ project_id = ""
enabled = true

# Address to bind the health/metrics listener to.
# Must be a valid IP:port socket address (e.g. "127.0.0.1:8080"); a hostname
# like "localhost:8080" or an out-of-range port is rejected at startup so a
# typo cannot silently disable /health, /ready, and /metrics.
# Keep this on localhost unless an internal proxy, VPN, or load balancer needs
# access to the endpoint.
bind_address = "127.0.0.1:8080"
Expand All @@ -167,10 +187,14 @@ bind_address = "127.0.0.1:8080"
enabled = true

[logging]
# Log level: "trace", "debug", "info", "warn", "error", "off"
# Log level: "trace", "debug", "info", "warn", "error", "off".
# Use level = "off" to silence all console output; it keeps the tracing
# subscriber installed so error reporting and runtime filter changes still work.
level = "info"

# Log format: "json" (for production) or "pretty" (for development)
# Log format: "json" (for production) or "pretty" (for development).
# Only "json" and "pretty" are accepted; any other value (including "off") is
# rejected at startup. To silence logs, use logging.level = "off".
format = "json"

[glitchtip]
Expand Down
10 changes: 9 additions & 1 deletion config/production.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
# Copy this to config/production.toml and adjust for your deployment.

[server]
# Recommended for production: supply the key via
# Recommended for production: supply the key via a permission-scoped file
# (mode 0600). The server refuses to start if the key file is group/world
# readable.
# TRANSPONDER_SERVER_PRIVATE_KEY_FILE=/run/secrets/transponder_private_key
# Generate with: transponder generate-keys --output /run/secrets/transponder_private_key
private_key = ""
Expand Down Expand Up @@ -50,6 +52,8 @@ allow_unencrypted_clearnet_relays = false

# Add onion relays only if you intend to run a Tor-enabled build and can spare
# additional memory/CPU for Tor circuits.
# Each entry must be a ws:// or wss:// URL with a .onion host; malformed or
# clearnet entries here are rejected at startup.
# Build with: docker build --build-arg CARGO_FEATURES='--features tor' -t transponder:local .
onion = []

Expand All @@ -75,6 +79,7 @@ project_id = ""

[health]
enabled = true
# Must be a valid IP:port socket address (rejected at startup otherwise).
# Keep native deployments bound to localhost by default. For Docker Compose,
# compose.prod.yml overrides this to 0.0.0.0 inside the container while
# publishing the host port on 127.0.0.1 only.
Expand All @@ -84,7 +89,10 @@ bind_address = "127.0.0.1:8080"
enabled = true

[logging]
# level = "off" silences all console output (subscriber stays installed).
level = "info"
# Only "json" or "pretty"; any other value (including "off") is rejected at
# startup. Silence logs with level = "off", not format.
format = "json"

[glitchtip]
Expand Down
Loading