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
198 changes: 144 additions & 54 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,75 +1,165 @@
# Public routing, discovery, and relay identity persistence
# Portal relay configuration.
#
# Copy to .env and edit. Every key here is read either by the relay binary or by
# Docker Compose; nothing else reads this file.
#
# Check the configuration before starting anything. The report gives the
# effective value and source of every key, names any key that nothing reads, and
# says which features are off and what is missing:
#
# docker compose run --rm -T portal config
#
# No --env-file: Compose has already built the container's environment from this
# file plus its own defaults, so the report describes what `docker compose up`
# will actually run. Passing --env-file instead reads the file against the relay
# binary's defaults, which differ -- MIN_PORT is 0 there and 40000 under Compose
# -- and would report features as blocked that the deployment enables.
#
# -T because `docker compose run` asks for a TTY otherwise, and older versions
# fail with "the input device is not a TTY".
#
# API_PORT and SNI_PORT are deliberately absent. The bundled topology fixes them
# at 4017 and 443 because the relay reaches its own API listener through its SNI
# router; overriding them breaks that wiring rather than moving it.


# ─────────────────────────────────────────────────────────────────────────────
# 1. Required
# ─────────────────────────────────────────────────────────────────────────────

# Public HTTPS origin browsers and tunnel clients use. Must be publicly
# resolvable when DISCOVERY=true; localhost and other local-only names are
# rejected by public discovery.
PORTAL_URL=https://localhost
# Optional directory inside the Portal container containing a custom SPA index.html.
# Leave empty to use the official frontend embedded in the Portal binary.
PORTAL_FRONTEND_DIR=
DISCOVERY=true

# Bearer token for the admin and policy APIs. Leaving this empty leaves those
# APIs unauthenticated. Generate one with: openssl rand -hex 32
ADMIN_TOKEN=


# ─────────────────────────────────────────────────────────────────────────────
# 2. Storage and ports
# ─────────────────────────────────────────────────────────────────────────────

# Directory for relay identity, policy state, and certificate material.
# Also the in-container mount point for ./.portal-certs.
IDENTITY_PATH=/portal-certs

# Public HTTPS/SNI
# Public and listen UDP port for the relay overlay. Required when DISCOVERY=true.
# Compose publishes this port.
WIREGUARD_PORT=51820
# Set when enabling public UDP or raw TCP lease ports.

# Inclusive lease port range shared by the UDP and raw TCP transports.
# 0 disables both. Enabling a transport without a range does nothing; the relay
# reports that at startup. Publish the same range in docker-compose.yml when set.
MIN_PORT=0
MAX_PORT=0


# ─────────────────────────────────────────────────────────────────────────────
# 3. Feature toggles - the defaults suit a single private relay
# ─────────────────────────────────────────────────────────────────────────────

# Serve relay discovery endpoints and poll discovery peers. Requires a publicly
# reachable PORTAL_URL and an open WIREGUARD_PORT/udp.
DISCOVERY=true

# Comma-separated relay API URLs to seed discovery from.
BOOTSTRAPS=

# Enable the UDP and raw TCP lease transports. Both need MIN_PORT/MAX_PORT.
UDP_ENABLED=false
TCP_ENABLED=false

# Supported managed values: embedded (default), cloudflare, gcloud, hetzner, njalla, route53, vultr.
# Reused for ACME DNS-01, managed A records, the relay ECH record,
# and opt-in tunnel ECH records.
ACME_DNS_PROVIDER=embedded
# Initial landing-page state. Admin changes are persisted in policy.json.
LANDING_PAGE_ENABLED=false

# Embedded authoritative DNS server, the default DNS provider.
# Requires a one-time NS delegation of the base domain at the parent zone
# (NS portal.example.com -> ns.portal.example.com + glue A to the relay IP)
# and open 53/tcp + 53/udp.
# ENS gasless automation (zone DNSSEC) is not supported with embedded yet.
EMBEDDED_DNS_PORT=53
# Directory inside the container holding a custom SPA index.html. Leave empty to
# serve the frontend embedded in the binary. Uncomment the matching mount in
# docker-compose.yml when setting this.
PORTAL_FRONTEND_DIR=

