2026-02-03 21:17:58 -08:00
---
2026-02-07 21:44:57 -08:00
title: Caddy
2026-03-15 10:29:45 -07:00
modified: 2026-03-15
2026-02-03 21:17:58 -08:00
tags:
- service
- networking
- tls
---
# Caddy
Reverse proxy for `*.ops.eblu.me` services with automatic TLS via ACME DNS-01.
## Quick Reference
| Property | Value |
|----------|-------|
| **Domain ** | `*.ops.eblu.me` |
| **HTTPS Port ** | 443 |
| **Config ** | `ansible/roles/caddy/templates/Caddyfile.j2` |
| **Binary ** | Custom build with Gandi DNS plugin |
## Why Caddy?
Caddy provides a single TLS termination point for all BlumeOps services:
- **Wildcard certificate** for `*.ops.eblu.me` via Let's Encrypt
- **DNS-01 challenge** using Gandi API (no port 80 needed)
- **Unified access** from k8s pods, containers, and tailnet clients
See [[routing]] for when to use `*.ops.eblu.me` vs `*.tail8d86e.ts.net` .
## Proxied Services
### Indri-Local Services
| Subdomain | Backend | Service |
|-----------|---------|---------|
| `forge.ops.eblu.me` | `localhost:3001` | [[forgejo]] |
| `registry.ops.eblu.me` | `localhost:5050` | [[zot]] |
| `jellyfin.ops.eblu.me` | `localhost:8096` | [[jellyfin]] |
### Kubernetes Services
K8s services are proxied via their Tailscale Ingress endpoints:
| Subdomain | Backend | Service |
|-----------|---------|---------|
| `grafana.ops.eblu.me` | `grafana.tail8d86e.ts.net` | [[grafana]] |
| `argocd.ops.eblu.me` | `argocd.tail8d86e.ts.net` | [[argocd]] |
2026-02-12 11:45:32 -08:00
| `cv.ops.eblu.me` | `cv.tail8d86e.ts.net` | [[cv]] |
2026-02-08 02:36:19 -08:00
| `docs.ops.eblu.me` | `docs.tail8d86e.ts.net` | [[docs]] (now publicly available at `docs.eblu.me` via [[flyio-proxy]]) |
2026-02-03 21:17:58 -08:00
| `feed.ops.eblu.me` | `feed.tail8d86e.ts.net` | [[miniflux]] |
| ... | ... | (see defaults/main.yml for full list) |
### TCP Services (Layer 4)
| Port | Backend | Service |
|------|---------|---------|
| 2222 | `localhost:2200` | Forgejo SSH |
| 5432 | `pg.tail8d86e.ts.net:5432` | [[postgresql]] |
## Configuration
Caddy is managed via the `caddy` Ansible role:
```bash
# Deploy caddy changes
mise run provision-indri -- --tags caddy
```
**Key files:**
- `ansible/roles/caddy/defaults/main.yml` - Service definitions
- `ansible/roles/caddy/templates/Caddyfile.j2` - Caddy config template
## Secrets
| Secret | Source | Description |
|--------|--------|-------------|
| `GANDI_BEARER_TOKEN` | 1Password | API token for DNS-01 challenges |
The token is written to `~/.config/caddy/gandi-token` (chmod 0600) and sourced by the Caddy wrapper script.
2026-02-08 10:05:38 -08:00
## Security Considerations
Restrict flyio-proxy ACLs to dedicated tag:flyio-target endpoints (#126)
## Summary
- Introduce `tag:flyio-target` so services must explicitly opt in to be reachable by the fly.io proxy
- Replace broad `tag:k8s` and `tag:homelab` grants with the new tag in the ACL rule and test
- Add `tailscale.com/tags: "tag:k8s,tag:flyio-target"` annotation to docs, loki, and prometheus Ingresses
- Switch Alloy push endpoints from `*.ops.eblu.me` (Caddy) to `*.tail8d86e.ts.net` (Tailscale Ingress)
- Update docs: flyio-proxy, caddy, tailscale, forgejo (future public access + security checklist), expose-service-publicly
## Manual step (not in PR)
Update the k8s operator OAuth client in the Tailscale admin console to include `tag:flyio-target` in its scope. Without this, the operator cannot assign the new tag to Ingress proxy nodes.
## Deployment order
1. **Pulumi ACLs** — `mise run tailnet-preview && mise run tailnet-up`
2. **OAuth client** — Manual update in Tailscale admin console
3. **K8s Ingresses** — `argocd app sync apps && argocd app sync docs loki prometheus`
4. **Fly.io proxy** — `mise run fly-deploy`
5. **Verify** — `mise run services-check`, check Grafana dashboards
## Test plan
- [ ] `mise run tailnet-preview` shows clean diff
- [ ] `argocd app diff docs`, `argocd app diff loki`, `argocd app diff prometheus` show only annotation additions
- [ ] After deploy: Grafana dashboards show continued log/metric flow
- [ ] `curl -sf https://docs.eblu.me` returns 200
- [ ] `mise run services-check` passes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/126
2026-02-08 21:54:18 -08:00
Caddy has no authentication layer — it is a plain reverse proxy. Access control relies entirely on Tailscale ACLs restricting which devices can reach indri on port 443. Currently `tag:homelab` and `autogroup:admin` can reach Caddy. The [[flyio-proxy]] no longer routes through Caddy — it pushes logs and metrics directly to [[loki]] and [[prometheus]] via their Tailscale Ingress endpoints.
2026-02-08 10:05:38 -08:00
2026-02-03 21:17:58 -08:00
## Custom Build
2026-03-15 10:29:45 -07:00
Caddy is built from source using `xcaddy` with two plugins:
- `github.com/caddy-dns/gandi` — ACME DNS-01 challenges via Gandi API
- `github.com/mholt/caddy-l4` — Layer 4 (TCP/UDP) proxying
2026-02-03 21:17:58 -08:00
```bash
2026-03-15 10:29:45 -07:00
# Source and build location (mirrored on forge)
2026-02-03 21:17:58 -08:00
~/code/3rd/caddy/bin/caddy
2026-03-15 10:29:45 -07:00
# Build via mise task in the caddy clone
cd ~/code/3rd/caddy && mise run build
2026-02-03 21:17:58 -08:00
```
2026-03-15 10:29:45 -07:00
Forge mirrors: `mirrors/caddy` , `mirrors/caddy-gandi` , `mirrors/xcaddy` , `mirrors/caddy-l4` .
2026-02-03 21:17:58 -08:00
## Related
2026-02-07 21:02:10 -08:00
- [[gandi]] - DNS hosting and ACME DNS-01 provider
2026-02-03 21:17:58 -08:00
- [[routing]] - Service routing architecture
- [[forgejo]] - Git forge (proxied by Caddy)
- [[zot]] - Container registry (proxied by Caddy)
- [[tailscale-operator]] - K8s services use Tailscale Ingress, then Caddy