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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
## 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
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
The external-secrets webhook injects conversionStrategy, decodingStrategy,
and metadataPolicy defaults on admission. Declaring them explicitly prevents
ArgoCD SSA from flagging the resource as OutOfSync.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>