Commit graph

562 commits

Author SHA1 Message Date
52f0308f1a Fix fail2ban crash: disable default sshd jail, make non-fatal
Alpine's fail2ban ships with sshd jail enabled by default. Since there's
no SSH server in the Fly.io container, fail2ban exits with an error
looking for sshd logs — crashing the container via set -e.

Disable the sshd jail explicitly and make fail2ban startup non-fatal
since nginx rate limiting is the primary defense.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 08:29:33 -08:00
dbfe7365d8 Document that ArgoCD excludes Endpoints resources
ArgoCD's resource.exclusions in argocd-cm skips all Endpoints objects
(they're normally auto-managed by the control plane). The manual
forge-external Endpoints must be applied directly with kubectl.

Removed endpoints-forge.yaml from kustomization resources and added
comments in both files explaining the situation and the apply command.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 08:24:24 -08:00
2759675704 Split Endpoints into separate file for kustomize discovery
Kustomize didn't pick up the Endpoints from the multi-document YAML
in svc-forge-external.yaml. Split into a separate endpoints-forge.yaml
and add to kustomization.yaml resources.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 08:19:03 -08:00
17dac7ea27 Fix forge ExternalName → ClusterIP+Endpoints for Tailscale Ingress
The Tailscale ingress operator requires backends with a ClusterIP.
ExternalName services don't have one, causing "invalid ClusterIP"
errors. Replace with a headless Service + manual Endpoints pointing
to indri's Tailscale IP (100.98.163.89).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 08:17:24 -08:00
538a8cf6c1 Rename HTTPS forge.ops.eblu.me → forge.eblu.me across codebase
Update all HTTPS references to use the new public domain. This
touches workflows, ArgoCD manifests, Ansible, mise-tasks, NixOS
config, and documentation (~29 files).

Deliberately kept as forge.ops.eblu.me:
- SSH repoURLs in argocd/apps/ (SSH stays tailnet-only)
- containers/*/Dockerfile and *.nix (internal CI efficiency)
- Caddy services table in routing.md
- Internal URL references in forgejo.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:57:48 -08:00
32bf20525f Add forge.eblu.me DNS CNAME and Fly.io TLS certificate
Add CNAME record pointing forge.eblu.me to blumeops-proxy.fly.dev
in Pulumi Gandi config. Add forge.eblu.me to fly-setup cert list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:54:20 -08:00
8f47145b40 Update Authentik Forgejo OAuth callback to forge.eblu.me
Update redirect_uris and meta_launch_url to use the new public domain.
OAuth flow will dead-end naturally since Authentik is not publicly
accessible — SSO only works from the tailnet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:53:28 -08:00
80c33ccaf1 Add forge.eblu.me to Fly.io proxy with rate limiting and fail2ban
nginx configuration:
- forge.eblu.me server block with WebSocket support, 512m body limit
- Rate limit login/signup/forgot-password at 3r/s per real client IP
  (keyed on Fly-Client-IP header, not Fly's internal remote_addr)
- Static asset caching (7d), no blanket caching for dynamic content
- Security headers (HSTS, X-Frame-Options, X-Content-Type-Options)
- Block /swagger (API docs only available via tailnet)
- X-Real-IP set to real client IP for Forgejo audit logs
- geo-based deny list for fail2ban integration

fail2ban configuration:
- Custom filter matching 401/403 on login paths in nginx JSON log
- Ban after 5 failures in 10 minutes, ban duration 1 hour
- Custom nginx-deny action: writes IPs to deny file and reloads nginx
  (iptables won't work in Fly.io — remote_addr is Fly's proxy IP)
- Ban lists ephemeral across deploys (nginx rate limiting is persistent)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:52:58 -08:00
b49ff9f821 Add Tailscale Ingress for Forge via ExternalName Service
Create forge.tail8d86e.ts.net endpoint that proxies to Forgejo on
indri:3001. Uses ExternalName Service since Forgejo runs natively
on indri (not in k8s). Tagged with flyio-target for Fly.io proxy
access via existing ACLs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:51:28 -08:00
d2da346ac0 Harden Forgejo for public access: domain, proxy trust, registration lockdown
- Set forgejo_domain to forge.eblu.me (public URL in clone URLs)
- Set forgejo_ssh_domain to forge.ops.eblu.me (SSH stays tailnet-only)
- Add REVERSE_PROXY_LIMIT=2, REVERSE_PROXY_TRUSTED_PROXIES=* for
  correct client IP logging through Fly.io + Tailscale proxy chain
- Enable ALLOW_ONLY_EXTERNAL_REGISTRATION to block local signups

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:50:25 -08:00
d6584a2bd6 Docs: document forge.eblu.me public access, update routing and security guidance
Update forgejo.md with public access details and security controls.
Add forge.eblu.me to public services table in routing.md.
Update fail2ban guidance in expose-service-publicly.md to reflect
Fly.io container approach. Add changelog fragment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 07:49:49 -08:00
d4082985a6 Remove deprecated forge egress proxy from tailscale-operator
The egress proxy (tailscale-forge device) has been unused since Caddy
took over forge routing. No k8s resources reference it as a backend.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 06:56:33 -08:00
a32c99a252 Limit ollama to one loaded model and one parallel request
Prevents OOM when switching between models — only one 14B model
fits in 16GB VRAM at a time with KV cache for context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 21:23:12 -08:00
203e3cd567 Add NodePort service for ollama LAN access
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 20:57:18 -08:00
31d925814f Deploy Ollama LLM server on ringtail (#277)
## Summary
- Deploy Ollama as a new ArgoCD-managed service on ringtail's k3s cluster with GPU acceleration
- Declarative model management via `models.txt` + sidecar sync script (mirrors kiwix torrent pattern)
- Initial models: `qwen2.5:14b`, `deepseek-r1:14b`, `phi4:14b`, `gemma3:12b`
- hostPath PV on `/mnt/storage1/ollama` for fast local model storage (200Gi)
- Tailscale ingress at `ollama.ops.eblu.me` for API access from tailnet
- Enable GPU time-slicing (`replicas: 2`) on nvidia-device-plugin so Frigate and Ollama share the RTX 4080

## Deployment and Testing
- [ ] Deploy nvidia-device-plugin changes first: `argocd app sync nvidia-device-plugin`
- [ ] Verify GPU time-slicing: `kubectl describe node ringtail --context=k3s-ringtail` shows `nvidia.com/gpu: 2`
- [ ] Sync `apps` app with `--revision feature/ollama-ringtail`
- [ ] Set ollama app to branch: `argocd app set ollama --revision feature/ollama-ringtail && argocd app sync ollama`
- [ ] Verify model-sync sidecar pulls models: `kubectl logs -n ollama deploy/ollama -c model-sync --context=k3s-ringtail`
- [ ] Test API: `curl https://ollama.ops.eblu.me/api/tags`
- [ ] Test inference: `curl https://ollama.ops.eblu.me/api/generate -d '{"model":"qwen2.5:14b","prompt":"Hello"}'`
- [ ] Verify Frigate still works after GPU sharing change
- [ ] After merge: `argocd app set ollama --revision main && argocd app sync ollama`

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/277
2026-03-02 20:39:51 -08:00
Forgejo Actions
0f79c61c42 Update docs release to v1.12.1
- Built changelog from towncrier fragments

[skip ci]
2026-03-02 18:17:07 -08:00
7a1875936c Switch git hooks from pre-commit to prek (#276) v1.12.1
## Summary

- Replace pre-commit with [prek](https://github.com/j178/prek), a faster Rust-native drop-in alternative
- Migrate config from `.pre-commit-config.yaml` (YAML) to `prek.toml` (TOML)
- Add new built-in checks: case conflicts, private key detection, executable shebangs
- Install prek via mise native registry (`aqua:j178/prek`) instead of pipx
- Update all doc references across README, contributing guide, and how-to docs

## Notes

- `check-yaml` still uses the remote `pre-commit-hooks` repo because prek's builtin fast path doesn't support `--unsafe` yet (needed for Ansible custom YAML tags)
- All existing custom hooks (docs validation, container version check, mikado invariant, workflow validation) work unchanged
- Tested: all hooks pass on clean tree, deliberate doc link breakage is caught

## Test plan

- [x] `prek run --all-files` passes all checks
- [x] Broken wiki-link correctly caught by `docs-check-links`
- [x] taplo-format auto-fixes TOML formatting on commit
- [x] commit-msg hook (mikado invariant) fires correctly

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/276
2026-03-02 18:15:23 -08:00
940219338a Slightly less confusing output when mikado cards share a prereq 2026-03-02 17:58:18 -08:00
2d54f93c68 Add changelog for impl-card-guard feature
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:45:01 -08:00
6131f881a8 Enforce impl commits can't modify Mikado card files
The mikado-branch-invariant-check hook now inspects staged files (commit-msg
hook) and historical commit files (standalone mode) to reject impl commits
that touch markdown files with Mikado frontmatter (requires:, status:, or
branch: mikado/). Cards should only be modified by plan, close, or finalize.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:44:34 -08:00
10d0636cee Review navidrome and miniflux: both at latest versions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 07:32:17 -08:00
9465b75815 Add changelog for authentik source chain doc review
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 07:28:09 -08:00
08b9570ac7 Review build-authentik-from-source Mikado chain docs
Fix go-server-derivation: wrong path target (webui not authentik-django)
and missing internal/web/static.go patch. Remove stale DRF fork content
from mirror-build-deps (no longer needed as of 2026.2.0). Add
last-reviewed to all 5 cards without it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 07:28:09 -08:00
Forgejo Actions
847e47eaf3 Update docs release to v1.12.0
- Built changelog from towncrier fragments

[skip ci]
2026-03-01 17:24:09 -08:00
2a2811d7a5 Review authentik-api-client-generation doc: fix stale content v1.12.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 17:21:46 -08:00
c9d273dc81 Update authentik changelog fragment to mention version upgrade
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 17:11:41 -08:00
503775085d Deploy authentik 2026.2.0 with migration ordering fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 16:32:10 -08:00
2d4098e480 Fix authentik 2026.2.0 migration ordering bug (#275)
All checks were successful
Build Container / detect (push) Successful in 2s
Build Container (Nix) / detect (push) Successful in 1s
Build Container / build (authentik) (push) Successful in 1s
Build Container (Nix) / build (authentik) (push) Successful in 3m6s
## Summary

- Patch `authentik_rbac/0010` migration to depend on `authentik_core/0056`, fixing non-deterministic ordering that crashes startup with `FieldError: Cannot resolve keyword 'group_id'`
- Upstream bug: goauthentik/authentik#19616, #20634 — no fix released yet
- Document the issue in the lessons-learned table

## Deployment and Testing

- [ ] CI builds container image
- [ ] Deploy from branch: `argocd app set authentik --revision fix/authentik-migration-ordering && argocd app sync authentik`
- [ ] Pods reach Running/Ready without crash-looping
- [ ] `kubectl logs` show 0056 migrating before 0010
- [ ] authentik UI loads at authentik.ops.eblu.me
- [ ] `mise run services-check`
- [ ] After merge: `argocd app set authentik --revision main && argocd app sync authentik`

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/275
2026-03-01 16:28:36 -08:00
90621e4155 Deploy authentik 2026.2.0 with entry_points fix
Update image tag to v2026.2.0-78027eb-nix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 16:04:29 -08:00
78027eb783 Fix authentik: entry_points API for Python 3.14
All checks were successful
Build Container (Nix) / detect (push) Successful in 1s
Build Container / detect (push) Successful in 2s
Build Container / build (authentik) (push) Successful in 2s
Build Container (Nix) / build (authentik) (push) Successful in 3m7s
Python 3.14's EntryPoints uses string keys, not integer indices.
eps[0] raises KeyError(0); use next(iter(eps)) instead.
Verified on ringtail with the actual venv python.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 16:02:49 -08:00
e2c650b027 Deploy authentik 2026.2.0 with BASE_DIR fix
Update image tag to v2026.2.0-e49d966-nix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 15:55:50 -08:00
e49d966229 Fix authentik: symlink authentik/ for BASE_DIR resolution
All checks were successful
Build Container (Nix) / detect (push) Successful in 1s
Build Container / detect (push) Successful in 2s
Build Container / build (authentik) (push) Successful in 2s
Build Container (Nix) / build (authentik) (push) Successful in 3m4s
Django's BASE_DIR is $out but source lives in site-packages. Code
like BASE_DIR / "authentik" / "sources" / "scim" / "schemas" / ...
needs a top-level symlink to find data files alongside Python source.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 15:55:26 -08:00
c0e29476f3 Deploy authentik 2026.2.0 with TMPDIR fix
Update image tag to v2026.2.0-b7bfb0b-nix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 15:53:09 -08:00
b7bfb0bfae Fix authentik container: set TMPDIR=/tmp
All checks were successful
Build Container (Nix) / detect (push) Successful in 1s
Build Container / detect (push) Successful in 2s
Build Container / build (authentik) (push) Successful in 1s
Build Container (Nix) / build (authentik) (push) Successful in 45s
lifecycle/ak uses ${TMPDIR}/authentik-mode — without TMPDIR set it
tries to write /authentik-mode in root, which user 65534 can't do.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 15:52:36 -08:00
38da372f94 Deploy authentik 2026.2.0 with /tmp fix
Update image tag to v2026.2.0-2ac353b-nix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 15:51:17 -08:00
2ac353b7bf Fix authentik container: create /tmp for unprivileged user
All checks were successful
Build Container (Nix) / detect (push) Successful in 1s
Build Container / detect (push) Successful in 2s
Build Container / build (authentik) (push) Successful in 1s
Build Container (Nix) / build (authentik) (push) Successful in 54s
buildLayeredImage doesn't create /tmp by default. The container runs
as user 65534 (nobody) which can't mkdir /tmp at runtime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 15:48:05 -08:00
098f3e517c Deploy authentik 2026.2.0 (source-built) to ArgoCD
Update image tag to v2026.2.0-efa9806-nix — the first source-built
authentik container from the build-authentik-from-source chain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 15:44:35 -08:00
efa9806bfa C2: Build authentik from source (Mikado chain) (#274)
All checks were successful
Build Container / detect (push) Successful in 3s
Build Container (Nix) / detect (push) Successful in 1s
Build Container / build (authentik) (push) Successful in 2s
Build Container (Nix) / build (authentik) (push) Successful in 22s
## Mikado Chain: build-authentik-from-source

Replace `pkgs.authentik` from nixpkgs with a custom Nix derivation built from source.
This removes the dependency on the nixpkgs packaging timeline and gives full version control.

Target version: **2025.12.4** (nixpkgs reference, upgrading from deployed 2025.10.1).

### Dependency Graph

```
build-authentik-from-source (goal)
├── authentik-go-server-derivation
│   ├── authentik-api-client-generation  ← IN PROGRESS
│   └── authentik-python-backend-derivation
├── authentik-web-ui-derivation
│   └── authentik-api-client-generation  ← IN PROGRESS
└── authentik-python-backend-derivation
```

### Ready Leaves
- `authentik-api-client-generation` — Go + TypeScript client generation from OpenAPI schema
- `authentik-python-backend-derivation` — Django backend with 60+ deps, 4 in-tree packages

### Architecture
Ported from [nixpkgs `pkgs/by-name/au/authentik/package.nix`](https://github.com/NixOS/nixpkgs/tree/master/pkgs/by-name/au/authentik):
- `source.nix` — shared version/source fetch
- `client-go.nix` — Go API client generation
- `client-ts.nix` — TypeScript API client generation
- `api-go-vendor-hook.nix` — Go vendor directory injection hook
- (more components to follow as leaves are closed)

### Related Cards
- [[build-authentik-from-source]] — Goal card
- [[authentik-api-client-generation]]
- [[authentik-python-backend-derivation]]
- [[authentik-web-ui-derivation]]
- [[authentik-go-server-derivation]]

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/274
2026-03-01 13:45:00 -08:00
0aaf9bb8b2 Add Dagger local build step to authentik source build goal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 08:39:25 -08:00
7094ea7d3e Start C2 Mikado chain: build authentik from source
Create goal card and 4 prerequisite cards for building authentik from a
custom Nix derivation instead of using pkgs.authentik from nixpkgs. This
removes the dependency on the nixpkgs packaging timeline and gives full
version control over authentik releases.

Chain: mikado/authentik-source-build
Leaf nodes: authentik-api-client-generation, authentik-python-backend-derivation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 08:20:17 -08:00
922265c88f Add changelog fragment for grafana doc review
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 07:28:47 -08:00
8d1e98617b Review build-grafana-container docs: stamp reviewed, fix cross-links
Also fix stale grafana.md reference card (Helm → Kustomize).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 07:28:06 -08:00
02eb169403 Pin blumeops-pg to PostgreSQL 18.3
Replace floating :18 tag with pinned :18.3 (upstream out-of-cycle
release fixing 18.2 regressions). Stamps service as reviewed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 16:25:32 -08:00
2312e5fbf8 Update ringtail flake inputs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:22:29 -08:00
7cecaf0471 Review forgejo-runner docs: stamp reviewed, fix cross-links
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:10:20 -08:00
776caa87f5 Sync Frigate zone coordinates from live API
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 07:52:09 -08:00
Forgejo Actions
fa223f8e3b Update docs release to v1.11.5
- Built changelog from towncrier fragments

[skip ci]
2026-02-26 07:56:02 -08:00
be3cdad1cb Add HA for CV and Docs: zero-downtime deploys (#273) v1.11.5
## Summary
- Set `replicas: 2` with `maxUnavailable: 0` / `maxSurge: 1` on CV and Docs deployments so rolling updates never drop below 2 ready pods
- Add PodDisruptionBudgets (`minAvailable: 1`) to protect against node drains and cluster maintenance
- Add Fly.io cache purge step to `cv-deploy.yaml` workflow (docs already had this) so CV deploys don't serve stale cached content

## Deployment and Testing
- [ ] `argocd app diff cv` / `argocd app diff docs` from branch
- [ ] Deploy from branch: `argocd app set cv --revision feature/ha-cv-docs-zero-downtime && argocd app sync cv`
- [ ] Verify 2 pods running: `kubectl get pods -n cv --context=minikube-indri`
- [ ] Test rolling restart: `kubectl rollout restart deployment/cv -n cv --context=minikube-indri`
- [ ] During rollout, confirm continuous availability via `curl -I https://cv.eblu.me`
- [ ] After merge: reset ArgoCD to main, re-sync both apps

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/273
2026-02-26 07:53:21 -08:00
8e9d89ca73 docs-review: print file path instead of content for LLM usage
The LLM should read the file itself using its tools rather than
receiving it inline in the task output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 07:24:37 -08:00
9a7acffa26 Review manage-forgejo-mirrors doc: clarify cron default, stamp reviewed
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 07:17:18 -08:00