The box's compose got these during the security audit and the repo copy did not, so a rebuild from the repo would have undone them: - Redis requires a password (FN_REDIS_PASSWORD, written to a config file inside the container; healthcheck via REDISCLI_AUTH); FN_REDIS_URL carries it. - The internal network is internal: true, so the database and Redis have no route off the box. The relay reaches S3 and APNs through web. - relay, redis and obfs4bridge drop every capability; db keeps the five its entrypoint needs. All four set no-new-privileges. - obfs4bridge is on web only. .env.example documents FN_REDIS_PASSWORD. The deploy itself uses the box's compose, so this changes nothing that is running. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
351 lines
17 KiB
Bash
351 lines
17 KiB
Bash
# Copy to .env and fill in. Every variable in this file reaches the relay:
|
|
# the compose files pass the whole file through with `env_file`, so a setting
|
|
# here is a setting the relay reads, not one compose reads for its own
|
|
# interpolation and drops. Generate the three secrets with:
|
|
#
|
|
# openssl rand -hex 32 # POSTGRES_PASSWORD
|
|
#
|
|
# POSTGRES_PASSWORD is hex, not base64, and that is not a style choice: it is
|
|
# interpolated into `postgres://flamenet:<password>@db:5432/flamenet`, where
|
|
# base64's `/`, `+` and `=` are all meaningful. A password containing `/` makes
|
|
# that URL invalid and the relay dies at start-up with a bare `Code=-1000` and
|
|
# the connection string echoed back at you, which reads like the database is
|
|
# unreachable rather than like the password needs escaping.
|
|
#
|
|
# openssl genpkey -algorithm ed25519 -out issuer.pem
|
|
# openssl pkey -in issuer.pem -outform DER | tail -c 32 | base64 # PRIVATE
|
|
# openssl pkey -in issuer.pem -pubout -outform DER | tail -c 32 | base64 # PUBLIC
|
|
#
|
|
# Keep issuer.pem. Losing it invalidates every token you have issued; leaking it
|
|
# lets anyone mint tokens this relay will accept.
|
|
|
|
# Host port the relay is published on, on 127.0.0.1 only. Change this rather
|
|
# than editing docker-compose.yml: 8080 is frequently already taken on a
|
|
# machine that self-hosts other things, and editing the compose file makes the
|
|
# next `git pull` conflict.
|
|
FN_HOST_PORT=8080
|
|
|
|
POSTGRES_PASSWORD=
|
|
FN_ISSUER_PRIVATE_KEY=
|
|
FN_ISSUER_PUBLIC_KEY=
|
|
|
|
# The name this relay answers to. Tokens are bound to it, so a token minted by
|
|
# one relay is not valid at another. Once real phones use this relay, set it to
|
|
# the hostname they dial, exactly as they dial it. The default is fine for a
|
|
# relay on localhost.
|
|
FN_RELAY_AUDIENCE=flamenet-relay
|
|
|
|
# Release day for the iPhone app is these two and a restart, not an edit.
|
|
# FN_IOS_LIVE flips every sentence on the public pages that says the iPhone
|
|
# build is a TestFlight beta; FN_IOS_STORE_URL is where the download buttons
|
|
# point once it is on. Leave both alone until Apple has approved the app.
|
|
FN_IOS_LIVE=false
|
|
FN_IOS_STORE_URL=
|
|
|
|
# Who runs this relay, for the contact, safety and terms pages and for
|
|
# /.well-known/security.txt. Nothing here is a literal in a page: an
|
|
# unconfigured relay says no address is published rather than naming ours.
|
|
# One address covers every role; any role can be overridden on its own.
|
|
FN_CONTACT_EMAIL=
|
|
# FN_CONTACT_SUPPORT=
|
|
# FN_CONTACT_ABUSE=
|
|
# FN_CONTACT_SECURITY=
|
|
# FN_CONTACT_PRIVACY=
|
|
# FN_CONTACT_LEGAL=
|
|
# FN_CONTACT_POSTMASTER=
|
|
|
|
# Where the hourly digest of reports is mailed. Needs the SMTP settings below.
|
|
# Blank, reports are still kept in the database and read with
|
|
# Scripts/reports.sh, and the log says nobody was told. This is also the
|
|
# fallback for FN_CONTACT_EMAIL when that is blank.
|
|
FN_REPORTS_TO=
|
|
|
|
# The funding.json manifests this domain vouches for, whitespace or comma
|
|
# separated, served at /.well-known/funding-manifest-urls. Anything not
|
|
# starting with https:// is dropped. Unset, that path is a 404: the manifest
|
|
# carries an operator's legal name, contact address and bank details, so a
|
|
# relay you run must not tell a funder that donations for it reach somebody
|
|
# else.
|
|
FN_FUNDING_MANIFESTS=
|
|
# How long a report, and the plaintext copy the reporter chose to attach, is
|
|
# kept before the retention sweep deletes it. Shown on /metadata.
|
|
FN_REPORT_RETENTION_DAYS=365
|
|
|
|
# Donations. Leave both blank and the donate page says so plainly instead of
|
|
# showing a form. `FN_DONATE_SECRET` is a Stripe secret key; it is used only to
|
|
# open the payment; the card fields are Stripe's own, mounted on the page in a
|
|
# frame this origin cannot read, so no card data reaches this service.
|
|
FN_DONATE_PROVIDER=
|
|
FN_DONATE_SECRET=
|
|
# The connected account donations are charged on, if the key above belongs to a
|
|
# Stripe platform rather than to the account meant to receive the money. Blank
|
|
# charges the key's own account, which is what a self-hoster wants.
|
|
FN_DONATE_ACCOUNT=
|
|
# Public by design, and handed to the browser. Without it there is no form.
|
|
FN_DONATE_PUBLISHABLE_KEY=
|
|
|
|
# Shared FlameNet accounts. Leave blank: this is the self-hosting default and
|
|
# the right answer for almost everyone.
|
|
#
|
|
# Blank, the relay is its own identity provider: `/account/signup` and
|
|
# `/account/signin` write to the `users` table in the Postgres below, and an
|
|
# account here means nothing anywhere else. That is one service to run, and no
|
|
# dependency on anybody's infrastructure.
|
|
#
|
|
# Set, the relay ALSO accepts a signed session from the shared FlameNet account
|
|
# service, so one passphrase works across FlameNet products. It has to be the
|
|
# same secret that service signs with, and it is only useful if you run that
|
|
# service too. There is no shared secret you can be issued for the FlameNet
|
|
# one, by design.
|
|
#
|
|
# Note the client half is not a server setting. The app decides where a
|
|
# passphrase may be sent from a list of hostnames pinned in the app itself, so a
|
|
# relay cannot nominate its own account service and collect the authentication
|
|
# secrets of everyone who signs in. Setting this on a relay the app has not
|
|
# pinned changes nothing on the client.
|
|
FN_ENCLAVE_JWT_SECRET=
|
|
|
|
# Outbound mail, for password resets and report delivery on a self-hosted
|
|
# relay.
|
|
#
|
|
# Leave FN_SMTP_HOST blank and password reset is OFF. The sign-in screen says
|
|
# so rather than offering a link that goes nowhere. There is no half-configured
|
|
# state on purpose: a reset that mints a token and then cannot deliver it leaves
|
|
# the account locked out AND a live token in the database.
|
|
#
|
|
# HOST and FROM are required together. Credentials are optional, because a relay
|
|
# submitting to an MTA on the same host commonly does so without auth.
|
|
#
|
|
# Accounts on the shared FlameNet service are NOT reset here. Their verifier
|
|
# lives in that service, and this relay could not change it if it wanted to.
|
|
FN_SMTP_HOST=
|
|
FN_SMTP_PORT=587
|
|
FN_SMTP_USERNAME=
|
|
FN_SMTP_PASSWORD=
|
|
FN_SMTP_FROM=
|
|
# STARTTLS. Only set false for a loopback MTA; the failure mode otherwise is a
|
|
# reset link crossing a network in the clear.
|
|
FN_SMTP_TLS=true
|
|
|
|
# Require an account to prove its email address before this relay will issue it
|
|
# a token, that is, before it can send anything. Off by default.
|
|
#
|
|
# This belongs next to SMTP because it needs SMTP. Enforcement is
|
|
# `required AND canSendMail`, so setting this on a relay with no mail configured
|
|
# does nothing at all, deliberately: an account that cannot be sent a code
|
|
# could never spend one, and locking somebody out of their own server would be
|
|
# the worst possible reading of a security feature.
|
|
#
|
|
# The trade is friction at exactly the moment somebody is deciding whether to
|
|
# bother, which is why it gates SENDING rather than signing up: an account is
|
|
# created immediately and proves itself before it can reach anyone.
|
|
#
|
|
# Accounts authenticated by the shared FlameNet service are unaffected. They
|
|
# have no local email and nothing to prove to this relay.
|
|
#
|
|
# Accepts true, 1 or yes. Anything else, including a typo, leaves it off.
|
|
FN_EMAIL_VERIFICATION_REQUIRED=false
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# New accounts per IP per hour. Default 50.
|
|
#
|
|
# An IP is not a person. Mobile carriers put thousands of subscribers behind
|
|
# one CGNAT address, and an office, a campus or a cafe is one address too.
|
|
# This used to default to 5, which meant the sixth person on a shared
|
|
# connection was told "Too many accounts created from here recently" on their
|
|
# first ever screen, for something they did not do.
|
|
#
|
|
# The key is the IP and nothing else, and an IP is trivially rotated, so this
|
|
# is a speed bump against lazy bulk registration, not a defence. Raise it
|
|
# further if your users share an address; lower it only if you know they do not.
|
|
FN_SIGNUP_MAX_PER_HOUR=50
|
|
# Invite-only phase (relay#43). Off, and inert without an end date: a gate
|
|
# with no expiry is one somebody has to remember to remove, and nobody does.
|
|
# FN_VOUCH_REQUIRED=false
|
|
# FN_VOUCH_UNTIL=0
|
|
|
|
# Federation (docker-compose.federated.yml only)
|
|
#
|
|
# Your relay's public hostname. On a federated relay this IS the audience:
|
|
# peers verify you by it, and your users' addresses end in it
|
|
# (fnid@$FN_PUBLIC_HOST). DNS for it must point at this machine.
|
|
FN_PUBLIC_HOST=
|
|
|
|
# Who may exchange mail with this relay.
|
|
# closed the default: federate with nobody. Every variable in this
|
|
# file now reaches the relay, so this has to be the value a
|
|
# relay that is not federating actually wants.
|
|
# allowlist only the peers named below. The same list gates both
|
|
# directions: who is admitted and who this relay will dial.
|
|
# docker-compose.federated.yml sets this itself.
|
|
# open any relay that authenticates. Run this only once you have
|
|
# thought about spam from strangers' servers.
|
|
# The contact graph (spec 8.9c). On, the default, this relay keeps and serves
|
|
# the buddy list, exactly as it always has. Off, it stops serving and writing
|
|
# it, and presence is answered from the per-contact tokens the apps publish
|
|
# instead.
|
|
#
|
|
# Do not turn this off until every app talking to this relay is new enough to
|
|
# hold its own contact list. An older one reads its contacts from here and
|
|
# turning this off empties them. Scripts/client-versions.sh says what is still
|
|
# polling. The switch is reversible; dropping the table afterwards is not.
|
|
FN_ROSTER_ON_SERVER=true
|
|
|
|
FN_FEDERATION=closed
|
|
# Comma-separated peers. Pin each one's federation key: host=key, or
|
|
# host=key1|key2 during a rotation. A bare host is unpinned and logs a warning.
|
|
# relay.flamenet.io=<base64 key>,relay.example.org=<base64 key>
|
|
FN_FEDERATION_PEERS=
|
|
|
|
# This relay's federation signing identity, a 32-byte Ed25519 seed:
|
|
# openssl genpkey -algorithm ed25519 | openssl pkey -outform DER | tail -c 32 | base64
|
|
# Losing it breaks trust with every peer; leaking it lets the holder speak
|
|
# as your relay to peers. Treat it like the issuer key.
|
|
FN_FEDERATION_KEY=
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calls (TURN): voice and video across NAT
|
|
#
|
|
# docker-compose.federated.yml runs coturn as part of the stack and refuses to
|
|
# start without all three of these. docker-compose.prod.yml keeps coturn
|
|
# behind `--profile calls`, so with the profile off nothing here is read and
|
|
# /e2e/turn answers 503, which is a supported deployment rather than a fault;
|
|
# with it on, all three are required. docker-compose.yml ships no coturn at
|
|
# all. Messaging needs none of this.
|
|
#
|
|
# One shared secret between the relay (which mints short-lived credentials)
|
|
# and coturn (which honours them). It must match exactly. A mismatch fails
|
|
# only the calls that actually need a relay, so it presents as intermittent:
|
|
# openssl rand -hex 32
|
|
FN_TURN_SECRET=
|
|
# The hostname clients dial for STUN/TURN over UDP, and coturn's realm.
|
|
# Keep this record UNPROXIED in DNS. Cloudflare does not proxy UDP, and a
|
|
# proxied record makes every relayed call fail while the record looks healthy.
|
|
FN_TURN_HOST=
|
|
# The machine's PUBLIC IP, for coturn's candidates. Behind NAT this is the
|
|
# address the outside world sees, not the one on the interface.
|
|
FN_TURN_EXTERNAL_IP=
|
|
#
|
|
# Open on the host firewall, or calls connect and then carry no audio:
|
|
# 3478/udp, 3478/tcp, and 49160-49200/udp
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Device hygiene
|
|
#
|
|
# Registered devices per account. Every device is a mailbox and a prekey
|
|
# pool; every reinstall that skips sign-out leaves one behind. Both apps
|
|
# ship a Devices screen for removing the extras.
|
|
FN_MAX_DEVICES=3
|
|
# Days of silence before the sweep expires a device (prekeys and queued
|
|
# mail go with it, recorded in the key log). The newest device of each
|
|
# account is never expired, however old: a dormant account must stay
|
|
# reachable, not stranded. 0 disables.
|
|
FN_DEVICE_EXPIRY_DAYS=90
|
|
|
|
# S3-compatible blob storage (C3). Unset = blobs are files under
|
|
# FN_ATTACHMENTS_DIR, exactly as before. Setting the endpoint makes the other
|
|
# four required; a partial configuration refuses to boot. Reads fall back to
|
|
# the local directory, so switching mid-life strands no existing blob.
|
|
FN_S3_ENDPOINT=
|
|
FN_S3_BUCKET=
|
|
FN_S3_REGION=
|
|
FN_S3_ACCESS_KEY_ID=
|
|
FN_S3_SECRET_KEY=
|
|
FN_S3_PREFIX=attachments/
|
|
|
|
# Rolling 30-day per-account attachment budget. Totals only, never per-blob.
|
|
FN_ATTACHMENT_BYTES_PER_MONTH=524288000
|
|
|
|
# Redis. docker-compose.yml runs without it: a single relay holds rate limits,
|
|
# replay protection and held-poll wake-ups in memory. docker-compose.prod.yml and
|
|
# .federated.yml run a Redis and set this themselves. Set it here only if you
|
|
# point the base file at a Redis of your own.
|
|
# FN_REDIS_URL=redis://redis:6379
|
|
#
|
|
# docker-compose.prod.yml gives its Redis a password. Set both, with the same
|
|
# value: hex is safest, since the URL carries it unencoded.
|
|
# FN_REDIS_PASSWORD=<openssl rand -hex 32>
|
|
# FN_REDIS_URL=redis://:<same password>@redis:6379
|
|
|
|
# Concurrency knobs, measured in docs/CAPACITY.md. Redis connections per event
|
|
# loop: 32 by default for one relay, which waits in memory. With
|
|
# FN_RELAY_REPLICAS above 1 each held poll pins one for its BRPOP, the default
|
|
# is 512, and capacity for open apps is roughly event-loops x this.
|
|
# FN_REDIS_MAX_CONNECTIONS_PER_LOOP=512
|
|
#
|
|
# Postgres connections are different, and a held poll does NOT pin one. It
|
|
# borrows one per cursor query for a few milliseconds. What draws on this is
|
|
# the QUERY rate, measured at nine round trips per message in docs/COST.md.
|
|
#
|
|
# Raise this to 16 when running behind PgBouncer (docker-compose.pooled.yml).
|
|
# That reads backwards and is right: behind a pooler these stop being Postgres
|
|
# backends and become cheap client connections, and the pooler's
|
|
# DEFAULT_POOL_SIZE is what bounds the expensive ones. See docs/POOLING.md.
|
|
FN_PG_MAX_CONNECTIONS_PER_LOOP=4
|
|
|
|
# The soft ceiling /status describes the load against. There is no default,
|
|
# and deliberately so: a held connection does not pin a database connection,
|
|
# so max_connections is not the number of polls a relay can hold, and a
|
|
# plausible-looking guess on a status page is worse than an admission. Unset,
|
|
# the page reports the load and says it cannot tell you whether that is a lot.
|
|
FN_CAPACITY_CEILING=
|
|
|
|
# How many relays share this database. Leave at 1 unless you are running
|
|
# several behind a load balancer.
|
|
#
|
|
# More than one REQUIRES FN_REDIS_URL, and the relay refuses to start
|
|
# without it. Envelopes, the key log and wake-ups all cross instances by
|
|
# themselves, but replay protection, the sign-in lockout and the attachment
|
|
# quota are held in memory. Two instances would keep two copies: a single-use
|
|
# proof signature would be accepted once per relay, and an attacker would get
|
|
# one set of sign-in attempts per relay, with your load balancer spreading the
|
|
# guesses for them. Nothing would error; the guarantees would just stop
|
|
# holding.
|
|
FN_RELAY_REPLICAS=1
|
|
|
|
# ---- Telling your users about updates (optional) ---------------------------
|
|
#
|
|
# The Android app checks the relay it is signed in to for a newer build, not
|
|
# a fixed address of ours, so an app that promises not to phone home does not
|
|
# check in with us from every device. The consequence for you is that a relay
|
|
# publishing nothing produces no update prompts, and your users only learn
|
|
# about a new build when you tell them.
|
|
#
|
|
# Set these and this relay publishes a release manifest at
|
|
# /downloads/android-latest.json, which is exactly what the app reads.
|
|
#
|
|
# You must also serve the APK itself at /downloads/flamenet-messenger.apk on
|
|
# this host. The app builds the download URL from the relay's own origin and
|
|
# ignores anything a manifest claims about where the file lives, so a relay
|
|
# can point its users at builds on itself, and at nothing else.
|
|
#
|
|
# VERSION_CODE must go UP with every release and must match the APK. Android
|
|
# refuses to install a build whose code is lower than the installed one.
|
|
# FN_ANDROID_VERSION_CODE=4
|
|
# FN_ANDROID_VERSION_NAME=0.4
|
|
# Optional. Published so somebody can check the file they downloaded; a
|
|
# malformed digest is dropped rather than published, because a wrong
|
|
# verification hash is worse than none.
|
|
# FN_ANDROID_SHA256=
|
|
# FN_ANDROID_NOTES=What changed in this build.
|
|
|
|
# ---- The Today file (optional) ----------------------------------------------
|
|
#
|
|
# One public file at /today.json, the same bytes for everybody: headlines,
|
|
# markets and weather, built and signed elsewhere. The relay serves it only
|
|
# after the signature checks against FN_TODAY_KEYS, and a newer copy that fails
|
|
# the check never replaces one that passed. With no source set, /today.json
|
|
# answers 404 "not_offered".
|
|
#
|
|
# Set ONE source. FN_TODAY_FILE is a path (inside the container, so mount it),
|
|
# read again whenever the file changes; replace it by renaming a new file over
|
|
# it. FN_TODAY_SOURCE is an http(s) URL fetched every fifteen minutes; to mirror
|
|
# another relay's file, point it at that relay's /today.json.
|
|
FN_TODAY_FILE=
|
|
FN_TODAY_SOURCE=
|
|
# Comma-separated base64 Ed25519 public keys the file may be signed with.
|
|
FN_TODAY_KEYS=
|
|
# Where the last good copy is kept, so a restart can serve it at once.
|
|
# /data/today-kept.json sits on the volume the compose files keep.
|
|
FN_TODAY_CACHE=
|