-
-
Notifications
You must be signed in to change notification settings - Fork 1
Security and Threat Model
Kinboard is designed for a single trusted home network. Read that sentence once more — the entire authorization model rests on it.
Identity is a 2-tuple:
-
Family — identified by a 6-character
join_code. Anyone with the code can register a device and read/write the family's data. -
Device — identified by a UUID stored in a cookie. Each device that joins a family gets its own row in
public.deviceswith alast_seentimestamp.
There are no user accounts. There is no password protection on data access by default beyond the join code. There is an optional 4-digit PIN gate on the /settings/* area (see "Settings PIN" below).
Every database row is gated by a Row-Level Security policy that checks family_id against a header derived from the join code. The Supabase service-role key bypasses RLS and is held only by the Next.js server process — never exposed to the browser.
- A trusted LAN. Everyone in the house has the join code. Nobody else does. A new family member joining just needs the code, like a Wi-Fi password.
- Low-friction kiosk and PWA UX. No login screens to confuse the kids. No tokens to refresh. No password resets.
- Self-hosters who already secure their LAN. The rest of your home network has the same trust model — your printer doesn't need OAuth either.
-
Public internet exposure without a wrapping auth layer. The join code is 6 alphanumeric characters (~30 bits of entropy). Brute force is possible. Don't expose
/joinpublicly without something like Authelia, Authentik, Cloudflare Access, or Traefik forward-auth in front. - Untrusted users on the same family. Anyone with the code can read every event, todo, recipe, photo URL, OAuth token, and device fingerprint of every other device in the family. The model assumes "everyone in the family has the same trust level."
- Compliance scenarios. No audit logs, no role-based access, no per-resource ACL. Don't use Kinboard for anything that needs HIPAA, GDPR-data-controller distinction, or similar.
If something goes wrong, these categories are at risk:
-
OAuth refresh tokens for Google Calendar (in
settings.valueJSONB, AES-encrypted at rest by Postgres only if you enable disk encryption — Kinboard itself does not encrypt the column) - Long-lived access tokens for Home Assistant
- API keys for Immich, OpenWeatherMap, Bring! account credentials
-
VAPID push notification keys at host level (in
webapp/docker/.env) - Family content: events, todos, shopping lists, recipe library, uploaded avatars, screensaver photo URLs, camera RTSP URLs (and embedded credentials), HA entity history
-
The Supabase JWT secret (
JWT_SECRET) and service role key — host-level, inwebapp/docker/.env
A vulnerability that lets an unauthenticated client read or write any of the above qualifies as high-severity. See SECURITY.md in the repo for disclosure.
For the typical home deployment:
- Don't expose the stack directly to the public internet. Use a reverse proxy with auth in front, or restrict via WireGuard / Tailscale / Cloudflare Tunnel + Access.
-
Generate strong secrets via
setup.sh. Don't hand-edit.envto use easily-guessed passwords. - Set the Settings PIN if you have curious kids or visiting guests with the join code. See below.
-
Disable signup if you self-host Supabase Auth. Kinboard doesn't actually use GoTrue auth flows for the family identity model, so leaving signup off avoids accidental account creation. Set
GOTRUE_DISABLE_SIGNUP=trueinwebapp/docker/.env. -
Limit the Postgres port to localhost. The default
docker-compose.ymlexposes 5432 to the host network so you canpsqlfor ops. If you don't need that, change"5432:5432"to"127.0.0.1:5432:5432". -
Rotate the family join code occasionally. Currently no UI for this — direct DB update needed (
UPDATE families SET join_code=... WHERE id=...).
-
Disable kiosk auto-login on the wall display if family members have different trust levels. Kinboard's PIN gate covers
/settings/*but not the dashboard itself. - Apply Postgres at-rest encryption at the filesystem layer (LUKS, ZFS encryption, etc.).
Settings → Settings PIN sets a 4-digit code that's required to enter /settings/*. The PIN itself is stored as plaintext in the settings.value JSONB (under the settings_pin key) — it's a "keep curious kids out" feature, not a real auth boundary.
Once set, the PIN persists for the browser session via sessionStorage. Closing the tab requires re-entry; navigating between settings sub-pages does not.
- Stack runs on a host that's not directly reachable from the WAN
- If using Traefik publicly, an auth middleware sits in front of it
-
setup.shwas run with no manual edits to the generated secrets -
webapp/docker/.envis mode 600 (chmod 600) - Backups encrypt the
pg_dumpoutput - No real production data lives in development checkouts (
webapp/.env.localshould be empty or have non-prod values) - If multiple families share a host, each runs in its own Compose project (
PROJECT_NAMEdiffers)
Email security@svenger87.de. Don't open public issues. Acknowledgement within 7 days, fix targeted within 30 days for high-severity. Full text in SECURITY.md.
Kinboard on GitHub · Sponsor · Buy me a coffee · Report a bug · MIT-licensed
Getting started
Operations
Integrations
Kiosk hardware
Built-in features
- Dashboard
- Calendar
- Shopping
- Recipes & meal planning
- Tasks & todos
- Notes
- Birthdays
- School schedule
- Smart home & energy
- Screensaver
- Family members
- Devices
- Notifications
- Themes
Plugins (per-family on/off)
Contributing