# Cloudflare API token (required when ACME_DNS_PROVIDER=cloudflare)
CLOUDFLARE_TOKEN=
# Trust X-Forwarded-* and X-Real-IP. Portal owns its public port in the bundled
# topology, so client addresses already come from the socket; enable this only
# when a proxy you control genuinely sits in front. TRUSTED_PROXY_CIDRS empty
# means the default private and loopback ranges.
TRUST_PROXY_HEADERS=false
TRUSTED_PROXY_CIDRS=

# Google Cloud DNS settings. (required when ACME_DNS_PROVIDER=gcloud)
GCP_PROJECT_ID=
GCP_MANAGED_ZONE=
GOOGLE_APPLICATION_CREDENTIALS=
# Relay-owned Sui x402 facilitator under /api/x402.
# X402_ENABLED=true without X402_PAY_TO cannot receive payments.
X402_ENABLED=false
X402_TESTNET=false
X402_PAY_TO=

# Hetzner DNS settings (required when ACME_DNS_PROVIDER=hetzner)
HETZNER_API_TOKEN=
# pprof diagnostics. Keep the address on loopback unless the port is otherwise
# protected. PPROF_PORT only matters when you also uncomment the pprof port
# mapping in docker-compose.yml, which exposes it to the host.
PPROF_ENABLED=false
PPROF_ADDR=127.0.0.1:6060
# PPROF_PORT=6060

# Route53 settings (required when ACME_DNS_PROVIDER=route53)
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_SESSION_TOKEN=
AWS_REGION=
AWS_DEFAULT_REGION=
AWS_HOSTED_ZONE_ID=
# Required only when ACME_DNS_PROVIDER=route53 and ENS_GASLESS_ENABLED=true and no ACTIVE KSK already exists.
AWS_DNSSEC_KMS_KEY_ARN=

# Vultr DNS settings (required when ACME_DNS_PROVIDER=vultr)
VULTR_API_KEY=
# ─────────────────────────────────────────────────────────────────────────────
# 4. DNS provider - pick one, fill only that block
#
# Used for ACME DNS-01, managed A records, ECH HTTPS records, and optional ENS
# DNS automation. Unset means embedded, not "off": to manage certificates
# yourself instead, place fullchain.pem and privatekey.pem under IDENTITY_PATH
# and they are used when present.
#
# Only one provider's credentials are ever read. The unused blocks below stay
# commented out on purpose.
# ─────────────────────────────────────────────────────────────────────────────

# Njalla DNS settings (required when ACME_DNS_PROVIDER=njalla)
NJALLA_TOKEN=
# embedded (default) | cloudflare | gcloud | hetzner | njalla | route53 | vultr
ACME_DNS_PROVIDER=embedded

# ENS gasless DNS import automation. When enabled, Portal uses ACME_DNS_PROVIDER
# for DNSSEC and ENS TXT automation, even when certificate files are managed manually.
# Not supported with ACME_DNS_PROVIDER=embedded yet.
ENS_GASLESS_ENABLED=false
# -> ACME_DNS_PROVIDER=embedded
# An authoritative DNS server inside the relay, so no provider API
# credentials are needed. Requires a one-time NS delegation of the base
# domain at the parent zone (NS portal.example.com -> ns.portal.example.com
# with a glue A record to the relay IP) and open 53/tcp + 53/udp. A
# container running without root needs CAP_NET_BIND_SERVICE to bind 53.
# ENS gasless automation is not supported with embedded yet.
EMBEDDED_DNS_PORT=53

# Admin/auth configuration. Use a long random value for production relays.
ADMIN_TOKEN=
# -> ACME_DNS_PROVIDER=cloudflare
# The token needs Zone:Read as well as DNS:Edit. DNS:Edit alone cannot
# locate the zone, and issuance fails with "no cloudflare zone found".
# CLOUDFLARE_TOKEN=

# Optional embedded Sui x402 facilitator exposed under /api/x402.
X402_ENABLED=false
X402_TESTNET=false
X402_PAY_TO=
# -> ACME_DNS_PROVIDER=gcloud
# Project is auto-detected from ADC or GCE metadata when omitted.
# GOOGLE_APPLICATION_CREDENTIALS is read by the Google Cloud SDK itself,
# not by a relay flag: mount the service account file and point this at
# the in-container path.
# GCP_PROJECT_ID=
# GCP_MANAGED_ZONE=
# GOOGLE_APPLICATION_CREDENTIALS=

