# MeshBay Hub — example configuration. # # Copy to /etc/meshbay/hub.toml and edit. The package deliberately does not # install a working config: it would either ship a placeholder secret that # somebody runs in production, or overwrite yours on upgrade. # # Read from the first of these that exists: # /etc/meshbay/hub.toml <- where a packaged hub looks # ~/.config/meshbay/hub.toml <- a development hub, run as yourself # # Every value below can also come from the environment, which is what the # systemd unit's EnvironmentFile (/etc/meshbay/hub.env) is for. Secrets belong # there rather than in an `Environment=` line: `systemctl cat` shows a unit to # any user on the machine. [hub] # This hub's identity, as members and nodes know it. Changing it after anyone # has joined invalidates what they trust. id = "hub.example.org" # Ed25519 private key. Generate with: # meshbay-hub --help (see the key subcommands) # The packaged service runs as `meshbay`, so: # chown meshbay:meshbay /etc/meshbay/hub_private.pem && chmod 600 it private_key_path = "/etc/meshbay/hub_private.pem" # Accounts granted the admin role at creation. Everything else is set in the UI. admin_usernames = [] [database] # PostgreSQL in production. The default without this key is an in-memory # SQLite, which is a test fixture and loses everything on restart. # Prefer MESHBAY_DATABASE_URL in /etc/meshbay/hub.env — it carries a password. url = "postgresql+asyncpg://meshbay:CHANGEME@localhost/meshbay_hub" [server] # Loopback: TLS is Caddy's job, and the hub should not be reachable directly. host = "127.0.0.1" port = 8000 workers = 1 [jwt] # The access token is not the session — the refresh token is, and the SPA # renews against it long before this runs out. What this bounds is a token # that leaks. access_token_ttl = 14400 # 4 h # How long a session lasts (idle, maximum, browser sign-out) is set in the # admin panel, not here. [captcha] # reCAPTCHA v2 on registration and password reset, so no mail is ever sent # before a human has been seen. Absent, or either key empty, disables it # entirely — which is right for development and for a hub nobody can reach. # See docs/captcha.md. site_key = "" secret_key = "" # Hostnames a solved captcha may have been solved on, checked against the one # `siteverify` reports — what Google observed, not what the client claims. # # Leave empty while the reCAPTCHA key does its own origin check: it is then # already done, one layer up. Set it when you turn that check off in the # reCAPTCHA console, and the two go together — turning the console check off # without setting this leaves no origin check anywhere. # # The desktop client is why it exists. Its interface ships inside the package # and is served from `app://meshbay`, so the hostname Google sees is not this # hub's and never can be; with the console check on, the widget shows # "Invalid domain for site key" and nothing client-side reaches that decision. # # allowed_hosts = ["hub.example.org", "localhost"] allowed_hosts = [] # A solve Google cannot attribute to a domain reports an *empty* hostname — # the desktop client's `app://` origin does, and so does any other non-web # client. No `allowed_hosts` entry matches that, hence a flag rather than a # blank list entry. # # What it admits is every non-web client, not only this project's: a file:// # page or somebody else's Electron application look identical from here. The # captcha still has to be solved per token; what is given up is the origin # restriction for those clients. Leave it off unless you ship the desktop # client. See docs/captcha.md §6. allow_unattributed_host = false [mail] # What this hub will send, and how much of it. These are the **defaults**: an # administrator changes them from the Settings panel, and what they set is # stored in the database. A value here is what a missing setting falls back # to, so an instance that never touches the panel behaves as this file says. # # Why the bounds exist at all: the hub reaches its local Postfix with no # authentication, and three API paths reach the hub — two of them at an # address the caller types. Unbounded, that is an open relay wearing this # instance's reputation. # Everything this instance sends, in one hour. The one bound registration # being open cannot buy past: per-account and per-IP limits bound a caller, # and a caller is something an attacker makes more of. hourly_budget = 200 # Of that budget, the share kept back for the two messages a person is # actively waiting on — a passphrase reset and a group invitation. Without it # a flood of sign-ups spends the hour and locks out the people who need a # message to arrive. Sign-ups and address changes may spend # `hourly_budget - hourly_reserved_for_recovery`. hourly_reserved_for_recovery = 50 # Per recipient, across every purpose, account and endpoint. This is the bound # that describes what a person being flooded actually receives, and the only # one that does. destination_daily_cap = 10 destination_cooldown_seconds = 120 # Between two sign-up codes to one pending account. Reaching this branch needs # no token and no captcha — the caller chose the username and address a moment # earlier — so it is the widest of the three doors and wants its own delay. verification_resend_cooldown = 120 # Between two passphrase-reset codes for one account, whoever asks. The code # itself lives an hour, so this stays far below its lifetime: someone who did # not receive the first waits, rather than being unable to use the second. reset_cooldown = 300 # Before an account may propose a *different* address. Long, because this is # the only path where a signed-in account chooses who receives a message. # Asking again for the address already pending is exempt — it reaches no new # recipient, and without the exemption a typo would lock the account out of # correcting it for two days. email_change_cooldown = 172800 # 48 hours