281 lines
18 KiB
Plaintext
281 lines
18 KiB
Plaintext
{
|
|
"_README": [
|
|
"SmartestHome — the single source of truth for every build script in tools/.",
|
|
"",
|
|
"Copy this to CoreSystemConfig.json (that filename is gitignored), fill it in, then",
|
|
"run any builder in tools/. Nothing in tools/ has its own copy of an IP, a port, a",
|
|
"token or a URL — every build script reads this file, so a value can only ever be",
|
|
"wrong in one place instead of six.",
|
|
"",
|
|
"ANYTHING DERIVABLE IS DERIVED, NEVER TYPED. You give the subnet prefix once and one",
|
|
"last octet per host; the builders compute every address and every service URL from",
|
|
"those. That is what makes the container-host and llm-host ISOs a TWINNED pair: the",
|
|
"container host learns the LLM host's address because it was computed from the same",
|
|
"two numbers, not because someone typed it into two files and kept them in sync.",
|
|
"",
|
|
"THIS FILE WILL CONTAIN SECRETS once filled in (Wi-Fi PSK, service tokens, MQTT and",
|
|
"HA credentials) and those secrets get burnt into the ISOs the builders produce.",
|
|
"Treat both this file and the built ISOs as credentials: .gitignore covers them, but",
|
|
"an ISO on a USB stick in a drawer is still every token in this household.",
|
|
"",
|
|
"Run 'tools/validate-config.py' at any time to check it. Every builder runs it first",
|
|
"and refuses to build on an error."
|
|
],
|
|
|
|
"household": {
|
|
"_comment": "Baked into every image. timezone must be a real zoneinfo name; keyboard_layout an xkb layout ('localectl list-x11-keymap-layouts').",
|
|
"timezone": "Europe/Vienna",
|
|
"locale": "en_US.UTF-8",
|
|
"keyboard_layout": "de",
|
|
"debian_release": "bookworm"
|
|
},
|
|
|
|
"network": {
|
|
"_comment": "subnet_prefix is the first three octets of the smart-home VLAN, no trailing dot. Every host below places itself in it by last octet, which is also how the validator guarantees they can actually reach each other.",
|
|
"subnet_prefix": "192.168.30",
|
|
"netmask": "255.255.255.0",
|
|
"gateway_last_octet": 1,
|
|
"dns_servers": ["192.168.30.1"],
|
|
"wifi": {
|
|
"_comment": "Used by kiosk images that have no wired connection. Leave ssid empty if every device is wired; the validator will then not complain about an empty psk.",
|
|
"ssid": "",
|
|
"psk": ""
|
|
}
|
|
},
|
|
|
|
"container_host": {
|
|
"_comment": "The Phase 1 machine: Home Assistant, Mosquitto, Zigbee2MQTT, Frigate, Grocy and this repo's own services. Everything else in the household points at this address.",
|
|
"hostname": "smarthome-core",
|
|
"ip_last_octet": 12,
|
|
"install_disk": "/dev/sda",
|
|
"admin_username": "amir",
|
|
"enable": {
|
|
"_comment": "Mirrors setup-container-host.sh's ENABLE_* flags. Turning one off here also stops the builders from demanding the tokens it would have needed.",
|
|
"identity": true,
|
|
"pantry_vision": true,
|
|
"digest_engine": false,
|
|
"admin_canvas": false,
|
|
"trash_calendar": false,
|
|
"transit": false,
|
|
"trip_planning": false,
|
|
"chores": true,
|
|
"ntfy": true,
|
|
"node_red": true,
|
|
"netdata": true,
|
|
"homepage": true,
|
|
"portainer": true,
|
|
"mealie": false,
|
|
"gallery_smb": false,
|
|
"photos_web": false,
|
|
"workshop": false,
|
|
"music_assistant": false,
|
|
"backups": false
|
|
}
|
|
},
|
|
|
|
"llm_host": {
|
|
"_comment": "The Phase 3 Ollama machine. Its address is what the container host's OLLAMA_HOST is derived from — change ip_last_octet here and every consumer follows automatically.",
|
|
"hostname": "smarthome-llm",
|
|
"ip_last_octet": 13,
|
|
"install_disk": "/dev/sda",
|
|
"admin_username": "amir",
|
|
"tier": "auto",
|
|
"text_model_gpu": "qwen2.5:14b-instruct",
|
|
"text_model_cpu": "qwen2.5:7b-instruct",
|
|
"vision_model": "llava",
|
|
"pull_vision_model": true,
|
|
"keep_alive": "30m",
|
|
"max_loaded_models": 1,
|
|
"num_parallel": 1
|
|
},
|
|
|
|
"ports": {
|
|
"_comment": "The whole published-port inventory, matching docs/network-integration.md §4. The validator FAILS on any duplicate here, which is the mechanism that keeps a collision from reaching a deployment. NOTE music_assistant: its own default is 8095, which collides with pantry_vision — and because it runs network_mode:host, Compose's own port-conflict check never fires (project-plan open decision #31). 8101 is set here to resolve that, but you must also configure Music Assistant itself to listen on it; this file cannot make it move.",
|
|
"home_assistant": 8123,
|
|
"mqtt": 1883,
|
|
"zigbee2mqtt": 8080,
|
|
"node_red": 1880,
|
|
"homepage": 3000,
|
|
"ntfy": 8090,
|
|
"portainer": 9000,
|
|
"gallery_smb": 445,
|
|
"mealie": 9925,
|
|
"frigate": 5000,
|
|
"grocy": 9283,
|
|
"digest_web": 8091,
|
|
"admin_web": 8094,
|
|
"pantry_vision": 8095,
|
|
"pantry_web": 8096,
|
|
"identity": 8097,
|
|
"identity_web": 8098,
|
|
"transit": 8099,
|
|
"otp": 8100,
|
|
"music_assistant": 8101,
|
|
"workshop": 8102,
|
|
"workshop_web": 8103,
|
|
"photos_web": 2283,
|
|
"ollama": 11434,
|
|
"proxy_http": 80,
|
|
"proxy_https": 443
|
|
},
|
|
|
|
"secrets": {
|
|
"_comment": "Generate the tokens with: openssl rand -hex 32. Each is required only if the service that uses it is enabled above; the validator says which. ha_token is a Long-Lived Access Token from HA's own UI (profile -> Security) and cannot be generated ahead of time — leave it empty for the first build and re-run once HA is up. The opnsense_api_* pair is the same kind of thing: only OPNsense can mint it (System -> Access -> Users -> API keys), so it is never generated here either.",
|
|
"identity_token": "",
|
|
"pantry_vision_token": "",
|
|
"transit_token": "",
|
|
"mqtt_username": "",
|
|
"mqtt_password": "",
|
|
"ha_token": "",
|
|
"_opnsense_keys": "One entry per firewall in the opnsense list above, keyed by its `name`. A single-firewall household writes {\"main\": {...}} and is done. Keyed rather than positional because a list that has to line up with another list by index is a bug waiting for the day somebody reorders one of them.",
|
|
"opnsense_keys": {
|
|
"main": { "api_key": "", "api_secret": "" }
|
|
},
|
|
"_checkmk": "A CheckMK automation user with the read-only Guest role, and its AUTOMATION SECRET (Setup -> Users -> the user -> Automation secret) \u2014 not its login password.",
|
|
"checkmk_username": "",
|
|
"checkmk_secret": "",
|
|
"_freeipa_bind_password": "The password for freeipa.bind_dn. Prefer a dedicated service account with READ-ONLY access to the household subtree \u2014 the mirror never writes to the directory, so a bind that can is a grant with no matching use.",
|
|
"freeipa_bind_password": "",
|
|
"_workshop_token": "Required when container_host.enable.workshop is true. openssl rand -hex 32",
|
|
"workshop_token": "",
|
|
"_gitea": "Optional, and only for the workshop assistant's own repositories. Gitea -> Settings -> Applications -> Generate New Token, scope write:repository. STRONGLY PREFER a dedicated 'workshop-bot' user scoped to one organisation over a token on your own account: this token can create repositories, and the blast radius of a leaked env file should be one org of generated repos rather than everything you own. The assistant commits and pushes but never rewrites history, and repo deletion is not implemented at all \u2014 see workshop/README.md.",
|
|
"gitea_url": "",
|
|
"gitea_token": "",
|
|
"gitea_owner": "",
|
|
"ssh_authorized_key": "",
|
|
"kiosk_password": "",
|
|
"admin_password_hash": ""
|
|
},
|
|
|
|
"opnsense": [
|
|
{
|
|
"_comment": "EVERY OPNsense firewall in the household, as a list. It was a single object until more than one firewall became plausible; a list is the shape that does not need changing again, and a one-firewall household just has one entry. `name` distinguishes them everywhere downstream — in the digest's network section, in the workshop health table, and in the alert text itself — so 'the IDS is quiet' can never silently mean 'one of the two is quiet'. Used by digest-engine's network digest (GET /api/ids/service/status and POST /api/ids/service/query_alerts \u2014 two read endpoints, nothing else, ever) and by workshop's health poller. A build writes this block plus the secrets into IDSconf.json; long-form field docs live in digest-engine/IDSconf.json.example. An empty list skips that file entirely. TWO THINGS THIS DOES NOT DO: it does not enable Suricata (Services -> Intrusion Detection on the firewall, then download a ruleset), and it does not enable the ingest (ENABLE_OPNSENSE_IDS_INGEST=true in digest-engine.env). SCOPE EACH API KEY: its own OPNsense user with only the 'Services: Intrusion Detection' privilege \u2014 which still covers api/ids/* including start/stop, because OPNsense ACLs are page-level, so the read-only guarantee comes from this project calling exactly two endpoints and not from the firewall enforcing it.",
|
|
"name": "main",
|
|
"base_url": "",
|
|
"verify_tls": true,
|
|
"interfaces": [],
|
|
"max_alerts_scanned": 5000,
|
|
"top_signatures": 8,
|
|
"top_hosts": 5,
|
|
"packet_capture_reference": ""
|
|
}
|
|
],
|
|
|
|
"checkmk": {
|
|
"_comment": "An existing CheckMK server, polled read-only for host/service state. Feeds two places: digest-engine's network section (so a failing disk shows up in the quarter-daily digest) and workshop's own infra_status table (so the workshop display can overlay system health on the gallery). Nothing here ever acknowledges, downtimes or reschedules anything \u2014 the API user only needs to read. site is the CheckMK site name, which is the path segment in every URL: http://<host>/<site>/check_mk/api/1.0/...",
|
|
"_credentials": "CheckMK: Setup -> Users -> add a user, Roles = 'Guest' (read-only), then its 'Automation secret' \u2014 NOT its login password. Put the username and secret in secrets.checkmk_username / secrets.checkmk_secret. A Guest-role automation user cannot change anything, which is the actual boundary here rather than a promise about which endpoints get called.",
|
|
"base_url": "",
|
|
"site": "cmk",
|
|
"verify_tls": true,
|
|
"_only_problems": "true keeps the payload to hosts and services that are not OK, which is what both consumers want; false pulls everything and is mostly useful once, to see what the site knows about.",
|
|
"only_problems": true,
|
|
"max_rows": 200
|
|
},
|
|
|
|
"identity_provider": {
|
|
"_comment": "An EXTERNAL identity provider (Keycloak) in front of the household's web surfaces. NOTHING IN THIS REPO IMPLEMENTS SSO YET \u2014 this block exists so the decision and its values are recorded in one place before the work happens, and so the proxy config can be generated from it when it does. Leave issuer_url empty to skip it entirely, which is the current default and the tested path.",
|
|
"_pairs_with_freeipa": "KEYCLOAK AND FREEIPA ARE ONE DECISION, NEVER TWO. Keycloak federates FreeIPA as its user store; it is not a place people are created. Configuring Keycloak without FreeIPA would mean a second, parallel set of household accounts \u2014 which is the specific outcome having a directory exists to prevent \u2014 so the validator refuses either one alone. Group membership comes from FreeIPA and rides through Keycloak as a claim; roles are never assigned in Keycloak itself, or the directory stops being the answer to 'who is in this household'. WHAT IT WOULD PROTECT: the proxy-fronted web UIs (Home Assistant, the photo frontend, workshop, identity's admin panel). WHAT IT MUST NEVER PROTECT: the kiosk-to-service APIs \u2014 pantry-vision, identity's /presence, workshop's own API \u2014 which are bearer-token gated because a wall panel cannot complete an interactive login. Putting an OIDC redirect in front of those turns every kiosk into a dead screen.",
|
|
"issuer_url": "",
|
|
"_realm": "Keycloak realm name; part of the issuer URL too, kept separately because the proxy config and any client library both want it on its own.",
|
|
"realm": "smarthome",
|
|
"client_id": "smarthome-proxy",
|
|
"_protected_hosts": "Which proxied hostnames would sit behind SSO. Advisory until the work is done \u2014 see the _comment above.",
|
|
"protected_hosts": []
|
|
},
|
|
|
|
"freeipa": {
|
|
"_comment": "An existing FreeIPA domain, mirrored INTO identity as a source of people and group memberships. NOT IMPLEMENTED YET \u2014 this block records the decision and its values ahead of the work, like identity_provider above. Leave server empty to skip it, which is the current default and the only tested path.",
|
|
"_direction": "ONE WAY, FreeIPA -> identity, and it must stay that way. identity holds household facts FreeIPA has no opinion about (BLE identifiers, chore reminder style, digest preferences, a colour) and writing any of that back would make a directory serving real logins into a store of smart-home preferences. A mirrored person is matched on uid and their name/group memberships are refreshed; everything else identity knows about them is left alone.",
|
|
"_groups": "Group membership maps to roles here. `chore_exempt_group` members are dropped from the chore rotation exactly as the per-person flag does today \u2014 which means a FreeIPA group can grant an exemption but must never remove one somebody set by hand, or a directory sync would silently re-enrol a guest. `household_group` is who gets mirrored at all: without it every service account in the directory becomes a household member.",
|
|
"_naming": "EVERY group this project reads is named USR_HA_<parameter>, matching the field name after the _group suffix is dropped: household_group -> USR_HA_household, chore_exempt_group -> USR_HA_chore_exempt, admin_group -> USR_HA_admins. The prefix is what makes a directory shared with other systems auditable \u2014 'which groups does the smart home read?' is answerable with one filter instead of by reading this file. The validator warns on anything that does not follow it rather than erroring, since an existing directory may already have its own convention and renaming groups in FreeIPA is not a thing a config file should force.",
|
|
"server": "",
|
|
"domain": "",
|
|
"base_dn": "",
|
|
"bind_dn": "",
|
|
"verify_tls": true,
|
|
"household_group": "USR_HA_household",
|
|
"chore_exempt_group": "USR_HA_chore_exempt",
|
|
"admin_group": "USR_HA_admins",
|
|
"_sync_interval_minutes": "How often the mirror runs, once it exists. Directory changes are not urgent \u2014 somebody joining the household is a thing you also tell the door panel about.",
|
|
"sync_interval_minutes": 60
|
|
},
|
|
|
|
"proxy": {
|
|
"_comment": "One HTTPS front door for this repo's own services (Caddy), plus an HTTP->HTTPS redirect. Not a household-wide gateway — Home Assistant, Grocy and friends keep their own ports. hostname is what the certificate is issued for and what you type in the browser; it must resolve to the container host (a DNS override on OPNsense, a hosts entry, or just use the IP with tls: internal). tls 'internal' makes Caddy run its own CA — no external dependency, but browsers show a warning until you install its root (tools/export-proxy-ca.sh). tls 'custom' uses cert_file/key_file, which is how you'd use a real cert obtained via a DNS-01 challenge without exposing anything. See proxy/README.md.",
|
|
"enabled": true,
|
|
"hostname": "home.example.lan",
|
|
"tls": "internal",
|
|
"cert_file": "",
|
|
"key_file": ""
|
|
},
|
|
|
|
"voice": {
|
|
"_comment": "Defaults for any kiosk with voice_satellite enabled; a kiosk may override wake_word individually.",
|
|
"wake_word": "ok_nabu"
|
|
},
|
|
|
|
"kiosks": [
|
|
{
|
|
"_comment": "type must be one of: thin-client, touch-panel, door-panel, kitchen-display. hostname must be unique and a valid DNS label — it is what the HA device shows up as.",
|
|
"type": "door-panel",
|
|
"hostname": "door-panel",
|
|
"room": "hallway",
|
|
"friendly_name": "Door panel",
|
|
"kiosk_username": "kiosk",
|
|
"voice_satellite": true,
|
|
"enable_installer": false
|
|
},
|
|
{
|
|
"type": "kitchen-display",
|
|
"hostname": "kitchen-display",
|
|
"room": "kitchen",
|
|
"friendly_name": "Kitchen fridge display",
|
|
"kiosk_username": "kiosk",
|
|
"voice_satellite": false,
|
|
"enable_installer": false
|
|
},
|
|
{
|
|
"type": "thin-client",
|
|
"hostname": "thin-client-living",
|
|
"room": "living_room",
|
|
"friendly_name": "Living room thin client",
|
|
"kiosk_username": "kiosk",
|
|
"voice_satellite": false,
|
|
"enable_installer": false,
|
|
"enable_steam_link": true,
|
|
"enable_gesture_control": false
|
|
},
|
|
{
|
|
"type": "touch-panel",
|
|
"hostname": "touch-panel-kitchen",
|
|
"room": "kitchen",
|
|
"friendly_name": "Kitchen touch panel",
|
|
"kiosk_username": "kiosk",
|
|
"voice_satellite": false,
|
|
"enable_installer": false
|
|
}
|
|
],
|
|
|
|
"audio_endpoints": [
|
|
{
|
|
"_comment": "Headless Spotify Connect appliances for rooms with no thin client. arch picks the toolchain — and they are genuinely different toolchains producing different artifacts, not one image for both: 'amd64' is a mini PC + USB DAC built with live-build (an .iso), 'arm64' is a Raspberry Pi + HiFiBerry Amp2 built with rpi-image-gen (an .img). build-all.sh builds every entry here, so listing both architectures gets you both. hostname doubles as the Spotify Connect device name and must be unique across kiosks too — they're all devices on one network.",
|
|
"hostname": "audio-endpoint-livingroom",
|
|
"room": "living_room",
|
|
"friendly_name": "Living room",
|
|
"arch": "amd64"
|
|
},
|
|
{
|
|
"hostname": "audio-endpoint-kitchen",
|
|
"room": "kitchen",
|
|
"friendly_name": "Kitchen",
|
|
"arch": "arm64"
|
|
}
|
|
],
|
|
|
|
"build": {
|
|
"_comment": "Where finished images land, relative to the repo root. Gitignored — see .gitignore — because these carry every secret in this file. arm64_prebake bakes each arm64 audio endpoint's hostname into its own image, so every device in the household is named by the build rather than typed into Raspberry Pi Imager afterwards; it costs one full rpi-image-gen run per room. Set it false to build a single generic .img and name each unit at flash time instead.",
|
|
"output_dir": "iso-out",
|
|
"arm64_prebake": true
|
|
}
|
|
}
|