# Enable only when an explicitly configured upstream proxy supplies forwarded client IP headers.
# Optionally restrict which proxy source ranges may supply those headers; leave empty for default private/loopback proxy ranges.
TRUST_PROXY_HEADERS=false
TRUSTED_PROXY_CIDRS=
# -> ACME_DNS_PROVIDER=hetzner
# HETZNER_API_TOKEN=

# Initial landing-page state. Admin changes are persisted in policy.json.
LANDING_PAGE_ENABLED=false
# -> ACME_DNS_PROVIDER=route53
# Omit the static keys to use the default AWS credential chain.
# AWS_DNSSEC_KMS_KEY_ARN is needed only with ENS_GASLESS_ENABLED=true when
# no ACTIVE KSK exists yet.
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# AWS_SESSION_TOKEN=
# AWS_REGION=
# AWS_HOSTED_ZONE_ID=
# AWS_DNSSEC_KMS_KEY_ARN=

# -> ACME_DNS_PROVIDER=vultr
# VULTR_API_KEY=

# -> ACME_DNS_PROVIDER=njalla
# NJALLA_TOKEN=

# ENS gasless DNS import automation. Needs a DNS provider that can manage
# DNSSEC, so it does not work with the embedded server yet; pick one of the
# managed providers above. Not needed for normal relay operation; leave false
# unless you specifically want it.
ENS_GASLESS_ENABLED=false
43 changes: 42 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help install fmt vet lint lint-auto test tidy all run build build-frontend build-docs build-tunnel build-server build-server-bin clean load-test
.PHONY: help install fmt vet lint lint-auto test tidy all run build build-frontend build-docs build-tunnel build-server build-server-bin clean load-test check-env-example env-reference

.DEFAULT_GOAL := help

Expand All @@ -16,6 +16,8 @@ help:
@echo " make fmt - Apply gofmt/goimports"
@echo " make lint-auto - Run autofix lint/format pipeline"
@echo " make test - Run Go and frontend tests"
@echo " make check-env-example - Fail if .env.example is missing a configuration key"
@echo " make env-reference - Print every configuration key, generated from the flags"
@echo " make build - Build Go tunnel and relay server artifacts"
@echo " make build-frontend - Build React frontend (Tailwind CSS 4)"
@echo " make build-docs - Build documentation site (SvelteKit)"
Expand Down Expand Up @@ -52,6 +54,45 @@ tidy:
go mod tidy
go mod verify

# The keys themselves are owned by the flag definitions in
# cmd/relay-server/main.go and by the catalog of keys other components read.
# .env.example and the configuration reference are documentation of that set.
# Adding a flag without documenting it is how configuration drifts away from the
# code, so fail loudly here rather than let an operator find the gap in
# production. Keys the bundled topology pins are excluded on purpose; see
# cmd/relay-server/envcatalog.go.
CONFIG_DOC := docs/src/routes/configuration/+page.md

check-env-example:
@go run ./cmd/relay-server config --format names > /tmp/portal-env-names.txt
@status=0; \
missing=""; \
while read -r name; do \
grep -qE "^#? *$$name=" .env.example || missing="$$missing $$name"; \
done < /tmp/portal-env-names.txt; \
if [ -n "$$missing" ]; then \
echo "[env] .env.example does not document:"; \
for name in $$missing; do echo " - $$name"; done; \
status=1; \
fi; \
missing=""; \
while read -r name; do \
grep -qF "\`$$name\`" $(CONFIG_DOC) || missing="$$missing $$name"; \
done < /tmp/portal-env-names.txt; \
if [ -n "$$missing" ]; then \
echo "[env] $(CONFIG_DOC) does not document:"; \
for name in $$missing; do echo " - $$name"; done; \
status=1; \
fi; \
if [ "$$status" -ne 0 ]; then \
echo "[env] run 'make env-reference' to see each key with its usage text"; \
exit 1; \
fi; \
echo "[env] .env.example and $(CONFIG_DOC) document every configuration key"

env-reference:
@go run ./cmd/relay-server config --format env

all: fmt vet lint test build

run:
Expand Down
Loading
Loading