docs: distill the managed skill library into the vault (waves 1-7)
All ~173 managed skills read in full and merged, one polished reference page per skill-cluster: - LLM cross-cutting (17 pages): GGUF Compatibility, Llama Cpp Serving Sizing, Llama Cpp Multi-Device Splits, PCIe Link and Atomics, RDNA4 Container Serving, RDNA4 MoE Expert Offload VRAM Budget, vLLM Boot Forensics, vLLM Porting and Compile Triage, LLM Bench Windows, LLM Performance Diagnosis, Model Swap and Rollback, Tcclaviger Fork Ops, Heterogeneous Split Triage, FreeToken Builds + deltas folded into the retired runbooks (GLM DFlash2 port laws, Lucebox dual-R9700, R9V qualification pipeline, FLM pack registration, ik hybrid envelope, frank-medium split rebalance, llama.cpp GGUF preflight, batch-ubatch split section, bench-window D-state/stale-read notes, second-endpoint bring-up). - Architecture (3): Stale Signal Audit, Quadlet Lifecycle, Deploy State Triage. - Operations (6 + index): Remote Job Runner, Payload and Disk Reclaim, Container Host Reachability, Fleet Ops Playbook, Fleet Audit and Posture, SSH and Remote Access. - Infrastructure: Observability Provisioning + Homepage Status Probes; updates to Authentik, Anubis, LAN-DNS (per-client DoH bypass, Porkbun zone ops), Swap Configuration, OpenWebUI, Hermes (pre-existing page preserved and extended). - FrankBook-Pro: ARR-Stack filled (API split, schema-first probing, auth-posture, auth-config trap); qBittorrent E2E verification. - Desktop/Agent/Research: Noctalia and Niri, OMP Harness, Agent Docs and Edits, Command Center Dotfiles, PICO-8 Asset Standards. - Indexes: LLM Cross-Cutting References section, Operations/ and Agent/ index pages, root-index Skill Distillation section, AGENTS.md pointer. - log.md carries the wave plan. check-vault-links.py 0 unresolved (185 pages); ./tests/run.sh 366/366.
This commit is contained in:
1 parent
8618ec6dbe
commit
33eae71d33
43 files changed
+5436
-2
No files matched your search
@@ -342,6 +342,168 @@ The silent re-auth path was proven end-to-end: after deleting
|
||||
`qa-e2e-admin`'s rows, the e2e admin spec ran against the stale storage
|
||||
state and passed with no login prompt (2 tests, 2.9 s).
|
||||
|
||||
## Guard-chain flip recipe (adding a guarded vhost)
|
||||
|
||||
Move a public vhost from `reverse_proxy <app>:<port>` onto the chain:
|
||||
client → TLS edge → Anubis :8923 → outpost decision → internal
|
||||
`http://:8017` listener → Host-matched handler → real upstream.
|
||||
|
||||
**Order (never reorder):**
|
||||
|
||||
1. **Provider first.** Create the outpost ProxyProvider before touching
|
||||
routes, or authed traffic hits the outpost error page instead of the app.
|
||||
Fleet convention for `external_host` is the exact browser-facing origin
|
||||
(the inventory table carries both bare origins and trailing slashes;
|
||||
routing matches on hostname, verified 2026-09-02).
|
||||
`internal_host = http://caddy:8017`, all three flows set, and an
|
||||
Application attached — the two provisioning rules above.
|
||||
2. **Route flip in ONE commit/deploy**: the vhost becomes
|
||||
`reverse_proxy anubis:8923` with `header_up X-Real-Ip {remote_host}`, and
|
||||
inside the `http://:8017` listener add `@<svc> host <vhost>` +
|
||||
`handle @<svc> { reverse_proxy <upstream> }`. Anubis guard set, Caddy TLS
|
||||
edge, `:8017` routing table, and provider list change together.
|
||||
3. Deploy via the repo deploy flow (commit first; deploy pushes main itself).
|
||||
|
||||
**Verification (all three):**
|
||||
|
||||
- Unauthenticated probe from a client: `curl -s https://<vhost>/` → HTTP 200
|
||||
challenge page; grep the body for `challenge|not a bot|anubis` (~8
|
||||
markers on a live challenge). A 502 here instead of the challenge = chain
|
||||
mis-wired.
|
||||
- Post-auth routing proof: grep the LIVE config inside the container —
|
||||
`podman exec caddy sh -c "grep -n -A3 <handler> /etc/caddy/Caddyfile"` —
|
||||
after the deploy prints its reload line; confirm the reload actually ran,
|
||||
don't trust exit 0.
|
||||
- Outpost reachability: `podman exec authentik-server getent hosts caddy`
|
||||
resolves the compose DNS alias (10.89.1.x) that `internal_host` dials.
|
||||
|
||||
**Pitfalls:** probing `http://localhost:8017/` from the host returns HTTP
|
||||
000 (connection refused) — the post-auth listener lives in the caddy
|
||||
container's netns; probe inside it or grep the config. Non-browser clients
|
||||
cannot solve the JS challenge — before flipping, audit service-to-service
|
||||
traffic: ingestion/push paths must target internal addresses (`loki:3100`),
|
||||
never a public hostname of a guarded service. Exempt-by-construction
|
||||
categories stay unguarded: the IdP itself (`auth.`), git smart-HTTP
|
||||
endpoints, services with their own complete auth — document the exemption
|
||||
in the vhost comment.
|
||||
|
||||
## Driving the ORM (`ak shell`)
|
||||
|
||||
All authentik object work happens through the server container's ORM shell —
|
||||
no repo change, no deploy (these providers are NOT repo-managed; capture new
|
||||
provider+application pairs as a blueprint or setup script to avoid drift).
|
||||
|
||||
```bash
|
||||
ssh dankclaw 'podman exec -i authentik-server ak shell' <<'EOF' | sed -n '/229 objects/,$p'
|
||||
from authentik.core.models import User, Group
|
||||
...
|
||||
EOF
|
||||
```
|
||||
|
||||
- The heredoc IS the program: `ak shell` reads all of stdin and execs it
|
||||
(heredoc-through-ssh quoting breaks; the raw-stdin pattern is the proven
|
||||
one).
|
||||
- Bootstrap prints JSON log lines, then
|
||||
`### authentik shell (2026.8.3)` and `229 objects imported automatically…`
|
||||
— pipe through `sed -n '/229 objects/,$p'` to drop the noise.
|
||||
- 229 models are pre-imported. Import paths (each wrong guess costs a failed
|
||||
exec): `User`, `Group`, `Application`, `ApplicationEntitlement`,
|
||||
`PropertyMapping` → `authentik.core.models`; `ProxyProvider` →
|
||||
`authentik.providers.proxy.models` (NOT `authentik.outposts.models`);
|
||||
`LDAPProvider` → `authentik.providers.ldap.models`;
|
||||
`authentik.core.models.application` does NOT exist as an import path; there
|
||||
is no `authentik.policies.group` module in this build.
|
||||
- For live DB truth prefer the postgres container
|
||||
(`podman exec authentik-postgres psql -U authentik -d authentik -At -c
|
||||
"select * from …"`) — **query with `select *` and stop guessing column
|
||||
names**: 2026.x renamed columns (`Provider.objects.filter(type=…)` raises
|
||||
FieldError — `type` moved to subclass tables; the outpost `enabled` column
|
||||
may not exist under that name; provider pk is `oauth2provider_ptr_id`).
|
||||
|
||||
## User provisioning playbook (user-level account)
|
||||
|
||||
There is no `User.create_user` classmethod, and `is_superuser` is NOT a
|
||||
constructor kwarg (it is a property derived from group membership) — passing
|
||||
it raises `TypeError: User() got unexpected keyword arguments`:
|
||||
|
||||
```python
|
||||
from authentik.core.models import User, Group
|
||||
u = User(username="ridley", email="ridley@franksdank.cloud", name="Ridley", is_active=True)
|
||||
u.set_password("...") # password arrives on stdin only, never argv
|
||||
u.save()
|
||||
for g in ("Non-Admin Users", "jellyfin_users", "openwebui_users"):
|
||||
u.groups.add(Group.objects.get(name=g))
|
||||
```
|
||||
|
||||
`path` defaults to `users` (regular). The access map (verified live
|
||||
2026-10-04): there are NO per-app policy bindings on the proxy providers
|
||||
(all use the implicit-consent "Authorize Application" flow), so guarded-vhost
|
||||
access is decided entirely by GROUP MEMBERSHIP consumed downstream —
|
||||
|
||||
- `homepage-admins` → Caddy RBAC for homepage `/admin` (`X-Authentik-Groups`
|
||||
regex gate) and the homepage admin panel; regular users get only
|
||||
`category: user` panels from `services.json`.
|
||||
- `openwebui_users` / `openwebui_admins` → ApplicationEntitlements
|
||||
"Open WebUI Users"/"Open WebUI Admins" (bound by group), consumed by the
|
||||
"Open WebUI roles" property mapping → `roles` claim. Verify live with
|
||||
`u.app_entitlements(app)`.
|
||||
- `jellyfin_users` / `jellyfin_admins` → Jellyfin via the LDAP outpost
|
||||
(filter `(memberOf=cn=jellyfin_users,ou=groups,dc=franksdank,dc=cloud)`).
|
||||
- `Non-Admin Users` = the plain-user marker group (precedents: alex,
|
||||
qa-e2e).
|
||||
- EXCLUDE from a user-level account: `homepage-admins`, `jellyfin_admins`,
|
||||
`openwebui_admins`, `authentik Admins`, `authentik Read-only`.
|
||||
- Other user-category vhosts (SearXNG, Gitea, Calibre-Web, RomM, Copyparty,
|
||||
Plex, Seerr, Marreta) need no per-user group: any authenticated user
|
||||
passes the outpost.
|
||||
|
||||
Verify login behaviorally — `authenticate()` is the exact PasswordStage code
|
||||
path:
|
||||
|
||||
```python
|
||||
from django.contrib.auth import authenticate
|
||||
assert authenticate(request=None, username="ridley", password="...") # -> User
|
||||
assert authenticate(request=None, username="ridley", password="nope") is None
|
||||
```
|
||||
|
||||
Look flows up by slug, not name:
|
||||
`Flow.objects.get(slug="default-authentication-flow")` (its `.name` is
|
||||
"Welcome to authentik!" — `.get(name=…)` fails). Stage order:
|
||||
Identification(10) → Password(20) → AuthenticatorValidate(30; no-op with no
|
||||
enrolled device) → UserLogin(100). With no MFA enrolled, nothing else blocks.
|
||||
|
||||
More field gotchas (each cost a failed exec): `ApplicationEntitlement.application`
|
||||
can be null (guard with `getattr`); `LDAPProvider` has
|
||||
`search_mode`/`bind_mode`/`base_dn`, lacks `search_base`/`of_groups`/
|
||||
`organization`; `OAuth2Provider` has no `.scopes`; `Outpost.config` is an
|
||||
`OutpostConfig` object, not a dict; `Policy` has no uniform `.policy_cls`
|
||||
(use `type(p).__name__`); `User` has no `.status`.
|
||||
|
||||
Never rotate `AUTHENTIK_SECRET_KEY` to fix claim issues — it invalidates
|
||||
every outpost session fleet-wide; purge the stale `proxysession` rows
|
||||
instead (section above).
|
||||
|
||||
## Probing the outpost from inside the netns
|
||||
|
||||
Rootless podman gives each container its own netns; **host `ss -ltn` cannot
|
||||
see listeners inside other containers' namespaces** — a missing listener on
|
||||
the host is an observation artifact, not a fault. Distroless images have no
|
||||
shell (`podman exec authentik-server sh` fails), so probe by joining the
|
||||
namespace:
|
||||
|
||||
```
|
||||
podman run --rm --network container:<container> --entrypoint /bin/busybox \
|
||||
docker.io/library/busybox:latest \
|
||||
wget -q -T 4 -t 1 -O /dev/null -S "<url>" 2>&1 | head -8
|
||||
```
|
||||
|
||||
- Quote URLs over ssh: the remote shell is zsh and bare `?` in query strings
|
||||
triggers globbing (`no matches found`).
|
||||
- Boot transience: immediately after a restart the same probe can return 503
|
||||
(configure task pending); poll until the answer changes before concluding.
|
||||
- `podman inspect --format "{{.Config.NetMode}}"` errors on this podman
|
||||
version — the field is `.HostConfig.NetworkMode`.
|
||||
|
||||
## Failure modes
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|
||||
Reference in new issue
Block a user