New Grimoire

This commit is contained in:
traveler 2026-04-12 09:53:51 -05:00
parent 77d589a13d
commit cc574f8aed
157 changed files with 29420 additions and 0 deletions

View file

@ -0,0 +1,522 @@
---
title: Caddy Reverse Proxy
description: Curreent and future config
published: true
date: 2026-02-25T01:50:20.558Z
tags:
editor: markdown
dateCreated: 2026-02-23T22:09:16.106Z
---
# Caddy Reverse Proxy
**Host:** znas (Docker Swarm node)
**Internal IP:** 192.168.5.10
**Data Path:** `/export/Docker/caddy/`
**Networks:** `netgrimoire` (service network), `vpn`
**Ports:** 80 (mapped to host 8900), 443
---
## Overview
Caddy serves as the primary reverse proxy for all public and internal web services. It uses the `caddy-docker-proxy` pattern, which allows services to register themselves with Caddy by adding Docker labels to their compose files — no manual Caddyfile edits required per service.
Configuration is **hybrid**: some services are defined entirely via Docker labels, others are defined statically in the Caddyfile, and most use both (labels for routing, Caddyfile for shared snippets). The `caddy-docker-proxy` container merges both sources at runtime.
---
## Current State
### Image
```yaml
image: lucaslorentz/caddy-docker-proxy:ci-alpine
```
This image provides the Docker Proxy module only. It has no CrowdSec, GeoIP, or rate limiting built in.
### Docker Compose (`/export/Docker/caddy/docker-compose.yml`)
```yaml
configs:
caddy-basic-content:
file: ./Caddyfile
labels:
caddy:
services:
caddy:
image: lucaslorentz/caddy-docker-proxy:ci-alpine
ports:
- 8900:80
- 443:443
environment:
- CADDY_INGRESS_NETWORKS=netgrimoire
networks:
- netgrimoire
- vpn
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /export/Docker/caddy/Caddyfile:/etc/caddy/Caddyfile
- /export/Docker/caddy:/data
#- /export/Docker/caddy/logs:/var/log/caddy # Placeholder for CrowdSec log mount
deploy:
placement:
constraints:
- node.hostname == znas
networks:
netgrimoire:
external: true
vpn:
external: true
```
### Caddyfile (`/export/Docker/caddy/Caddyfile`)
The Caddyfile defines shared authentication snippets and static site blocks. These snippets are available to all services — including label-defined ones — via `import`.
```caddyfile
# ─────────────────────────────────────────────────────────────────────────────
# AUTH SNIPPETS
# ─────────────────────────────────────────────────────────────────────────────
(authentik) {
route /outpost.goauthentik.io/* {
reverse_proxy http://authentik:9000
}
forward_auth http://authentik:9000 {
uri /outpost.goauthentik.io/auth/caddy
header_up X-Forwarded-URI {http.request.uri}
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email \
X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt \
X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider \
X-Authentik-Meta-App X-Authentik-Meta-Version
}
}
(authelia) {
forward_auth http://authelia:9091 {
uri /api/verify?rd=https://login.wasted-bandwidth.net/
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
}
# ─────────────────────────────────────────────────────────────────────────────
# MAIL SNIPPETS
# ─────────────────────────────────────────────────────────────────────────────
(email-proxy) {
redir https://mail.netgrimoire.com/sogo 301
}
(mailcow-proxy) {
reverse_proxy nginx-mailcow:80
}
# ─────────────────────────────────────────────────────────────────────────────
# STATIC SITE BLOCKS — NETGRIMOIRE.COM
# ─────────────────────────────────────────────────────────────────────────────
cloud.netgrimoire.com {
reverse_proxy http://nextcloud-aio-apache:11000
}
log.netgrimoire.com {
reverse_proxy http://graylog:9000
}
win.netgrimoire.com {
reverse_proxy http://192.168.5.10:8006
}
docker.netgrimoire.com {
reverse_proxy http://portainer:9000
}
immich.netgrimoire.com {
reverse_proxy http://192.168.5.10:2283
}
npm.netgrimoire.com {
reverse_proxy http://librenms:8000
}
#jellyfin.netgrimoire.com {
# reverse_proxy http://jellyfin:8096
#}
# ─────────────────────────────────────────────────────────────────────────────
# AUTHENTICATED — NETGRIMOIRE.COM
# ─────────────────────────────────────────────────────────────────────────────
dozzle.netgrimoire.com {
import authentik
reverse_proxy http://192.168.4.72:8043
}
dns.netgrimoire.com {
import authentik
reverse_proxy http://192.168.5.7:5380
}
webtop.netgrimoire.com {
import authentik
reverse_proxy http://webtop:3000
}
jackett.netgrimoire.com {
import authentik
reverse_proxy http://gluetun:9117
}
transmission.netgrimoire.com {
import authentik
reverse_proxy http://gluetun:9091
}
scrutiny.netgrimoire.com {
import authentik
reverse_proxy http://192.168.5.10:8081
}
# ─────────────────────────────────────────────────────────────────────────────
# AUTHENTICATED — WASTED-BANDWIDTH.NET (Authelia)
# ─────────────────────────────────────────────────────────────────────────────
stash.wasted-bandwidth.net {
import authelia
reverse_proxy http://192.168.5.10:9999
}
namer.wasted-bandwidth.net {
import authelia
reverse_proxy http://192.168.5.10:6980
}
# ─────────────────────────────────────────────────────────────────────────────
# PUBLIC — PNCHARRIS.COM / WASTED-BANDWIDTH.NET
# ─────────────────────────────────────────────────────────────────────────────
fish.pncharris.com {
reverse_proxy http://web
}
www.wasted-bandwidth.net {
reverse_proxy http://web
}
# ─────────────────────────────────────────────────────────────────────────────
# MAILCOW — MULTI-DOMAIN
# ─────────────────────────────────────────────────────────────────────────────
mail.netgrimoire.com, autodiscover.netgrimoire.com, autoconfig.netgrimoire.com, \
mail.wasted-bandwidth.net, autodiscover.wasted-bandwidth.net, autoconfig.wasted-bandwidth.net, \
mail.gnarlypandaproductions.com, autodiscover.gnarlypandaproductions.com, autoconfig.gnarlypandaproductions.com, \
mail.pncfishandmore.com, autodiscover.pncfishandmore.com, autoconfig.pncfishandmore.com, \
mail.pncharrisenterprises.com, autodiscover.pncharrisenterprises.com, autoconfig.pncharrisenterprises.com, \
mail.pncharris.com, autodiscover.pncharris.com, autoconfig.pncharris.com, \
mail.florosafd.org, autodiscover.florosafd.org, autoconfig.florosafd.org {
import mailcow-proxy
}
```
### Docker Label Pattern (label-defined services)
Services not in the Caddyfile are registered via labels on their own containers. The snippet defined in the Caddyfile is available to them via `caddy.import`:
```yaml
labels:
- caddy=homepage.netgrimoire.com
- caddy.import=authentik
- caddy.reverse_proxy={{upstreams 3000}}
```
For services that need no auth:
```yaml
labels:
- caddy=myservice.netgrimoire.com
- caddy.reverse_proxy={{upstreams 8080}}
```
---
## Authentication Layers
Two identity proxies are in use, each serving different domains/use cases:
| Provider | Domain Pattern | Snippet |
|----------|----------------|---------|
| Authentik | `*.netgrimoire.com` internal tools | `import authentik` |
| Authelia | `*.wasted-bandwidth.net` | `import authelia` |
Services without an auth import are either public (e.g. `fish.pncharris.com`) or carry their own authentication (e.g. Nextcloud, Graylog, Portainer).
---
## Current Security Posture
CrowdSec protection exists only at the **OPNsense firewall level** — IP reputation blocking before traffic reaches Caddy. CrowdSec does not currently inspect HTTP traffic at the application layer. This means:
- Known-bad IPs are blocked at the perimeter
- Application-layer attacks (SQLi in URLs, malicious paths, bad user agents, brute force on specific endpoints) are not blocked at the Caddy level
- Services behind Authentik/Authelia have an additional protection layer; unauthenticated public services do not
---
## Future State: CrowdSec + GeoIP + Rate Limiting
### Target Image
```yaml
image: ghcr.io/serfriz/caddy-crowdsec-geoip-ratelimit-security-dockerproxy:latest
```
This is a drop-in replacement for `lucaslorentz/caddy-docker-proxy`. All existing Docker labels and Caddyfile site blocks continue to work unchanged. The image is automatically rebuilt monthly when Caddy releases updates — no custom image maintenance required.
**Included modules:**
- `caddy-docker-proxy` — same label-based config as current
- `caddy-crowdsec-bouncer` — inline HTTP blocking based on CrowdSec decisions
- `caddy-geoip` — GeoIP filtering at the application layer
- `caddy-ratelimit` — per-endpoint rate limiting
- `caddy-security` — additional auth/security middleware
### Updated Compose
```yaml
configs:
caddy-basic-content:
file: ./Caddyfile
labels:
caddy:
services:
caddy:
image: ghcr.io/serfriz/caddy-crowdsec-geoip-ratelimit-security-dockerproxy:latest
ports:
- 8900:80
- 443:443
environment:
- CADDY_INGRESS_NETWORKS=netgrimoire
- CADDY_DOCKER_EVENT_THROTTLE_INTERVAL=2000 # Prevents non-deterministic reload with CrowdSec module
- CROWDSEC_API_KEY=BYSLg/wKOa7wlHYzChJpBVJA06Ukc7G6fKJCvBwjyZg
networks:
- netgrimoire
- vpn
- crowdsec_net
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /export/Docker/caddy/Caddyfile:/etc/caddy/Caddyfile
- /export/Docker/caddy:/data
- caddy-logs:/var/log/caddy
deploy:
placement:
constraints:
- node.hostname == znas
crowdsec:
image: crowdsecurity/crowdsec
restart: unless-stopped
environment:
COLLECTIONS: "crowdsecurity/caddy crowdsecurity/http-cve crowdsecurity/whitelist-good-actors"
BOUNCER_KEY_CADDY: BYSLg/wKOa7wlHYzChJpBVJA06Ukc7G6fKJCvBwjyZg # Pre-registers the Caddy bouncer automatically
volumes:
- crowdsec-db:/var/lib/crowdsec/data
- ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml
- caddy-logs:/var/log/caddy:ro
networks:
- crowdsec_net
deploy:
placement:
constraints:
- node.hostname == znas
volumes:
caddy-logs:
crowdsec-db:
networks:
netgrimoire:
external: true
vpn:
external: true
crowdsec_net:
driver: overlay # Swarm overlay network
```
### CrowdSec Log Acquisition (`./crowdsec/acquis.yaml`)
```yaml
filenames:
- /var/log/caddy/access.log
labels:
type: caddy
```
### Environment File (`.env`)
```env
CROWDSEC_API_KEY=<generate-with-cscli-or-set-before-first-boot>
```
The `BOUNCER_KEY_CADDY` env var in the CrowdSec container pre-registers the bouncer key at startup. Set the same value in `.env` as `CROWDSEC_API_KEY` and both sides will be in sync on first boot — no need to run `cscli bouncers add` manually.
### Updated Caddyfile Additions
Add a global block at the top of the Caddyfile and a new `crowdsec` snippet. All other existing content remains unchanged.
```caddyfile
# ─────────────────────────────────────────────────────────────────────────────
# GLOBAL BLOCK — add this at the very top before any snippets
# ─────────────────────────────────────────────────────────────────────────────
{
crowdsec {
api_url http://crowdsec:8080
api_key {$CROWDSEC_API_KEY}
}
log {
output file /var/log/caddy/access.log {
roll_size 50mb
roll_keep 5
}
format json
}
}
# ─────────────────────────────────────────────────────────────────────────────
# CROWDSEC SNIPPET — add alongside existing auth snippets
# ─────────────────────────────────────────────────────────────────────────────
(crowdsec) {
route {
crowdsec
}
}
```
### Applying CrowdSec to Existing Services
Once the snippet exists, add `import crowdsec` to site blocks and container labels. This is a **gradual rollout** — services without it remain fully functional, just without Caddy-level CrowdSec inspection (they still have OPNsense perimeter protection).
**In the Caddyfile:**
```caddyfile
# Before
cloud.netgrimoire.com {
reverse_proxy http://nextcloud-aio-apache:11000
}
# After
cloud.netgrimoire.com {
import crowdsec
reverse_proxy http://nextcloud-aio-apache:11000
}
# With auth
dozzle.netgrimoire.com {
import crowdsec
import authentik
reverse_proxy http://192.168.4.72:8043
}
```
**In Docker labels:**
```yaml
labels:
- caddy=homepage.netgrimoire.com
- caddy.import=crowdsec
- caddy.import=authentik
- caddy.reverse_proxy={{upstreams 3000}}
```
### CrowdSec Rollout Priority
Roll out `import crowdsec` in this order based on risk exposure:
**High priority — do first (public, no auth):**
- `cloud.netgrimoire.com` (Nextcloud)
- `immich.netgrimoire.com`
- `docker.netgrimoire.com` (Portainer)
- `fish.pncharris.com`
- `www.wasted-bandwidth.net`
**Medium priority — high value behind auth:**
- `log.netgrimoire.com` (Graylog)
- `win.netgrimoire.com` (Proxmox)
- All `dozzle`, `dns`, `webtop`, `jackett`, `transmission`, `scrutiny`
**Lower priority — already protected by Authelia/Authentik:**
- `stash.wasted-bandwidth.net`
- `namer.wasted-bandwidth.net`
- All label-defined services behind auth
**Skip:**
- Mailcow block — handled by nginx-mailcow, different threat model
### Behavior if CrowdSec Container Goes Down
The bouncer is designed to **fail open** by default. If `crowdsec` is unreachable, Caddy continues serving traffic normally — enforcement is temporarily suspended but the site stays up. This is the safe default for a homelab. To change this behavior, set `enable_hard_fails true` in the global crowdsec block (will cause 500 errors if CrowdSec is down — not recommended for homelab).
---
## Bootstrap Steps
When ready to migrate to the new image:
**Step 1 — Add the CrowdSec global block and snippet to the Caddyfile** before changing the image. This ensures the Caddyfile is valid for the new image on startup.
**Step 2 — Create `./crowdsec/acquis.yaml`** with the content above.
**Step 3 — Create `.env`** with a strong random value for `CROWDSEC_API_KEY`:
```bash
openssl rand -hex 32
```
**Step 4 — Update the image and add the CrowdSec service to the compose file**, then redeploy:
```bash
docker stack deploy -c docker-compose.yml caddy
```
**Step 5 — Verify CrowdSec is reading Caddy logs:**
```bash
docker exec <crowdsec_container> cscli metrics
```
Look for the `Acquisition Metrics` table showing hits from `/var/log/caddy/access.log`.
**Step 6 — Test a ban manually:**
```bash
docker exec <crowdsec_container> cscli decisions add --ip 1.2.3.4 --duration 5m
# Verify the IP gets a 403 from Caddy
curl -I https://yoursite.com --resolve yoursite.com:443:1.2.3.4
docker exec <crowdsec_container> cscli decisions delete --ip 1.2.3.4
```
**Step 7 — Gradually add `import crowdsec`** to site blocks and labels per the priority order above.
---
## File Layout
```
/export/Docker/caddy/
├── Caddyfile # Shared snippets and static site blocks
├── docker-compose.yml # Caddy + CrowdSec services
├── .env # CROWDSEC_API_KEY (future)
├── data/ # Caddy data volume (TLS certs, etc.)
├── logs/ # caddy-logs volume mount point (future)
└── crowdsec/
└── acquis.yaml # Tells CrowdSec where to read Caddy logs (future)
```
---
## Known Issues / Notes
- Port 80 is mapped to host port 8900 — this is intentional for Swarm. OPNsense NAT handles the external 80→8900 translation.
- The `CADDY_DOCKER_EVENT_THROTTLE_INTERVAL=2000` setting is **required** with the CrowdSec module to prevent non-deterministic domain matching behavior during container label reloads (see [issue #61](https://github.com/hslatman/caddy-crowdsec-bouncer/issues/61)).
- Jellyfin is commented out in the Caddyfile — likely served via a different path or disabled temporarily.
- The `web` upstream referenced by `fish.pncharris.com` and `www.wasted-bandwidth.net` resolves to a container named `web` on the `netgrimoire` network.
- Authelia redirect URL is `https://login.wasted-bandwidth.net/` — update if this changes.
- The serfriz image is rebuilt on the **1st of each month** for module updates, and on every new Caddy release. Force a module update by recreating the container: `docker service update --force caddy_caddy`.

View file

@ -0,0 +1,144 @@
---
title: Docker Swarm Template Standard
description: Canonical YAML template and label rules for all Netgrimoire swarm services
published: true
date: 2026-04-12T00:00:00.000Z
tags: keystone, docker, swarm
editor: markdown
dateCreated: 2026-04-12T00:00:00.000Z
---
# Docker Swarm Template Standard
All Swarm YAML files in `services/swarm/` and `services/swarm/stack/` must follow this standard. The Gremlin audit workflow checks compliance weekly.
---
## Canonical Template
```yaml
# Deploy: docker stack deploy -c <service>.yaml <service>
services:
<servicename>:
image: <image>:latest
environment:
TZ: America/Chicago
volumes:
- /DockerVol/<servicename>:/config
# - /data/nfs/znas/Docker/<servicename>:/data
networks:
- netgrimoire
deploy:
restart_policy:
condition: any
delay: 5s
max_attempts: 3
window: 120s
placement:
constraints:
- node.hostname == znas
- node.platform.arch != aarch64
- node.platform.arch != arm
labels:
# Caddy
caddy: <servicename>.netgrimoire.com
caddy.reverse_proxy: <servicename>:<PORT>
caddy.import: crowdsec
caddy.import_1: authentik
# Uptime Kuma
kuma.<servicename>.http.name: <Service Name>
kuma.<servicename>.http.url: https://<servicename>.netgrimoire.com
# Homepage
homepage.group: <Group>
homepage.name: <Service Name>
homepage.icon: <service>.png
homepage.href: https://<servicename>.netgrimoire.com
homepage.description: <Description>
# DIUN
diun.enable: "true"
networks:
netgrimoire:
external: true
```
---
## Forbidden Fields
Never use these at the service level:
| Field | Reason |
|-------|--------|
| `version:` | Deprecated in Compose v2+ |
| `container_name:` | Incompatible with Swarm replicas |
| `restart:` | Use `deploy.restart_policy` instead |
| `depends_on:` | Not supported in Swarm mode |
| `endpoint_mode: dnsrr` | Breaks internal DNS — always use VIP |
---
## Volume Path Rules
| Path | When to Use |
|------|-------------|
| `/DockerVol/<service>` | Config, SQLite DBs, small app state. **Only valid with a `node.hostname` placement constraint.** |
| `/data/nfs/znas/Docker/<service>` | Bulk data, media, or any service without a hostname constraint |
---
## Placement Constraints
**Default (all services):**
```yaml
constraints:
- node.hostname == znas
- node.platform.arch != aarch64
- node.platform.arch != arm
```
ARM exclusion prevents accidental scheduling on Pi vault/worker nodes. Override only if the service is ARM-specific.
For services pinned to docker4 (Gremlin stack):
```yaml
constraints:
- node.hostname == docker4
- node.platform.arch != aarch64
- node.platform.arch != arm
```
---
## Caddy Label Rules
```yaml
caddy: servicename.netgrimoire.com # no https:// prefix
caddy.reverse_proxy: servicename:PORT # container name:port, NOT {{upstreams PORT}}
caddy.import: crowdsec # always both
caddy.import_1: authentik # always both, no exceptions
```
Never use `{{upstreams PORT}}` — it breaks during `docker stack config` preprocessing.
**Wasted-bandwidth services** use `wasted-bandwidth.net` domain and `caddy.import_1: authelia` instead of authentik.
---
## Deploy Workflow
```bash
# From services repo root
git add . && git commit -m "Add/update <service>" && git push
# On znas (or docker4 for Gremlin services)
cd ~/services && git pull
cd swarm/stack/<StackName>
set -a && source .env && set +a
docker stack config --compose-file <service>.yaml > resolved.yml
docker stack deploy --compose-file resolved.yml <service>
rm resolved.yml
docker stack services <service>
```