Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Changes in this section will be promoted to a dated release entry on merge to `m

### Added

- **anythingllm-docker β€” `cloudflare_proxied` mode: tenants can live behind Cloudflare's proxy (2026-07-23)** β€” the per-brand `<brand>.weown.chat` hosting model (Jason 2026-07-21) puts customer instances behind Cloudflare Pro (orange cloud), where the template's ACME HTTP-01 cannot complete β€” exactly the 525 `f1visa.weown.chat` shows today. New copier bool `cloudflare_proxied` (default false β€” existing direct-DNS sites render identically): Caddy serves a **Cloudflare Origin CA certificate** instead of running ACME (`tls /certs/origin.crt /certs/origin.key`; zone SSL = Full (strict)). The PEMs arrive as `CF_ORIGIN_CERT`/`CF_ORIGIN_KEY` through the same ADR-006 Infisical entrypoint wrapper as every other secret β€” the caddy container gains the wrapper, and its command materialises them as 0600 files inside the container at start (never on host disk; re-fetched on restart). Origin-CA over DNS-01 was deliberate: no custom Caddy build and no DNS-edit API token sitting on customer droplets β€” one wildcard cert for `*.weown.chat`, readable only by operators at the initiative-project root, copied per-tenant-folder at provision time (droplet MIs stay path-scoped). **Verification**: copier renders clean in both modes and the rendered compose parses; the cert-materialise command proven against a multi-line PEM in env (content byte-identical, 0600 perms); fleet wiring (registry flag β†’ render-deploy/provision) landed in weown-fleet.

- **`gitea-docker/` β€” new copier template: Gitea with Keycloak SSO (2026-07-17)** β€” self-hosted git for WeOwn internal use (Jason-approved Gitea + Keycloak), cloned from `keycloak-docker/` per the repo template standard and keeping its full stack: ADR-006 in-container Infisical injection, Path-C thin cloud-init + ansible app layer, Layer-2 bootstrap-secret rotation, skinny backups with DO Spaces offload, DO monitoring alerts. Gitea-specific: **all logins via Keycloak OIDC** (confidential client, `gitea admin auth add-oauth` bootstrap documented in the rendered README with a stdin-only secret prompt; local registration disabled, SSO auto-registration on, `USERNAME=email`, `ACCOUNT_LINKING=auto`), git-over-SSH on a dedicated firewall port (`gitea_ssh_port`, default 2222; inline-justified `trivy:ignore:AVD-DIG-0001`), `GITEA__*` env-style config with only non-secret values in compose (`SECRET_KEY`/`INTERNAL_TOKEN`/`JWT_SECRET` from Infisical in prod), `INSTALL_LOCK=true` (env-only config, no web installer), no admin vhost (avoids keycloak-docker's D437 exposure), health via `/api/healthz`. **Verification**: template renders clean via copier; rendered compose/ansible YAML parse; `tofu validate` passes; all scripts `bash -n` clean; **local end-to-end SSO proven** β€” rendered `compose.local.yaml` + local Keycloak 24 (realm `weown`, confidential `gitea` client with `email profile offline_access` scopes) β†’ browser-equivalent OIDC dance β†’ Keycloak login β†’ callback β†’ user auto-registered active β†’ authenticated session. Reference configs: `mcp/infra` gitea + keycloak_client roles.
- **anythingllm-docker β€” customer dashboard (the product surface) + Simple-SSO + two-workspace bootstrap (2026-07-15)** β€” the customer-facing managed-service layer. New `template/dashboard/` is a **zero-npm-dependency Node server** (bind-mounted read-only into a `node:20-alpine` container, no build/registry image for v0) that is the customer's ONLY surface: they authenticate to the **dashboard** (HMAC session cookie, sha256 password hash from Infisical, per-IP login throttle, `X-Dashboard` CSRF header, `SameSite=Strict; Secure`), and the server proxies scoped actions to the AnythingLLM API with the admin Developer API key held **server-side** β€” the customer never receives ALLM credentials. Two sections: **Public** (upload public docs β†’ `ws-public`, embed-snippet screen) and **Private** (upload business docs β†’ `ws-private`, authenticated chat proxied with `userId` attribution). Caddy routes `/app*` β†’ dashboard, everything else β†’ ALLM (embed widget + WeOwn-operated admin UI). `SIMPLE_SSO_ENABLED` added to compose so the dashboard can mint single-use 1-hour login links (`/api/v1/users/{id}/issue-auth-token` β†’ `/sso/simple`) for a UI hop. New `scripts/bootstrap-product.sh` provisions the two workspaces + domain-allowlisted embed widget + all dashboard secrets in-process (nothing on disk/history). ansible uploads the dashboard app; docs updated. **Verification**: `node --check` on the server; **live 8-check smoke test** (healthz, unauth-reject, wrong-pw-reject, login, CSRF-reject, authed doc-list, bearer-authenticated chat proxy, logout-clears-session) all pass against a running server + stub upstream; compose YAML parses with all three services; rendered ansible parses.
- **Per-customer LLM key provisioning + single-tenant instance lifecycle doc (2026-07-15)** — `scripts/provision-openrouter-key.sh` mints a **per-customer, budget-capped** OpenRouter key via the OpenRouter Management API (`POST /api/v1/keys`, `limit` + `limit_reset: monthly`) and writes it into that customer's Infisical app project as `OPENROUTER_API_KEY` — the value never touches the terminal, disk, or shell history; the provisioning key is sourced in-process from the operator Infisical project (`OPENROUTER_PROVISIONING_KEY`, `read -rs` fallback). Refuses to overwrite a live key without `--force` (no silent orphaning); `--dry-run` for shared hosts. This **automates the previously-manual** "create a key in the OpenRouter dashboard and paste it" step in the per-site `bootstrap-*-infisical.sh` scripts, and gives each dedicated instance blast-radius isolation + a hard monthly spend cap. New [`docs/CUSTOMER_INSTANCE_PROVISIONING.md`](docs/CUSTOMER_INSTANCE_PROVISIONING.md) sequences the existing deploy pieces into a per-customer provision→deploy→operate→deprovision lifecycle (technical only; commercial/sales flow kept out of this public repo). **Verification**: `bash -n` + `shellcheck` clean. **Compliance**: NIST CSF 2.0 PR.AC-4 (least privilege per tenant), PR.DS-1; CIS v8 3.11.
Expand Down
5 changes: 5 additions & 0 deletions anythingllm-docker/copier.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ caddy_image:
help: "Caddy Docker image"
default: "reg.mini.dev/caddy:2"

cloudflare_proxied:
type: bool
help: "Domain sits behind Cloudflare's proxy (orange cloud). Caddy then serves a Cloudflare Origin CA certificate (CF_ORIGIN_CERT/CF_ORIGIN_KEY from Infisical) instead of running ACME β€” HTTP-01 cannot complete through the proxy, and the zone should be set to SSL mode 'Full (strict)'. Leave false for grey-cloud/direct-DNS domains (e.g. chat.weown.dev), which keep normal auto-HTTPS."
default: false

# =============================================================================
# LLM Configuration
# =============================================================================
Expand Down
9 changes: 9 additions & 0 deletions anythingllm-docker/template/docker/Caddyfile.jinja
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@

{{ domain }} {
{% if cloudflare_proxied %}
# Behind Cloudflare's proxy (orange cloud): ACME HTTP-01 can't complete
# through the proxy, so Caddy serves the Cloudflare Origin CA certificate
# instead (trusted by Cloudflare only β€” which is the entire audience of
# this listener). The PEMs are written by the container entrypoint from
# CF_ORIGIN_CERT / CF_ORIGIN_KEY (Infisical). Zone SSL mode must be
# "Full (strict)".
tls /certs/origin.crt /certs/origin.key
{% endif %}
# Customer dashboard β€” the customer-facing surface (auth + doc manager +
# private chat). Everything under /app is the dashboard container; the
# dashboard proxies scoped calls to AnythingLLM server-side.
Expand Down
17 changes: 17 additions & 0 deletions anythingllm-docker/template/docker/compose.prod.yaml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#
# Required Infisical secrets:
# ANYTHINGLLM_IMAGE, OPENROUTER_API_KEY, JWT_SECRET, ADMIN_EMAIL, EMBEDDING_ENGINE
# + CF_ORIGIN_CERT / CF_ORIGIN_KEY when cloudflare_proxied (Origin CA PEMs,
# copied into the tenant folder by weown-fleet provisioning)
# Optional Infisical secrets:
# OPENROUTER_MODEL_PREF, OPENROUTER_TIMEOUT_MS,
# AGENT_SEARCH_PROVIDER, AGENT_SEARXNG_API_URL,
Expand Down Expand Up @@ -176,6 +178,16 @@ services:
restart: unless-stopped
depends_on:
- anythingllm
{% if cloudflare_proxied %}
# Cloudflare-proxied domain: the entrypoint wrapper injects CF_ORIGIN_CERT /
# CF_ORIGIN_KEY (the Cloudflare Origin CA PEMs, from Infisical) and the
# command materialises them as 0600 files inside the container before caddy
# starts β€” never on the host disk, re-fetched on every restart
# (bounce-to-refresh, same as every other secret). `$$` is compose escaping
# for a literal `$` reaching the shell.
entrypoint: ["/opt/{{ project_name | replace('-', '_') }}/entrypoint-infisical.sh"]
command: ["/bin/sh", "-c", "umask 077 && mkdir -p /certs && printf '%s\n' \"$$CF_ORIGIN_CERT\" > /certs/origin.crt && printf '%s\n' \"$$CF_ORIGIN_KEY\" > /certs/origin.key && exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile"]
{% endif %}
environment:
DOMAIN: {{ domain }}
ports:
Expand All @@ -184,6 +196,11 @@ services:
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
{% if cloudflare_proxied %}
- /usr/bin/infisical:/usr/bin/infisical:ro
- /opt/{{ project_name | replace('-', '_') }}/.infisical-auth.env.container:/.infisical-auth.env:ro
- /opt/{{ project_name | replace('-', '_') }}/entrypoint-infisical.sh:/opt/{{ project_name | replace('-', '_') }}/entrypoint-infisical.sh:ro
{% endif %}
- caddy_data:/data
- caddy_config:/config
# Bind-mount access logs to host so otel-agent's filelog/caddy receiver
Expand Down
Loading