commit e7b8d4df178a8b16438c7e98f3883770fecf865d Author: Your Name Date: Mon Jun 1 21:24:09 2026 -0400 initial commit: Dawarich LXC role (CT 459 on pve02, .159) Self-hosted location history. 4-container compose: Rails 8 app + Sidekiq + PostGIS 16-3.4 + Redis 7, plus watchtower. Authentik OIDC end-to-end. Image pinned at freikin/dawarich:1.7.11 (OIDC support requires >= 1.7.8). PostGIS DB lives in this LXC, not on the central DB VM (.172) — central image is postgres:16-alpine without postgis, swapping it carries broader blast radius than colocating here. Convention exception captured in homelab-docs project_dawarich memory. Roles: - dawarich: system + Docker + compose + weekly prune timer - alloy: logs+journald → Loki, node metrics → Prometheus Bring-up sequence proven 2026-06-01. README documents the 5-trap build chain (image version, entrypoint scripts, solid_cache SQLite bind mount, APPLICATION_HOSTS+localhost, force_ssl+healthcheck). Co-Authored-By: Claude Opus 4.7 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8231d00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +vars/vault.yml.dec +*.retry +__pycache__/ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..0dbb49b --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# homelab-ansible-lxc-dawarich + +Configuration for **dawarich** (LXC 459 on pve02, `192.168.1.159`) — self-hosted +location-history app, [Dawarich](https://dawarich.app). Ingests location data +from OwnTracks, the Dawarich iOS/Android apps, and Google Takeout exports; +renders heatmaps, places visited, distance/time statistics, and timeline view. + +- **URL:** `dawarich.balders.ca` (Caddy → `192.168.1.159:3000`, Authentik OIDC) +- **Image:** `freikin/dawarich:1.7.11` (pinned in `vars/main.yml`) — **must be ≥ 1.7.8** for OIDC. +- **Tier:** T2 (Proxmox) +- **Auth:** Authentik OIDC (SSO-only — `ALLOW_EMAIL_PASSWORD_REGISTRATION=false`) +- **DB:** PostGIS 16-3.4 **local to this LXC**. The central DB VM (.172) runs + `postgres:16-alpine` which doesn't bundle the PostGIS extension; rather than + swap that image (16 other databases) we run a colocated container here. See + memory `project_dawarich` for the convention-exception rationale. + +## Architecture + +``` + Phone (OwnTracks / Dawarich app) + │ HTTP POST /api/v1/owntracks/points?api_key=… + ▼ + Caddy (TLS, Authentik OIDC on UI; API bypassed by query-string auth) + │ + ▼ + dawarich_app (Rails 8 + Puma) ──▶ dawarich_db (PostGIS 16-3.4) + │ ▲ + ▼ │ + dawarich_sidekiq ─── enqueue ─────────── dawarich_redis + └──── solid_queue ──▶ /dawarich_db_data (SQLite) +``` + +## Roles + +| Role | Purpose | +|------------|---------| +| `dawarich` | System setup, Docker, registry mirror, 4-container compose, prune timer | +| `alloy` | Docker logs + journald → Loki, node metrics → Prometheus | + +## Prerequisites (before first deploy) + +1. **LXC provisioned** — `module.dawarich` in homelab-terraform (CTID 459, + pve02, `.159`). `tf.sh apply lxc` then bootstrap the cbalders user manually + on the new CT (the bpg module only seeds root keys; cbalders + NOPASSWD sudo + is post-create). See `feedback_lxc_bootstrap_user`. +2. **`vars/vault.yml`** — holds `vault_infisical_client_secret`. Shared + universal-auth client across all LXC repos — copy from `homelab-ansible-lxc-rag/vars/vault.yml`. +3. **Infisical `/dawarich` folder + 5 secrets** — run `./scripts/bootstrap-secrets.sh`. It creates the folder if missing and pushes: + - `vault_dawarich_db_password` (`openssl rand -hex 32`) + - `vault_dawarich_secret_key_base` (`openssl rand -hex 64`) + - `vault_dawarich_otp_primary_key` (`openssl rand -hex 32`) + - `vault_dawarich_otp_deterministic_key` (`openssl rand -hex 32`) + - `vault_dawarich_otp_salt` (`openssl rand -hex 32`) +4. **Infisical `/oidc` secrets** — generate `vault_dawarich_oidc_client_id` + (40 hex) and `vault_dawarich_oidc_client_secret` (64 hex), push as `--type shared`. + These values ARE the credentials — pi-auth creates the Authentik provider + with them, it doesn't fetch them back. Generation pattern: + ```bash + infisical secrets set "vault_dawarich_oidc_client_id=$(openssl rand -hex 20)" \ + --projectId 50062d7c-06ff-4d5c-8ca3-6c0cdba9f270 --env prod --path /oidc --type shared + infisical secrets set "vault_dawarich_oidc_client_secret=$(openssl rand -hex 32)" \ + --projectId 50062d7c-06ff-4d5c-8ca3-6c0cdba9f270 --env prod --path /oidc --type shared + ``` +5. **pi-auth site.yml mapping** — `homelab-ansible-pi-auth/site.yml` must have + a `dawarich_oidc_client_id` / `dawarich_oidc_client_secret` set_fact line + reading from `oidc_secrets.secrets.vault_dawarich_oidc_client_{id,secret}`. + Without this, pi-auth's loop fails with `'dawarich_oidc_client_id' is undefined`. + +## Deploy + +```bash +./deploy.sh # full deploy +./deploy.sh --tags dawarich # dawarich role only +./logs.sh -f # follow dawarich_app +./logs.sh dawarich_sidekiq -f # follow background jobs +``` + +## Bring-up order (proven 2026-06-01) + +1. `cd ../homelab-terraform && ./tf.sh apply lxc` → CT 459 created. +2. Bootstrap `cbalders` user + SSH keys + NOPASSWD sudo on the new CT: + ```bash + ssh root@pve02 'pct exec 459 -- bash -c " + useradd -m -s /bin/bash -G sudo cbalders && + mkdir -p /home/cbalders/.ssh && cat > /home/cbalders/.ssh/authorized_keys < + EOF + chown -R cbalders:cbalders /home/cbalders/.ssh && chmod 700 /home/cbalders/.ssh && chmod 600 /home/cbalders/.ssh/authorized_keys && + apt-get update -qq && apt-get install -y -qq sudo && + mkdir -p /etc/sudoers.d && + echo \"cbalders ALL=(ALL) NOPASSWD:ALL\" > /etc/sudoers.d/cbalders && + chmod 440 /etc/sudoers.d/cbalders"' + ``` +3. `./scripts/bootstrap-secrets.sh` → 5 `/dawarich` secrets in Infisical. +4. Push `/oidc` secrets (see prereq 4 above). +5. Add `dawarich_oidc_client_{id,secret}` set_fact mapping in `homelab-ansible-pi-auth/site.yml`. +6. First `./deploy.sh` here → app boots, migrations run (~3 min cold-start). +7. `cd ../homelab-ansible-pi-auth && ./deploy.sh` → creates Authentik provider + application. +8. `cd ../homelab-ansible-proxy && ./deploy.sh` → adds Caddy vhost + Technitium CNAME. +9. `cd ../homelab-ansible-pve && ./deploy.sh` → adds CTID 459 to `pbs-prod-daily`. +10. Browser-test `https://dawarich.balders.ca/users/sign_in` → click "Sign in with Authentik". + +## Mobile clients + +After first OIDC login, the user goes to Settings → API Keys in the Dawarich +UI to generate a per-user token. Use it as the bearer for either: + +- **OwnTracks** (any platform) — HTTP recorder mode, URL: + `https://dawarich.balders.ca/api/v1/owntracks/points?api_key=` +- **Dawarich iOS/Android app** — paste the token + host on first launch. + +## Google Takeout import + +1. Take the `location-history.json` out of the Takeout archive. +2. Drop it into `/opt/dawarich/watched/` on the LXC (NFS mount or `scp`). +3. Sidekiq picks it up via the import watcher within ~30s and emits progress + to `dawarich_sidekiq` logs. Large histories (>10y) can take an hour. + +## Gotchas (build-day 2026-06-01) + +- **Image version pin.** OIDC support landed in `freikin/dawarich:1.7.8`. The + `0.27.x` line silently ignores `OIDC_*` env vars (no OmniAuth controllers in + the codebase). Pinned at `1.7.11`. Bump app + sidekiq lockstep — same image + tag in two containers, breaking schema migrations on minor bumps. +- **Image entrypoint scripts are required.** Image ships `ENTRYPOINT ["bundle", "exec"]` + with no CMD. Compose MUST set `entrypoint: ["/usr/local/bin/web-entrypoint.sh"]` + (and `sidekiq-entrypoint.sh` for sidekiq). Those scripts run `db:prepare`, + create the SQLite files for solid_cache/solid_queue/solid_cable at + `/dawarich_db_data/`, and migrate Postgres before exec'ing the command. +- **Multi-DB SQLite persistence.** The solid_cache/solid_queue/solid_cable + files live at `/dawarich_db_data/` — bind-mount `/opt/dawarich/db_data:/dawarich_db_data` + to keep in-flight Sidekiq jobs across recreates. +- **`APPLICATION_HOSTS` must include localhost.** Rails 8 host authorization + blocks loopback if only the public host is listed — kills Docker HEALTHCHECK + and the Ansible health-wait. +- **Rails 8 `force_ssl` + healthcheck.** `APPLICATION_PROTOCOL=https` makes Rails + 301-redirect every plain-HTTP request to HTTPS. The Docker healthcheck is a + TCP-socket probe (`ruby -rsocket -e 'TCPSocket.new("localhost",3000).close'`) + rather than HTTP, because curl/wget either chase the redirect to TLS (Puma is + plain-HTTP behind Caddy → "SSL wrong version number") or fail 2xx. The Ansible + `Wait for Dawarich to be healthy` task is `ignore_errors: true` for the same reason. +- **SSO button is POST, not GET.** The signin page renders a `
`; the button submits with a Rails CSRF token. `GET /users/auth/openid_connect` returns 404 — that's CSRF working, not a routing bug. + +## Notes + +- Postgres data lives at `/opt/dawarich/postgres` (uid 70, alpine `postgres` + user). Pre-chowned in the role to avoid the empty-dir trap. +- Redis data at `/opt/dawarich/redis` (uid 999) — see `feedback_redis_uid_999`. +- PBS LXC snapshot covers everything nightly (the postgis + db_data volumes). + No central pg_backup wiring — DB is local. +- Watchtower exposes its HTTP API on port 8088 inside the LXC (matches the + fleet pattern; 8080 collides with potential second app on this CT). diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..21e4d89 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,6 @@ +[defaults] +interpreter_python = /usr/bin/python3 +deprecation_warnings = False +host_key_checking = False +result_format = yaml +callbacks_enabled = profile_tasks, timer diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..5991217 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# ============================================================================== +# deploy.sh — Deploy Dawarich LXC (Rails + Sidekiq + PostGIS + Redis) +# +# Usage: +# ./deploy.sh # full deploy (prompts for vault password) +# ./deploy.sh --tags dawarich # dawarich role only +# ./deploy.sh -v # verbose output +# ============================================================================== +set -euo pipefail + +HOST_IP="$(grep -E '^[0-9]' inventory.ini | head -1 | awk '{print $1}')" +HOST_USER="cbalders" + +echo "==> Checking connectivity to ${HOST_USER}@${HOST_IP} ..." +if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "${HOST_USER}@${HOST_IP}" true 2>/dev/null; then + echo " Cannot SSH to ${HOST_IP} — refreshing host key ..." + ssh-keygen -R "$HOST_IP" 2>/dev/null || true + ssh-keyscan -H "$HOST_IP" >> ~/.ssh/known_hosts 2>/dev/null +fi + +echo "==> Installing Ansible collections ..." +ansible-galaxy collection install -r requirements.yml --force 2>/dev/null + +echo "==> Running deploy playbook ..." +ansible-playbook -i inventory.ini site.yml --ask-vault-pass "$@" + +echo "==> Verifying ..." +ssh "${HOST_USER}@${HOST_IP}" bash -s <<'VERIFY' +echo "Containers:" +sudo docker ps --format ' {{.Names}}: {{.Status}}' | sort +echo "Dawarich web (TCP socket probe — Rails force_ssl redirects HTTP, smoke via Caddy after Caddy deploy):" +sudo docker exec dawarich_app ruby -rsocket -e 'TCPSocket.new("localhost",3000).close' 2>/dev/null && echo " :3000 LISTENING" || echo " UNREACHABLE" +echo "Postgis (probed from dawarich_app):" +sudo docker exec dawarich_app sh -c 'pg_isready -h dawarich_db -U dawarich' 2>/dev/null || echo " (pg unreachable)" +VERIFY + +echo "==> Done." diff --git a/inventory.ini b/inventory.ini new file mode 100644 index 0000000..f891f2d --- /dev/null +++ b/inventory.ini @@ -0,0 +1 @@ +192.168.1.159 ansible_user=cbalders ansible_become=yes diff --git a/logs.sh b/logs.sh new file mode 100755 index 0000000..9f78d66 --- /dev/null +++ b/logs.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# logs.sh — View container logs from the Dawarich LXC +# +# Usage: +# ./logs.sh # last 50 lines from dawarich_app +# ./logs.sh -f # tail/follow dawarich_app +# ./logs.sh dawarich_sidekiq # sidekiq logs +# ./logs.sh dawarich_db -f # follow db logs + +HOST_IP="$(grep -E '^[0-9]' inventory.ini | head -1 | awk '{print $1}')" +HOST_USER="cbalders" + +CONTAINER="dawarich_app" +FOLLOW=false +LINES=50 + +for arg in "$@"; do + case "$arg" in + -f|--follow) FOLLOW=true ;; + [0-9]*) LINES="$arg" ;; + *) CONTAINER="$arg" ;; + esac +done + +if [ "$FOLLOW" = true ]; then + echo "==> Tailing $CONTAINER on dawarich ($HOST_IP) — Ctrl+C to stop" + ssh "${HOST_USER}@${HOST_IP}" "sudo docker logs $CONTAINER --tail $LINES -f" +else + echo "==> Last $LINES lines of $CONTAINER on dawarich ($HOST_IP)" + ssh "${HOST_USER}@${HOST_IP}" "sudo docker logs $CONTAINER --tail $LINES 2>&1" +fi diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..6a77f32 --- /dev/null +++ b/requirements.yml @@ -0,0 +1,2 @@ +collections: + - name: infisical.vault diff --git a/roles/alloy/handlers/main.yml b/roles/alloy/handlers/main.yml new file mode 100644 index 0000000..10a3407 --- /dev/null +++ b/roles/alloy/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart alloy + command: docker compose -f /opt/alloy/docker-compose.yml up -d --force-recreate + args: + chdir: /opt/alloy diff --git a/roles/alloy/tasks/main.yml b/roles/alloy/tasks/main.yml new file mode 100644 index 0000000..4eebe14 --- /dev/null +++ b/roles/alloy/tasks/main.yml @@ -0,0 +1,37 @@ +--- +- name: Create Alloy directories + file: + path: "{{ item }}" + state: directory + owner: cbalders + group: cbalders + mode: '0755' + loop: + - /opt/alloy + - /var/lib/alloy + +- name: Deploy Alloy compose + template: + src: docker-compose.yml.j2 + dest: /opt/alloy/docker-compose.yml + owner: cbalders + group: cbalders + mode: '0644' + notify: restart alloy + +- name: Deploy Alloy config + template: + src: config.alloy.j2 + dest: /opt/alloy/config.alloy + owner: cbalders + group: cbalders + mode: '0644' + notify: restart alloy + +# Bring Alloy up (idempotent — docker compose up -d is a no-op if running +# and config hasn't changed). The handler force-recreates on config edit. +- name: Ensure Alloy is running + command: docker compose -f /opt/alloy/docker-compose.yml up -d + args: + chdir: /opt/alloy + changed_when: false diff --git a/roles/alloy/templates/config.alloy.j2 b/roles/alloy/templates/config.alloy.j2 new file mode 100644 index 0000000..a1f2239 --- /dev/null +++ b/roles/alloy/templates/config.alloy.j2 @@ -0,0 +1,161 @@ +// Alloy — Docker container logs + journald → Loki. River syntax. +// Per-LXC config; only the `host:` label changes via alloy_host_label var. + +// ============================================================ +// Docker — discover + scrape container logs +// ============================================================ + +discovery.docker "containers" { + host = "unix:///var/run/docker.sock" + refresh_interval = "15s" +} + +discovery.relabel "containers" { + targets = discovery.docker.containers.targets + + rule { + source_labels = ["__meta_docker_container_name"] + regex = "/(.+)" + target_label = "container" + } + rule { + source_labels = ["__meta_docker_container_log_stream"] + target_label = "stream" + } + rule { + source_labels = ["__meta_docker_container_label_com_docker_compose_service"] + target_label = "service" + } + rule { + source_labels = ["__meta_docker_container_label_com_docker_compose_project"] + target_label = "compose_project" + } + rule { + target_label = "job" + replacement = "docker" + } + rule { + target_label = "host" + replacement = "{{ alloy_host_label }}" + } +} + +loki.source.docker "containers" { + host = "unix:///var/run/docker.sock" + targets = discovery.relabel.containers.output + forward_to = [loki.process.docker.receiver] +} + +loki.process "docker" { + // Replay protection on first boot — Loki rejects > 7d. + stage.drop { + older_than = "24h" + drop_counter_reason = "older_than_24h" + } + // pulse-agent broken-pipe noise on hosts running pulse-agent --enable-host. + stage.drop { + expression = ".*broken pipe.*" + drop_counter_reason = "broken_pipe_noise" + } + forward_to = [loki.write.default.receiver] +} + +// ============================================================ +// Journald — LXC systemd units +// ============================================================ + +loki.source.journal "host" { + path = "/var/log/journal" + max_age = "1m" + forward_to = [loki.process.journal.receiver] + + relabel_rules = loki.relabel.journal.rules + labels = { + job = "journald", + host = "{{ alloy_host_label }}", + } +} + +loki.relabel "journal" { + forward_to = [] + + rule { + source_labels = ["__journal__systemd_unit"] + target_label = "unit" + } + rule { + source_labels = ["__journal__hostname"] + target_label = "instance" + } + rule { + source_labels = ["__journal_priority_keyword"] + target_label = "severity" + } +} + +loki.process "journal" { + forward_to = [loki.write.default.receiver] +} + +// ============================================================ +// Loki — push +// ============================================================ + +loki.write "default" { + endpoint { + url = "{{ alloy_loki_url }}" + } +} + +// ============================================================ +// Prometheus — embedded node_exporter (replaces standalone) +// ============================================================ + +prometheus.exporter.unix "node" { + // Container view of host filesystems — bind-mounted in compose. + rootfs_path = "/host/rootfs" + procfs_path = "/host/proc" + sysfs_path = "/host/sys" + // Default collectors match upstream node_exporter set. +} + +prometheus.scrape "node" { + targets = prometheus.exporter.unix.node.targets + forward_to = [prometheus.relabel.node.receiver] + scrape_interval = "15s" + job_name = "{{ alloy_prom_job | default('node_lxc') }}" +} + +// Rewrite the instance + job labels to match the historical +// node_exporter scrape, so existing dashboards continue to slice by +// the same instance string. Alloy's exporter component injects +// job="integrations/unix" on its targets — we have to overwrite it +// here; `job_name` in prometheus.scrape only names the scrape pool, +// it doesn't relabel the metrics. +prometheus.relabel "node" { + forward_to = [prometheus.remote_write.observe.receiver] + rule { + target_label = "instance" + replacement = "{{ alloy_prom_instance | default(ansible_default_ipv4.address + ':9100') }}" + } + rule { + target_label = "job" + replacement = "{{ alloy_prom_job | default('node_lxc') }}" + } + // Match the static labels Prom previously injected at scrape time + // (see homelab-ansible-vm-observability/roles/configs/files/prometheus.yml). + rule { + target_label = "group" + replacement = "{{ alloy_prom_group | default('lxc') }}" + } + rule { + target_label = "hostname" + replacement = "{{ alloy_prom_hostname | default(alloy_host_label) }}" + } +} + +prometheus.remote_write "observe" { + endpoint { + url = "{{ alloy_prom_remote_write | default('http://observe.lan.balders.ca:9090/api/v1/write') }}" + } +} diff --git a/roles/alloy/templates/docker-compose.yml.j2 b/roles/alloy/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..5091cbf --- /dev/null +++ b/roles/alloy/templates/docker-compose.yml.j2 @@ -0,0 +1,33 @@ +--- +# Self-contained Alloy stack on /opt/alloy. Runs independently from the +# host's main service (kestra/infisical/mcp/etc) so a service-side compose +# down doesn't take logging with it. +services: + alloy: + image: grafana/alloy:latest + container_name: alloy + restart: unless-stopped + command: + - run + - /etc/alloy/config.alloy + - --storage.path=/var/lib/alloy/data + - --server.http.listen-addr=0.0.0.0:12345 + ports: + - "12345:12345" + # Share the host's PID namespace so prometheus.exporter.unix reads + # /proc with the host kernel's cgroup view (cgroup-aware MemAvailable). + # Without this, /proc/meminfo returns hybrid values: MemTotal from the + # host cgroup but Cached/SReclaimable from the container, leading to + # a ~25% MemAvailable inflation. See docs/audit/alloy-consolidation-2026-05-21.md. + pid: host + volumes: + - /opt/alloy/config.alloy:/etc/alloy/config.alloy:ro + - /var/lib/alloy:/var/lib/alloy + - /var/log/journal:/var/log/journal:ro + - /run/log/journal:/run/log/journal:ro + - /etc/machine-id:/etc/machine-id:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + # Host metric collection for prometheus.exporter.unix (node_exporter replacement) + - /:/host/rootfs:ro,rslave + - /proc:/host/proc:ro + - /sys:/host/sys:ro diff --git a/roles/dawarich/files/docker-prune.service b/roles/dawarich/files/docker-prune.service new file mode 100644 index 0000000..e5a196a --- /dev/null +++ b/roles/dawarich/files/docker-prune.service @@ -0,0 +1,8 @@ +[Unit] +Description=Prune dangling docker images (Watchtower leaves them behind) +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/docker image prune -af diff --git a/roles/dawarich/files/docker-prune.timer b/roles/dawarich/files/docker-prune.timer new file mode 100644 index 0000000..85c2622 --- /dev/null +++ b/roles/dawarich/files/docker-prune.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Weekly docker image prune + +[Timer] +OnCalendar=Sun 03:30 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/roles/dawarich/handlers/main.yml b/roles/dawarich/handlers/main.yml new file mode 100644 index 0000000..bd9a70f --- /dev/null +++ b/roles/dawarich/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- name: reload systemd + systemd: + daemon_reload: true diff --git a/roles/dawarich/tasks/main.yml b/roles/dawarich/tasks/main.yml new file mode 100644 index 0000000..661494c --- /dev/null +++ b/roles/dawarich/tasks/main.yml @@ -0,0 +1,207 @@ +--- +# ---------- System setup ---------- +- name: Set timezone + copy: + content: "{{ timezone }}" + dest: /etc/timezone + mode: '0644' + register: tz_file + +- name: Apply timezone + command: dpkg-reconfigure -f noninteractive tzdata + when: tz_file.changed + +- name: Set hostname + copy: + content: "dawarich" + dest: /etc/hostname + mode: '0644' + +- name: Apply hostname + command: hostname dawarich + changed_when: false + +- name: Ensure hostname in /etc/hosts + lineinfile: + path: /etc/hosts + regexp: '^127\.0\.1\.1' + line: "127.0.1.1 dawarich" + +- name: Update apt cache + apt: + update_cache: true + cache_valid_time: 3600 + register: apt_cache_result + failed_when: false + changed_when: apt_cache_result.changed | default(false) + +- name: Install base packages + apt: + name: "{{ packages }}" + state: present + +- name: Create user + user: + name: "{{ item.name }}" + groups: "{{ item.groups }}" + shell: "{{ item.shell }}" + append: true + loop: "{{ users }}" + +- name: Deploy SSH authorized keys + authorized_key: + user: cbalders + key: "{{ item }}" + state: present + loop: "{{ ssh_authorized_keys }}" + +# ---------- Docker ---------- +- name: Check if Docker is installed + command: docker --version + register: docker_check + changed_when: false + failed_when: false + +- name: Install Docker + when: docker_check.rc != 0 + block: + - name: Download Docker install script + get_url: + url: https://get.docker.com + dest: /tmp/get-docker.sh + mode: '0755' + + - name: Run Docker install script + command: /tmp/get-docker.sh + + - name: Remove install script + file: + path: /tmp/get-docker.sh + state: absent + +- name: Ensure Docker service is running + systemd: + name: docker + state: started + enabled: true + +- name: Add cbalders to docker group + user: + name: cbalders + groups: docker + append: true + +# ---------- Docker registry mirror ---------- +- name: Ensure /etc/docker exists + file: + path: /etc/docker + state: directory + mode: '0755' + +- name: Read existing daemon.json + slurp: + src: /etc/docker/daemon.json + register: daemon_json_raw + failed_when: false + +- name: Configure Docker registry mirror + copy: + content: "{{ ((daemon_json_raw.content | b64decode | from_json) if daemon_json_raw.content is defined else {}) | combine({'registry-mirrors': ['http://registry.lan.balders.ca:5000']}) | to_nice_json }}" + dest: /etc/docker/daemon.json + owner: root + group: root + mode: '0644' + register: docker_daemon_config + +- name: Restart Docker if mirror config changed + systemd: + name: docker + state: restarted + when: docker_daemon_config.changed + +# ---------- Dawarich stack ---------- +- name: Create app directories + file: + path: "{{ item.path }}" + state: directory + owner: "{{ item.owner | default('cbalders') }}" + group: "{{ item.group | default('cbalders') }}" + mode: '0755' + loop: + - { path: /opt/dawarich } + # Postgis container runs as uid 70 (alpine 'postgres' user). Pre-chown + # so the first boot doesn't fail with "directory is not empty / wrong + # permissions" the way Nextcloud did (feedback_nextcloud_db_perms). + - { path: /opt/dawarich/postgres, owner: '70', group: '70' } + # Redis runs as uid 999 on bind volumes (feedback_redis_uid_999). + - { path: /opt/dawarich/redis, owner: '999', group: '999' } + # Dawarich Rails app writes uploads/cache here as uid 1000. + - { path: /opt/dawarich/public } + - { path: /opt/dawarich/watched } + - { path: /opt/dawarich/storage } + # solid_cache + solid_queue + solid_cable SQLite files (writable by the + # rails uid inside the container — image runs as uid 1000). + - { path: /opt/dawarich/db_data } + +- name: Deploy docker-compose + template: + src: docker-compose.yml.j2 + dest: /opt/dawarich/docker-compose.yml + owner: cbalders + group: cbalders + mode: '0640' + register: compose_changed + +- name: Pull images + command: docker compose pull + args: + chdir: /opt/dawarich + changed_when: false + +- name: Recreate containers if config changed + command: docker compose up -d --force-recreate + args: + chdir: /opt/dawarich + when: compose_changed.changed + changed_when: true + +- name: Ensure containers are running + command: docker compose up -d + args: + chdir: /opt/dawarich + when: not compose_changed.changed + changed_when: false + +# Dawarich runs migrations on boot (entrypoint includes db:migrate). First +# boot can take ~60-90s because the initial schema + PostGIS extension load +# is heavy. Don't fail the play if it's not ready yet — Watchtower-driven +# recreates will converge. +- name: Wait for Dawarich to be healthy + uri: + url: "http://localhost:{{ dawarich_port }}/api/v1/health" + method: GET + register: dawarich_health + until: dawarich_health.status is defined and dawarich_health.status == 200 + retries: 30 + delay: 5 + ignore_errors: true + +# ---------- Weekly docker prune (Watchtower side-effect cleanup) ---------- +- name: Deploy docker-prune systemd units + copy: + src: "{{ item }}" + dest: "/etc/systemd/system/{{ item }}" + owner: root + group: root + mode: '0644' + loop: + - docker-prune.service + - docker-prune.timer + notify: reload systemd + +- name: Enable docker-prune timer + systemd: + name: docker-prune.timer + enabled: true + state: started + daemon_reload: true diff --git a/roles/dawarich/templates/docker-compose.yml.j2 b/roles/dawarich/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..1eba855 --- /dev/null +++ b/roles/dawarich/templates/docker-compose.yml.j2 @@ -0,0 +1,187 @@ +# {{ ansible_managed }} +# Dawarich — self-hosted location history. Four containers + watchtower: +# dawarich_db postgis/postgis:16-3.4-alpine (PostGIS-enabled Postgres) +# dawarich_redis redis:7-alpine (cache + Sidekiq queue) +# dawarich_app freikin/dawarich (Rails web tier, port 3000) +# dawarich_sidekiq freikin/dawarich (background jobs) +# DB lives here (not the central DB VM) because the central image is plain +# postgres:16-alpine without the postgis extension. See memory project_dawarich. + +services: + dawarich_db: + image: "{{ postgis_image }}" + container_name: dawarich_db + restart: unless-stopped + shm_size: 1g + environment: + POSTGRES_USER: "{{ dawarich_db_user }}" + POSTGRES_PASSWORD: "{{ dawarich_db_password }}" + POSTGRES_DB: "{{ dawarich_db_name }}" + volumes: + - /opt/dawarich/postgres:/var/lib/postgresql/data + # No published ports — only the app + sidekiq reach it over the compose net. + healthcheck: + test: ["CMD-SHELL", "pg_isready -U {{ dawarich_db_user }} -d {{ dawarich_db_name }}"] + interval: 15s + timeout: 5s + retries: 10 + start_period: 30s + + dawarich_redis: + image: "{{ redis_image }}" + container_name: dawarich_redis + restart: unless-stopped + volumes: + - /opt/dawarich/redis:/data + # No published ports — internal-only. + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 15s + timeout: 3s + retries: 5 + + dawarich_app: + image: "{{ dawarich_image }}" + container_name: dawarich_app + restart: unless-stopped + depends_on: + dawarich_db: + condition: service_healthy + dawarich_redis: + condition: service_healthy + ports: + - "{{ dawarich_port }}:3000" + volumes: + - /opt/dawarich/public:/var/app/public + - /opt/dawarich/watched:/var/app/tmp/imports/watched + - /opt/dawarich/storage:/var/app/storage + # Solid_cache + solid_queue + solid_cable SQLite files. Without this + # bind, the queue DB is ephemeral and in-flight Sidekiq jobs are lost + # on container recreate. Path matches the entrypoint's expectation. + - /opt/dawarich/db_data:/dawarich_db_data + # The image ships a `web-entrypoint.sh` that runs `db:prepare` (creates + # the solid_cache + solid_queue SQLite files in /var/app/storage, runs + # Postgres migrations) before exec'ing the command. We MUST use it — + # without it, Rails 8 boots fail with "No database file specified" on + # solid_cache's first lookup. The image default entrypoint is `bundle exec` + # alone, which skips the prep. + entrypoint: ["/usr/local/bin/web-entrypoint.sh"] + command: ["bin/rails", "server", "-p", "3000", "-b", "::"] + environment: + RAILS_ENV: production + SELF_HOSTED: "true" + # localhost + ::1 needed for Docker healthcheck (Rails 8 host + # authorization rejects any host not in this list, including loopback). + APPLICATION_HOSTS: "{{ dawarich_domain }},localhost,127.0.0.1,::1" + APPLICATION_PROTOCOL: "https" + DOMAIN: "{{ dawarich_domain }}" + TIME_ZONE: "{{ timezone }}" + DISTANCE_UNIT: "km" + # Database + DATABASE_HOST: dawarich_db + DATABASE_PORT: "5432" + DATABASE_USERNAME: "{{ dawarich_db_user }}" + DATABASE_PASSWORD: "{{ dawarich_db_password }}" + DATABASE_NAME: "{{ dawarich_db_name }}" + RAILS_MAX_THREADS: "5" + # Redis + REDIS_URL: "redis://dawarich_redis:6379" + # Secrets + SECRET_KEY_BASE: "{{ dawarich_secret_key_base }}" + OTP_ENCRYPTION_PRIMARY_KEY: "{{ dawarich_otp_primary_key }}" + OTP_ENCRYPTION_DETERMINISTIC_KEY: "{{ dawarich_otp_deterministic_key }}" + OTP_ENCRYPTION_KEY_DERIVATION_SALT: "{{ dawarich_otp_salt }}" + # Registration policy — OIDC-only when enabled. Anything else and a + # local user form silently appears on /users/sign_up. + ALLOW_EMAIL_PASSWORD_REGISTRATION: "{{ dawarich_allow_email_password_registration | string | lower }}" +{% if dawarich_oidc_enabled %} + # ---- Auth: Authentik OIDC ---- + OIDC_CLIENT_ID: "{{ dawarich_oidc_client_id }}" + OIDC_CLIENT_SECRET: "{{ dawarich_oidc_client_secret }}" + OIDC_ISSUER: "{{ dawarich_oidc_issuer }}" + OIDC_REDIRECT_URI: "{{ dawarich_oidc_redirect_uri }}" + OIDC_PROVIDER_NAME: "{{ dawarich_oidc_provider_name }}" + OIDC_AUTO_REGISTER: "{{ dawarich_oidc_auto_register | string | lower }}" + OIDC_PKCE_ENABLED: "true" +{% endif %} + # TCP socket check, not HTTP. Rails 8 force_ssl (APPLICATION_PROTOCOL=https) + # redirects every plain-HTTP path to https — curl/wget can't satisfy a 2xx + # without TLS, and Puma is plain-HTTP (Caddy terminates TLS upstream). The + # socket check confirms Puma is listening, which is what we actually care + # about for orchestration. + healthcheck: + test: ["CMD-SHELL", "ruby -rsocket -e 'TCPSocket.new(\"localhost\",3000).close' || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 120s + + dawarich_sidekiq: + image: "{{ dawarich_image }}" + container_name: dawarich_sidekiq + restart: unless-stopped + depends_on: + dawarich_db: + condition: service_healthy + dawarich_redis: + condition: service_healthy + dawarich_app: + condition: service_started + volumes: + - /opt/dawarich/public:/var/app/public + - /opt/dawarich/watched:/var/app/tmp/imports/watched + - /opt/dawarich/storage:/var/app/storage + # Solid_cache + solid_queue + solid_cable SQLite files. Without this + # bind, the queue DB is ephemeral and in-flight Sidekiq jobs are lost + # on container recreate. Path matches the entrypoint's expectation. + - /opt/dawarich/db_data:/dawarich_db_data + # Same entrypoint pattern as the web tier — sidekiq's script ensures the + # DB is migrated before workers start consuming jobs. + entrypoint: ["/usr/local/bin/sidekiq-entrypoint.sh"] + command: ["sidekiq"] + environment: + RAILS_ENV: production + SELF_HOSTED: "true" + # localhost + ::1 needed for Docker healthcheck (Rails 8 host + # authorization rejects any host not in this list, including loopback). + APPLICATION_HOSTS: "{{ dawarich_domain }},localhost,127.0.0.1,::1" + APPLICATION_PROTOCOL: "https" + DOMAIN: "{{ dawarich_domain }}" + TIME_ZONE: "{{ timezone }}" + DATABASE_HOST: dawarich_db + DATABASE_PORT: "5432" + DATABASE_USERNAME: "{{ dawarich_db_user }}" + DATABASE_PASSWORD: "{{ dawarich_db_password }}" + DATABASE_NAME: "{{ dawarich_db_name }}" + REDIS_URL: "redis://dawarich_redis:6379" + SECRET_KEY_BASE: "{{ dawarich_secret_key_base }}" + OTP_ENCRYPTION_PRIMARY_KEY: "{{ dawarich_otp_primary_key }}" + OTP_ENCRYPTION_DETERMINISTIC_KEY: "{{ dawarich_otp_deterministic_key }}" + OTP_ENCRYPTION_KEY_DERIVATION_SALT: "{{ dawarich_otp_salt }}" + BACKGROUND_PROCESSING_CONCURRENCY: "{{ dawarich_sidekiq_concurrency }}" + + watchtower: + container_name: dawarich_watchtower + image: containrrr/watchtower:latest + restart: unless-stopped + dns: + - 192.168.1.1 + ports: + - "8088:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - DOCKER_API_VERSION=1.40 + - WATCHTOWER_HTTP_API_UPDATE=true + - WATCHTOWER_HTTP_API_TOKEN={{ watchtower_api_token }} + - TZ={{ timezone }} + - WATCHTOWER_CLEANUP=true + - WATCHTOWER_INCLUDE_RESTARTING=true + - WATCHTOWER_NO_STARTUP_MESSAGE=true + - WATCHTOWER_NOTIFICATION_REPORT=true + - WATCHTOWER_NOTIFICATIONS=shoutrrr + - WATCHTOWER_NOTIFICATION_URL={{ watchtower_gotify_url }} + - WATCHTOWER_NOTIFICATIONS_HOSTNAME=DAWARICH + - WATCHTOWER_NOTIFICATION_TITLE_TAG=DAWARICH + # Render empty when nothing was updated/failed — see feedback_watchtower_pushover. + - 'WATCHTOWER_NOTIFICATION_TEMPLATE={% raw %}{{- if .Report -}}{{- with .Report -}}{{- if or .Updated .Failed -}}{{ len .Updated }} updated, {{ len .Failed }} failed{{- range .Updated}} | updated {{.Name}}{{- end -}}{{- range .Failed}} | FAILED {{.Name}}{{- end -}}{{- end -}}{{- end -}}{{- end -}}{% endraw %}' diff --git a/scripts/bootstrap-secrets.sh b/scripts/bootstrap-secrets.sh new file mode 100755 index 0000000..0933064 --- /dev/null +++ b/scripts/bootstrap-secrets.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# ============================================================================== +# bootstrap-secrets.sh — Generate + push the 5 Dawarich app secrets to Infisical +# +# Run ONCE before the first deploy. Idempotent in the sense that re-running it +# rotates every key — which will invalidate existing Rails sessions + the OTP- +# encrypted columns. Do NOT re-run blindly on an existing install. +# ============================================================================== +set -euo pipefail + +INF_PROJECT="50062d7c-06ff-4d5c-8ca3-6c0cdba9f270" +ENV="prod" +PATH_PREFIX="/dawarich" + +command -v infisical >/dev/null || { echo "ERROR: infisical CLI not installed"; exit 1; } +command -v openssl >/dev/null || { echo "ERROR: openssl not installed"; exit 1; } + +push() { + local name="$1" value="$2" + echo " -> $name" + infisical secrets set "${name}=${value}" \ + --projectId "$INF_PROJECT" --env "$ENV" --path "$PATH_PREFIX" \ + --type shared >/dev/null +} + +echo "==> Generating + pushing 5 Dawarich secrets to ${PATH_PREFIX}/ ..." +push vault_dawarich_db_password "$(openssl rand -hex 32)" +push vault_dawarich_secret_key_base "$(openssl rand -hex 64)" +push vault_dawarich_otp_primary_key "$(openssl rand -hex 32)" +push vault_dawarich_otp_deterministic_key "$(openssl rand -hex 32)" +push vault_dawarich_otp_salt "$(openssl rand -hex 32)" + +echo "==> Done." +echo +echo "Next steps:" +echo " 1) Apply terraform (module.dawarich) — provisions CT 459 on pve02 at .159." +echo " 2) Bootstrap cbalders user + SSH keys + NOPASSWD sudo on the new CT (see README)." +echo " 3) Generate + push 2 OIDC credentials (pi-auth uses these to CREATE the Authentik client — they are not fetched back):" +echo " infisical secrets set \"vault_dawarich_oidc_client_id=\$(openssl rand -hex 20)\" --projectId 50062d7c-06ff-4d5c-8ca3-6c0cdba9f270 --env prod --path /oidc --type shared" +echo " infisical secrets set \"vault_dawarich_oidc_client_secret=\$(openssl rand -hex 32)\" --projectId 50062d7c-06ff-4d5c-8ca3-6c0cdba9f270 --env prod --path /oidc --type shared" +echo " 4) Confirm homelab-ansible-pi-auth/site.yml has the dawarich_oidc_client_{id,secret} set_fact mapping." +echo " 5) First ./deploy.sh — picks up dawarich:1.7.11 (OIDC requires >= 1.7.8), runs migrations, comes up healthy." +echo " 6) cd ../homelab-ansible-pi-auth && ./deploy.sh — creates Authentik provider + application." +echo " 7) cd ../homelab-ansible-proxy && ./deploy.sh — adds Caddy vhost + Technitium CNAME." +echo " 8) cd ../homelab-ansible-pve && ./deploy.sh — adds CTID 459 to pbs-prod-daily." +echo " 9) Browser-test https://dawarich.balders.ca/users/sign_in → 'Sign in with Authentik'." diff --git a/site.yml b/site.yml new file mode 100644 index 0000000..53ead6b --- /dev/null +++ b/site.yml @@ -0,0 +1,101 @@ +--- +# ============================================================================== +# Dawarich LXC — Self-hosted location history (Google Timeline alternative) +# ============================================================================== +# Rails app + Sidekiq worker + Redis + PostGIS, all on this LXC. PostGIS lives +# here (not central DB VM .172) because the DB VM's postgres:16-alpine image +# doesn't bundle the postgis extension — see memory project_dawarich for the +# convention-exception rationale. +# +# Ingests: OwnTracks (HTTP, no MQTT broker needed), Dawarich's own iOS/Android +# app, Google Takeout exports. Auth: Authentik OIDC end-to-end. +# ============================================================================== + +- name: Deploy Dawarich LXC + hosts: all + become: true + vars_files: + - vars/main.yml + - vars/vault.yml + + pre_tasks: + - name: Deploy banner + debug: + msg: "===== {{ ansible_play_name }} → {{ inventory_hostname }} ({{ ansible_host }}) =====" + + - name: Install infisicalsdk on controller + pip: + name: infisicalsdk + state: present + extra_args: --break-system-packages + delegate_to: localhost + become: false + run_once: true + + - name: Authenticate with Infisical + infisical.vault.login: + url: "{{ infisical_url }}" + auth_method: universal_auth + universal_auth_client_id: "{{ infisical_client_id }}" + universal_auth_client_secret: "{{ infisical_client_secret }}" + register: infisical_login + delegate_to: localhost + become: false + run_once: true + + - name: Read dawarich secrets + infisical.vault.read_secrets: + login_data: "{{ infisical_login.login_data }}" + project_id: "{{ infisical_project_id }}" + env_slug: "prod" + path: "/dawarich" + as_dict: true + register: repo_secrets + delegate_to: localhost + become: false + run_once: true + + # OIDC creds live in /oidc alongside every other Authentik client (the same + # pair pi-auth pushes into Authentik). default('') keeps the play + # idempotent before secrets exist / OIDC is flipped on. + - name: Read oidc secrets + infisical.vault.read_secrets: + login_data: "{{ infisical_login.login_data }}" + project_id: "{{ infisical_project_id }}" + env_slug: "prod" + path: "/oidc" + as_dict: true + register: oidc_secrets + delegate_to: localhost + become: false + run_once: true + + - name: Read shared secrets + infisical.vault.read_secrets: + login_data: "{{ infisical_login.login_data }}" + project_id: "{{ infisical_project_id }}" + env_slug: "prod" + path: "/shared" + as_dict: true + register: shared_secrets + delegate_to: localhost + become: false + run_once: true + + - name: Map secrets to vars + set_fact: + dawarich_db_password: "{{ repo_secrets.secrets.vault_dawarich_db_password }}" + dawarich_secret_key_base: "{{ repo_secrets.secrets.vault_dawarich_secret_key_base }}" + # OTP encryption keys — Dawarich ships built-in defaults but rotating them + # off the public defaults is a one-line hardening win. All three required. + dawarich_otp_primary_key: "{{ repo_secrets.secrets.vault_dawarich_otp_primary_key }}" + dawarich_otp_deterministic_key: "{{ repo_secrets.secrets.vault_dawarich_otp_deterministic_key }}" + dawarich_otp_salt: "{{ repo_secrets.secrets.vault_dawarich_otp_salt }}" + dawarich_oidc_client_id: "{{ oidc_secrets.secrets.vault_dawarich_oidc_client_id | default('') }}" + dawarich_oidc_client_secret: "{{ oidc_secrets.secrets.vault_dawarich_oidc_client_secret | default('') }}" + watchtower_gotify_url: "{{ shared_secrets.secrets.vault_watchtower_gotify_url }}" + watchtower_api_token: "{{ shared_secrets.secrets.vault_watchtower_api_token }}" + + roles: + - dawarich + - alloy diff --git a/vars/main.yml b/vars/main.yml new file mode 100644 index 0000000..f4bb31a --- /dev/null +++ b/vars/main.yml @@ -0,0 +1,84 @@ +--- +timezone: America/Toronto + +packages: + - apt-utils + - bash-completion + - ca-certificates + - curl + - git + - gnupg + - htop + - net-tools + - openssh-server + - python3 + - python3-pip + - sudo + - vim + - wget + +users: + - name: cbalders + groups: sudo + shell: /bin/bash + +ssh_authorized_keys: + - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINALaic1jpoP6t1urbZqJLI1eU5NeTVD9k8AAMAvOvvk OfficeMini" + - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGzTHdCiQjhIHsGB8oMpyKtr9TZXrXeIRKwcwe698zMW Generated By Termius" + +# Alloy ships Docker logs + journald to Loki on observe.lan.balders.ca + node +# metrics to Prometheus. Pattern mirrors every other LXC. +alloy_host_label: dawarich +alloy_loki_url: http://observe.lan.balders.ca:3100/loki/api/v1/push +alloy_prom_job: node_lxc +alloy_prom_group: lxc +alloy_prom_hostname: dawarich + +# ------------------------------------------------------------------------------ +# Dawarich — self-hosted location history +# ------------------------------------------------------------------------------ +# Pin image tags (Dawarich ships breaking schema changes on minor bumps). Bump +# in lockstep across app + sidekiq, never split — they share the same DB +# migrations. Check release notes before bumping. +# Pinned tag — OIDC support first landed in 1.7.8 (2026-05-16). DO NOT +# downgrade below 1.7.8 without disabling OIDC. Bump in lockstep with sidekiq. +dawarich_image: "freikin/dawarich:1.7.11" +# Official PostGIS image — alpine variant. Strict superset of postgres:16-alpine +# so swapping to it later (if we ever centralize on DB VM) is non-breaking. +postgis_image: "postgis/postgis:16-3.4-alpine" +redis_image: "redis:7-alpine" + +dawarich_port: 3000 +dawarich_site_url: "https://dawarich.balders.ca" +dawarich_domain: "dawarich.balders.ca" + +# Database (local — central DB VM doesn't load PostGIS extension; see memory +# project_dawarich for the convention exception rationale). +dawarich_db_name: "dawarich" +dawarich_db_user: "dawarich" + +# Background processing — keep modest to leave headroom for the web tier. +# Dawarich docs default 5; bump cautiously if Sidekiq queue depth grows. +dawarich_sidekiq_concurrency: 5 + +# Geocoding — disabled in v1. Dawarich falls back to its internal lightweight +# lookup. Self-hosted Photon stack is a future addition; uncomment + set host +# when wired. +# dawarich_photon_api_host: "photon.lan.balders.ca" + +# OIDC via Authentik. Client id/secret live in Infisical /oidc/ (pushed into +# Authentik by homelab-ansible-pi-auth's oidc_clients role). +dawarich_oidc_enabled: true +dawarich_oidc_issuer: "https://auth.balders.ca/application/o/dawarich/" +dawarich_oidc_redirect_uri: "https://dawarich.balders.ca/users/auth/openid_connect/callback" +dawarich_oidc_provider_name: "Authentik" +# false → only OIDC users can sign in (we want SSO-only). First OIDC login +# becomes the canonical user account. +dawarich_allow_email_password_registration: false +dawarich_oidc_auto_register: true + +# Infisical (secrets source) +infisical_url: "https://secrets.balders.ca" +infisical_project_id: "50062d7c-06ff-4d5c-8ca3-6c0cdba9f270" +infisical_client_id: "828d2cc8-eb25-4b1e-a711-c9a4b1580106" +infisical_client_secret: "{{ vault_infisical_client_secret }}" diff --git a/vars/vault.yml b/vars/vault.yml new file mode 100644 index 0000000..d164004 --- /dev/null +++ b/vars/vault.yml @@ -0,0 +1,10 @@ +$ANSIBLE_VAULT;1.1;AES256 +66366365356236623964366166336662353433626337323337343365316662636332356636336534 +6364616163666431333863613639353837623165636264390a363030376536373966316230356335 +30623466653337326133666539343966656362613964353763636539623634396364633137323733 +3636613464393534660a313334393333343835616235613833346663373537363738383064363437 +34373430306665376639633032373961653134303233613164633738356166376234663039303138 +65313065383061636263393262353139646239383638303036313662373663316132333666366537 +65333866356235373830323734623730356138653338663538616666643230303835653461343236 +31616161333461356665316238363133316134376665353437386564313939356137313331613333 +35653238383931376131323834383633313930396533323032363863666138383332