Alpine ships alpine-ssh.conf with sshd and sshd-ddos jails enabled.
These fail on startup because there's no SSH server or /var/log/messages
in the container. Remove the file after install instead of trying to
override via [DEFAULT] (per-jail enabled=true beats DEFAULT).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use [DEFAULT] enabled = false to disable all inherited jails globally.
The previous fix only disabled sshd, but sshd-ddos (and potentially
others) also fail looking for missing log files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
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>