Running obacht at home
On this page
One obacht server serves the API and the parent app. It needs Postgres and a hostname with HTTPS. The parent app can only be installed, and can only send push notifications, over HTTPS, and release builds of the Android agent only talk HTTPS.
Two ways to run it: the NixOS module, or the Helm chart on Kubernetes.
NixOS
Add the flake as an input and enable the module. It creates a local Postgres database
obacht and connects over the Unix socket, so no password is needed.
# flake.nix of the host
inputs.obacht.url = "git+ssh://git@github.com/make-it-so-dev/obacht";
# configuration.nix (with `obacht` passed in from the flake inputs)
imports = [ obacht.nixosModules.default ];
services.obacht = {
enable = true;
pushContact = "mailto:you@example.org";
# listen = "127.0.0.1:8080"; # default
# usageRetentionDays = 90; # default
};
Put a TLS reverse proxy in front. Live updates are server-sent events, so turn off response buffering. With Caddy:
services.caddy = {
enable = true;
virtualHosts."obacht.example.org".extraConfig = ''
reverse_proxy 127.0.0.1:8080 {
flush_interval -1
}
'';
};
With nginx:
services.nginx.virtualHosts."obacht.example.org" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:8080";
extraConfig = ''
proxy_buffering off;
proxy_read_timeout 1h;
'';
};
};
To use an existing Postgres instead, set database.createLocally = false and put
DATABASE_URL=postgres://... in a file outside the Nix store, referenced by
services.obacht.environmentFile.
Optional and experimental: to import Steam playtime, get a Steam Web API key at https://steamcommunity.com/dev/apikey
and put OBACHT_STEAM_API_KEY=... in the environmentFile (Helm: steam.existingSecret).
Optional: parents can log in through an OpenID Connect provider such as Authelia, Keycloak or Authentik instead. It is then the only parent login: password login, setup, adding parents by hand and password changes are off, so no password set once and never rotated can get around the provider. Parents are the members of one provider group, checked at every login.
Register obacht at the provider as a client with the redirect URL
https://obacht.example.org/api/v1/auth/oidc/callback and the scopes openid profile groups,
create a group for the parents, then:
services.obacht.oidc = {
issuer = "https://auth.example.org";
clientId = "obacht";
clientSecretFile = "/run/secrets/obacht-oidc"; # outside the Nix store
redirectUrl = "https://obacht.example.org/api/v1/auth/oidc/callback";
parentGroup = "obacht-parents";
name = "Authelia"; # on the login button
};
(Helm: oidc.* values.)
- Group members get an account on their first login. A parent account from before with the
same username (the provider's
preferred_username) is taken over instead, and its password is deleted. - Groups are read from the
groupsclaim of the ID token, or from the userinfo endpoint if the ID token has none (Authelia's default). Other names:oidc.groupsClaim,oidc.scopes. - Removing someone from the group refuses their next login and ends their sessions then. Provider login sessions last 7 days (password ones 30), so a removed parent is logged out within a week either way; remove the parent on the Account page to end it at once.
- When the server starts with a provider, all login sessions from passwords end. API tokens keep working.
- The way back: remove the provider settings. Parents that came from the provider have no
password; if no parent has one,
POST /api/v1/setup(the "first start" form in the app) works again to create one.
The module is tested in CI by booting a VM (nix build .#checks.x86_64-linux.module -L).
Kubernetes (Helm)
See deploy/helm/obacht/README.md. While the repository is
private, the cluster needs a pull secret for ghcr.io/make-it-so-dev/obacht, for example
from a GitHub token with read:packages:
kubectl create secret docker-registry ghcr \
--docker-server=ghcr.io --docker-username=<github user> --docker-password=<token>
helm install obacht deploy/helm/obacht \
--set database.existingSecret=obacht-db \
--set 'imagePullSecrets[0].name=ghcr' \
--set pushContact=mailto:you@example.org \
--set ingress.enabled=true --set ingress.host=obacht.example.org
First start
- Open
https://obacht.example.organd create the parent account. - Add a child and set limits and bedtimes.
- Optional (experimental, only with a Steam key): under the child's Devices, link their Steam account so time played on any PC counts toward the same limits (the profile and its game details must be public).
- On your phone, open the page and add it to the home screen (on iPhone this is required for notifications), then turn on notifications on the home screen.
Android phones
The APKs come from CI. CI only builds them when Android code changes, so under Actions
open the CI workflow, click Run workflow on main, and download the
android-debug-apks artifact from that run. It holds two builds:
play: works on any phone. Blocks apps through the accessibility service; the child could turn obacht off in the settings (see Platform limitations).owner: obacht becomes device owner and pauses blocked apps; it cannot be uninstalled while paired. Device owner can only be set on a phone without accounts, so use a freshly reset phone (or skip adding accounts during setup), then add accounts afterwards.
Install and set up:
adb install app-play-debug.apk # or app-owner-debug.apk
# owner flavor only, before adding any account to the phone:
adb shell dpm set-device-owner io.obacht.agent.owner/.AdminReceiver
Owner flavor by QR code
Instead of adb, a freshly reset phone can set up the owner flavor itself. The phone
downloads the APK during setup, so put app-owner-debug.apk somewhere it can reach over
Wi-Fi, for example python3 -m http.server 8000 in the download folder on a computer in
the same network. Then print the QR code (inside nix develop .#android):
WIFI_SSID=Home WIFI_PASSWORD=... \
scripts/owner-qr.sh app-owner-debug.apk http://192.168.1.10:8000/app-owner-debug.apk https://obacht.example.org
On the phone's welcome screen, tap six times on the same spot, connect to Wi-Fi if the code doesn't carry it, and scan the code. Android downloads obacht, makes it device owner and finishes setup; all system apps stay enabled. The last argument is optional and prefills the server address on the pairing screen. The code is tied to the APK's signing key, so print a new one when the APK comes from a different build machine.
To test this on a spare phone and record what Play Protect does for the evidence log, follow the QR provisioning test.
Then pair:
- In the parent app, open the child, then Show pairing code.
- In obacht on the phone, enter the server address (
https://obacht.example.org), the code and a name for the phone. - Grant usage access and notifications. With
play, also turn on obacht in Accessibility and the removal protection; both screens explain what they do.
Debug builds also accept a plain http:// address (for example http://192.168.1.10:8080)
for testing without TLS.
To remove obacht from a phone, remove the device in the parent app first: that lifts every
restriction. With owner, the pairing screen then offers Remove obacht as device owner,
after which the app can be uninstalled normally.
DNS filter (early)
For devices obacht has no agent for, the server can act as a filtering DNS resolver.
Turn it on with the resolvers that allowed lookups go to: services.obacht.dnsUpstream = "tls://dns.quad9.net"; on NixOS, dnsUpstream: tls://dns.quad9.net in the Helm values, or
OBACHT_DNS_UPSTREAM directly. A comma-separated list, tried in order of how well they
answer:
192.168.1.2or9.9.9.9:53: plain DNS (UDP, TCP for large answers). Fine for a resolver on your own network or host (Unbound, AdGuard Home, your router).tls://dns.quad9.net(port 853) ortls://9.9.9.9: DNS over TLS.https://dns.quad9.net/dns-query: DNS over HTTPS.
Host names are looked up once at start with the system's resolver; the certificate must be
valid for the name or IP address given. For a resolver at home with its own certificate,
point OBACHT_DNS_UPSTREAM_CA_FILE at it (PEM). obacht keeps answers in a cache for as
long as their TTL allows, and gives each device its remaining TTL (never more than five
minutes, see below).
Then add a DNS device for a child in the parent app (the child's devices, "Consoles and
TVs") or with POST /api/v1/children/{id}/dns-devices ({"name": "Living room TV"}). The answer holds a secret DNS over HTTPS address,
https://obacht.example.org/api/v1/dns-query/obdns_..., shown only once. Whatever
resolves through it follows the child's rules:
- Websites are apps named
web:plus their domain. Add one under the child's apps ("Websites"), then block, limit or categoriseweb:tiktok.comlike any app (PUT /api/v1/children/{id}/apps/web:tiktok.com); the rule covers subdomains such asm.tiktok.com. - Domains on website lists count as the list's category or are blocked (see below).
- Every other domain is only blocked by schedules for all apps, such as bedtime, or a pause.
- Blocked domains answer "no such domain". Allowed answers are cached at most until the next change, and never longer than five minutes.
Which domains a device looks up is never logged or stored. For the same reason, use a
resolver on your own network or an encrypted one (tls://, https://) as upstream: with
plain DNS to a public resolver, the network on the way sees every lookup.
Website lists
Instead of adding sites one by one, parents can subscribe to lists of domains that others
maintain, e.g. of AI chatbots or adult sites: in the parent app under Account ("Website
lists"), or with POST /api/v1/domain-lists
({"name": "AI chatbots", "url": "https://...", "counts_as": {"kind": "category", "category": "ai"}}
or "counts_as": {"kind": "blocked"}). obacht downloads the list right away and again once
a day; if a download fails, the domains from before stay in use. It reads plain lists (one
domain per line), hosts files and simple ad-block rules (||example.com^), up to 500,000
domains and 32 MB per list.
Lists apply to every child: a category list only to children who have that category, with its limits and schedules, a blocked list to all. A listed domain covers its subdomains like a rule; the most specific entry wins, and a parent's own rule for the same domain wins over every list. Nothing is subscribed by default; check a list's license and contents before adding it.
This needs a client that speaks DNS over HTTPS (Firefox, Chrome, Windows 11, or a router
or forwarder such as dnsproxy), or DNS over TLS (below). Consoles and most TVs only speak
plain DNS: use AdGuard Home for those, or a router that forwards their DNS over TLS.
DNS over TLS
DNS over TLS (port 853) is what Android and Android TV call "Private DNS", and what many
routers (a FRITZ!Box, OPNsense, dnsproxy) can forward to. Each DNS device gets its own
host name, <secret>.dns.example.org, so obacht knows whose rules apply. You need:
- A domain for it, e.g.
dns.example.org, with a wildcard DNS record*.dns.example.orgpointing at the server (often a CNAME to the server's name). - A certificate for
*.dns.example.org. Wildcards need the ACME DNS challenge, e.g.security.acme.certs."dns.example.org" = { domain = "*.dns.example.org"; dnsProvider = ...; group = "obacht"; };on NixOS or a cert-managerCertificatewith a DNS01 solver on Kubernetes. obacht reads the files again within an hour of a renewal.
On NixOS
services.obacht.dot = {
domain = "dns.example.org";
certFile = "/var/lib/acme/dns.example.org/fullchain.pem";
keyFile = "/var/lib/acme/dns.example.org/key.pem";
openFirewall = true;
};
in the Helm values dot.domain and dot.tlsSecret (a kubernetes.io/tls Secret; the
chart adds a LoadBalancer Service for port 853), or OBACHT_DOT_DOMAIN,
OBACHT_DOT_CERT_FILE, OBACHT_DOT_KEY_FILE and OBACHT_DOT_LISTEN (default
0.0.0.0:853). It needs OBACHT_DNS_UPSTREAM too.
Each DNS device then has its own host name, e.g. d3f9...e1.dns.example.org, listed with
the device in the parent app (dot_host in GET /api/v1/devices). Unlike the DNS over
HTTPS address it is not a secret: devices send it unencrypted when they connect (TLS server
name) and look it up through ordinary DNS, so anyone on the way can see it. Someone who
copies it gets filtered DNS from your server, nothing else (not the child's queries);
"New address" gives the device a new name and a new DNS over HTTPS address. Queries for
names obacht doesn't know are refused.
Limits
Against misuse of an address or name, each DNS device may ask 20 times a second on average (with bursts of 300, a TV starting an app); beyond that its queries are refused until it slows down. Addresses and names obacht doesn't know are only looked up in the database about 10 times a second across everyone, so made-up ones can't flood it, and DNS over TLS takes at most 16 open connections per IP address. A device's rules and the child's usage are kept in memory and refreshed the moment they change.
If a device gets refused in normal use (a TV starting an app asks many names at once),
raise the limits: OBACHT_DNS_QUERIES_PER_SEC (default 20), OBACHT_DNS_QUERY_BURST
(default 300) and OBACHT_DNS_LOOKUPS_PER_SEC (default 10); in the Helm values under
dnsLimits, on NixOS through environmentFile.
Through AdGuard Home
If you run AdGuard Home, obacht can let it do the DNS and only decide what's blocked. Point obacht at it: on NixOS
services.obacht.adguard = {
url = "http://127.0.0.1:3000";
username = "admin";
passwordFile = "/run/secrets/adguard-password";
};
in the Helm values adguard.url, adguard.username and adguard.existingSecret, or
OBACHT_ADGUARD_URL, OBACHT_ADGUARD_USERNAME and OBACHT_ADGUARD_PASSWORD(_FILE).
Each DNS device then becomes an AdGuard Home client. Add it with its addresses on the
home network, so plain DNS from it is recognised:
{"name": "Switch", "addresses": ["192.168.1.50"]} (an IP or MAC address; you can also
add them to the client in AdGuard Home later, and a fixed IP from your router helps). The
answer holds adguard_client_id, which also works in AdGuard's own encrypted addresses:
https://<adguard>/dns-query/<id> and <id>.<adguard> for DNS over TLS.
obacht keeps a block of user rules in AdGuard Home, marked ! obacht: ..., with what each
of its clients may not resolve right now, and rewrites it on every change and at least once
a minute. Your own rules outside that block stay as they are. The same rules apply as for
the built-in filter, with one difference: during bedtime or a pause, an allowed site also
lets its subdomains through even if they have their own block rule. Domains of website
lists become rules for each DNS device, so very large lists make for a long rule block;
for a list that should hold for every device on your network anyway (e.g. ads), add it in
AdGuard Home itself.
obacht's clients are created with AdGuard's query log and statistics turned off. Don't turn them on: parents only see visited domains if the child opted in.
Network devices through UniFi (early)
With a UniFi console, obacht can take a child's console or TV offline at the router: at bedtime, during a pause, and when its category's time is used up on the child's other devices. It needs no DNS settings on the device and works for anything on the network.
Create a local user on the console for obacht (UniFi OS: Admins & Users, "Restrict to local access only", with a role limited to the Network application), then point obacht at it: on NixOS
services.obacht.unifi = {
url = "https://192.168.1.1";
username = "obacht";
passwordFile = "/run/secrets/unifi-password";
caFile = ./unifi-console.pem; # or acceptInvalidCerts = true; for its self-signed certificate
};
in the Helm values unifi.url, unifi.username and unifi.existingSecret, or
OBACHT_UNIFI_URL, OBACHT_UNIFI_USERNAME and OBACHT_UNIFI_PASSWORD(_FILE) (plus
OBACHT_UNIFI_SITE, OBACHT_UNIFI_CA_FILE and OBACHT_UNIFI_ACCEPT_INVALID_CERTS). An API
key (OBACHT_UNIFI_API_KEY(_FILE), unifi.apiKeyFile) can replace the user where the
console accepts one for the Network application's classic API; check that it can block a
client before relying on it. For a self-hosted Network application without UniFi OS, set
OBACHT_UNIFI_OS=false (unifi.unifiOs = false).
Then link devices in the parent app (the child's devices, "Consoles and TVs (UniFi)"): "Find
on the network" lists what UniFi knows, or type the MAC address. With
POST /api/v1/children/{id}/network-devices
({"name": "Switch", "mac": "aa:bb:cc:dd:ee:ff", "category": "games"}):
- With a category, the device is offline like an app of that category: when its time or the total time for today is used up (on the phone, the PC, Steam), during schedules for it or for all apps, and during a pause. The device's own time can't be measured.
- Without a category, only schedules for all apps (bedtime) and pauses take it offline.
obacht blocks and unblocks the device as a UniFi client (block-sta/unblock-sta), on
every change and at least once a minute; a blocked client can't join any of the site's
networks. obacht only touches linked devices, and decides for them: a block set by hand in
UniFi is lifted when the rules allow the device online. Unlinking a device (or removing
the child) lets it back online. Devices that use a private (random) Wi-Fi address need it
turned off for the home network, so they keep their MAC address.
This page is part of obacht's documentation, licensed under CC BY 4.0. Source on GitHub · CC BY 4.0 · Version: main (976f125)