Securing your install¶
Ribbon holds your family's names, birthdays and gift plans, so treat it like any other service with logins. The defaults are safe for a home network. This page is the checklist for anything beyond that.
The short version
- Put an HTTPS reverse proxy in front and set
PUBLIC_URL. - Do not publish port 8080 to the internet; publish only the proxy.
- Set
SETUP_CODEbefore the first owner exists if the server can be reached from outside. - Set
TRUSTED_PROXIESto just your proxy if other devices on your LAN are not trusted. - Back up the
ribbon_datavolume somewhere off the machine. - Turn on passkeys (or SSO) and keep the container hardening from
docker-compose.yml.
HTTPS and a reverse proxy¶
Ribbon speaks plain HTTP on port 8080 and leaves TLS to whatever you already run in front of it. On plain HTTP, passwords and session cookies cross the network readable. Ribbon helps by refusing password sign-in over plain HTTP from addresses outside your private network, but that is a seatbelt, not a substitute for HTTPS.
HTTPS is also needed for the "install as an app" prompt, the offline page and passkeys (a browser secure context: an https:// address or http://localhost).
Pick the recipe that matches what you already run, all in HTTPS and reverse proxies:
| You run | Use |
|---|---|
| Nothing yet | Caddy overlay bundled in the repo: one command, automatic Let's Encrypt certificate |
| Nginx Proxy Manager | NPM recipe |
| Traefik | Traefik labels |
| Tailscale | tailscale serve: private HTTPS, nothing on the internet |
| Cloudflare | Cloudflare Tunnel |
A minimal Caddy site, for reference:
Do not publish port 8080 when a proxy is in front
Publish only the proxy, or firewall 8080 to the proxy host. If the proxy runs on the same Docker network you can drop ports: from the Ribbon service entirely (the Caddy overlay does this for you).
The settings that matter¶
PUBLIC_URL¶
The address people type, for example https://ribbon.example.com. Ribbon uses it to decide whether a request is HTTPS (so it can mark the session cookie Secure with the __Host- prefix and send Strict-Transport-Security), and for calendar links, SSO redirects and passkey origins. Set it whenever you use a proxy, then docker compose up -d.
TRUSTED_PROXIES¶
Which peers may tell Ribbon about the real connection through X-Forwarded-Proto, -Host and -For. Headers from anyone else are ignored, so a stranger who reaches port 8080 directly cannot pretend the connection is HTTPS or forge their address.
The default trusts loopback and private ranges, which suits a home setup with a proxy on another machine on the LAN. The cost is that another LAN device could forge X-Forwarded-For to look public or private (it can no longer use that to dodge sign-in lockout). If your network is not fully trusted, set it to just the proxy:
TRUSTED_PROXIES=172.18.0.5/32 # your proxy's address
# or, to trust nobody (and set PUBLIC_URL instead):
TRUSTED_PROXIES=none
SETUP_CODE¶
Until the first owner exists, /api/setup is open to anyone who can reach the server. A server exposed before setup could be claimed by a stranger. SETUP_CODE closes that: the person creating the first owner must type the code. Guesses are rate limited (10 per 15 minutes per address) and the comparison is constant time.
Without a code, Ribbon only creates the first owner from a private, home-network address and the setup page explains why it refused. A start-up warning is logged while no owner exists.
ALLOW_PUBLIC_SETUP¶
ALLOW_PUBLIC_SETUP=true lets the first owner be created from a public address with no code. Not recommended. Prefer SETUP_CODE.
ALLOW_INSECURE_LOGIN¶
By default Ribbon refuses password sign-in over plain HTTP from addresses outside the private ranges ("Sign-in over plain HTTP is only allowed from your home network"). ALLOW_INSECURE_LOGIN=true turns that guard off. Only use it if you have a reason, such as a tunnel that terminates TLS in a way Ribbon cannot see and you cannot yet set PUBLIC_URL.
Passkeys and single sign-on¶
Passkeys let people sign in with a fingerprint, face or security key. Add one under Settings → Account (it needs HTTPS or localhost). Ribbon requires user verification (a PIN, fingerprint or face), so a stolen unlocked device or a key with no PIN is not enough. Passkey sign-in has its own rate limits, separate from the password lockout.
Single sign-on (OpenID Connect with PKCE) works with a standard OpenID Connect provider such as Authelia, Authentik or Keycloak. (It is covered by automated tests against a fake provider; it has not yet been checked against a particular real one, so please report any trouble.)
- At your provider, create a client for Ribbon with the redirect URI
<PUBLIC_URL>/api/auth/oidc/callback. - Set
OIDC_ISSUER,OIDC_CLIENT_IDandOIDC_CLIENT_SECRETin.env, and rundocker compose up -d. - Each person links SSO to their existing login under Settings → Account, or set
OIDC_AUTO_LINK_BY_EMAIL=trueto match by verified email. - Once it works for everyone,
DISABLE_PASSWORD_LOGIN=truehides the password form. It only takes effect when SSO is configured, so it cannot lock you out by accident.
Ribbon checks every provider endpoint against the same https rule as the issuer (plain http:// only on a private address, or with OIDC_ALLOW_HTTP_ISSUER=true) and refuses redirects, so a tampered discovery document cannot point the server at an internal address.
Keep a way back in
Passkeys and SSO have separate lockout counters from passwords. If someone is locked out by wrong passwords, a passkey or SSO sign-in still works, and the lockout ends on its own after 15 minutes.
Sign-in protection¶
- Passwords are hashed with scrypt (N=2^16, r=8, p=2); older hashes are upgraded when someone next signs in.
- Sessions are stored on the server (only a hash of the token is kept) and last 14 days. The cookie is
HttpOnly,SameSite=Lax, andSecureover HTTPS. - Every change needs a CSRF token and a same-site
Origin. - Lockout: 5 wrong passwords for one account from one address, 20 for one account from anywhere, or 30 from one address across accounts lock sign-in for 15 minutes. A forged
X-Forwarded-Forcannot dodge it. The counters are in memory and reset when the container restarts.
Container hardening¶
The image runs as the unprivileged node user and only /data is writable. The shipped docker-compose.yml also does:
Ribbon needs no extra privileges, so keep both if you write your own compose file. A further step you can try is a read-only root filesystem (read_only: true with a tmpfs for /tmp); it has not been tested by the project.
The image also has a health check on /api/health, which answers over plain HTTP so Docker health checks keep working behind any proxy.
Backups and secrets¶
- Back up the
ribbon_datavolume. Ribbon writes nightly snapshots into it, but those live on the same disk, so copy them elsewhere. See Backups & restore. Backup files are owner-only on disk (0600, folder0700) because they hold every household's data. - Keep
.envout of git and out of screenshots; it can holdSETUP_CODE,XAI_API_KEYandOIDC_CLIENT_SECRET. - Treat your ICS calendar link like a password. Regenerating it revokes the old one.
Security audit summary¶
Ribbon 0.3.0-dev.2 had a full review against a running production build, not just the code. Every finding was reproduced with a proof-of-concept request before the fix and re-checked after; each fix has a regression test.
Result: no way to read or change another household's data, no SQL injection, no stored XSS and no sign-in bypass were found. One availability bug (a hanging request), several brute-force and takeover weaknesses and a number of smaller hardening gaps were found and fixed.
| Severity | Count | Examples (all fixed) |
|---|---|---|
| High | 2 | First-owner takeover on an exposed server; a malformed URL path that hung the connection |
| Medium | 7 | Bypassable sign-in lockout; global passkey lockout; Secret Santa peeking after the draw; restore with no owner; demoting the instance admin; scrypt strength; unbounded request bodies |
| Low | 12 | CSV link checks, calendar-feed injection, passkey user verification, SSO endpoint checks, Ask limits, backup file permissions, headers, CI hygiene, container capabilities, dependencies |
Accepted risks include a CSP that still allows inline scripts (the framework's server rendering needs them), household members being trusted editors of their shared ledger, and the instance admin being able to read everything in backups. Each has its reasoning in the report.
The complete list, with how each item was verified, what was checked and found sound, and what was not tested, is in the full security audit. To report a problem privately, see the security policy (do not open a public issue).