Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2de66045dc | ||
|
|
09d5b28e3d | ||
|
|
b3940685c4 | ||
|
|
8609175410 | ||
|
|
5d7df0c0c8 |
+32
-18
@@ -1,22 +1,36 @@
|
|||||||
# Copy to .env and fill in. DO NOT commit .env.
|
# Copy to .env and fill in real values. DO NOT commit .env to git.
|
||||||
#
|
#
|
||||||
# Secrets themselves live as files under authelia/secrets/ (also gitignored)
|
# Env var prefixes:
|
||||||
# so they can be mounted into the container without env-var leakage. This
|
# FRIGATE_ - referenced via {FRIGATE_*} substitution in frigate_config/config.yml
|
||||||
# .env only holds non-secret tunables.
|
# FN_ - read directly by frigate-notify (Viper). Note the DOUBLE underscore
|
||||||
|
# between YAML hierarchy levels: frigate.mqtt.password -> FN_FRIGATE__MQTT__PASSWORD
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---- Camera credentials ----
|
||||||
# Your root domain. This single value flows into authelia/configuration.yml
|
FRIGATE_RTSP_USER=admin
|
||||||
# (via Authelia's {{ env "DOMAIN" }} template substitution) and into
|
FRIGATE_RTSP_PASSWORD=pick_a_url_safe_password
|
||||||
# caddy/Caddyfile (via Caddy's {env.DOMAIN} substitution).
|
FRIGATE_FRONT_DOOR_IP=192.168.1.100
|
||||||
# No manual find-and-replace needed -- just set this.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
DOMAIN=example.com
|
|
||||||
|
|
||||||
# Pin your image versions. Bump to current stable when you upgrade --
|
FRIGATE_RTSP_USER1=admin
|
||||||
# check https://github.com/authelia/authelia/releases and
|
FRIGATE_RTSP_PASSWORD1=changeme
|
||||||
# https://github.com/crazy-max/docker-fail2ban/releases.
|
FRIGATE_BACK_DOOR_IP=192.168.1.101
|
||||||
AUTHELIA_VERSION=4.39.19
|
|
||||||
FAIL2BAN_VERSION=1.1.0-r0
|
|
||||||
|
|
||||||
# Used by both containers for log timestamps. Set to your IANA zone.
|
FRIGATE_RTSP_USER2=admin
|
||||||
TZ=America/New_York
|
FRIGATE_RTSP_PASSWORD2=changeme
|
||||||
|
FRIGATE_SQUIRREL_IP=192.168.1.102
|
||||||
|
|
||||||
|
# Future Anpviz camera with mic
|
||||||
|
# FRIGATE_RTSP_USER3=admin
|
||||||
|
# FRIGATE_RTSP_PASSWORD3=changeme
|
||||||
|
# FRIGATE_ANPVIZ_IP=192.168.1.103
|
||||||
|
|
||||||
|
# ---- MQTT broker ----
|
||||||
|
# Used by Frigate via {FRIGATE_MQTT_*} substitution
|
||||||
|
FRIGATE_MQTT_USER=frigate
|
||||||
|
FRIGATE_MQTT_PASSWORD=pick_a_strong_mqtt_password
|
||||||
|
|
||||||
|
# ---- frigate-notify ----
|
||||||
|
# Same MQTT password as above. Note double underscores between YAML levels.
|
||||||
|
FN_FRIGATE__MQTT__PASSWORD=same_value_as_FRIGATE_MQTT_PASSWORD
|
||||||
|
FN_FRIGATE__SERVER=http://frigate:5000
|
||||||
|
FN_FRIGATE__PUBLIC_URL=https://frigate.yourdomain.com
|
||||||
|
FN_ALERTS__NTFY__SERVER=https://ntfy.yourdomain.com
|
||||||
|
|||||||
+23
-17
@@ -1,23 +1,26 @@
|
|||||||
# Secrets — never commit
|
# Secrets & credentials
|
||||||
.env
|
.env
|
||||||
authelia/secrets/JWT_SECRET
|
|
||||||
authelia/secrets/SESSION_SECRET
|
|
||||||
authelia/secrets/STORAGE_ENCRYPTION_KEY
|
|
||||||
authelia/secrets/SMTP_PASSWORD
|
|
||||||
|
|
||||||
# Authelia runtime state
|
# Mosquitto runtime state
|
||||||
authelia/users_database.yml
|
mosquitto/config/passwd
|
||||||
authelia/db.sqlite3
|
mosquitto/data/*
|
||||||
authelia/db.sqlite3-*
|
mosquitto/log/*
|
||||||
authelia/authelia.log
|
!mosquitto/data/.gitkeep
|
||||||
authelia/notifications/notification.txt
|
!mosquitto/log/.gitkeep
|
||||||
|
|
||||||
# fail2ban runtime state
|
# Frigate runtime state
|
||||||
fail2ban/data/db/
|
frigate_config/frigate.db*
|
||||||
fail2ban/data/fail2ban.sqlite3
|
frigate_config/model_cache/
|
||||||
fail2ban/data/*.bak
|
frigate_config/backup.db
|
||||||
fail2ban/data/jail.d/*.bak
|
frigate_config/backup_config.yaml
|
||||||
fail2ban/data/filter.d/*.bak
|
frigate_config/*.bak
|
||||||
|
frigate_config/config.yaml-b4-*
|
||||||
|
frigate_config/*.16
|
||||||
|
frigate_config/*.old
|
||||||
|
|
||||||
|
# Frigate media (way too big for git; lives on a separate volume)
|
||||||
|
media/
|
||||||
|
frigate_media/
|
||||||
|
|
||||||
# Editor / OS junk
|
# Editor / OS junk
|
||||||
*.swp
|
*.swp
|
||||||
@@ -25,3 +28,6 @@ fail2ban/data/filter.d/*.bak
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# Archives
|
||||||
|
*.tar.gz
|
||||||
|
|||||||
@@ -1,456 +0,0 @@
|
|||||||
# HANDOFF -- script to add a site behind Authelia
|
|
||||||
|
|
||||||
This doc is for a fresh session where you'll write a script that
|
|
||||||
automates the "add a new site behind Authelia" workflow currently done
|
|
||||||
by hand. Everything below is the context you need to write it
|
|
||||||
correctly.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
One command, e.g.
|
|
||||||
|
|
||||||
```
|
|
||||||
./add-site --subdomain doorbell --upstream 192.168.1.60:5555 --case 1 --policy two_factor
|
|
||||||
```
|
|
||||||
|
|
||||||
does all of:
|
|
||||||
|
|
||||||
1. Adds the matching rule to `authelia/configuration.yml` under
|
|
||||||
`access_control.rules:`.
|
|
||||||
2. Adds (or modifies) the matching site block in the user's Caddyfile.
|
|
||||||
If a site block already exists with `basic_auth { ... }`, removes
|
|
||||||
that and inserts `import authelia` instead.
|
|
||||||
3. Validates Authelia config (`authelia validate-config`).
|
|
||||||
4. Validates Caddy config (`caddy validate`).
|
|
||||||
5. Restarts Authelia, then reloads Caddy. Order matters.
|
|
||||||
6. Optionally `curl`-tests the new URL and reports.
|
|
||||||
|
|
||||||
Idempotent: re-running with the same args is a no-op.
|
|
||||||
|
|
||||||
## Stack context (what's already running)
|
|
||||||
|
|
||||||
- **Authelia 4.39.19**, file backend, SQLite local storage, in-memory
|
|
||||||
sessions, filesystem notifier. Compose project lives at
|
|
||||||
`~/docker/authelia/`. Container name `authelia`.
|
|
||||||
- **fail2ban** as a sidecar in the same compose project. Watches
|
|
||||||
`./authelia/authelia.log` and `/var/log/caddy/access.log`.
|
|
||||||
- **Caddy** is in its own compose project at `~/docker/caddy/`
|
|
||||||
(assumption -- script should accept the path as a parameter).
|
|
||||||
Container name `caddy`. On the external docker network `caddy_net`.
|
|
||||||
- Both Caddy and Authelia are on `caddy_net`. Caddy reaches Authelia
|
|
||||||
as `authelia:9091`.
|
|
||||||
- The portal is `auth.{DOMAIN}` with `policy: bypass`.
|
|
||||||
- `default_policy: deny` -- every gated domain MUST have a rule.
|
|
||||||
- DOMAIN substitution:
|
|
||||||
- Authelia uses Go templates: `'{{ env "DOMAIN" }}'`. Requires
|
|
||||||
`X_AUTHELIA_CONFIG_FILTERS=template` env var (already set in the
|
|
||||||
docker-compose.yml).
|
|
||||||
- Caddy uses `{env.DOMAIN}`. The Caddy compose passes `DOMAIN`
|
|
||||||
through to the Caddy container.
|
|
||||||
|
|
||||||
## Files the script touches
|
|
||||||
|
|
||||||
| File | What lives there | Who edits |
|
|
||||||
|------|------------------|-----------|
|
|
||||||
| `~/docker/authelia/authelia/configuration.yml` | `access_control.rules:` list | the script |
|
|
||||||
| `<user's Caddyfile>` (path: parameter) | site blocks | the script |
|
|
||||||
| `~/docker/authelia/.env` | `DOMAIN=...`, `TZ=...`, version pins | read-only (script reads DOMAIN from here OR the environment) |
|
|
||||||
|
|
||||||
The script does NOT touch:
|
|
||||||
- `authelia/users_database.yml` (user management is separate)
|
|
||||||
- `authelia/secrets/*` (manual one-time bootstrap)
|
|
||||||
- `frigate_config/config.yml` or any other app's own config
|
|
||||||
(case 2a/2b require app-side edits the script can't safely automate
|
|
||||||
-- it should print instructions instead)
|
|
||||||
- DNS, TLS, anything outside the local Caddy + Authelia configs
|
|
||||||
|
|
||||||
## Manual workflow the script automates
|
|
||||||
|
|
||||||
For reference, here's what a human does today to add `foo.example.com`
|
|
||||||
as a case-1 site:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Edit authelia/configuration.yml -- add under access_control.rules:
|
|
||||||
# - domain: 'foo.{{ env "DOMAIN" }}'
|
|
||||||
# policy: 'two_factor'
|
|
||||||
|
|
||||||
# 2. Edit your real Caddyfile -- add a new block (or modify existing):
|
|
||||||
# foo.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.1.60:5555
|
|
||||||
# }
|
|
||||||
|
|
||||||
# 3. Validate before reloading
|
|
||||||
docker compose -f ~/docker/authelia/docker-compose.yml run --rm authelia \
|
|
||||||
authelia validate-config --config /config/configuration.yml
|
|
||||||
|
|
||||||
docker compose -f ~/docker/caddy/docker-compose.yml exec caddy \
|
|
||||||
caddy validate --config /etc/caddy/Caddyfile
|
|
||||||
|
|
||||||
# 4. Restart Authelia FIRST (so the rule is live before Caddy starts
|
|
||||||
# forwarding to it -- otherwise default_policy: deny returns 403)
|
|
||||||
docker compose -f ~/docker/authelia/docker-compose.yml restart authelia
|
|
||||||
|
|
||||||
# 5. Reload Caddy (zero-downtime)
|
|
||||||
docker compose -f ~/docker/caddy/docker-compose.yml exec caddy \
|
|
||||||
caddy reload --config /etc/caddy/Caddyfile
|
|
||||||
|
|
||||||
# 6. Test
|
|
||||||
curl -sI -o /dev/null -w "%{http_code}\n" https://foo.example.com
|
|
||||||
# expect 302 redirect to auth.example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cases the script must handle
|
|
||||||
|
|
||||||
The full case taxonomy is in `authelia/configuration.yml` and
|
|
||||||
`caddy/snippets.caddyfile`. Summary:
|
|
||||||
|
|
||||||
### Case 1 -- no app auth, Authelia is the only gate
|
|
||||||
|
|
||||||
**Authelia rule:**
|
|
||||||
```yaml
|
|
||||||
- domain: 'SUBDOMAIN.{{ env "DOMAIN" }}'
|
|
||||||
policy: 'two_factor' # or one_factor
|
|
||||||
```
|
|
||||||
|
|
||||||
**Caddy block:**
|
|
||||||
```caddyfile
|
|
||||||
SUBDOMAIN.{env.DOMAIN} {
|
|
||||||
import accesslog
|
|
||||||
import authelia
|
|
||||||
reverse_proxy UPSTREAM_IP:UPSTREAM_PORT
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Most common case. The migration path from `basic_auth` -- the script
|
|
||||||
should handle "block exists with basic_auth, swap it for import authelia".
|
|
||||||
|
|
||||||
### Case 2a -- app supports trusted-header proxy auth
|
|
||||||
|
|
||||||
Same Caddy block as case 1. Same Authelia rule. **Plus** an app-side
|
|
||||||
config change the script CANNOT safely automate (each app is different:
|
|
||||||
Frigate's `auth.enabled: False`, Grafana's `[auth.proxy]` section,
|
|
||||||
Gitea's `ENABLE_REVERSE_PROXY_AUTHENTICATION`, etc.). Script should
|
|
||||||
print app-specific instructions from a lookup table and require
|
|
||||||
`--ack-app-config-done` to proceed.
|
|
||||||
|
|
||||||
### Case 2b -- app supports OIDC
|
|
||||||
|
|
||||||
Same Caddy block as case 1. Same Authelia rule. **Plus** an
|
|
||||||
`identity_providers.oidc.clients[]` entry to add to
|
|
||||||
`configuration.yml`. Each app needs its own `client_id`,
|
|
||||||
`client_secret`, `redirect_uris`, etc.
|
|
||||||
|
|
||||||
This is more involved. **Suggested**: out of scope for v1 of the script;
|
|
||||||
print a pointer to Authelia's OIDC docs and skip.
|
|
||||||
|
|
||||||
### Case 3 -- app keeps its own auth, Authelia adds 2FA in front
|
|
||||||
|
|
||||||
Caddy block and Authelia rule are identical to case 1. The user just
|
|
||||||
keeps logging into the app after Authelia. The script doesn't need to
|
|
||||||
distinguish case 1 from case 3 mechanically -- the only difference is
|
|
||||||
the user's mental model.
|
|
||||||
|
|
||||||
### Case 4 -- no Authelia involvement
|
|
||||||
|
|
||||||
```caddyfile
|
|
||||||
SUBDOMAIN.{env.DOMAIN} {
|
|
||||||
import accesslog # NO import authelia
|
|
||||||
reverse_proxy UPSTREAM_IP:UPSTREAM_PORT
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**No Authelia rule.** The script's job is just the Caddy edit + reload.
|
|
||||||
Useful for things like Plex/Emby where Authelia's redirect breaks
|
|
||||||
native clients.
|
|
||||||
|
|
||||||
## Validation hooks the script must run
|
|
||||||
|
|
||||||
In order, before any reload:
|
|
||||||
|
|
||||||
1. **Authelia config:**
|
|
||||||
```bash
|
|
||||||
docker compose -f ~/docker/authelia/docker-compose.yml run --rm authelia \
|
|
||||||
authelia validate-config --config /config/configuration.yml
|
|
||||||
```
|
|
||||||
Exit 0 = good. Anything else = abort, restore the file from backup.
|
|
||||||
|
|
||||||
2. **Caddy config:**
|
|
||||||
```bash
|
|
||||||
docker compose -f ~/docker/caddy/docker-compose.yml exec caddy \
|
|
||||||
caddy validate --config /etc/caddy/Caddyfile
|
|
||||||
```
|
|
||||||
Exit 0 = good. Anything else = abort, restore Caddyfile from backup.
|
|
||||||
|
|
||||||
After reload:
|
|
||||||
|
|
||||||
3. **HTTP probe:**
|
|
||||||
```bash
|
|
||||||
curl -sI -o /dev/null -w "%{http_code}\n" https://SUBDOMAIN.DOMAIN
|
|
||||||
```
|
|
||||||
- Case 1 / 2a / 2b / 3: expect `302` (redirect to Authelia).
|
|
||||||
- Case 4: expect `200` or whatever the upstream returns.
|
|
||||||
- `403` means the Caddy block has `import authelia` but the Authelia
|
|
||||||
rule isn't in place (or wasn't picked up). Most common script bug.
|
|
||||||
|
|
||||||
## Reload semantics
|
|
||||||
|
|
||||||
**Order**: Authelia restart, THEN Caddy reload. Reverse order is briefly
|
|
||||||
broken: Caddy starts forwarding to Authelia for a domain Authelia
|
|
||||||
doesn't yet have a rule for, and `default_policy: deny` returns 403 to
|
|
||||||
the user.
|
|
||||||
|
|
||||||
**Authelia restart**: full container restart. ~3-5 second outage on
|
|
||||||
auth.{DOMAIN}. Acceptable for household use; if you want zero-downtime
|
|
||||||
later, Authelia supports config reload via SIGHUP -- not used here.
|
|
||||||
|
|
||||||
**Caddy reload**: `caddy reload` is genuinely zero-downtime. It loads
|
|
||||||
the new config, validates, and atomically swaps. If validation fails
|
|
||||||
the old config keeps running.
|
|
||||||
|
|
||||||
**Rollback**: if anything fails, the script should revert the file
|
|
||||||
edits from a `.bak` it took before mutating. Easiest pattern:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp authelia/configuration.yml authelia/configuration.yml.bak
|
|
||||||
cp $CADDYFILE_PATH $CADDYFILE_PATH.bak
|
|
||||||
# ... edits ...
|
|
||||||
# if any validate fails:
|
|
||||||
mv authelia/configuration.yml.bak authelia/configuration.yml
|
|
||||||
mv $CADDYFILE_PATH.bak $CADDYFILE_PATH
|
|
||||||
```
|
|
||||||
|
|
||||||
## Idempotency
|
|
||||||
|
|
||||||
The script must detect and short-circuit when the desired end state
|
|
||||||
already exists:
|
|
||||||
|
|
||||||
- **Authelia rule already present**: parse `access_control.rules:`, look
|
|
||||||
for an entry with matching `domain:`. If found and policy matches, skip
|
|
||||||
the YAML edit. If found and policy differs, prompt or fail (don't
|
|
||||||
silently overwrite).
|
|
||||||
- **Caddy block already present with `import authelia`**: parse the
|
|
||||||
Caddyfile, look for `SUBDOMAIN.{env.DOMAIN} {` block. If found and
|
|
||||||
it has `import authelia`, skip the Caddy edit.
|
|
||||||
- **Caddy block exists with `basic_auth`**: this is the migration case.
|
|
||||||
Remove the `basic_auth { ... }` lines, add `import authelia` if
|
|
||||||
missing. Preserve everything else (transport, header_up, etc).
|
|
||||||
- **Caddy block exists without auth at all**: just add `import authelia`.
|
|
||||||
|
|
||||||
After all checks: if neither file actually changed, skip both reloads
|
|
||||||
(important -- restarting Authelia for a no-op kicks every active
|
|
||||||
session).
|
|
||||||
|
|
||||||
## Failure modes the script must handle
|
|
||||||
|
|
||||||
- `DOMAIN` env var not set / `.env` not present -> abort early with
|
|
||||||
clear error.
|
|
||||||
- `caddy_net` docker network doesn't exist -> abort.
|
|
||||||
- Authelia container not running -> can still edit config and validate;
|
|
||||||
reload step needs to be skipped or attempted with helpful error.
|
|
||||||
- Caddy container not running -> same.
|
|
||||||
- Caddyfile path doesn't exist or isn't writable -> abort.
|
|
||||||
- The Caddyfile doesn't have `(authelia)` and `(accesslog)` snippets
|
|
||||||
defined -> the script could either inject them at the top of the file
|
|
||||||
(fragile) or refuse and tell the user to run a one-time setup step
|
|
||||||
first. **Recommended**: refuse, with a clear "run `./bootstrap-caddy`
|
|
||||||
first" message.
|
|
||||||
- Subdomain conflicts with an existing block that's NOT just a
|
|
||||||
`basic_auth` migration target (e.g. an entirely different upstream)
|
|
||||||
-> prompt, don't auto-overwrite.
|
|
||||||
- A site block exists with `basic_auth` AND something else complicated
|
|
||||||
(custom matchers, multiple `handle` blocks) -> migration is hard.
|
|
||||||
Recommended: detect the simple case (single `basic_auth { ... }`
|
|
||||||
inside the block) and refuse the complex case.
|
|
||||||
- `validate-config` or `caddy validate` fails -> rollback both files,
|
|
||||||
report the validator's stderr, exit non-zero.
|
|
||||||
- HTTP probe fails post-reload -> log the symptom but don't auto-revert;
|
|
||||||
user may have DNS not pointing yet, etc.
|
|
||||||
|
|
||||||
## YAML editing -- preserve comments
|
|
||||||
|
|
||||||
`authelia/configuration.yml` has substantial comments (the
|
|
||||||
"ALSO PASTE INTO CADDYFILE" blocks, case explanations). A naive YAML
|
|
||||||
round-trip will eat them. Use a comment-preserving library:
|
|
||||||
|
|
||||||
- **Python**: `ruamel.yaml` with `YAML(typ='rt')` (round-trip mode).
|
|
||||||
- **Go**: `gopkg.in/yaml.v3` is comment-aware.
|
|
||||||
- **`yq`** (the Go-based one from mikefarah): preserves comments
|
|
||||||
reasonably well for simple ops. Adding a list item:
|
|
||||||
```bash
|
|
||||||
yq -i '.access_control.rules += [{"domain": "foo.{{ env \"DOMAIN\" }}", "policy": "two_factor"}]' \
|
|
||||||
authelia/configuration.yml
|
|
||||||
```
|
|
||||||
Note the escaping pain with `{{ env "DOMAIN" }}`. Test before
|
|
||||||
committing.
|
|
||||||
|
|
||||||
The script should anchor inserts at a stable location. The least-bad
|
|
||||||
anchor is the END of `access_control.rules:` -- always append, never
|
|
||||||
splice in the middle.
|
|
||||||
|
|
||||||
## Caddyfile editing -- there is no good parser
|
|
||||||
|
|
||||||
Caddyfile has its own grammar; standard YAML/JSON tools won't touch it.
|
|
||||||
Options, in order of pragmatism:
|
|
||||||
|
|
||||||
1. **Text-based pattern matching** (recommended for v1). Anchors:
|
|
||||||
- Find `^SUBDOMAIN\.\{env\.DOMAIN\} \{$` to detect existing block.
|
|
||||||
- For the basic_auth migration: use a small state machine to find
|
|
||||||
`basic_auth {` ... `}` inside the matched block and delete those
|
|
||||||
lines, then ensure `import authelia` and `import accesslog` lines
|
|
||||||
exist.
|
|
||||||
- For new-block insertion: append at end of file, separated by a
|
|
||||||
blank line.
|
|
||||||
|
|
||||||
2. **`caddy adapt`**: converts Caddyfile to JSON. You could edit the
|
|
||||||
JSON, then... there's no Caddyfile emitter. Adapt is one-way. Skip.
|
|
||||||
|
|
||||||
3. **`caddy fmt`**: normalizes whitespace in a Caddyfile, doesn't
|
|
||||||
semantically edit. Useful AFTER your edits to clean up.
|
|
||||||
|
|
||||||
The text-based approach is fragile for arbitrary Caddyfiles but
|
|
||||||
predictable for the conventions this stack uses (one site block per
|
|
||||||
subdomain, snippets imported at top, no exotic matchers in gated
|
|
||||||
sites).
|
|
||||||
|
|
||||||
## Suggested architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
add-site
|
|
||||||
├── lib/
|
|
||||||
│ ├── env.sh # find DOMAIN, container names, Caddyfile path
|
|
||||||
│ ├── yaml_edit.sh # ruamel.yaml or yq wrapper for access_control
|
|
||||||
│ ├── caddyfile_edit.sh # awk/sed-based site-block patcher
|
|
||||||
│ ├── validate.sh # authelia + caddy validators
|
|
||||||
│ └── reload.sh # restart authelia + reload caddy in order
|
|
||||||
├── cases/
|
|
||||||
│ ├── case-1.sh # no-app-auth recipe
|
|
||||||
│ ├── case-2a.md # printable app-side instructions table
|
|
||||||
│ ├── case-2b.md # OIDC out-of-scope notice + pointer
|
|
||||||
│ └── case-4.sh # no-Authelia recipe
|
|
||||||
├── add-site # main entrypoint
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
Or in Python with `ruamel.yaml` and a small Caddyfile patcher class.
|
|
||||||
Either is fine; the bash version has fewer install steps for an
|
|
||||||
end-user.
|
|
||||||
|
|
||||||
## Concrete test cases the script must pass
|
|
||||||
|
|
||||||
Use these as fixtures.
|
|
||||||
|
|
||||||
### Test 1: fresh case-1 add
|
|
||||||
|
|
||||||
Pre-state:
|
|
||||||
- `authelia/configuration.yml` has only the `auth.{DOMAIN}` bypass rule.
|
|
||||||
- Caddyfile has `(authelia)` and `(accesslog)` snippets defined, no
|
|
||||||
`foo.{env.DOMAIN}` block.
|
|
||||||
|
|
||||||
Invocation:
|
|
||||||
```
|
|
||||||
./add-site --subdomain foo --upstream 192.168.1.60:5555 --case 1
|
|
||||||
```
|
|
||||||
|
|
||||||
Post-state:
|
|
||||||
- `access_control.rules:` has new entry for `foo.{{ env "DOMAIN" }}`.
|
|
||||||
- Caddyfile has new `foo.{env.DOMAIN} { ... }` block with `import
|
|
||||||
authelia` and `import accesslog`.
|
|
||||||
- `validate-config` and `caddy validate` both pass.
|
|
||||||
- `curl -sI https://foo.example.com` returns 302.
|
|
||||||
|
|
||||||
### Test 2: idempotent re-run
|
|
||||||
|
|
||||||
Run Test 1's invocation twice. Second run: no file edits, no reloads,
|
|
||||||
exit 0 with "already configured" message.
|
|
||||||
|
|
||||||
### Test 3: basic_auth migration
|
|
||||||
|
|
||||||
Pre-state:
|
|
||||||
- Caddyfile has `foo.{env.DOMAIN} { basic_auth { user $2a$... }
|
|
||||||
reverse_proxy ... }`.
|
|
||||||
|
|
||||||
Invocation:
|
|
||||||
```
|
|
||||||
./add-site --subdomain foo --case 1 --migrate-basic-auth
|
|
||||||
```
|
|
||||||
|
|
||||||
Post-state:
|
|
||||||
- The `basic_auth { ... }` lines are gone.
|
|
||||||
- `import authelia` and `import accesslog` are present.
|
|
||||||
- `reverse_proxy` line is unchanged.
|
|
||||||
- Authelia rule added.
|
|
||||||
|
|
||||||
### Test 4: validation failure rollback
|
|
||||||
|
|
||||||
Pre-state: introduce a typo by hand into the YAML insert template
|
|
||||||
(e.g. `polciy:` instead of `policy:`). Simulate by mocking the
|
|
||||||
template.
|
|
||||||
|
|
||||||
Expected: `validate-config` fails, both files restored from `.bak`,
|
|
||||||
non-zero exit, no reload attempted.
|
|
||||||
|
|
||||||
### Test 5: case-4 (no Authelia)
|
|
||||||
|
|
||||||
Invocation:
|
|
||||||
```
|
|
||||||
./add-site --subdomain plex --upstream 192.168.1.5:32400 --case 4
|
|
||||||
```
|
|
||||||
|
|
||||||
Post-state:
|
|
||||||
- Caddyfile has new `plex.{env.DOMAIN}` block with `import accesslog`,
|
|
||||||
NO `import authelia`.
|
|
||||||
- `access_control.rules:` is UNCHANGED.
|
|
||||||
- Caddy reloads, Authelia is NOT restarted.
|
|
||||||
|
|
||||||
## Out of scope (tell the user, don't try to automate)
|
|
||||||
|
|
||||||
- DNS A-record creation. Caddy will fail to issue a cert for a domain
|
|
||||||
that doesn't resolve. Print a "make sure DNS is pointing at this
|
|
||||||
host" reminder when the script starts.
|
|
||||||
- TLS / Let's Encrypt failures. Caddy auto-provisions; if it fails,
|
|
||||||
it's usually DNS or rate-limit. The script should not try to debug.
|
|
||||||
- App-side proxy-auth config (case 2a). Each app is different. Print
|
|
||||||
the lookup-table snippet from `caddy/snippets.caddyfile` for that
|
|
||||||
app and require `--ack-app-config-done` before running.
|
|
||||||
- OIDC client setup (case 2b). Big enough that it deserves its own
|
|
||||||
tool. Out of scope.
|
|
||||||
- User management (`users_database.yml`).
|
|
||||||
- Secret rotation (`authelia/secrets/*`).
|
|
||||||
- TOTP enrollment.
|
|
||||||
|
|
||||||
## Quick reference: existing files
|
|
||||||
|
|
||||||
If you want to read what's there to understand the conventions:
|
|
||||||
|
|
||||||
- `docker-compose.yml` -- the auth stack compose, including the
|
|
||||||
`X_AUTHELIA_CONFIG_FILTERS=template` and `DOMAIN=${DOMAIN}` env vars.
|
|
||||||
- `authelia/configuration.yml` -- reference for the rule format,
|
|
||||||
comment style, and the "ALSO PASTE INTO CADDYFILE" blocks under
|
|
||||||
each case in `access_control.rules:`.
|
|
||||||
- `caddy/snippets.caddyfile` -- canonical examples of Caddy site
|
|
||||||
blocks for every case, including app-specific notes the case-2a
|
|
||||||
table can be extracted from.
|
|
||||||
- `.env.example` -- the shape of `.env` (DOMAIN, TZ, version pins).
|
|
||||||
- `README.md` -- the comprehensive bootstrap walkthrough; the
|
|
||||||
"Adding a new protected site" section is what the script
|
|
||||||
automates.
|
|
||||||
|
|
||||||
## When in doubt
|
|
||||||
|
|
||||||
- For YAML edits, dry-run with
|
|
||||||
`docker compose run --rm authelia authelia config template --config
|
|
||||||
/config/configuration.yml` -- prints the rendered config so you can
|
|
||||||
see exactly what Authelia will see.
|
|
||||||
- For Caddyfile edits, `caddy fmt --overwrite Caddyfile` normalizes
|
|
||||||
whitespace and `caddy validate` catches syntax errors. Run BOTH
|
|
||||||
before any reload.
|
|
||||||
- The single most useful debug command for "why is my site getting
|
|
||||||
403":
|
|
||||||
```bash
|
|
||||||
docker compose -f ~/docker/authelia/docker-compose.yml exec authelia \
|
|
||||||
tail -f /config/authelia.log
|
|
||||||
```
|
|
||||||
Then hit the URL. The log line tells you exactly which rule (or
|
|
||||||
default_policy) made the call.
|
|
||||||
@@ -1,770 +1,273 @@
|
|||||||
# Authelia + fail2ban
|
# Home camera stack
|
||||||
|
|
||||||
Self-hosted authentication portal (Authelia) with an IP-banning sidecar
|
Frigate NVR + Mosquitto MQTT + frigate-notify -> ntfy push notifications,
|
||||||
(fail2ban). Sits next to your dockerized Caddy and gates every public
|
with face recognition and license plate recognition on the Frigate side.
|
||||||
subdomain behind a single sign-on portal at `auth.example.com`.
|
Includes a planned Pi Zero W "doorbell speaker" stack (push-to-talk web
|
||||||
|
page fronted by Caddy) that's wired but not yet deployed.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
Internet
|
Cameras (RTSP) Phone / browser
|
||||||
|
|
| |
|
||||||
v
|
v v
|
||||||
+-------+ caddy_net (docker) +--------------------+
|
+------------+ MQTT events +----------------+ +-------------+
|
||||||
| Caddy |--- forward_auth -------------->| Authelia |
|
| Frigate | <-------------> | Mosquitto | | Pi |
|
||||||
+---+---+ | /api/authz/... |
|
| (NVR) | | broker | | (planned) |
|
||||||
| reverse_proxy +--------+-----------+
|
+-----+------+ +-------+--------+ | speaker |
|
||||||
| |
|
| WebRTC / MSE | +------+------+
|
||||||
v v
|
v v ^
|
||||||
Frigate (LAN), NAS, Pi, etc. ./authelia/db.sqlite3
|
+------------+ +------------------+ |
|
||||||
./authelia/authelia.log
|
| Caddy |<--HTTPS--------| frigate-notify | |
|
||||||
^
|
| proxy | | (event consumer) | |
|
||||||
| tail
|
+------------+ +--------+---------+ |
|
||||||
+------+--------+
|
| | |
|
||||||
| fail2ban | host net
|
| cam.yourdomain.com ntfy push |
|
||||||
| DOCKER-USER | + iptables
|
| doorbell.yourdomain.com |
|
||||||
+---------------+
|
+-------HTTPS---------------> PTT button ------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
- One docker-compose file, two services, one external network (`caddy_net`).
|
## What's deployed vs planned
|
||||||
- File-backed users database, SQLite storage, no Redis, no external DB.
|
|
||||||
- Filesystem notifier for password reset (swap to SMTP later, one block change).
|
| Component | Status |
|
||||||
- fail2ban bans via the `DOCKER-USER` iptables chain: drops happen at the host
|
|---|---|
|
||||||
edge before traffic reaches any docker-published port.
|
| Frigate 0.17 | Deployed |
|
||||||
- Caddy is not in this stack. Copy blocks from `caddy/snippets.caddyfile` into your Caddy setup.
|
| Mosquitto MQTT broker | Deployed |
|
||||||
|
| frigate-notify -> ntfy | Deployed |
|
||||||
|
| Face recognition | Configured, needs training |
|
||||||
|
| License plate recognition | Configured |
|
||||||
|
| Caddy reverse proxy | Whatever your existing Caddy does |
|
||||||
|
| Pi Zero W doorbell speaker | Planned, not deployed yet |
|
||||||
|
|
||||||
## Repo layout
|
## Repo layout
|
||||||
|
|
||||||
```
|
```
|
||||||
authelia-stack/
|
home-cameras/
|
||||||
├── docker-compose.yml
|
|-- docker-compose.yml # frigate + mosquitto + frigate-notify
|
||||||
├── .env.example # copy to .env
|
|-- .env.example # template -- copy to .env, fill in
|
||||||
├── .gitignore
|
|-- .gitignore
|
||||||
├── README.md
|
|-- README.md # this file
|
||||||
│
|
|
|
||||||
├── authelia/
|
|-- frigate_config/
|
||||||
│ ├── configuration.yml # main config -- edit your domain here
|
| |-- config.yml # production: main stream for detect
|
||||||
│ ├── users_database.yml.example # copy to users_database.yml (gitignored)
|
| `-- alternatives/
|
||||||
│ ├── secrets/ # gitignored; secret files mounted as /secrets
|
| `-- config-simple.yml # lower-CPU fallback: sub-stream detect
|
||||||
│ └── notifications/ # filesystem notifier writes here (gitignored)
|
|
|
||||||
│
|
|-- frigate-notify/
|
||||||
├── fail2ban/
|
| `-- config.yml # MQTT in, ntfy out, face-aware templates
|
||||||
│ └── data/ # mounted as /data in the container
|
|
|
||||||
│ ├── filter.d/
|
|-- mosquitto/
|
||||||
│ │ ├── authelia.local # matches Authelia text-log auth failures
|
| |-- config/
|
||||||
│ │ └── caddy-4xx.local # matches Caddy JSON 4xx responses
|
| | `-- mosquitto.conf # broker config (allow_anonymous false)
|
||||||
│ └── jail.d/
|
| |-- data/.gitkeep
|
||||||
│ ├── authelia.local # 3 fails/10 min -> 1 hr IP ban
|
| `-- log/.gitkeep
|
||||||
│ └── caddy.local # 30 fails/2 min -> 30 min IP ban
|
|
|
||||||
│
|
|-- caddy/
|
||||||
└── caddy/
|
| `-- Caddyfile # reverse proxy for both subdomains
|
||||||
└── snippets.caddyfile # per-service snippets to add to your existing Caddyfile
|
|
|
||||||
|
`-- pi/ # runs on the Pi, NOT on the Frigate host
|
||||||
|
|-- README.md # Pi-specific setup
|
||||||
|
|-- server.py # Flask + WebSocket PTT receiver
|
||||||
|
|-- doorbell.service # systemd unit
|
||||||
|
`-- install.sh # one-shot installer
|
||||||
```
|
```
|
||||||
|
|
||||||
## Prerequisites
|
## First-run on the Frigate host
|
||||||
|
|
||||||
- Docker + docker compose v2.
|
Prerequisites:
|
||||||
- Caddy already running, in Docker, joined to an external network named
|
- Docker + docker compose v2
|
||||||
`caddy_net`. If your network is named differently, replace `caddy_net`
|
- DNS records for any subdomains you intend to use, pointing at your Caddy
|
||||||
everywhere in this repo.
|
host
|
||||||
- Caddy v2.5.1 or newer (for the `forward_auth` directive; tested on v2.11.2).
|
- An existing Caddy instance (separate from this stack) handling TLS at the
|
||||||
- A root domain you control. DNS A records for `auth.<root>` and every
|
edge, OR adapt for whatever reverse proxy you use
|
||||||
protected subdomain must point at the Caddy host's public IP.
|
- Coral USB stick plugged in (or adjust `detectors:` for a different accel)
|
||||||
|
- A media disk mounted on the host; update the `/media/frigate` path in
|
||||||
|
`docker-compose.yml`
|
||||||
|
|
||||||
## Which sites go behind Authelia?
|
Steps:
|
||||||
|
|
||||||
There are four ways a site can relate to Authelia. Pick one per site.
|
|
||||||
|
|
||||||
| Case | App has built-in auth? | Supports proxy auth? | What to do |
|
|
||||||
|------|------------------------|----------------------|------------|
|
|
||||||
| **1** | No | n/a | `import authelia` in Caddy + rule in Authelia. Authelia is the only login. |
|
|
||||||
| **2a** | Yes | Yes (Remote-User header) | `import authelia` + disable app's own login form. Single login via headers. |
|
|
||||||
| **2b** | Yes | Yes (OIDC) | `import authelia` + configure Authelia as OIDC provider in the app. Single login via token exchange. |
|
|
||||||
| **3** | Yes | No | `import authelia` in Caddy. App auth is unchanged. User logs into Authelia then the app. Two logins. |
|
|
||||||
| **4** | Yes | — | Plain `reverse_proxy`. No `import authelia`, no rule. App handles auth. |
|
|
||||||
|
|
||||||
Concretely:
|
|
||||||
|
|
||||||
- **`doorbell.example.com`** (Pi PTT page) -- **case 1**. No app auth at all.
|
|
||||||
Authelia is the only gate. Use `two_factor` -- this URL controls a speaker.
|
|
||||||
- **`cam.example.com`** (Frigate UI) -- **case 2a**. Frigate 0.14+ supports
|
|
||||||
proxy auth via `Remote-User` header. Disable Frigate's login form and let
|
|
||||||
Authelia drive both the access gate and the role mapping (admin vs. viewer).
|
|
||||||
- **`books.example.com`** (Audiobookshelf) -- **case 2b**. App redirects to
|
|
||||||
Authelia, Authelia issues a JWT token, app accepts it. No password set in
|
|
||||||
the app itself.
|
|
||||||
- **Router admin / NAS UI** -- **case 3** if you want a 2FA gate in front,
|
|
||||||
**case 4** if you just leave it to the app.
|
|
||||||
|
|
||||||
### What is OIDC?
|
|
||||||
|
|
||||||
OpenID Connect (OIDC) is an identity protocol layered on top of OAuth 2.0.
|
|
||||||
The short version: instead of an app checking your password itself, it
|
|
||||||
redirects you to Authelia, Authelia authenticates you and issues a signed
|
|
||||||
token (JWT), and the app trusts that token. The app never handles your
|
|
||||||
password — it only ever sees the token.
|
|
||||||
|
|
||||||
Authelia becomes the **identity provider** (IdP). Apps like Audiobookshelf,
|
|
||||||
Immich, Jellyfin, and Mealie become **relying parties** — they trust Authelia's
|
|
||||||
tokens and use them to identify users.
|
|
||||||
|
|
||||||
The practical difference from proxy-header auth (case 2a):
|
|
||||||
- **Headers**: Caddy adds `Remote-User` to every request and the app reads it.
|
|
||||||
Works silently. Requires the app to support header-based auth.
|
|
||||||
- **OIDC**: The browser does a full redirect dance (app → Authelia → app).
|
|
||||||
Users see the Authelia login page. Requires the app to support OIDC/OAuth.
|
|
||||||
|
|
||||||
Both result in the same thing: one Authelia credential covers the app.
|
|
||||||
|
|
||||||
OIDC requires additional setup in `authelia/configuration.yml` —
|
|
||||||
an `identity_providers.oidc` block with a client entry per app, each with
|
|
||||||
its own `client_id` and `client_secret`. See Authelia's OIDC docs for the
|
|
||||||
full config. The Caddy side is identical to case 2a: `import authelia`.
|
|
||||||
|
|
||||||
Default policy in `configuration.yml` is `deny`, so a domain with no rule
|
|
||||||
AND no `import authelia` in Caddy never reaches Authelia at all.
|
|
||||||
|
|
||||||
### Should you use Authelia at all?
|
|
||||||
|
|
||||||
A password manager with per-service credentials and per-service TOTP is a
|
|
||||||
solid security posture. Authelia improves on it in specific situations:
|
|
||||||
|
|
||||||
| Situation | Password manager alone | Authelia |
|
|
||||||
|-----------|----------------------|----------|
|
|
||||||
| App has **no auth at all** (Homer, Dozzle, doorbell page) | Can't help | Gates it with 2FA, zero app changes |
|
|
||||||
| App has auth but **no native TOTP** (Uptime Kuma, Syncthing, phpIPAM) | Password-only | Adds 2FA in front for free |
|
|
||||||
| Multiple people need access | Change credentials in N places | Disable one account in Authelia |
|
|
||||||
| True SSO -- log in once, reach 10 services | Still authenticates 10× (autofilled) | One session covers all gated services |
|
|
||||||
| Consistent rate-limiting / brute-force protection | Wildly varies per app | fail2ban + regulation applied uniformly |
|
|
||||||
|
|
||||||
**Where Authelia is marginal for a single user:**
|
|
||||||
- All your services already have native TOTP support → Authelia adds mostly
|
|
||||||
friction. You're right that 2FA doesn't require Authelia: Vaultwarden,
|
|
||||||
Nextcloud, Grafana, and Gitea all support TOTP natively. If you've already
|
|
||||||
set that up in a manager like Bitwarden, Authelia's 2FA argument is weaker.
|
|
||||||
- The Authelia password itself becomes keys-to-the-kingdom for everything
|
|
||||||
gated behind it, which is why the TOTP requirement on Authelia matters more
|
|
||||||
than on any individual service.
|
|
||||||
|
|
||||||
**The real sweet spot:**
|
|
||||||
- You have services with zero auth (case 1: Homer, Prometheus, NUT web UI,
|
|
||||||
Gatus) -- something has to gate them.
|
|
||||||
- You have services with auth but no TOTP -- Authelia gives them 2FA without
|
|
||||||
touching the app at all.
|
|
||||||
- You manage access for more than one person.
|
|
||||||
|
|
||||||
### How to tell if an app supports proxy auth (case 2)
|
|
||||||
|
|
||||||
Look for any of these in the app's docs: "Remote-User header", "trusted
|
|
||||||
upstream", "trusted proxies", "header-based auth", "SSO via reverse proxy".
|
|
||||||
|
|
||||||
| App | Case | Notes |
|
|
||||||
|-----|------|-------|
|
|
||||||
| **Proxy-header auth (case 2a)** | | |
|
|
||||||
| Frigate 0.14+ | 2a | `auth.enabled: False` + `proxy:` block in config.yml |
|
|
||||||
| Grafana | 2a | `[auth.proxy]` in grafana.ini; `GF_AUTH_PROXY_ENABLED=true` |
|
|
||||||
| Gitea / Forgejo | 2a | `ENABLE_REVERSE_PROXY_AUTHENTICATION=true` in app.ini |
|
|
||||||
| Nextcloud | 2a | `trusted_proxies` + `user_external` app + HTTP header auth |
|
|
||||||
| Paperless-ngx | 2a | `PAPERLESS_ENABLE_HTTP_REMOTE_USER=true` |
|
|
||||||
| Miniflux | 2a | `AUTH_PROXY_HEADER=Remote-User` env var |
|
|
||||||
| Home Assistant | 2a | `trusted_networks` auth provider + header forwarding |
|
|
||||||
| BookStack | 2a | `AUTH_METHOD=http` + `HTTP_AUTH_HEADER=Remote-User` in .env |
|
|
||||||
| **OIDC auth (case 2b)** | | |
|
|
||||||
| Audiobookshelf | 2b | Native OIDC; configure in Settings > Authentication |
|
|
||||||
| Jellyfin | 2b | Requires `Jellyfin.Plugin.SSO` from Plugin Catalogue |
|
|
||||||
| Immich | 2b | No header auth; OIDC only. Admin > OAuth settings |
|
|
||||||
| Mealie | 2b | OIDC supported; or use case 3 as a simpler gate |
|
|
||||||
| Portainer | 2b | OIDC in Settings > Authentication; or case 3 as simple gate |
|
|
||||||
| **No built-in auth (case 1)** | | |
|
|
||||||
| Homer / Heimdall | 1 | No auth at all -- Authelia is the only gate |
|
|
||||||
| Dozzle | 1 | No auth by default -- Authelia is the only gate |
|
|
||||||
| Prometheus | 1 | No auth built in; always gate, metrics expose internals |
|
|
||||||
| Alertmanager | 1 | No auth built in |
|
|
||||||
| Gatus | 1 | Status page; optional built-in OIDC but simpler to gate here |
|
|
||||||
| WatchYourLAN | 1 | Network ARP scanner, no built-in auth |
|
|
||||||
| NUT web UI | 1 | NUT daemon has no web UI; frontend web apps vary -- most have no auth |
|
|
||||||
| **App keeps own auth (case 3)** | | |
|
|
||||||
| Uptime Kuma | 3 | No proxy auth, no native TOTP -- Authelia is the only way to add 2FA |
|
|
||||||
| qBittorrent | 3 | Web UI has own auth; no proxy headers |
|
|
||||||
| Plex | 4 ⚠ | Do NOT use `import authelia` -- native clients go through Caddy but cannot complete browser-redirect auth; use Case 4, Plex handles its own auth |
|
|
||||||
| Emby | 4 ⚠ | Same as Plex |
|
|
||||||
| Gotify | 3 | Notification server; own auth |
|
|
||||||
| ntfy | 3 | Notification server; token-based auth |
|
|
||||||
| wg-easy | 3 | WireGuard web UI; WireGuard clients bypass Caddy (UDP 51820) |
|
|
||||||
| Umami | 3 | Analytics; own auth |
|
|
||||||
| phpIPAM | 3 | IP address management; own auth |
|
|
||||||
| Checkmk | 3 | Monitoring; own auth (LDAP in enterprise edition) |
|
|
||||||
| Snipe-IT | 3 | Asset management; own auth |
|
|
||||||
| Zammad | 3 | Help desk / ticketing; own auth |
|
|
||||||
| Lubelog | 3 | Vehicle maintenance; own auth |
|
|
||||||
| UniFi | 3 | Network controller; own auth |
|
|
||||||
| MeshCentral | 3 | Remote management; own auth; OIDC in enterprise builds |
|
|
||||||
| Vaultwarden | 3 or 4 | Strong native auth + TOTP; many skip Authelia here entirely |
|
|
||||||
| Router / NAS admin | 3 or 4 | Depends on firmware; case 4 is usually fine |
|
|
||||||
| **App handles own auth (case 4)** | | |
|
|
||||||
| Syncthing | 4 | Decent native auth; proxy auth not supported |
|
|
||||||
|
|
||||||
## Getting git and authenticating to GitHub
|
|
||||||
|
|
||||||
### Set your preferred editor first
|
|
||||||
|
|
||||||
The setup steps below use `$EDITOR` to open config files. It's a standard
|
|
||||||
shell variable that points to whichever editor you like. If it isn't set,
|
|
||||||
running `$EDITOR somefile` fails because the shell tries to execute an empty
|
|
||||||
string.
|
|
||||||
|
|
||||||
Check whether it's already set:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
echo $EDITOR # blank = not set
|
git clone https://github.com/YOU/home-cameras.git
|
||||||
```
|
cd home-cameras
|
||||||
|
|
||||||
Set it for the current session, or make it permanent by adding the export
|
# 1) Configure secrets
|
||||||
to `~/.bashrc` (bash) or `~/.zshrc` (zsh):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pick one -- whatever you actually have installed:
|
|
||||||
export EDITOR=nano
|
|
||||||
export EDITOR=vim
|
|
||||||
export EDITOR=micro
|
|
||||||
export EDITOR="code --wait" # VS Code (the --wait keeps the terminal paused until you close the file)
|
|
||||||
|
|
||||||
# Make it permanent:
|
|
||||||
echo 'export EDITOR=nano' >> ~/.bashrc && source ~/.bashrc
|
|
||||||
# or for zsh:
|
|
||||||
echo 'export EDITOR=nano' >> ~/.zshrc && source ~/.zshrc
|
|
||||||
```
|
|
||||||
|
|
||||||
Once set, `$EDITOR somefile` opens that file in your chosen editor every time.
|
|
||||||
|
|
||||||
### Install git
|
|
||||||
|
|
||||||
If git isn't installed on the server yet:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Debian / Ubuntu / Raspberry Pi OS
|
|
||||||
sudo apt update && sudo apt install -y git
|
|
||||||
|
|
||||||
# Fedora / RHEL / Rocky / AlmaLinux
|
|
||||||
sudo dnf install -y git
|
|
||||||
```
|
|
||||||
|
|
||||||
The easiest way to authenticate is the **GitHub CLI** (`gh`). Install it by
|
|
||||||
adding GitHub's official apt repository (their Linux equivalent of a Homebrew
|
|
||||||
tap), then run `gh auth login` to authenticate interactively:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Add the GitHub CLI apt repository
|
|
||||||
sudo apt install -y curl
|
|
||||||
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
|
||||||
| sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
|
||||||
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
|
|
||||||
echo "deb [arch=$(dpkg --print-architecture) \
|
|
||||||
signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] \
|
|
||||||
https://cli.github.com/packages stable main" \
|
|
||||||
| sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
|
||||||
sudo apt update && sudo apt install -y gh
|
|
||||||
|
|
||||||
# Authenticate -- follow the prompts (browser or paste a token)
|
|
||||||
gh auth login
|
|
||||||
```
|
|
||||||
|
|
||||||
When prompted: choose **GitHub.com**, **HTTPS**, and **Login with a web
|
|
||||||
browser** (or paste a personal access token if the server has no browser).
|
|
||||||
Once done, `gh` passes credentials to `git` automatically -- no extra config
|
|
||||||
needed.
|
|
||||||
|
|
||||||
On macOS the whole thing is two lines:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew install git gh
|
|
||||||
gh auth login
|
|
||||||
```
|
|
||||||
|
|
||||||
## First-run setup
|
|
||||||
|
|
||||||
### 0. Clone the repo
|
|
||||||
|
|
||||||
The Frigate stack lives on `main`. This auth stack is on the `authelia` branch -- clone it separately into its own directory.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone -b authelia \
|
|
||||||
https://github.com/outis1one/frigate_w_audio.git \
|
|
||||||
~/docker/authelia
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/docker/authelia
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1. Create the external Docker network
|
|
||||||
|
|
||||||
Caddy must join this same network so it can reach Authelia by container name. Skip if `caddy_net` already exists.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker network create caddy_net 2>/dev/null || true
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Generate secrets
|
|
||||||
|
|
||||||
Authelia loads these from files so they never appear in `docker inspect` or process listings.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p authelia/secrets
|
|
||||||
openssl rand -hex 32 > authelia/secrets/JWT_SECRET
|
|
||||||
openssl rand -hex 32 > authelia/secrets/SESSION_SECRET
|
|
||||||
openssl rand -hex 32 > authelia/secrets/STORAGE_ENCRYPTION_KEY
|
|
||||||
chmod 600 authelia/secrets/*
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Set your domain
|
|
||||||
|
|
||||||
`DOMAIN` is the only value you set here. It flows into `authelia/configuration.yml` via Go template substitution and into your Caddyfile via `{env.DOMAIN}` -- no find-and-replace needed anywhere else.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$EDITOR .env
|
$EDITOR .env
|
||||||
```
|
|
||||||
|
|
||||||
Set `DOMAIN=yourdomain.com` and `TZ=Your/Timezone`. Save and close.
|
# 2) Make mosquitto dirs writable by the container's mosquitto user (UID 1883)
|
||||||
|
sudo chown -R 1883:1883 mosquitto/
|
||||||
|
|
||||||
### 4. Add access control rules
|
# 3) Bootstrap mosquitto BEFORE applying the production config.
|
||||||
|
# The committed mosquitto.conf has allow_anonymous false + password_file,
|
||||||
|
# which means we need to create the password file first OR temporarily
|
||||||
|
# flip to allow_anonymous true to start.
|
||||||
|
#
|
||||||
|
# Easiest: temporarily edit mosquitto/config/mosquitto.conf:
|
||||||
|
# allow_anonymous false -> allow_anonymous true
|
||||||
|
# comment out: password_file /mosquitto/config/passwd
|
||||||
|
# Then start:
|
||||||
|
docker compose up -d mosquitto
|
||||||
|
docker compose logs mosquitto --tail 10 # expect "running"
|
||||||
|
|
||||||
**This step and step 8 (Caddy wiring) must be done together for every site you want to gate. Both are required -- neither alone is enough.**
|
# 4) Create the MQTT user (use the password from your .env)
|
||||||
|
docker compose exec mosquitto mosquitto_passwd -c -b \
|
||||||
|
/mosquitto/config/passwd frigate \
|
||||||
|
"$(grep ^FRIGATE_MQTT_PASSWORD .env | cut -d= -f2)"
|
||||||
|
|
||||||
```bash
|
sudo chown 1883:1883 mosquitto/config/passwd
|
||||||
$EDITOR authelia/configuration.yml
|
sudo chmod 0640 mosquitto/config/passwd
|
||||||
```
|
|
||||||
|
|
||||||
Scroll to `access_control.rules`. Uncomment the rule for each site you want to protect and choose a policy:
|
# 5) Restore mosquitto.conf to its committed state:
|
||||||
|
# allow_anonymous true -> allow_anonymous false
|
||||||
|
# uncomment: password_file /mosquitto/config/passwd
|
||||||
|
git checkout mosquitto/config/mosquitto.conf
|
||||||
|
docker compose restart mosquitto
|
||||||
|
|
||||||
```yaml
|
# 6) Verify auth works
|
||||||
- domain: 'cam.{{ env "DOMAIN" }}'
|
sudo apt install -y mosquitto-clients
|
||||||
policy: 'two_factor'
|
mosquitto_sub -h 127.0.0.1 -u frigate \
|
||||||
```
|
-P "$(grep ^FRIGATE_MQTT_PASSWORD .env | cut -d= -f2)" \
|
||||||
|
-t 'test/#' -v &
|
||||||
|
mosquitto_pub -h 127.0.0.1 -u frigate \
|
||||||
|
-P "$(grep ^FRIGATE_MQTT_PASSWORD .env | cut -d= -f2)" \
|
||||||
|
-t 'test/hello' -m 'ok'
|
||||||
|
# expect: test/hello ok
|
||||||
|
kill %1
|
||||||
|
|
||||||
#### Why both sides are required
|
# 7) Bring up the rest
|
||||||
|
|
||||||
Caddy and Authelia each control one half of the gate:
|
|
||||||
|
|
||||||
| What you configure | What it does |
|
|
||||||
|--------------------|-------------|
|
|
||||||
| `import authelia` in a Caddy site block | Sends that site's requests to Authelia for a decision |
|
|
||||||
| Rule in `access_control.rules` | Tells Authelia what decision to make |
|
|
||||||
|
|
||||||
The default policy is `deny`. If a request reaches Authelia with no matching rule, it gets a **403 Forbidden -- no login prompt, no redirect, just blocked**. This is true even for an already-logged-in user.
|
|
||||||
|
|
||||||
Miss either side and here is what happens:
|
|
||||||
|
|
||||||
| Caddy `import authelia` | Rule in `configuration.yml` | Result |
|
|
||||||
|------------------------|----------------------------|--------|
|
|
||||||
| Missing | Present | Site is open -- Authelia is never consulted |
|
|
||||||
| Present | Missing | 403 Forbidden, no login prompt |
|
|
||||||
| Both missing | | Site is open -- Authelia is never consulted |
|
|
||||||
| Both present | | Works correctly |
|
|
||||||
|
|
||||||
#### Which policy to use
|
|
||||||
|
|
||||||
| Policy | Requires |
|
|
||||||
|--------|---------|
|
|
||||||
| `bypass` | Nothing -- Authelia waves the request through. Used for the portal itself only. |
|
|
||||||
| `one_factor` | Password only |
|
|
||||||
| `two_factor` | Password + TOTP. Use this for everything. |
|
|
||||||
|
|
||||||
### 5. Create your first user
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp authelia/users_database.yml.example authelia/users_database.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$EDITOR authelia/users_database.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
Fill in `username`, `email`, and `displayname`. Then generate the password hash:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose run --rm authelia \
|
|
||||||
authelia crypto hash generate argon2
|
|
||||||
```
|
|
||||||
|
|
||||||
Authelia prompts for the password and a confirmation without echoing --
|
|
||||||
the plaintext never hits your shell history or `ps aux`. Copy the
|
|
||||||
`Digest: $argon2id$...` line from the output and paste the digest
|
|
||||||
(everything from `$argon2id` onward) as the `password:` value in
|
|
||||||
`users_database.yml`.
|
|
||||||
|
|
||||||
### 6. Pre-create the Authelia log file
|
|
||||||
|
|
||||||
Docker creates a **directory** at a bind-mount path if the source file does not exist yet. That breaks fail2ban's read-only mount. Create it as an empty file first:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
touch authelia/authelia.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Validate the config
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose run --rm authelia \
|
|
||||||
authelia validate-config --config /config/configuration.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
Expect: `Configuration: validation complete` with no errors. Fix any YAML issues before continuing.
|
|
||||||
|
|
||||||
### 8. Wire Caddy
|
|
||||||
|
|
||||||
See [Wire Caddy into Authelia](#wire-caddy-into-authelia) below. Add the Caddy site block for each site alongside the rule you added in step 4.
|
|
||||||
|
|
||||||
### 9. Bring it up
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
docker compose logs -f
|
||||||
```
|
```
|
||||||
|
|
||||||
Confirm both services started cleanly:
|
Healthy startup looks like:
|
||||||
|
- Frigate: `frigate.comms.mqtt INFO : MQTT connected`
|
||||||
|
- frigate-notify: `Successfully connected to http://frigate:5000` then
|
||||||
|
`Connected to MQTT.` then `Subscribed to MQTT topic: frigate/events`
|
||||||
|
- mosquitto: incoming client connections from both
|
||||||
|
|
||||||
|
## Caddy
|
||||||
|
|
||||||
|
On whichever host runs Caddy, copy `caddy/Caddyfile` (or merge the relevant
|
||||||
|
site blocks into your existing one), edit IPs and domains, then:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose logs -f authelia
|
sudo caddy validate --config /etc/caddy/Caddyfile
|
||||||
|
sudo systemctl reload caddy
|
||||||
```
|
```
|
||||||
|
|
||||||
Expect: `Authelia is listening on ...`
|
Until the Pi is deployed, comment out the `doorbell.yourdomain.com` block.
|
||||||
|
|
||||||
|
## Train face recognition
|
||||||
|
|
||||||
|
1. Let Frigate run with normal foot traffic for a day. Face crops are
|
||||||
|
captured automatically and appear under **Face Library -> Train** in
|
||||||
|
the Frigate UI.
|
||||||
|
2. For each person, label 5-10 **diverse** clear crops -- different angles,
|
||||||
|
lighting, times of day. Diversity beats quantity; 30 near-identical
|
||||||
|
frames from one event hurts recognition.
|
||||||
|
3. Walk past the camera again. Events should now show the person's name as
|
||||||
|
the sub-label, and frigate-notify's ntfy push will say their name
|
||||||
|
instead of "person".
|
||||||
|
|
||||||
|
Training images live on the Frigate host at
|
||||||
|
`/media/frigate/clips/faces/<n>/`. Not version-controlled (privacy).
|
||||||
|
|
||||||
|
## Switching config profiles
|
||||||
|
|
||||||
|
This repo ships with two Frigate configs:
|
||||||
|
|
||||||
|
- `frigate_config/config.yml` -- active. Main 2688x1520 stream for both
|
||||||
|
detect and record. Better face recognition at distance, higher CPU.
|
||||||
|
- `frigate_config/alternatives/config-simple.yml` -- lower-CPU fallback.
|
||||||
|
Sub-stream 640x480 for detect, main for record only.
|
||||||
|
|
||||||
|
To switch:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose logs -f fail2ban
|
cp frigate_config/config.yml frigate_config/config.yml.bak
|
||||||
|
cp frigate_config/alternatives/config-simple.yml frigate_config/config.yml
|
||||||
|
docker compose restart frigate
|
||||||
```
|
```
|
||||||
|
|
||||||
Expect: `Jail authelia is now active`
|
## Adding a camera with a mic
|
||||||
|
|
||||||
## Wire Caddy into Authelia
|
When adding a camera with a built-in microphone (e.g. an Anpviz with mic),
|
||||||
|
see the comment block at the bottom of `frigate_config/config.yml` for the
|
||||||
|
exact edits needed to enable live audio in the Frigate UI.
|
||||||
|
|
||||||
Open `caddy/snippets.caddyfile`. It contains copy-paste blocks for your
|
## Pi doorbell speaker (when you're ready)
|
||||||
existing Caddyfile, not a replacement for it:
|
|
||||||
|
|
||||||
- `(authelia)` and `(accesslog)` snippet definitions -- paste once near the
|
See `pi/README.md`. Summary:
|
||||||
top of your Caddyfile.
|
1. Flash Pi OS Lite, scp `pi/` to the Pi, run `install.sh`.
|
||||||
- `auth.{env.DOMAIN}` -- the Authelia portal block.
|
2. Add the `doorbell.yourdomain.com` site block in Caddy.
|
||||||
- Per-service examples for all four cases (Frigate, Grafana, Gitea,
|
3. Open the URL on your phone.
|
||||||
Uptime Kuma, Homer, etc.) with the required per-app config notes inline.
|
|
||||||
|
|
||||||
Copy the blocks you need into your real Caddyfile and replace `192.168.x.x`
|
|
||||||
with real upstream IPs. The domain is already templated as `{env.DOMAIN}` --
|
|
||||||
just make sure the `DOMAIN` environment variable is available to Caddy:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Dockerized Caddy -- add to its .env or compose environment:
|
|
||||||
DOMAIN=yourdomain.com
|
|
||||||
|
|
||||||
# System Caddy -- add to /etc/caddy/caddy.env (or wherever systemd reads env):
|
|
||||||
echo 'DOMAIN=yourdomain.com' | sudo tee -a /etc/caddy/caddy.env
|
|
||||||
# Then make sure the systemd unit loads it:
|
|
||||||
# EnvironmentFile=/etc/caddy/caddy.env (in the [Service] section)
|
|
||||||
sudo systemctl daemon-reload && sudo systemctl reload caddy
|
|
||||||
```
|
|
||||||
|
|
||||||
**Every** site block should have `import accesslog` -- even case 4 sites.
|
|
||||||
fail2ban's caddy-4xx jail watches the one log file and covers all your
|
|
||||||
subdomains automatically. Scanners spray everything, not just gated sites.
|
|
||||||
|
|
||||||
### Caddy access log path
|
|
||||||
|
|
||||||
fail2ban mounts `/var/log/caddy` from the host as read-only. Your Caddy
|
|
||||||
service must write to the same path. In your Caddy compose:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
caddy:
|
|
||||||
volumes:
|
|
||||||
- /var/log/caddy:/var/log/caddy
|
|
||||||
```
|
|
||||||
|
|
||||||
Create the directory before starting:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo mkdir -p /var/log/caddy
|
|
||||||
sudo chown caddy:caddy /var/log/caddy # adjust to your Caddy UID
|
|
||||||
```
|
|
||||||
|
|
||||||
## Switching Frigate to Authelia (case 2)
|
|
||||||
|
|
||||||
Edit `frigate_config/config.yml` in your Frigate stack:
|
|
||||||
|
|
||||||
First, find your `caddy_net` subnet -- you need this for `trusted_proxies`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker network inspect caddy_net | jq '.[0].IPAM.Config'
|
|
||||||
```
|
|
||||||
|
|
||||||
Then edit `frigate_config/config.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
auth:
|
|
||||||
enabled: False
|
|
||||||
trusted_proxies:
|
|
||||||
- 172.18.0.0/16 # replace with your caddy_net subnet from above
|
|
||||||
|
|
||||||
proxy:
|
|
||||||
header_map:
|
|
||||||
user: remote-user # matches `copy_headers Remote-User` in (authelia) snippet
|
|
||||||
role: remote-groups
|
|
||||||
default_role: viewer
|
|
||||||
separator: '|'
|
|
||||||
# Optional shared secret -- prevents LAN header spoofing.
|
|
||||||
# auth_secret: 'your-32-byte-hex'
|
|
||||||
```
|
|
||||||
|
|
||||||
To generate the optional `auth_secret`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openssl rand -hex 32
|
|
||||||
```
|
|
||||||
|
|
||||||
Set the same value in the `header_up X-Proxy-Secret` line in your Caddy site block.
|
|
||||||
|
|
||||||
Add the rule to `authelia/configuration.yml` (step 4 of first-run), then restart both services:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose restart authelia
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose restart frigate # in your Frigate stack
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify in a private browser window: `https://cam.yourdomain.com` should go to Authelia and back without a Frigate login screen.
|
|
||||||
|
|
||||||
## First login + TOTP enrollment
|
|
||||||
|
|
||||||
1. Visit a protected subdomain in a private browser window.
|
|
||||||
2. Caddy bounces you to `https://auth.example.com` -- log in with your
|
|
||||||
username and plaintext password.
|
|
||||||
3. If the rule is `two_factor`, Authelia prompts you to register a second
|
|
||||||
factor. Pick **TOTP** and scan the QR with your authenticator app.
|
|
||||||
4. Authelia writes a confirmation link to the filesystem notifier file:
|
|
||||||
```bash
|
|
||||||
docker compose exec authelia cat /config/notifications/notification.txt
|
|
||||||
```
|
|
||||||
Click the link to confirm TOTP registration.
|
|
||||||
5. Enter the TOTP code -- you're in. The `authelia_session` cookie is scoped
|
|
||||||
to your root domain and covers every protected subdomain automatically.
|
|
||||||
|
|
||||||
## User management
|
|
||||||
|
|
||||||
### Add a user
|
|
||||||
|
|
||||||
Append to `authelia/users_database.yml`, generate a hash:
|
|
||||||
```bash
|
|
||||||
docker compose run --rm authelia \
|
|
||||||
authelia crypto hash generate argon2
|
|
||||||
```
|
|
||||||
Authelia prompts for the password (no echo, not in shell history). Paste
|
|
||||||
the printed digest as `password:`. Restart or wait 5 minutes for auto-reload.
|
|
||||||
|
|
||||||
### Disable a user
|
|
||||||
|
|
||||||
Set `disabled: true` on their entry. Takes effect at next refresh.
|
|
||||||
|
|
||||||
### Reset TOTP (force re-enrollment)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose exec authelia \
|
|
||||||
authelia storage user totp delete --username USERNAME \
|
|
||||||
--config /config/configuration.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
## fail2ban
|
|
||||||
|
|
||||||
fail2ban does **not** need its own separate stack or compose file. It lives
|
|
||||||
alongside Authelia in this same `docker-compose.yml`. It uses host networking
|
|
||||||
(no docker network needed) and watches two log sources:
|
|
||||||
|
|
||||||
| Jail | Log | Trigger | Ban |
|
|
||||||
|------|-----|---------|-----|
|
|
||||||
| `authelia` | `./authelia/authelia.log` | 3 failed logins in 10 min | 1 hour |
|
|
||||||
| `caddy-4xx` | `/var/log/caddy/access.log` | 30 HTTP 4xx in 2 min | 30 min |
|
|
||||||
|
|
||||||
The **caddy-4xx jail covers every site** on your Caddyfile as long as each
|
|
||||||
block has `import accesslog`. You don't need per-site jails.
|
|
||||||
|
|
||||||
Defense in depth: Authelia's `regulation` block locks the *user account*
|
|
||||||
after 3 bad passwords. fail2ban bans the *source IP* independently.
|
|
||||||
|
|
||||||
### Verify jails are active
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose exec fail2ban fail2ban-client status
|
|
||||||
docker compose exec fail2ban fail2ban-client status authelia
|
|
||||||
docker compose exec fail2ban fail2ban-client status caddy-4xx
|
|
||||||
```
|
|
||||||
|
|
||||||
### Test filters against real logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose exec fail2ban fail2ban-regex \
|
|
||||||
/var/log/authelia/authelia.log \
|
|
||||||
/data/filter.d/authelia.local
|
|
||||||
|
|
||||||
docker compose exec fail2ban fail2ban-regex \
|
|
||||||
/var/log/caddy/access.log \
|
|
||||||
/data/filter.d/caddy-4xx.local
|
|
||||||
```
|
|
||||||
|
|
||||||
If nothing matches: confirm `log.format: 'text'` in `authelia/configuration.yml`
|
|
||||||
and `format json` in the `(accesslog)` snippet in your Caddyfile.
|
|
||||||
|
|
||||||
### Manually unban an IP
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose exec fail2ban fail2ban-client set authelia unbanip 1.2.3.4
|
|
||||||
docker compose exec fail2ban fail2ban-client set caddy-4xx unbanip 1.2.3.4
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tune thresholds
|
|
||||||
|
|
||||||
Edit `fail2ban/data/jail.d/*.local`, then:
|
|
||||||
```bash
|
|
||||||
docker compose restart fail2ban
|
|
||||||
```
|
|
||||||
|
|
||||||
## Day-to-day
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose ps # services running?
|
|
||||||
docker compose logs -f authelia # follow Authelia
|
|
||||||
docker compose logs -f fail2ban # follow fail2ban
|
|
||||||
docker compose pull && docker compose up -d # upgrade images
|
|
||||||
```
|
|
||||||
|
|
||||||
Bump `AUTHELIA_VERSION` in `.env` when upgrading Authelia. After any
|
|
||||||
upgrade, re-run `validate-config` -- the schema evolves between releases.
|
|
||||||
|
|
||||||
## Switching the notifier to SMTP
|
|
||||||
|
|
||||||
Replace `notifier:` in `authelia/configuration.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
notifier:
|
|
||||||
disable_startup_check: false
|
|
||||||
smtp:
|
|
||||||
address: 'smtps://smtp.example.com:465'
|
|
||||||
username: 'authelia@example.com'
|
|
||||||
sender: 'Authelia <authelia@example.com>'
|
|
||||||
subject: '[Authelia] {title}'
|
|
||||||
# password loaded via AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE
|
|
||||||
```
|
|
||||||
|
|
||||||
Add the secret file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo 'your_smtp_password' > authelia/secrets/SMTP_PASSWORD
|
|
||||||
chmod 600 authelia/secrets/SMTP_PASSWORD
|
|
||||||
```
|
|
||||||
|
|
||||||
Add to the `authelia` service `environment:` block in `docker-compose.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE=/secrets/SMTP_PASSWORD
|
|
||||||
```
|
|
||||||
|
|
||||||
Restart and check for a successful startup message:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose restart authelia && docker compose logs -f authelia
|
|
||||||
```
|
|
||||||
|
|
||||||
Expect: `Notifier SMTP startup check successful`
|
|
||||||
|
|
||||||
## Security notes
|
## Security notes
|
||||||
|
|
||||||
- `.env`, `authelia/secrets/*`, `authelia/users_database.yml`, and
|
- `.env` has RTSP credentials, MQTT password, ntfy URL. Never commit it.
|
||||||
`authelia/db.sqlite3*` are all gitignored. Run `git status` before every
|
This repo's `.gitignore` blocks it; review `git status` before committing.
|
||||||
commit to confirm nothing sensitive is staged.
|
- The Pi's Flask server binds only to `127.0.0.1`. Caddy is what exposes it.
|
||||||
- Authelia is not port-mapped to the host. Only containers on `caddy_net`
|
Do NOT bind `server.py` to `0.0.0.0` -- it has no auth of its own.
|
||||||
can reach it; only Caddy is configured to forward_auth there.
|
- Mosquitto's port 1883 is LAN-only. Use a VPN for any remote MQTT clients.
|
||||||
- TOTP secrets in the SQLite DB are encrypted at rest with
|
- For the doorbell page, optionally add basic auth in Caddy (`caddy
|
||||||
`STORAGE_ENCRYPTION_KEY`. Back up both the DB and the key file -- losing
|
hash-password`).
|
||||||
either means every user must re-enroll TOTP.
|
|
||||||
- `regulation` is per-user account lockout; fail2ban is per-IP. Both are on.
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Redirect loop between a site and `auth.example.com`
|
### Frigate can't reach cameras
|
||||||
|
|
||||||
Cookie domain mismatch. The `domain:` under `session.cookies[]` must be the
|
|
||||||
bare root domain (`example.com`), and every protected site must be a subdomain
|
|
||||||
of it served over HTTPS. Mixed HTTP/HTTPS won't work; the session cookie is
|
|
||||||
`Secure`.
|
|
||||||
|
|
||||||
### "access denied" with no login prompt
|
|
||||||
|
|
||||||
`default_policy: deny` -- a request reached Authelia with no matching rule for that domain. Add a rule under `access_control.rules` in `authelia/configuration.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- domain: 'yoursite.{{ env "DOMAIN" }}'
|
|
||||||
policy: 'two_factor'
|
|
||||||
```
|
|
||||||
|
|
||||||
Then restart Authelia:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose restart authelia
|
docker compose exec frigate ping -c 2 <camera_ip>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Authelia container restarts forever
|
RTSP path varies by camera vendor:
|
||||||
|
- Dahua / Amcrest: `/cam/realmonitor?channel=1&subtype=0`
|
||||||
|
- Hikvision / Anpviz H-series: `/Streaming/Channels/101`
|
||||||
|
|
||||||
|
If your password contains `%`, `@`, `/`, `?`, `#`, `&`, or `+`, either
|
||||||
|
URL-encode it or change the password to avoid those characters.
|
||||||
|
|
||||||
|
### Mosquitto restarts in a loop
|
||||||
|
|
||||||
|
Almost always permission on `mosquitto/config/passwd`:
|
||||||
|
```bash
|
||||||
|
sudo chown -R 1883:1883 mosquitto/
|
||||||
|
sudo chmod 0640 mosquitto/config/passwd
|
||||||
|
docker compose restart mosquitto
|
||||||
|
```
|
||||||
|
|
||||||
|
Or the config file or password file simply doesn't exist yet -- see the
|
||||||
|
"First-run on the Frigate host" section above for the bootstrap flow.
|
||||||
|
|
||||||
|
### frigate-notify connects to MQTT but doesn't send pings
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose logs authelia | head -50
|
docker compose logs frigate-notify --tail 30
|
||||||
```
|
```
|
||||||
|
|
||||||
Most often: missing/empty secret files in `authelia/secrets/`, bad YAML in
|
- "webapi" in logs but expecting MQTT? Check `webapi.enabled: false` and
|
||||||
`configuration.yml`, or an invalid argon2 hash in `users_database.yml`.
|
`mqtt.enabled: true` in `frigate-notify/config.yml`.
|
||||||
|
- MQTT auth fails? Verify `FN_FRIGATE__MQTT__PASSWORD` (note DOUBLE
|
||||||
|
underscores) matches what you set with `mosquitto_passwd`.
|
||||||
|
- Confirm what reached the container:
|
||||||
|
`docker inspect frigate-notify --format '{{range .Config.Env}}{{println .}}{{end}}' | grep FN_`
|
||||||
|
|
||||||
### Caddy can't resolve `authelia`
|
### Always see "person" instead of trained name
|
||||||
|
|
||||||
Caddy isn't on `caddy_net`. Add this to your Caddy service in its compose file:
|
- Increase `alerts.general.recheck_delay` in `frigate-notify/config.yml`
|
||||||
|
from 10 to 15 or 20 seconds.
|
||||||
|
- Check the Frigate UI event timeline -- if the event itself doesn't
|
||||||
|
show a sub_label, the face crop was too small / too blurry / too
|
||||||
|
obscured for recognition.
|
||||||
|
|
||||||
```yaml
|
## Hardware reference
|
||||||
services:
|
|
||||||
caddy:
|
|
||||||
networks:
|
|
||||||
- caddy_net
|
|
||||||
|
|
||||||
networks:
|
Current:
|
||||||
caddy_net:
|
- NVR host: x86_64 + Docker, USB Coral
|
||||||
external: true
|
- Cameras: Amcrest (Dahua RTSP)
|
||||||
```
|
- Notifier: self-hosted ntfy
|
||||||
|
|
||||||
Then recreate the Caddy container:
|
Planned:
|
||||||
|
- Anpviz 4K camera with built-in mic (front door audio)
|
||||||
```bash
|
- Back door + squirrel feeder cameras
|
||||||
docker compose up -d caddy
|
- Pi Zero W + USB speaker at the door
|
||||||
```
|
|
||||||
|
|
||||||
### fail2ban bans don't block traffic
|
|
||||||
|
|
||||||
fail2ban is writing to the wrong iptables chain. With dockerized Caddy you
|
|
||||||
need `chain = DOCKER-USER` (already set in the shipped jail files). Verify:
|
|
||||||
```bash
|
|
||||||
sudo iptables -L DOCKER-USER -n
|
|
||||||
# Should show f2b-* jump rules.
|
|
||||||
```
|
|
||||||
|
|
||||||
### fail2ban: authelia jail missing / "No such file" on authelia.log
|
|
||||||
|
|
||||||
Docker created a directory at `./authelia/authelia.log` instead of a file
|
|
||||||
because the file didn't exist when the container started:
|
|
||||||
```bash
|
|
||||||
docker compose down fail2ban
|
|
||||||
rm -rf authelia/authelia.log # remove the directory Docker created
|
|
||||||
touch authelia/authelia.log # create as an empty file
|
|
||||||
docker compose up -d fail2ban
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frigate still shows its own login after switching to proxy auth
|
|
||||||
|
|
||||||
1. Confirm `auth.enabled: False` in `frigate_config/config.yml`.
|
|
||||||
2. Confirm `trusted_proxies` subnet matches your `caddy_net` subnet:
|
|
||||||
```bash
|
|
||||||
docker network inspect caddy_net | grep -A2 '"Config"'
|
|
||||||
```
|
|
||||||
3. Restart Frigate: `docker compose restart frigate` (in your Frigate stack).
|
|
||||||
|
|||||||
@@ -1,241 +0,0 @@
|
|||||||
---
|
|
||||||
###############################################################################
|
|
||||||
# Authelia configuration
|
|
||||||
#
|
|
||||||
# - File-based user database (no LDAP)
|
|
||||||
# - SQLite local storage (no Redis, no MySQL/Postgres)
|
|
||||||
# - Filesystem notifier (writes "emails" to /config/notifications/...)
|
|
||||||
# - Argon2id password hashing
|
|
||||||
# - Per-domain access policies under access_control.rules
|
|
||||||
#
|
|
||||||
# Secrets are NOT in this file. They are loaded from files mounted at
|
|
||||||
# /secrets via the AUTHELIA_*_FILE env vars in docker-compose.yml.
|
|
||||||
#
|
|
||||||
# Your domain comes from the DOMAIN variable in .env -- no manual
|
|
||||||
# find-and-replace needed. Authelia 4.38+ processes this file as a Go
|
|
||||||
# template, so {{ env "DOMAIN" }} is substituted at startup.
|
|
||||||
#
|
|
||||||
# After editing, validate before restarting:
|
|
||||||
# docker compose run --rm authelia authelia validate-config --config /config/configuration.yml
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
theme: 'dark'
|
|
||||||
|
|
||||||
server:
|
|
||||||
address: 'tcp://0.0.0.0:9091'
|
|
||||||
buffers:
|
|
||||||
read: 8192
|
|
||||||
write: 8192
|
|
||||||
|
|
||||||
log:
|
|
||||||
level: 'info'
|
|
||||||
format: 'text' # fail2ban filter expects text format -- don't change to json
|
|
||||||
file_path: '/config/authelia.log'
|
|
||||||
keep_stdout: true # also log to stdout for `docker logs`
|
|
||||||
|
|
||||||
identity_validation:
|
|
||||||
reset_password:
|
|
||||||
jwt_lifespan: '5 minutes'
|
|
||||||
jwt_algorithm: 'HS256'
|
|
||||||
# jwt_secret loaded via AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE
|
|
||||||
|
|
||||||
totp:
|
|
||||||
disable: false
|
|
||||||
issuer: '{{ env "DOMAIN" }}' # shown in your authenticator app
|
|
||||||
algorithm: 'sha1'
|
|
||||||
digits: 6
|
|
||||||
period: 30
|
|
||||||
|
|
||||||
authentication_backend:
|
|
||||||
password_change:
|
|
||||||
disable: false
|
|
||||||
password_reset:
|
|
||||||
disable: false
|
|
||||||
refresh_interval: '5 minutes'
|
|
||||||
file:
|
|
||||||
path: '/config/users_database.yml'
|
|
||||||
password:
|
|
||||||
algorithm: 'argon2'
|
|
||||||
argon2:
|
|
||||||
variant: 'argon2id'
|
|
||||||
iterations: 3
|
|
||||||
memory: 65536
|
|
||||||
parallelism: 4
|
|
||||||
key_length: 32
|
|
||||||
salt_length: 16
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Access control
|
|
||||||
#
|
|
||||||
# default_policy: deny -- every domain Caddy forward_auths here must have
|
|
||||||
# an explicit rule. Domains with no `import authelia` in Caddy never reach
|
|
||||||
# Authelia at all, so the deny doesn't apply to them.
|
|
||||||
#
|
|
||||||
# Policies:
|
|
||||||
# bypass Authelia waves the request through (used for the portal)
|
|
||||||
# one_factor password only
|
|
||||||
# two_factor password + TOTP
|
|
||||||
#
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# WHICH SITES NEED A RULE HERE?
|
|
||||||
#
|
|
||||||
# CASE 1 -- App has NO built-in auth (e.g. Pi doorbell PTT page).
|
|
||||||
# -> Rule required + `import authelia` in Caddy.
|
|
||||||
# -> Authelia is the ONLY login. Use two_factor for hardware-control pages.
|
|
||||||
#
|
|
||||||
# CASE 2 -- App has built-in auth AND supports trusted-header proxy auth
|
|
||||||
# (Frigate 0.14+, Grafana, Gitea, Nextcloud, Home Assistant ...).
|
|
||||||
# -> Rule required + `import authelia` in Caddy + disable the app's login
|
|
||||||
# form in its own config (see README.md per-app instructions).
|
|
||||||
# -> Single Authelia login covers both access AND the app's user/role
|
|
||||||
# mapping via Remote-User / Remote-Groups headers.
|
|
||||||
#
|
|
||||||
# CASE 3 -- App has built-in auth and CANNOT switch to proxy auth, but you
|
|
||||||
# still want a 2FA gate in front of it (extra security layer).
|
|
||||||
# -> Rule required + `import authelia` in Caddy.
|
|
||||||
# -> User logs into Authelia (2FA) THEN into the app's own login form.
|
|
||||||
# Two separate logins -- the app auth is unchanged.
|
|
||||||
# -> Useful for: router admin pages, NAS UIs, any legacy app.
|
|
||||||
#
|
|
||||||
# CASE 4 -- App handles its own auth and you don't want Authelia involved.
|
|
||||||
# -> NO rule here + NO `import authelia` in Caddy.
|
|
||||||
# -> Traffic skips Authelia entirely; the app handles everything.
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
access_control:
|
|
||||||
default_policy: 'deny'
|
|
||||||
rules:
|
|
||||||
|
|
||||||
# The Authelia portal itself is always bypass.
|
|
||||||
- domain: 'auth.{{ env "DOMAIN" }}'
|
|
||||||
policy: 'bypass'
|
|
||||||
|
|
||||||
# ===================================================================
|
|
||||||
# CASE 1 -- No app auth. Authelia is the only gate.
|
|
||||||
# Example: Pi doorbell PTT page (Flask server, no built-in auth).
|
|
||||||
# two_factor is right -- this URL controls a speaker in your house.
|
|
||||||
#
|
|
||||||
# ALSO PASTE INTO CADDYFILE (case 1 site block):
|
|
||||||
# doorbell.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# handle_path /frigate/* {
|
|
||||||
# reverse_proxy 192.168.x.x:8971 # CHANGE: Frigate IP
|
|
||||||
# }
|
|
||||||
# handle {
|
|
||||||
# reverse_proxy 192.168.x.x:5555 # CHANGE: Pi IP
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
# ===================================================================
|
|
||||||
# - domain: 'doorbell.{{ env "DOMAIN" }}'
|
|
||||||
# policy: 'two_factor'
|
|
||||||
|
|
||||||
# ===================================================================
|
|
||||||
# CASE 2a -- App supports trusted-header proxy auth.
|
|
||||||
# Authelia replaces the app's login form. Single login; the app reads
|
|
||||||
# Remote-User from the upstream request for its own role mapping.
|
|
||||||
# Example: Frigate 0.14+.
|
|
||||||
#
|
|
||||||
# ALSO PASTE INTO CADDYFILE (case 2a site block):
|
|
||||||
# cam.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:8971 { # CHANGE: Frigate IP
|
|
||||||
# transport http { read_timeout 60s; write_timeout 60s }
|
|
||||||
# # header_up X-Proxy-Secret "32-byte-hex" # if Frigate auth_secret set
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# ALSO EDIT frigate_config/config.yml IN THE FRIGATE REPO:
|
|
||||||
# auth:
|
|
||||||
# enabled: False
|
|
||||||
# trusted_proxies: [172.18.0.0/16] # caddy_net subnet
|
|
||||||
# proxy:
|
|
||||||
# header_map: {user: remote-user, role: remote-groups}
|
|
||||||
# default_role: viewer
|
|
||||||
# separator: '|'
|
|
||||||
# ===================================================================
|
|
||||||
# - domain: 'cam.{{ env "DOMAIN" }}'
|
|
||||||
# policy: 'two_factor'
|
|
||||||
|
|
||||||
# ===================================================================
|
|
||||||
# CASE 2b -- App supports OIDC. Authelia is the OIDC provider.
|
|
||||||
# Caddy block is identical to case 2a; the difference is on the app
|
|
||||||
# side (token exchange, not header). REQUIRES additional setup of
|
|
||||||
# identity_providers.oidc below this access_control block, with one
|
|
||||||
# client per app -- see Authelia OIDC docs.
|
|
||||||
# Example: Audiobookshelf.
|
|
||||||
#
|
|
||||||
# ALSO PASTE INTO CADDYFILE (case 2b site block):
|
|
||||||
# books.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:13378 # CHANGE IP
|
|
||||||
# }
|
|
||||||
# ===================================================================
|
|
||||||
# - domain: 'books.{{ env "DOMAIN" }}'
|
|
||||||
# policy: 'two_factor'
|
|
||||||
|
|
||||||
# ===================================================================
|
|
||||||
# CASE 3 -- App keeps its own login. Authelia adds a 2FA gate in
|
|
||||||
# front. User authenticates with Authelia (2FA), then with the app
|
|
||||||
# itself. Two logins, but Authelia's 2FA covers apps that don't
|
|
||||||
# support proxy headers OR OIDC OR native TOTP.
|
|
||||||
# Example: Uptime Kuma, Portainer (without OIDC), router admin.
|
|
||||||
#
|
|
||||||
# ALSO PASTE INTO CADDYFILE (case 3 site block):
|
|
||||||
# uptime.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:3001 # CHANGE IP
|
|
||||||
# }
|
|
||||||
# ===================================================================
|
|
||||||
# - domain: 'uptime.{{ env "DOMAIN" }}'
|
|
||||||
# policy: 'two_factor'
|
|
||||||
|
|
||||||
# ===================================================================
|
|
||||||
# CASE 4 -- App handles its own auth. NO rule needed here.
|
|
||||||
# Authelia is never consulted. Caddy still imports accesslog so
|
|
||||||
# fail2ban watches the subdomain for scanners.
|
|
||||||
# Example: Plex/Emby (native clients break with Authelia redirects),
|
|
||||||
# Syncthing, anything you've decided to leave alone.
|
|
||||||
#
|
|
||||||
# ONLY PASTE INTO CADDYFILE -- nothing here in configuration.yml:
|
|
||||||
# plex.{env.DOMAIN} {
|
|
||||||
# import accesslog # NO import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:32400 # CHANGE IP
|
|
||||||
# }
|
|
||||||
# ===================================================================
|
|
||||||
# (no rule -- case 4 is the absence of one)
|
|
||||||
|
|
||||||
session:
|
|
||||||
# secret loaded via AUTHELIA_SESSION_SECRET_FILE
|
|
||||||
cookies:
|
|
||||||
- name: 'authelia_session'
|
|
||||||
domain: '{{ env "DOMAIN" }}'
|
|
||||||
authelia_url: 'https://auth.{{ env "DOMAIN" }}'
|
|
||||||
default_redirection_url: 'https://{{ env "DOMAIN" }}'
|
|
||||||
expiration: '1 hour'
|
|
||||||
inactivity: '5 minutes'
|
|
||||||
remember_me: '1 month'
|
|
||||||
same_site: 'lax'
|
|
||||||
|
|
||||||
# In-app rate limiting. Locks the user account after repeated failures.
|
|
||||||
# fail2ban is the second line of defense: it bans the source IP.
|
|
||||||
# Together: Authelia locks the *user*, fail2ban bans the *IP*.
|
|
||||||
regulation:
|
|
||||||
max_retries: 3
|
|
||||||
find_time: '2 minutes'
|
|
||||||
ban_time: '5 minutes'
|
|
||||||
|
|
||||||
storage:
|
|
||||||
# encryption_key loaded via AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE
|
|
||||||
local:
|
|
||||||
path: '/config/db.sqlite3'
|
|
||||||
|
|
||||||
# Filesystem notifier -- password reset / new device emails get written to
|
|
||||||
# a file you can `tail -f`. Swap to `smtp:` when you wire up a real
|
|
||||||
# transactional sender (see README.md "Switching the notifier to SMTP").
|
|
||||||
notifier:
|
|
||||||
disable_startup_check: false
|
|
||||||
filesystem:
|
|
||||||
filename: '/config/notifications/notification.txt'
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
---
|
|
||||||
###############################################################################
|
|
||||||
# Authelia users database
|
|
||||||
#
|
|
||||||
# Copy this to users_database.yml (gitignored) and edit. Generate each
|
|
||||||
# password hash with:
|
|
||||||
#
|
|
||||||
# docker compose run --rm authelia \
|
|
||||||
# authelia crypto hash generate argon2
|
|
||||||
#
|
|
||||||
# Authelia prompts for the password and a confirmation without echoing,
|
|
||||||
# so the plaintext never hits shell history or `ps aux`. Output ends
|
|
||||||
# with `Digest: $argon2id$v=19$m=...`. Paste the digest (everything from
|
|
||||||
# `$argon2id` onward) as the `password:` value below.
|
|
||||||
#
|
|
||||||
# Restart Authelia for changes to take effect (or wait refresh_interval).
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
users:
|
|
||||||
|
|
||||||
yourname:
|
|
||||||
disabled: false
|
|
||||||
displayname: 'Your Name'
|
|
||||||
password: '$argon2id$v=19$m=65536,t=3,p=4$REPLACE_WITH_GENERATED_HASH'
|
|
||||||
email: 'you@example.com'
|
|
||||||
groups:
|
|
||||||
- 'admins'
|
|
||||||
|
|
||||||
# Add more users here. `groups` are referenced from access_control rules
|
|
||||||
# via `subject: 'group:admins'`.
|
|
||||||
#
|
|
||||||
# guest:
|
|
||||||
# disabled: false
|
|
||||||
# displayname: 'Guest'
|
|
||||||
# password: '$argon2id$v=19$m=65536,t=3,p=4$...'
|
|
||||||
# email: 'guest@example.com'
|
|
||||||
# groups:
|
|
||||||
# - 'guests'
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Caddyfile
|
||||||
|
#
|
||||||
|
# Install at /etc/caddy/Caddyfile (or merge with your existing one), edit the
|
||||||
|
# placeholders below, then: sudo systemctl reload caddy
|
||||||
|
#
|
||||||
|
# Caddy auto-provisions Let's Encrypt certs for every site block. DNS for
|
||||||
|
# both subdomains must point at this Caddy host's public IP first.
|
||||||
|
#
|
||||||
|
# Placeholders to replace:
|
||||||
|
# yourdomain.com -> your real domain
|
||||||
|
# 192.168.1.50 -> Frigate host LAN IP
|
||||||
|
# 192.168.1.60 -> Pi Zero W LAN IP (when you set up the doorbell)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ---------- Frigate authenticated UI ----------
|
||||||
|
cam.yourdomain.com {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
reverse_proxy 192.168.1.50:8971 {
|
||||||
|
transport http {
|
||||||
|
read_timeout 60s
|
||||||
|
write_timeout 60s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- Doorbell PTT page (Pi Zero W) ----------
|
||||||
|
# Comment out this whole block until the Pi is deployed.
|
||||||
|
doorbell.yourdomain.com {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
# Same-origin proxy to Frigate so the browser's WebRTC fetch works
|
||||||
|
# without CORS issues. /frigate/* is stripped before forwarding.
|
||||||
|
handle_path /frigate/* {
|
||||||
|
reverse_proxy 192.168.1.50:8971 {
|
||||||
|
transport http {
|
||||||
|
read_timeout 60s
|
||||||
|
write_timeout 60s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Everything else (HTML page + /audio WebSocket) goes to the Pi.
|
||||||
|
handle {
|
||||||
|
reverse_proxy 192.168.1.60:5555
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,484 +0,0 @@
|
|||||||
# =============================================================================
|
|
||||||
# Authelia + fail2ban -- Caddy snippets
|
|
||||||
#
|
|
||||||
# These are SNIPPETS TO ADD to your existing Caddyfile, not a replacement
|
|
||||||
# for it. Copy the (authelia) and (accesslog) snippet definitions once at
|
|
||||||
# the top of your Caddyfile, then copy whichever site blocks apply.
|
|
||||||
#
|
|
||||||
# DOMAIN is read from the environment -- set it wherever your Caddy reads
|
|
||||||
# env vars (Caddy's own .env, systemd EnvironmentFile, or compose env:).
|
|
||||||
# Only the upstream IPs need manual editing.
|
|
||||||
#
|
|
||||||
# Caddy v2.5.1+ required; tested on v2.11.2.
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# Paste these two snippet definitions once, near the top of your Caddyfile.
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Gate any site block with Authelia by adding `import authelia` inside it.
|
|
||||||
(authelia) {
|
|
||||||
forward_auth authelia:9091 {
|
|
||||||
uri /api/authz/forward-auth
|
|
||||||
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# JSON access log that fail2ban's caddy-4xx jail reads.
|
|
||||||
# Add `import accesslog` to EVERY site block (gated or not) so fail2ban
|
|
||||||
# catches scanners hitting all your subdomains, not just the protected ones.
|
|
||||||
(accesslog) {
|
|
||||||
log {
|
|
||||||
output file /var/log/caddy/access.log {
|
|
||||||
roll_size 10MiB
|
|
||||||
roll_keep 5
|
|
||||||
roll_keep_for 720h
|
|
||||||
}
|
|
||||||
format json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Authelia portal -- always required; never put `import authelia` here.
|
|
||||||
# =============================================================================
|
|
||||||
auth.{env.DOMAIN} {
|
|
||||||
import accesslog
|
|
||||||
reverse_proxy authelia:9091
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# CASE 1 -- App has NO built-in auth. Authelia is the only gate.
|
|
||||||
#
|
|
||||||
# Examples: Homer, Heimdall, Dozzle, Prometheus, Alertmanager, Gatus,
|
|
||||||
# WatchYourLAN, NUT web UI. Use two_factor in
|
|
||||||
# authelia/configuration.yml for any of these.
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# Homer / Heimdall dashboard (no auth whatsoever)
|
|
||||||
homer.{env.DOMAIN} {
|
|
||||||
import accesslog
|
|
||||||
import authelia
|
|
||||||
reverse_proxy 192.168.x.x:8080 # CHANGE IP:PORT
|
|
||||||
}
|
|
||||||
|
|
||||||
# Dozzle (Docker log viewer -- no auth by default)
|
|
||||||
dozzle.{env.DOMAIN} {
|
|
||||||
import accesslog
|
|
||||||
import authelia
|
|
||||||
reverse_proxy 192.168.x.x:8888 # CHANGE IP:PORT
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Prometheus ---
|
|
||||||
# No authentication built in. Always gate it -- metrics expose internal details.
|
|
||||||
# prom.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:9090 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Alertmanager ---
|
|
||||||
# No authentication built in.
|
|
||||||
# alerts.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:9093 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Gatus ---
|
|
||||||
# Status / uptime page. Has optional built-in OIDC but simpler to gate here.
|
|
||||||
# status.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:8080 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- WatchYourLAN ---
|
|
||||||
# Network ARP scanner. No built-in auth.
|
|
||||||
# lan.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:8840 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- NUT (Network UPS Tools) web UI ---
|
|
||||||
# The NUT daemon (upsd) has no web UI itself. Common frontends -- NUT-Monitor,
|
|
||||||
# upsd-web, various Docker images -- have minimal or no auth. Gate whichever
|
|
||||||
# you run here. Port varies by image.
|
|
||||||
# ups.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:PORT # CHANGE IP:PORT
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Pi doorbell PTT page (Flask server, no auth) ---
|
|
||||||
# Uncomment when the Pi is deployed.
|
|
||||||
# doorbell.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
#
|
|
||||||
# handle_path /frigate/* {
|
|
||||||
# reverse_proxy 192.168.x.x:8971 # CHANGE: Frigate IP
|
|
||||||
# }
|
|
||||||
# handle {
|
|
||||||
# reverse_proxy 192.168.x.x:5555 # CHANGE: Pi IP
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# CASE 2a -- App supports trusted-header proxy auth. Authelia replaces its
|
|
||||||
# own login form. Single login, app reads Remote-User for roles.
|
|
||||||
#
|
|
||||||
# Requires per-app config changes -- see notes in each block.
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# --- Frigate 0.14+ ---
|
|
||||||
# In frigate_config/config.yml:
|
|
||||||
# auth:
|
|
||||||
# enabled: False
|
|
||||||
# trusted_proxies: [172.18.0.0/16] # caddy_net subnet
|
|
||||||
# proxy:
|
|
||||||
# header_map: {user: remote-user, role: remote-groups}
|
|
||||||
# default_role: viewer
|
|
||||||
# separator: '|'
|
|
||||||
cam.{env.DOMAIN} {
|
|
||||||
import accesslog
|
|
||||||
import authelia
|
|
||||||
reverse_proxy 192.168.x.x:8971 { # CHANGE IP
|
|
||||||
transport http { read_timeout 60s; write_timeout 60s }
|
|
||||||
# header_up X-Proxy-Secret "32-byte-hex" # if auth_secret: set in Frigate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Grafana ---
|
|
||||||
# In grafana.ini (or GF_* env vars):
|
|
||||||
# [auth.proxy]
|
|
||||||
# enabled = true
|
|
||||||
# header_name = Remote-User
|
|
||||||
# header_property = username
|
|
||||||
# auto_sign_up = true
|
|
||||||
grafana.{env.DOMAIN} {
|
|
||||||
import accesslog
|
|
||||||
import authelia
|
|
||||||
reverse_proxy 192.168.x.x:3000 # CHANGE IP
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Gitea / Forgejo ---
|
|
||||||
# In app.ini:
|
|
||||||
# [service]
|
|
||||||
# ENABLE_REVERSE_PROXY_AUTHENTICATION = true
|
|
||||||
# REVERSE_PROXY_TRUSTED_PROXIES = *
|
|
||||||
git.{env.DOMAIN} {
|
|
||||||
import accesslog
|
|
||||||
import authelia
|
|
||||||
reverse_proxy 192.168.x.x:3000 # CHANGE IP
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Nextcloud ---
|
|
||||||
# In config/config.php:
|
|
||||||
# 'trusted_proxies' => ['172.18.0.0/16'],
|
|
||||||
# 'overwriteprotocol' => 'https',
|
|
||||||
# Plus user_external app + HTTP header auth set to Remote-User.
|
|
||||||
cloud.{env.DOMAIN} {
|
|
||||||
import accesslog
|
|
||||||
import authelia
|
|
||||||
reverse_proxy 192.168.x.x:80 { # CHANGE IP
|
|
||||||
header_up Host {upstream_hostport}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Paperless-ngx ---
|
|
||||||
# In compose env:
|
|
||||||
# PAPERLESS_ENABLE_HTTP_REMOTE_USER=true
|
|
||||||
# PAPERLESS_HTTP_REMOTE_USER_HEADER_NAME=HTTP_REMOTE_USER
|
|
||||||
paperless.{env.DOMAIN} {
|
|
||||||
import accesslog
|
|
||||||
import authelia
|
|
||||||
reverse_proxy 192.168.x.x:8000 # CHANGE IP
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Miniflux ---
|
|
||||||
# In compose env:
|
|
||||||
# AUTH_PROXY_HEADER=Remote-User
|
|
||||||
# AUTH_PROXY_USER_CREATION=true
|
|
||||||
miniflux.{env.DOMAIN} {
|
|
||||||
import accesslog
|
|
||||||
import authelia
|
|
||||||
reverse_proxy 192.168.x.x:8080 # CHANGE IP
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- BookStack ---
|
|
||||||
# In .env:
|
|
||||||
# AUTH_METHOD=http
|
|
||||||
# HTTP_AUTH_HEADER=Remote-User
|
|
||||||
# HTTP_AUTH_AUTO_INITIATE=true
|
|
||||||
# bookstack.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:80 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# CASE 2b -- App supports OIDC. Authelia acts as the OIDC provider.
|
|
||||||
#
|
|
||||||
# What is OIDC? OpenID Connect is an identity protocol on top of OAuth 2.0.
|
|
||||||
# Authelia becomes the "identity provider" (IdP). Apps redirect users to
|
|
||||||
# auth.DOMAIN, Authelia authenticates them and issues a signed token (JWT),
|
|
||||||
# then redirects back. The app trusts the token instead of checking a password.
|
|
||||||
#
|
|
||||||
# The Caddy config is identical to case 2a: `import authelia` gates the request.
|
|
||||||
# The difference is all on the app side -- it does a token exchange with
|
|
||||||
# Authelia's OIDC endpoint rather than reading a Remote-User header.
|
|
||||||
#
|
|
||||||
# SETUP REQUIRED in authelia/configuration.yml:
|
|
||||||
# Add an identity_providers.oidc block with a client entry for each app.
|
|
||||||
# Each app gets its own client_id and client_secret.
|
|
||||||
# See: https://www.authelia.com/configuration/identity-providers/openid-connect/
|
|
||||||
#
|
|
||||||
# Result: users never set a password in the app itself. After OIDC is working,
|
|
||||||
# disable all local accounts in the app -- Authelia is the only credential.
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# --- Audiobookshelf ---
|
|
||||||
# Native OIDC support. In Audiobookshelf Settings > Authentication:
|
|
||||||
# Enable OpenID Connect SSO
|
|
||||||
# Issuer URL: https://auth.DOMAIN
|
|
||||||
# Client ID / Secret: from identity_providers.oidc in configuration.yml
|
|
||||||
# Auto Register: on (creates user on first OIDC login)
|
|
||||||
# books.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:13378 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Jellyfin ---
|
|
||||||
# Requires the community SSO plugin (Jellyfin.Plugin.SSO).
|
|
||||||
# Install from the Plugin Catalogue, then configure OIDC pointing at Authelia.
|
|
||||||
# jellyfin.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:8096 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Immich ---
|
|
||||||
# No proxy-header auth; OIDC is the only Authelia path.
|
|
||||||
# In Immich Admin > Authentication Settings > OAuth:
|
|
||||||
# Issuer URL: https://auth.DOMAIN
|
|
||||||
# Client ID / Secret: from configuration.yml
|
|
||||||
# Auto register: on
|
|
||||||
# photos.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:2283 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Mealie ---
|
|
||||||
# OIDC supported. Set in Mealie's admin OIDC settings.
|
|
||||||
# Alternatively, skip OIDC and use case 3 as a simpler gate.
|
|
||||||
# mealie.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:9000 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Portainer ---
|
|
||||||
# Has OIDC for full SSO -- configure under Settings > Authentication.
|
|
||||||
# Or use case 3 (below) as a simpler gate without OIDC config.
|
|
||||||
# portainer.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:9000 # CHANGE IP (OIDC version)
|
|
||||||
# }
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# CASE 3 -- App keeps its own login. Authelia adds a 2FA gate in front.
|
|
||||||
#
|
|
||||||
# The user passes Authelia 2FA, then the app's own login appears.
|
|
||||||
# Use for apps that don't support proxy auth headers or OIDC, but you still
|
|
||||||
# want 2FA before they're even reachable.
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# --- Uptime Kuma ---
|
|
||||||
# No proxy auth, no native TOTP -- Authelia is the only way to add 2FA.
|
|
||||||
uptime.{env.DOMAIN} {
|
|
||||||
import accesslog
|
|
||||||
import authelia
|
|
||||||
reverse_proxy 192.168.x.x:3001 # CHANGE IP
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Portainer (simple gate, no OIDC) ---
|
|
||||||
portainer.{env.DOMAIN} {
|
|
||||||
import accesslog
|
|
||||||
import authelia
|
|
||||||
reverse_proxy 192.168.x.x:9000 # CHANGE IP
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Home Assistant ---
|
|
||||||
# Can also do case 2a via the trusted_networks auth provider + header forwarding.
|
|
||||||
# homeassistant.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:8123 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Vaultwarden ---
|
|
||||||
# Has its own strong auth + native TOTP. Many skip Authelia here entirely
|
|
||||||
# and rely on Vaultwarden's own 2FA (totally valid). Or use case 3 as an
|
|
||||||
# extra gate if you want 2FA even before the login page loads.
|
|
||||||
# vault.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:80 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- qBittorrent ---
|
|
||||||
# Web UI has its own auth. No proxy headers.
|
|
||||||
# torrent.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:8080 # CHANGE IP:PORT
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Plex ---
|
|
||||||
# DO NOT use `import authelia` with Plex/Emby. Plex is coupled to plex.tv
|
|
||||||
# cloud auth -- Authelia cannot replace it. More importantly, native clients
|
|
||||||
# (mobile, TV, desktop apps) connect via FQDN through Caddy but CANNOT
|
|
||||||
# complete Authelia's browser-redirect login flow (no cookie, no TOTP prompt).
|
|
||||||
# `import authelia` will break all native clients with a connection error.
|
|
||||||
#
|
|
||||||
# Correct approach: Case 4. Caddy terminates TLS and reverse proxies; Plex's
|
|
||||||
# own token auth handles access control. `import accesslog` keeps fail2ban
|
|
||||||
# watching the subdomain for scanners.
|
|
||||||
#
|
|
||||||
# plex.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# reverse_proxy 192.168.x.x:32400 # CHANGE IP -- NO import authelia
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Emby ---
|
|
||||||
# Same situation as Plex: native clients go through Caddy but cannot handle
|
|
||||||
# Authelia's login redirect. Use Case 4 -- Caddy + TLS, Emby's own auth.
|
|
||||||
#
|
|
||||||
# emby.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# reverse_proxy 192.168.x.x:8096 # CHANGE IP -- NO import authelia
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Gotify ---
|
|
||||||
# Notification server. Own auth, no proxy headers.
|
|
||||||
# gotify.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:8080 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- ntfy ---
|
|
||||||
# Notification server. Token-based auth, no proxy headers.
|
|
||||||
# ntfy.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:80 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- wg-easy ---
|
|
||||||
# WireGuard web UI. Own password, no proxy headers.
|
|
||||||
# NOTE: WireGuard clients connect directly to UDP 51820, not through Caddy.
|
|
||||||
# This gate only protects the web management UI.
|
|
||||||
# wg.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:51821 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Umami ---
|
|
||||||
# Web analytics. Own auth, no proxy headers.
|
|
||||||
# analytics.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:3000 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- phpIPAM ---
|
|
||||||
# IP address management. Own auth, no proxy headers.
|
|
||||||
# ipam.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:80 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Checkmk ---
|
|
||||||
# Monitoring. Own auth. No proxy headers in the free (Raw) edition.
|
|
||||||
# checkmk.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:5000 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Snipe-IT ---
|
|
||||||
# Asset management. Own auth, no proxy headers.
|
|
||||||
# assets.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:80 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Zammad ---
|
|
||||||
# Help desk / ticketing. Own auth, no proxy headers.
|
|
||||||
# tickets.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:3000 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Lubelog ---
|
|
||||||
# Vehicle maintenance log. Own auth, no proxy headers.
|
|
||||||
# cars.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:8080 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- UniFi Network Application ---
|
|
||||||
# Network controller. Own auth, no proxy headers (LDAP/RADIUS in enterprise).
|
|
||||||
# Serves HTTPS on 8443; skip TLS verify for the upstream.
|
|
||||||
# unifi.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:8443 { # CHANGE IP
|
|
||||||
# transport http { tls_insecure_skip_verify }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- MeshCentral ---
|
|
||||||
# Remote management server. Own auth; has OIDC in paid/enterprise builds.
|
|
||||||
# meshcentral.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# import authelia
|
|
||||||
# reverse_proxy 192.168.x.x:443 { # CHANGE IP
|
|
||||||
# transport http { tls_insecure_skip_verify }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# CASE 4 -- App handles its own auth. Authelia not involved.
|
|
||||||
# Still import accesslog so fail2ban covers this site.
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# --- Syncthing ---
|
|
||||||
# sync.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# reverse_proxy 192.168.x.x:8384 # CHANGE IP
|
|
||||||
# }
|
|
||||||
|
|
||||||
# --- Router / NAS admin UI ---
|
|
||||||
# router.{env.DOMAIN} {
|
|
||||||
# import accesslog
|
|
||||||
# reverse_proxy 192.168.x.x:443 {
|
|
||||||
# transport http { tls_insecure_skip_verify }
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
+62
-80
@@ -1,89 +1,71 @@
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Authelia + fail2ban
|
# Home camera stack
|
||||||
|
# - Frigate 0.17 (NVR + face recognition + LPR + audio detection)
|
||||||
|
# - Mosquitto (MQTT broker)
|
||||||
|
# - frigate-notify (event consumer -> ntfy push notifications)
|
||||||
#
|
#
|
||||||
# Self-hosted authentication portal (Authelia) plus an IP-banning sidecar
|
# First-run setup: see README.md.
|
||||||
# (fail2ban). Sits next to your dockerized Caddy on the main server and
|
|
||||||
# joins the same external `caddy_net` so Caddy reaches Authelia by
|
|
||||||
# container name (`authelia:9091`). Authelia is NOT port-mapped to the
|
|
||||||
# host -- there is no reason for anything outside the docker network to
|
|
||||||
# hit it directly.
|
|
||||||
#
|
|
||||||
# fail2ban runs in host network mode so its iptables bans drop packets
|
|
||||||
# at the host edge, which is the only place the bans actually work for
|
|
||||||
# traffic destined for docker-published ports.
|
|
||||||
#
|
|
||||||
# First-run: see README.md.
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
name: authelia
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
authelia:
|
frigate:
|
||||||
container_name: authelia
|
container_name: frigate
|
||||||
image: authelia/authelia:${AUTHELIA_VERSION:-4.39.19}
|
image: ghcr.io/blakeblackshear/frigate:0.17.1
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
stop_grace_period: 30s
|
||||||
- caddy_net
|
privileged: true # needed for USB Coral
|
||||||
expose:
|
shm_size: "512mb"
|
||||||
- 9091
|
env_file: .env
|
||||||
environment:
|
|
||||||
# Secrets are loaded from files mounted at /secrets (see volumes).
|
|
||||||
# The _FILE suffix is supported for any AUTHELIA_* env var.
|
|
||||||
- AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE=/secrets/JWT_SECRET
|
|
||||||
- AUTHELIA_SESSION_SECRET_FILE=/secrets/SESSION_SECRET
|
|
||||||
- AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/secrets/STORAGE_ENCRYPTION_KEY
|
|
||||||
- TZ=${TZ:-UTC}
|
|
||||||
# Enables Go-template substitution in configuration.yml so
|
|
||||||
# `{{ env "DOMAIN" }}` actually expands instead of being read as a
|
|
||||||
# literal string. Without this, Authelia parses the braces as part
|
|
||||||
# of the hostname and validate-config fails with `invalid character
|
|
||||||
# "{" in host name`. Inherited by `docker compose run --rm authelia`,
|
|
||||||
# so validate-config picks it up too.
|
|
||||||
- X_AUTHELIA_CONFIG_FILTERS=template
|
|
||||||
# Passed through so authelia/configuration.yml can use {{ env "DOMAIN" }}.
|
|
||||||
- DOMAIN=${DOMAIN}
|
|
||||||
volumes:
|
|
||||||
- ./authelia:/config
|
|
||||||
- ./authelia/secrets:/secrets:ro
|
|
||||||
healthcheck:
|
|
||||||
test: ['CMD', 'authelia', 'healthcheck']
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
start_period: 30s
|
|
||||||
|
|
||||||
fail2ban:
|
|
||||||
container_name: fail2ban
|
|
||||||
image: crazymax/fail2ban:${FAIL2BAN_VERSION:-1.1.0-r0}
|
|
||||||
restart: unless-stopped
|
|
||||||
# Host networking so iptables bans take effect on the host's edge,
|
|
||||||
# including DOCKER-USER chain rules that gate traffic to containers.
|
|
||||||
network_mode: host
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
- NET_RAW
|
|
||||||
environment:
|
|
||||||
- TZ=${TZ:-UTC}
|
|
||||||
- F2B_LOG_LEVEL=INFO
|
|
||||||
- F2B_DB_PURGE_AGE=7d
|
|
||||||
volumes:
|
|
||||||
- ./fail2ban/data:/data
|
|
||||||
# Authelia text log -- fail2ban watches this for 1FA/TOTP failures.
|
|
||||||
# Authelia writes it to /config/authelia.log = ./authelia/authelia.log.
|
|
||||||
# IMPORTANT: `touch authelia/authelia.log` before first `docker compose up`
|
|
||||||
# so Docker creates it as a file, not a directory (see README.md).
|
|
||||||
- ./authelia/authelia.log:/var/log/authelia/authelia.log:ro
|
|
||||||
# Caddy JSON access log -- covers ALL sites that import (accesslog),
|
|
||||||
# not just Authelia-gated ones. Mount the host directory read-only.
|
|
||||||
# Your Caddy must write here; see README.md "Caddy access log path".
|
|
||||||
- /var/log/caddy:/var/log/caddy:ro
|
|
||||||
# Wait for Authelia to pass its healthcheck before starting, so the
|
|
||||||
# authelia.log file exists before fail2ban tries to bind-mount it.
|
|
||||||
depends_on:
|
depends_on:
|
||||||
authelia:
|
- mosquitto
|
||||||
condition: service_healthy
|
devices:
|
||||||
|
- /dev/bus/usb:/dev/bus/usb # USB Coral
|
||||||
|
# - /dev/apex_0:/dev/apex_0 # PCIe Coral
|
||||||
|
# - /dev/dri/renderD128 # Intel/AMD hwaccel
|
||||||
|
volumes:
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
- ./frigate_config:/config
|
||||||
|
- /home/user/drives/sc-games/frigate:/media/frigate
|
||||||
|
- type: tmpfs
|
||||||
|
target: /tmp/cache
|
||||||
|
tmpfs:
|
||||||
|
size: 1000000000
|
||||||
|
ports:
|
||||||
|
- "8971:8971" # authenticated UI (proxied by Caddy)
|
||||||
|
- "5001:5000" # unauthenticated UI (LAN debug only)
|
||||||
|
- "8554:8554" # RTSP restream
|
||||||
|
- "8555:8555/tcp" # WebRTC TCP
|
||||||
|
- "8555:8555/udp" # WebRTC UDP
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://127.0.0.1:5000/api/version"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
start_period: 60s
|
||||||
|
|
||||||
networks:
|
mosquitto:
|
||||||
caddy_net:
|
container_name: mosquitto
|
||||||
external: true
|
hostname: mosquitto
|
||||||
|
image: eclipse-mosquitto:2
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "1883:1883" # MQTT (LAN ONLY -- never expose to internet)
|
||||||
|
volumes:
|
||||||
|
- ./mosquitto/config:/mosquitto/config
|
||||||
|
- ./mosquitto/data:/mosquitto/data
|
||||||
|
- ./mosquitto/log:/mosquitto/log
|
||||||
|
|
||||||
|
frigate-notify:
|
||||||
|
container_name: frigate-notify
|
||||||
|
hostname: frigate-notify
|
||||||
|
image: ghcr.io/0x2142/frigate-notify:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: .env
|
||||||
|
depends_on:
|
||||||
|
mosquitto:
|
||||||
|
condition: service_started
|
||||||
|
frigate:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- ./frigate-notify/config.yml:/app/config.yml:ro
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
# Matches Authelia's text-format log lines for failed authentication.
|
|
||||||
# Targets Authelia 4.38+. If you change `log.format` to `json` in
|
|
||||||
# authelia/configuration.yml, this regex needs updating.
|
|
||||||
#
|
|
||||||
# Test against a real log:
|
|
||||||
# docker compose exec fail2ban fail2ban-regex \
|
|
||||||
# /var/log/authelia/authelia.log \
|
|
||||||
# /data/filter.d/authelia.local
|
|
||||||
|
|
||||||
[Definition]
|
|
||||||
|
|
||||||
failregex = ^.*Unsuccessful (1FA|TOTP|Duo|U2F) authentication attempt by user.*remote_ip"?(:|=)"?<HOST>"?.*$
|
|
||||||
^.*user not found.*path=/api/reset-password/identity/start.*remote_ip"?(:|=)"?<HOST>"?.*$
|
|
||||||
|
|
||||||
ignoreregex =
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# Bans IPs that spray 401/403/404/429 across many requests against Caddy.
|
|
||||||
# Targets Caddy's default JSON access log shape (one JSON object per line).
|
|
||||||
# Verify against a real log:
|
|
||||||
# docker compose exec fail2ban fail2ban-regex \
|
|
||||||
# /var/log/caddy/access.log \
|
|
||||||
# /data/filter.d/caddy-4xx.local
|
|
||||||
|
|
||||||
[Definition]
|
|
||||||
|
|
||||||
failregex = ^.*"remote_ip":"<HOST>".*"status":(401|403|404|429).*$
|
|
||||||
|
|
||||||
ignoreregex = ^.*"uri":"/(favicon\.ico|robots\.txt|apple-touch-icon[^"]*)".*$
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
[authelia]
|
|
||||||
enabled = true
|
|
||||||
filter = authelia
|
|
||||||
logpath = /var/log/authelia/authelia.log
|
|
||||||
maxretry = 3
|
|
||||||
findtime = 10m
|
|
||||||
bantime = 1h
|
|
||||||
|
|
||||||
# DOCKER-USER is the chain Docker inserts before its own per-container
|
|
||||||
# rules; banning here drops packets destined for docker-published ports
|
|
||||||
# (i.e. your Caddy container's 80/443) before iptables routes them in.
|
|
||||||
chain = DOCKER-USER
|
|
||||||
banaction = iptables-allports
|
|
||||||
|
|
||||||
# Tuple-form action so we record where it came from. `port=anyport` is
|
|
||||||
# fine because chain=DOCKER-USER drops at the chain head regardless.
|
|
||||||
action = iptables-allports[name=authelia, chain=DOCKER-USER]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
[caddy-4xx]
|
|
||||||
enabled = true
|
|
||||||
filter = caddy-4xx
|
|
||||||
# Adjust if your Caddy writes elsewhere -- this must match the host path
|
|
||||||
# mounted into the fail2ban container in docker-compose.yml.
|
|
||||||
logpath = /var/log/caddy/access.log
|
|
||||||
maxretry = 30
|
|
||||||
findtime = 2m
|
|
||||||
bantime = 30m
|
|
||||||
|
|
||||||
chain = DOCKER-USER
|
|
||||||
banaction = iptables-allports
|
|
||||||
|
|
||||||
action = iptables-allports[name=caddy-4xx, chain=DOCKER-USER]
|
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
## frigate-notify config
|
||||||
|
## Docs: https://frigate-notify.0x2142.com
|
||||||
|
##
|
||||||
|
## Secrets and per-deployment values come from .env via the FN_* env vars.
|
||||||
|
## frigate-notify uses Viper-style env var lookup with DOUBLE underscores
|
||||||
|
## between YAML levels:
|
||||||
|
## frigate.server -> FN_FRIGATE__SERVER
|
||||||
|
## frigate.public_url -> FN_FRIGATE__PUBLIC_URL
|
||||||
|
## frigate.mqtt.password -> FN_FRIGATE__MQTT__PASSWORD
|
||||||
|
## alerts.ntfy.server -> FN_ALERTS__NTFY__SERVER
|
||||||
|
|
||||||
|
frigate:
|
||||||
|
server: # FN_FRIGATE__SERVER
|
||||||
|
ignoressl: true
|
||||||
|
public_url: # FN_FRIGATE__PUBLIC_URL
|
||||||
|
headers:
|
||||||
|
|
||||||
|
startup_check:
|
||||||
|
attempts: 5
|
||||||
|
interval: 30
|
||||||
|
|
||||||
|
webapi:
|
||||||
|
enabled: false # MQTT below is the primary event source
|
||||||
|
interval: 5
|
||||||
|
|
||||||
|
mqtt:
|
||||||
|
enabled: true
|
||||||
|
server: mosquitto # docker DNS name; constant for this stack
|
||||||
|
port: 1883
|
||||||
|
clientid: frigate-notify
|
||||||
|
username: frigate
|
||||||
|
password: # FN_FRIGATE__MQTT__PASSWORD
|
||||||
|
topic_prefix: frigate
|
||||||
|
|
||||||
|
cameras:
|
||||||
|
exclude:
|
||||||
|
|
||||||
|
|
||||||
|
alerts:
|
||||||
|
general:
|
||||||
|
# Title shows the recognized name (face recognition or LPR) when present,
|
||||||
|
# otherwise the generic label. Camera names are auto-titlecased by
|
||||||
|
# frigate-notify.
|
||||||
|
title: 'Frigate - {{ if .SubLabel }}{{ .SubLabel }}{{ else }}{{ .Label }}{{ end }} at {{ .Camera }}'
|
||||||
|
timeformat:
|
||||||
|
nosnap: allow
|
||||||
|
snap_bbox:
|
||||||
|
snap_timestamp:
|
||||||
|
snap_crop:
|
||||||
|
|
||||||
|
# Face recognition runs slightly AFTER the initial event. Without this
|
||||||
|
# delay, .SubLabel is empty and notifications always say "person".
|
||||||
|
recheck_delay: 10
|
||||||
|
|
||||||
|
quiet:
|
||||||
|
start:
|
||||||
|
end:
|
||||||
|
|
||||||
|
zones:
|
||||||
|
unzoned: allow
|
||||||
|
allow:
|
||||||
|
block:
|
||||||
|
|
||||||
|
labels:
|
||||||
|
min_score:
|
||||||
|
allow:
|
||||||
|
block:
|
||||||
|
|
||||||
|
sublabels:
|
||||||
|
# Once face recognition reliably IDs household members, list their names
|
||||||
|
# here to silence pings on them:
|
||||||
|
# block:
|
||||||
|
# - alex
|
||||||
|
# - bob
|
||||||
|
allow:
|
||||||
|
block:
|
||||||
|
|
||||||
|
discord:
|
||||||
|
enabled: false
|
||||||
|
webhook:
|
||||||
|
template:
|
||||||
|
|
||||||
|
gotify:
|
||||||
|
enabled: false
|
||||||
|
server:
|
||||||
|
token:
|
||||||
|
ignoressl:
|
||||||
|
template:
|
||||||
|
|
||||||
|
smtp:
|
||||||
|
enabled: false
|
||||||
|
server:
|
||||||
|
port:
|
||||||
|
tls:
|
||||||
|
user:
|
||||||
|
password:
|
||||||
|
recipient:
|
||||||
|
template:
|
||||||
|
|
||||||
|
telegram:
|
||||||
|
enabled: false
|
||||||
|
chatid:
|
||||||
|
token:
|
||||||
|
template:
|
||||||
|
|
||||||
|
pushover:
|
||||||
|
enabled: false
|
||||||
|
token:
|
||||||
|
userkey:
|
||||||
|
devices:
|
||||||
|
priority:
|
||||||
|
retry:
|
||||||
|
expire:
|
||||||
|
ttl:
|
||||||
|
template:
|
||||||
|
|
||||||
|
ntfy:
|
||||||
|
enabled: true
|
||||||
|
server: # FN_ALERTS__NTFY__SERVER
|
||||||
|
topic: "frigate"
|
||||||
|
ignoressl: false
|
||||||
|
|
||||||
|
# Priority + emoji change based on whether the person is recognized.
|
||||||
|
# Recognized -> priority 3 (normal) + wave; unknown -> priority 4 + alarm.
|
||||||
|
headers:
|
||||||
|
- X-Priority: '{{ if .SubLabel }}3{{ else }}4{{ end }}'
|
||||||
|
- X-Tags: '{{ if .SubLabel }}wave{{ else }}rotating_light{{ end }}'
|
||||||
|
|
||||||
|
template: |
|
||||||
|
{{ if .SubLabel -}}
|
||||||
|
{{ .SubLabel }} at {{ .Camera }}
|
||||||
|
{{- else -}}
|
||||||
|
{{ .Label }} at {{ .Camera }}
|
||||||
|
{{- end }}
|
||||||
|
{{ if gt (len .CurrentZones) 0 }}
|
||||||
|
Zone: {{ range $i, $z := .CurrentZones }}{{ if $i }}, {{ end }}{{ $z }}{{ end }}
|
||||||
|
{{- end }}
|
||||||
|
Score: {{ printf "%.0f" (mul .TopScore 100) }}%
|
||||||
|
Time: {{ .StartTime.Format "Mon 3:04 PM" }}
|
||||||
|
|
||||||
|
webhook:
|
||||||
|
enabled: false
|
||||||
|
server:
|
||||||
|
ignoressl:
|
||||||
|
headers:
|
||||||
|
template:
|
||||||
|
|
||||||
|
|
||||||
|
monitor:
|
||||||
|
enabled: false
|
||||||
|
url:
|
||||||
|
interval:
|
||||||
|
ignoressl:
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
##############################################################################
|
||||||
|
# Frigate 0.17 - SIMPLE (lower-CPU) variant
|
||||||
|
#
|
||||||
|
# Use if main config.yml uses too much CPU on your hardware.
|
||||||
|
#
|
||||||
|
# Differences vs main config.yml:
|
||||||
|
# - Sub-stream (640x480) used for detect; main stream for record only
|
||||||
|
# - Lower CPU: only the small sub-stream is decoded for detection
|
||||||
|
# - Face recognition still works for close-up faces; struggles at distance
|
||||||
|
# - LPR will rarely succeed (plate area too small in 640x480)
|
||||||
|
# - face_recognition.min_area lowered to 300 to catch smaller faces
|
||||||
|
#
|
||||||
|
# To activate:
|
||||||
|
# cp frigate_config/config.yml frigate_config/config.yml.bak
|
||||||
|
# cp frigate_config/alternatives/config-simple.yml frigate_config/config.yml
|
||||||
|
# docker compose restart frigate
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
version: 0.17-0
|
||||||
|
|
||||||
|
mqtt:
|
||||||
|
enabled: true
|
||||||
|
host: mosquitto
|
||||||
|
port: 1883
|
||||||
|
user: "{FRIGATE_MQTT_USER}"
|
||||||
|
password: "{FRIGATE_MQTT_PASSWORD}"
|
||||||
|
topic_prefix: frigate
|
||||||
|
client_id: frigate
|
||||||
|
stats_interval: 60
|
||||||
|
|
||||||
|
tls:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
audio:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
detectors:
|
||||||
|
coral:
|
||||||
|
type: edgetpu
|
||||||
|
device: usb
|
||||||
|
|
||||||
|
birdseye:
|
||||||
|
mode: continuous
|
||||||
|
|
||||||
|
semantic_search:
|
||||||
|
enabled: false
|
||||||
|
model_size: small
|
||||||
|
|
||||||
|
face_recognition:
|
||||||
|
enabled: true
|
||||||
|
model_size: small
|
||||||
|
min_area: 300 # smaller default since faces will be small on sub-stream
|
||||||
|
|
||||||
|
lpr:
|
||||||
|
enabled: true
|
||||||
|
model_size: small
|
||||||
|
|
||||||
|
classification:
|
||||||
|
bird:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
objects:
|
||||||
|
track:
|
||||||
|
- person
|
||||||
|
|
||||||
|
record:
|
||||||
|
enabled: true
|
||||||
|
continuous:
|
||||||
|
days: 0
|
||||||
|
motion:
|
||||||
|
days: 10
|
||||||
|
alerts:
|
||||||
|
retain:
|
||||||
|
days: 360
|
||||||
|
mode: motion
|
||||||
|
detections:
|
||||||
|
retain:
|
||||||
|
days: 360
|
||||||
|
mode: motion
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
enabled: true
|
||||||
|
bounding_box: true
|
||||||
|
crop: true
|
||||||
|
retain:
|
||||||
|
default: 360
|
||||||
|
|
||||||
|
go2rtc:
|
||||||
|
streams:
|
||||||
|
front_door:
|
||||||
|
- rtsp://{FRIGATE_RTSP_USER}:{FRIGATE_RTSP_PASSWORD}@{FRIGATE_FRONT_DOOR_IP}:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
|
||||||
|
front_door_sub:
|
||||||
|
- rtsp://{FRIGATE_RTSP_USER}:{FRIGATE_RTSP_PASSWORD}@{FRIGATE_FRONT_DOOR_IP}:554/cam/realmonitor?channel=1&subtype=1#backchannel=0
|
||||||
|
|
||||||
|
back_door:
|
||||||
|
- rtsp://{FRIGATE_RTSP_USER1}:{FRIGATE_RTSP_PASSWORD1}@{FRIGATE_BACK_DOOR_IP}:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
|
||||||
|
back_door_sub:
|
||||||
|
- rtsp://{FRIGATE_RTSP_USER1}:{FRIGATE_RTSP_PASSWORD1}@{FRIGATE_BACK_DOOR_IP}:554/cam/realmonitor?channel=1&subtype=1#backchannel=0
|
||||||
|
|
||||||
|
squirrel:
|
||||||
|
- rtsp://{FRIGATE_RTSP_USER2}:{FRIGATE_RTSP_PASSWORD2}@{FRIGATE_SQUIRREL_IP}:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
|
||||||
|
squirrel_sub:
|
||||||
|
- rtsp://{FRIGATE_RTSP_USER2}:{FRIGATE_RTSP_PASSWORD2}@{FRIGATE_SQUIRREL_IP}:554/cam/realmonitor?channel=1&subtype=1#backchannel=0
|
||||||
|
|
||||||
|
cameras:
|
||||||
|
|
||||||
|
front_door:
|
||||||
|
enabled: true
|
||||||
|
ffmpeg:
|
||||||
|
inputs:
|
||||||
|
- path: rtsp://127.0.0.1:8554/front_door
|
||||||
|
input_args: preset-rtsp-restream
|
||||||
|
roles:
|
||||||
|
- record
|
||||||
|
- path: rtsp://127.0.0.1:8554/front_door_sub
|
||||||
|
input_args: preset-rtsp-restream
|
||||||
|
roles:
|
||||||
|
- detect
|
||||||
|
detect:
|
||||||
|
enabled: true
|
||||||
|
width: 640
|
||||||
|
height: 480
|
||||||
|
fps: 5
|
||||||
|
motion:
|
||||||
|
mask:
|
||||||
|
- 0.582,0.426,0.582,0.476,0.989,0.534,0.994,0.467
|
||||||
|
- 0.984,0.614,0.513,0.99,0.991,0.996
|
||||||
|
- 0.001,0.163,0.085,0.165,0.095,0.255,0.003,0.263
|
||||||
|
|
||||||
|
back_door:
|
||||||
|
enabled: false
|
||||||
|
ffmpeg:
|
||||||
|
inputs:
|
||||||
|
- path: rtsp://127.0.0.1:8554/back_door
|
||||||
|
input_args: preset-rtsp-restream
|
||||||
|
roles:
|
||||||
|
- record
|
||||||
|
- path: rtsp://127.0.0.1:8554/back_door_sub
|
||||||
|
input_args: preset-rtsp-restream
|
||||||
|
roles:
|
||||||
|
- detect
|
||||||
|
detect:
|
||||||
|
enabled: true
|
||||||
|
width: 640
|
||||||
|
height: 480
|
||||||
|
fps: 5
|
||||||
|
|
||||||
|
squirrel:
|
||||||
|
enabled: false
|
||||||
|
ffmpeg:
|
||||||
|
inputs:
|
||||||
|
- path: rtsp://127.0.0.1:8554/squirrel
|
||||||
|
input_args: preset-rtsp-restream
|
||||||
|
roles:
|
||||||
|
- record
|
||||||
|
- path: rtsp://127.0.0.1:8554/squirrel_sub
|
||||||
|
input_args: preset-rtsp-restream
|
||||||
|
roles:
|
||||||
|
- detect
|
||||||
|
detect:
|
||||||
|
enabled: true
|
||||||
|
width: 640
|
||||||
|
height: 480
|
||||||
|
fps: 5
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
##############################################################################
|
||||||
|
# Frigate 0.17 production config
|
||||||
|
#
|
||||||
|
# - Main stream (2688x1520) used for detect + record on each camera
|
||||||
|
# (better face crops at distance, higher CPU)
|
||||||
|
# - Face recognition + LPR enabled (small models, CPU-friendly)
|
||||||
|
# - MQTT enabled for frigate-notify push events
|
||||||
|
#
|
||||||
|
# An alternate lower-CPU config that uses the sub-stream for detect lives at
|
||||||
|
# frigate_config/alternatives/config-simple.yml. To swap:
|
||||||
|
# cp frigate_config/config.yml frigate_config/config.yml.bak
|
||||||
|
# cp frigate_config/alternatives/config-simple.yml frigate_config/config.yml
|
||||||
|
# docker compose restart frigate
|
||||||
|
#
|
||||||
|
# Validate before restart:
|
||||||
|
# docker run --rm \
|
||||||
|
# -v $(pwd)/frigate_config/config.yml:/config/config.yml \
|
||||||
|
# --entrypoint python3 \
|
||||||
|
# ghcr.io/blakeblackshear/frigate:0.17.1 \
|
||||||
|
# -u -m frigate --validate-config
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
version: 0.17-0
|
||||||
|
|
||||||
|
mqtt:
|
||||||
|
enabled: true
|
||||||
|
host: mosquitto
|
||||||
|
port: 1883
|
||||||
|
user: "{FRIGATE_MQTT_USER}"
|
||||||
|
password: "{FRIGATE_MQTT_PASSWORD}"
|
||||||
|
topic_prefix: frigate
|
||||||
|
client_id: frigate
|
||||||
|
stats_interval: 60
|
||||||
|
|
||||||
|
tls:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
audio:
|
||||||
|
enabled: false # flip on when a mic-equipped camera arrives
|
||||||
|
|
||||||
|
detectors:
|
||||||
|
coral:
|
||||||
|
type: edgetpu
|
||||||
|
device: usb
|
||||||
|
|
||||||
|
birdseye:
|
||||||
|
mode: continuous
|
||||||
|
|
||||||
|
semantic_search:
|
||||||
|
enabled: false
|
||||||
|
model_size: small
|
||||||
|
|
||||||
|
face_recognition:
|
||||||
|
enabled: true
|
||||||
|
model_size: small
|
||||||
|
|
||||||
|
lpr:
|
||||||
|
enabled: true
|
||||||
|
model_size: small
|
||||||
|
# known_plates:
|
||||||
|
# owner:
|
||||||
|
# - "ABC-1234"
|
||||||
|
|
||||||
|
classification:
|
||||||
|
bird:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
objects:
|
||||||
|
track:
|
||||||
|
- person
|
||||||
|
|
||||||
|
# ---------- global record defaults (0.17 schema) ----------
|
||||||
|
|
||||||
|
record:
|
||||||
|
enabled: true
|
||||||
|
continuous:
|
||||||
|
days: 0
|
||||||
|
motion:
|
||||||
|
days: 10
|
||||||
|
alerts:
|
||||||
|
retain:
|
||||||
|
days: 360
|
||||||
|
mode: motion
|
||||||
|
detections:
|
||||||
|
retain:
|
||||||
|
days: 360
|
||||||
|
mode: motion
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
enabled: true
|
||||||
|
bounding_box: true
|
||||||
|
crop: true
|
||||||
|
retain:
|
||||||
|
default: 360
|
||||||
|
|
||||||
|
# ---------- go2rtc: restream from cameras ----------
|
||||||
|
|
||||||
|
go2rtc:
|
||||||
|
streams:
|
||||||
|
front_door:
|
||||||
|
- rtsp://{FRIGATE_RTSP_USER}:{FRIGATE_RTSP_PASSWORD}@{FRIGATE_FRONT_DOOR_IP}:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
|
||||||
|
front_door_sub:
|
||||||
|
- rtsp://{FRIGATE_RTSP_USER}:{FRIGATE_RTSP_PASSWORD}@{FRIGATE_FRONT_DOOR_IP}:554/cam/realmonitor?channel=1&subtype=1#backchannel=0
|
||||||
|
|
||||||
|
back_door:
|
||||||
|
- rtsp://{FRIGATE_RTSP_USER1}:{FRIGATE_RTSP_PASSWORD1}@{FRIGATE_BACK_DOOR_IP}:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
|
||||||
|
back_door_sub:
|
||||||
|
- rtsp://{FRIGATE_RTSP_USER1}:{FRIGATE_RTSP_PASSWORD1}@{FRIGATE_BACK_DOOR_IP}:554/cam/realmonitor?channel=1&subtype=1#backchannel=0
|
||||||
|
|
||||||
|
squirrel:
|
||||||
|
- rtsp://{FRIGATE_RTSP_USER2}:{FRIGATE_RTSP_PASSWORD2}@{FRIGATE_SQUIRREL_IP}:554/cam/realmonitor?channel=1&subtype=0#backchannel=0
|
||||||
|
squirrel_sub:
|
||||||
|
- rtsp://{FRIGATE_RTSP_USER2}:{FRIGATE_RTSP_PASSWORD2}@{FRIGATE_SQUIRREL_IP}:554/cam/realmonitor?channel=1&subtype=1#backchannel=0
|
||||||
|
|
||||||
|
# ---------- cameras ----------
|
||||||
|
|
||||||
|
cameras:
|
||||||
|
|
||||||
|
front_door:
|
||||||
|
enabled: true
|
||||||
|
ffmpeg:
|
||||||
|
inputs:
|
||||||
|
# Main stream used for both detect and record. Single connection,
|
||||||
|
# higher resolution -> better face recognition crops.
|
||||||
|
- path: rtsp://127.0.0.1:8554/front_door
|
||||||
|
input_args: preset-rtsp-restream
|
||||||
|
roles:
|
||||||
|
- detect
|
||||||
|
- record
|
||||||
|
detect:
|
||||||
|
enabled: true
|
||||||
|
width: 2688 # adjust if your main stream is different
|
||||||
|
height: 1520
|
||||||
|
fps: 5 # detection doesn't need high fps; saves CPU
|
||||||
|
motion:
|
||||||
|
mask:
|
||||||
|
- 0.582,0.426,0.582,0.476,0.989,0.534,0.994,0.467
|
||||||
|
- 0.984,0.614,0.513,0.99,0.991,0.996
|
||||||
|
- 0.001,0.163,0.085,0.165,0.095,0.255,0.003,0.263
|
||||||
|
|
||||||
|
back_door:
|
||||||
|
enabled: false
|
||||||
|
ffmpeg:
|
||||||
|
inputs:
|
||||||
|
- path: rtsp://127.0.0.1:8554/back_door
|
||||||
|
input_args: preset-rtsp-restream
|
||||||
|
roles:
|
||||||
|
- detect
|
||||||
|
- record
|
||||||
|
detect:
|
||||||
|
enabled: true
|
||||||
|
width: 2688
|
||||||
|
height: 1520
|
||||||
|
fps: 5
|
||||||
|
|
||||||
|
squirrel:
|
||||||
|
enabled: false
|
||||||
|
ffmpeg:
|
||||||
|
inputs:
|
||||||
|
- path: rtsp://127.0.0.1:8554/squirrel
|
||||||
|
input_args: preset-rtsp-restream
|
||||||
|
roles:
|
||||||
|
- detect
|
||||||
|
- record
|
||||||
|
detect:
|
||||||
|
enabled: true
|
||||||
|
width: 2688
|
||||||
|
height: 1520
|
||||||
|
fps: 5
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# HOW TO ADD A CAMERA WITH A MIC (e.g., future Anpviz)
|
||||||
|
#
|
||||||
|
# 1. Set audio.enabled: true at the top of this file.
|
||||||
|
#
|
||||||
|
# 2. In go2rtc.streams, add the ffmpeg audio transcode line so live view
|
||||||
|
# has both AAC (for MSE) and opus (for WebRTC):
|
||||||
|
# anpviz:
|
||||||
|
# - rtsp://{FRIGATE_RTSP_USER3}:{FRIGATE_RTSP_PASSWORD3}@{FRIGATE_ANPVIZ_IP}:554/...#backchannel=0
|
||||||
|
# - "ffmpeg:anpviz#audio=aac#audio=opus"
|
||||||
|
#
|
||||||
|
# 3. In cameras, add the 'audio' role and set the audio-aware record preset:
|
||||||
|
# anpviz:
|
||||||
|
# enabled: true
|
||||||
|
# ffmpeg:
|
||||||
|
# output_args:
|
||||||
|
# record: preset-record-generic-audio-aac
|
||||||
|
# inputs:
|
||||||
|
# - path: rtsp://127.0.0.1:8554/anpviz
|
||||||
|
# input_args: preset-rtsp-restream
|
||||||
|
# roles:
|
||||||
|
# - detect
|
||||||
|
# - record
|
||||||
|
# - audio
|
||||||
|
#
|
||||||
|
# 4. Add FRIGATE_RTSP_USER3 / _PASSWORD3 / FRIGATE_ANPVIZ_IP to .env.
|
||||||
|
#
|
||||||
|
# 5. RTSP paths vary by vendor:
|
||||||
|
# Anpviz H-series (Hikvision OEM): /Streaming/Channels/101 (main), /102 (sub)
|
||||||
|
# Anpviz U-series (Dahua OEM): /cam/realmonitor?channel=1&subtype=0 (main)
|
||||||
|
# /cam/realmonitor?channel=1&subtype=1 (sub)
|
||||||
|
##############################################################################
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Mosquitto MQTT broker
|
||||||
|
#
|
||||||
|
# This config assumes the password file exists at /mosquitto/config/passwd.
|
||||||
|
# On first deployment, see README "First-run on the Frigate host" for how
|
||||||
|
# to bootstrap that file.
|
||||||
|
|
||||||
|
listener 1883 0.0.0.0
|
||||||
|
protocol mqtt
|
||||||
|
|
||||||
|
persistence true
|
||||||
|
persistence_location /mosquitto/data/
|
||||||
|
|
||||||
|
log_dest stdout
|
||||||
|
log_dest file /mosquitto/log/mosquitto.log
|
||||||
|
|
||||||
|
allow_anonymous false
|
||||||
|
password_file /mosquitto/config/passwd
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
# Pi doorbell PTT
|
||||||
|
|
||||||
|
Turns a Raspberry Pi into a network speaker so a phone hitting
|
||||||
|
`https://doorbell.yourdomain.com` can see/hear the front-door Frigate feed
|
||||||
|
and hold a button to talk through a speaker mounted at the door.
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
- Any Raspberry Pi (Zero W 1st gen is enough; Zero 2 W is better for live
|
||||||
|
two-way; Pi 3A+ has a 3.5mm jack onboard and skips the OTG adapter)
|
||||||
|
- Audio output, one of:
|
||||||
|
- USB speaker + micro-USB-to-USB-A OTG adapter (simplest)
|
||||||
|
- 3.5mm powered speaker (Pi 3A+ has the jack; Zero W does not)
|
||||||
|
- I2S DAC HAT (best quality, requires GPIO header)
|
||||||
|
- microSD card, power supply, WiFi or USB ethernet
|
||||||
|
|
||||||
|
## Install on the Pi
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From your laptop/desktop:
|
||||||
|
scp -r pi/ pi@PI_LAN_IP:~/doorbell-src
|
||||||
|
|
||||||
|
# SSH to the Pi:
|
||||||
|
ssh pi@PI_LAN_IP
|
||||||
|
cd ~/doorbell-src
|
||||||
|
chmod +x install.sh
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer apt-installs ffmpeg + alsa-utils + Python deps, creates a
|
||||||
|
virtualenv, drops `server.py` into `~/doorbell/`, installs and enables the
|
||||||
|
systemd service, runs `speaker-test` to confirm ALSA output works, and
|
||||||
|
starts the service.
|
||||||
|
|
||||||
|
## Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:5555/healthz # -> ok
|
||||||
|
sudo journalctl -u doorbell -f # live logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wire it up
|
||||||
|
|
||||||
|
1. On the Caddy host, add the `doorbell.yourdomain.com` block from
|
||||||
|
`../caddy/Caddyfile` and reload Caddy.
|
||||||
|
2. DNS: point `doorbell.yourdomain.com` at the Caddy host's public IP.
|
||||||
|
3. Open `https://doorbell.yourdomain.com` on an Android phone.
|
||||||
|
4. Grant the one-time microphone permission.
|
||||||
|
5. Tap **Unmute camera** if browser autoplay swallowed the audio.
|
||||||
|
6. Hold the big green button to talk.
|
||||||
|
|
||||||
|
Add to home screen (Chrome menu -> Add to home screen) for an app-like
|
||||||
|
experience.
|
||||||
|
|
||||||
|
## Choosing the camera
|
||||||
|
|
||||||
|
Each Pi is hardcoded to one camera -- the one whose mic and speaker are
|
||||||
|
physically co-located with this Pi. The PTT button on this page only
|
||||||
|
talks to *this* Pi's speaker, so mixing cameras here would let a misclick
|
||||||
|
talk into the wrong room.
|
||||||
|
|
||||||
|
`server.py` near the top:
|
||||||
|
```python
|
||||||
|
CAMERA_NAME = "front_door"
|
||||||
|
```
|
||||||
|
|
||||||
|
The name must match a `go2rtc.streams` entry in
|
||||||
|
`frigate_config/config.yml` and the camera must be `enabled: true` in
|
||||||
|
Frigate. After editing:
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart doorbell
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multiple doorbell Pis
|
||||||
|
|
||||||
|
Run one copy of this app per Pi, each on its own subdomain (e.g.
|
||||||
|
`frontdoor.yourdomain.com`, `backdoor.yourdomain.com`). Add a Caddy
|
||||||
|
block per subdomain pointing at that Pi's LAN IP -- same shape as the
|
||||||
|
existing `doorbell.yourdomain.com` block in `../caddy/Caddyfile`.
|
||||||
|
|
||||||
|
To render quick-jump buttons to the other Pis at the top of the page,
|
||||||
|
fill in `PEER_LINKS` near the top of `server.py`:
|
||||||
|
```python
|
||||||
|
PEER_LINKS = [
|
||||||
|
{"label": "Back door", "url": "https://backdoor.yourdomain.com"},
|
||||||
|
{"label": "Squirrel", "url": "https://squirrel.yourdomain.com"},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
Leave it as `[]` (the default) and the row is hidden. Restart with
|
||||||
|
`sudo systemctl restart doorbell` after editing.
|
||||||
|
|
||||||
|
## Audio stack
|
||||||
|
|
||||||
|
ALSA-only -- no PipeWire/PulseAudio. Lighter on the Pi Zero. If you ever
|
||||||
|
need PipeWire (e.g., to share the speaker with another app), change
|
||||||
|
`'-f', 'alsa'` to `'-f', 'pulse'` in `server.py` and install the
|
||||||
|
PipeWire/Pulse compatibility shim.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### speaker-test fails
|
||||||
|
|
||||||
|
USB/3.5mm output isn't the default ALSA card. Check:
|
||||||
|
```bash
|
||||||
|
aplay -l
|
||||||
|
```
|
||||||
|
If your speaker isn't card 0, create `/etc/asound.conf`:
|
||||||
|
```
|
||||||
|
defaults.pcm.card 1
|
||||||
|
defaults.ctl.card 1
|
||||||
|
```
|
||||||
|
(Replace `1` with whatever card your speaker is.)
|
||||||
|
|
||||||
|
### Video plays but talk button stuck on "Disconnected"
|
||||||
|
|
||||||
|
The WebSocket isn't reaching the Pi. Most common: Caddy not proxying
|
||||||
|
`doorbell.yourdomain.com` -> Pi correctly. From the Caddy host:
|
||||||
|
```bash
|
||||||
|
curl -i http://PI_LAN_IP:5555/healthz # should return 200 ok
|
||||||
|
```
|
||||||
|
|
||||||
|
### Feedback loop when talking
|
||||||
|
|
||||||
|
The page auto-mutes the camera while the PTT button is held, so this
|
||||||
|
should not happen. If it does, increase distance between Pi speaker and
|
||||||
|
camera mic, or turn the speaker volume down.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Doorbell PTT server
|
||||||
|
After=network-online.target sound.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=pi
|
||||||
|
Group=audio
|
||||||
|
WorkingDirectory=/home/pi/doorbell
|
||||||
|
ExecStart=/home/pi/doorbell-venv/bin/python /home/pi/doorbell/server.py
|
||||||
|
Restart=always
|
||||||
|
RestartSec=3
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Pi Zero W setup for the doorbell PTT server.
|
||||||
|
# Run as the 'pi' user after flashing Raspberry Pi OS Lite (Bookworm).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# chmod +x install.sh
|
||||||
|
# ./install.sh
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo ">>> Installing OS packages..."
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y ffmpeg alsa-utils python3-venv python3-pip
|
||||||
|
|
||||||
|
echo ">>> Creating project dirs..."
|
||||||
|
mkdir -p "$HOME/doorbell"
|
||||||
|
|
||||||
|
echo ">>> Creating Python virtualenv..."
|
||||||
|
python3 -m venv "$HOME/doorbell-venv"
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$HOME/doorbell-venv/bin/activate"
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install flask flask-sock
|
||||||
|
|
||||||
|
echo ">>> Copying server.py..."
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cp "$SCRIPT_DIR/server.py" "$HOME/doorbell/server.py"
|
||||||
|
|
||||||
|
echo ">>> Installing systemd service..."
|
||||||
|
sudo cp "$SCRIPT_DIR/doorbell.service" /etc/systemd/system/doorbell.service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable doorbell
|
||||||
|
|
||||||
|
echo ">>> Testing audio output..."
|
||||||
|
echo "You should hear 'front left' in a moment. Ctrl-C if nothing plays."
|
||||||
|
speaker-test -D default -c 2 -t wav -l 1 || {
|
||||||
|
echo "!! speaker-test failed. Fix ALSA output before starting the service."
|
||||||
|
echo " Try: sudo raspi-config -> System Options -> Audio"
|
||||||
|
echo " Or: aplay -l and edit /etc/asound.conf"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo ">>> Starting doorbell service..."
|
||||||
|
sudo systemctl restart doorbell
|
||||||
|
sleep 2
|
||||||
|
sudo systemctl status doorbell --no-pager
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Done. Quick checks:"
|
||||||
|
echo " curl http://127.0.0.1:5555/healthz"
|
||||||
|
echo " sudo journalctl -u doorbell -f"
|
||||||
|
echo "=========================================="
|
||||||
+269
@@ -0,0 +1,269 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Doorbell PTT server for Pi Zero W.
|
||||||
|
|
||||||
|
Serves a single-page web app that:
|
||||||
|
* shows the Frigate WebRTC live feed (video + camera mic if present)
|
||||||
|
* provides a push-to-talk button that streams phone mic audio over a
|
||||||
|
WebSocket; this script decodes and plays it out ALSA.
|
||||||
|
|
||||||
|
Deployment:
|
||||||
|
* listens on 127.0.0.1:5555; expose publicly via Caddy reverse proxy
|
||||||
|
* runs under systemd as the 'pi' user
|
||||||
|
* requires: python3-flask, flask-sock, ffmpeg, alsa-utils
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from flask import Flask, render_template_string
|
||||||
|
from flask_sock import Sock
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
sock = Sock(app)
|
||||||
|
|
||||||
|
# Camera this Pi corresponds to. Must match a go2rtc stream name in
|
||||||
|
# frigate_config/config.yml. The PTT button talks to the speaker physically
|
||||||
|
# attached to this Pi, so this should be the camera at the same location.
|
||||||
|
CAMERA_NAME = "front_door"
|
||||||
|
|
||||||
|
# Optional jump-links to sibling doorbell Pis (each running its own copy of
|
||||||
|
# this app, hardcoded to its own camera). Rendered as a row of buttons above
|
||||||
|
# the PTT button when non-empty. Leave empty if there are no other Pis.
|
||||||
|
PEER_LINKS = [
|
||||||
|
# {"label": "Back door", "url": "https://backdoor.yourdomain.com"},
|
||||||
|
]
|
||||||
|
|
||||||
|
PAGE = """<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no,viewport-fit=cover">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<title>Doorbell</title>
|
||||||
|
<style>
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
html,body{margin:0;padding:0;height:100%;background:#000;color:#fff;
|
||||||
|
font-family:system-ui,-apple-system,sans-serif;overflow:hidden;
|
||||||
|
touch-action:none;-webkit-user-select:none;user-select:none}
|
||||||
|
#wrap{display:flex;flex-direction:column;height:100vh;height:100dvh}
|
||||||
|
#video{flex:1;min-height:0;background:#000;position:relative}
|
||||||
|
video{width:100%;height:100%;object-fit:contain;background:#000}
|
||||||
|
#controls{padding:16px;display:flex;flex-direction:column;gap:10px;
|
||||||
|
background:#111;padding-bottom:max(16px,env(safe-area-inset-bottom))}
|
||||||
|
#ptt{font-size:24px;padding:28px;border:none;border-radius:14px;
|
||||||
|
background:#2d6a2d;color:#fff;font-weight:700;touch-action:none;
|
||||||
|
transition:background .05s,transform .05s}
|
||||||
|
#ptt.active{background:#d33;transform:scale(.98)}
|
||||||
|
#ptt:disabled{background:#333;color:#666}
|
||||||
|
.row{display:flex;gap:8px}
|
||||||
|
.row button{flex:1;padding:10px;background:#333;color:#fff;border:none;
|
||||||
|
border-radius:8px;font-size:13px}
|
||||||
|
.row a{flex:1;padding:10px;background:#333;color:#fff;border-radius:8px;
|
||||||
|
font-size:13px;text-decoration:none;text-align:center;
|
||||||
|
display:flex;align-items:center;justify-content:center}
|
||||||
|
#status{font-size:12px;color:#888;text-align:center;min-height:1em}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="wrap">
|
||||||
|
<div id="video">
|
||||||
|
<video id="cam" autoplay playsinline muted></video>
|
||||||
|
</div>
|
||||||
|
<div id="controls">
|
||||||
|
<div class="row" id="peers" style="display:none"></div>
|
||||||
|
<button id="ptt" disabled>Connecting...</button>
|
||||||
|
<div class="row">
|
||||||
|
<button id="unmute">Unmute camera</button>
|
||||||
|
<button id="wake">Keep screen on</button>
|
||||||
|
<button id="reload">Reconnect</button>
|
||||||
|
</div>
|
||||||
|
<div id="status"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ---- CONFIG ---------------------------------------------------------
|
||||||
|
const CAMERA_NAME = {{ camera_name|tojson }};
|
||||||
|
const PEERS = {{ peers|tojson }};
|
||||||
|
const FRIGATE_WEBRTC_URL = "/frigate/api/go2rtc/api/webrtc?src=" + encodeURIComponent(CAMERA_NAME);
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
const ptt = $('ptt'), status = $('status'), video = $('cam'),
|
||||||
|
unmute = $('unmute'), reload = $('reload'), wake = $('wake'),
|
||||||
|
peersRow = $('peers');
|
||||||
|
|
||||||
|
let ws, mediaRecorder, micStream, wakeLock = null;
|
||||||
|
|
||||||
|
const log = m => { status.textContent = m; console.log('[doorbell]', m); };
|
||||||
|
|
||||||
|
if (PEERS.length) {
|
||||||
|
for (const p of PEERS) {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = p.url;
|
||||||
|
a.textContent = p.label;
|
||||||
|
peersRow.appendChild(a);
|
||||||
|
}
|
||||||
|
peersRow.style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startVideo(){
|
||||||
|
try {
|
||||||
|
const pc = new RTCPeerConnection();
|
||||||
|
pc.addTransceiver('video', {direction:'recvonly'});
|
||||||
|
pc.addTransceiver('audio', {direction:'recvonly'});
|
||||||
|
pc.ontrack = e => { video.srcObject = e.streams[0]; };
|
||||||
|
pc.oniceconnectionstatechange = () => log('ICE: ' + pc.iceConnectionState);
|
||||||
|
|
||||||
|
const offer = await pc.createOffer();
|
||||||
|
await pc.setLocalDescription(offer);
|
||||||
|
|
||||||
|
const resp = await fetch(FRIGATE_WEBRTC_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/sdp'},
|
||||||
|
body: pc.localDescription.sdp,
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if(!resp.ok) throw new Error('Frigate returned ' + resp.status);
|
||||||
|
const answer = await resp.text();
|
||||||
|
await pc.setRemoteDescription({type:'answer', sdp: answer});
|
||||||
|
log('Camera connected');
|
||||||
|
} catch(e){ log('Video error: ' + e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
unmute.onclick = () => {
|
||||||
|
video.muted = !video.muted;
|
||||||
|
unmute.textContent = video.muted ? 'Unmute camera' : 'Mute camera';
|
||||||
|
if(!video.muted) video.play().catch(()=>{});
|
||||||
|
};
|
||||||
|
|
||||||
|
reload.onclick = () => location.reload();
|
||||||
|
|
||||||
|
wake.onclick = async () => {
|
||||||
|
if(!('wakeLock' in navigator)){ log('Wake lock not supported'); return; }
|
||||||
|
if(wakeLock){
|
||||||
|
wakeLock.release(); wakeLock = null;
|
||||||
|
wake.textContent = 'Keep screen on';
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
wakeLock = await navigator.wakeLock.request('screen');
|
||||||
|
wake.textContent = 'Screen locked on';
|
||||||
|
wakeLock.addEventListener('release', () => {
|
||||||
|
wake.textContent = 'Keep screen on'; wakeLock = null;
|
||||||
|
});
|
||||||
|
} catch(e){ log('Wake lock failed: ' + e.message); }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function setupPTT(){
|
||||||
|
try {
|
||||||
|
micStream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: {echoCancellation: true, noiseSuppression: true, autoGainControl: true}
|
||||||
|
});
|
||||||
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
ws = new WebSocket(proto + '//' + location.host + '/audio');
|
||||||
|
ws.binaryType = 'arraybuffer';
|
||||||
|
ws.onopen = () => {
|
||||||
|
ptt.disabled = false;
|
||||||
|
ptt.textContent = 'Hold to talk';
|
||||||
|
log('Ready');
|
||||||
|
};
|
||||||
|
ws.onclose = () => {
|
||||||
|
ptt.disabled = true;
|
||||||
|
ptt.textContent = 'Disconnected';
|
||||||
|
log('WebSocket closed -- tap Reconnect');
|
||||||
|
};
|
||||||
|
ws.onerror = () => log('WebSocket error');
|
||||||
|
} catch(e){ log('Mic permission error: ' + e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function startTalking(e){
|
||||||
|
if(!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||||
|
e.preventDefault();
|
||||||
|
ptt.classList.add('active');
|
||||||
|
ptt.textContent = 'TALKING';
|
||||||
|
video.muted = true; // prevent feedback loop
|
||||||
|
mediaRecorder = new MediaRecorder(micStream, {mimeType:'audio/webm;codecs=opus'});
|
||||||
|
mediaRecorder.ondataavailable = ev => {
|
||||||
|
if(ev.data.size > 0 && ws.readyState === WebSocket.OPEN){
|
||||||
|
ev.data.arrayBuffer().then(buf => ws.send(buf));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mediaRecorder.start(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTalking(e){
|
||||||
|
e && e.preventDefault();
|
||||||
|
if(mediaRecorder && mediaRecorder.state === 'recording'){
|
||||||
|
mediaRecorder.stop();
|
||||||
|
}
|
||||||
|
ptt.classList.remove('active');
|
||||||
|
ptt.textContent = 'Hold to talk';
|
||||||
|
video.muted = false;
|
||||||
|
video.play().catch(()=>{});
|
||||||
|
}
|
||||||
|
|
||||||
|
ptt.addEventListener('touchstart', startTalking, {passive:false});
|
||||||
|
ptt.addEventListener('touchend', stopTalking, {passive:false});
|
||||||
|
ptt.addEventListener('touchcancel', stopTalking, {passive:false});
|
||||||
|
ptt.addEventListener('mousedown', startTalking);
|
||||||
|
ptt.addEventListener('mouseup', stopTalking);
|
||||||
|
ptt.addEventListener('mouseleave', stopTalking);
|
||||||
|
|
||||||
|
startVideo();
|
||||||
|
setupPTT();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template_string(PAGE, camera_name=CAMERA_NAME, peers=PEER_LINKS)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/healthz')
|
||||||
|
def healthz():
|
||||||
|
return 'ok'
|
||||||
|
|
||||||
|
|
||||||
|
@sock.route('/audio')
|
||||||
|
def audio(ws):
|
||||||
|
ff = subprocess.Popen(
|
||||||
|
[
|
||||||
|
'ffmpeg',
|
||||||
|
'-loglevel', 'error',
|
||||||
|
'-f', 'webm', '-i', 'pipe:0',
|
||||||
|
'-f', 'alsa', 'default',
|
||||||
|
],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = ws.receive()
|
||||||
|
if data is None:
|
||||||
|
break
|
||||||
|
if isinstance(data, (bytes, bytearray)):
|
||||||
|
try:
|
||||||
|
ff.stdin.write(data)
|
||||||
|
ff.stdin.flush()
|
||||||
|
except BrokenPipeError:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
ff.stdin.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
ff.terminate()
|
||||||
|
ff.wait(timeout=2)
|
||||||
|
except Exception:
|
||||||
|
ff.kill()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 127.0.0.1 only -- Caddy reverse-proxies from the public domain
|
||||||
|
app.run(host='127.0.0.1', port=5555, threaded=True)
|
||||||
Reference in New Issue
Block a user