Compare commits

...

7 commits

Author SHA1 Message Date
fef521909b feat(forgejo): expose publicly at git.brianpooe.com with SSO-free access
Public repos are now browsable/clonable anonymously (HTTPS) and over SSH,
proxied through the Hetzner VPS. Unlike other public apps this is
deliberately not behind the oauth2-proxy/Zitadel gate, since Forgejo
already enforces its own per-repo visibility. Root redirects to the
.profile landing page. Documents the REQUIRE_SIGNIN_VIEW gotcha and the
Zitadel OIDC redirect_uri fix needed whenever FORGEJO_DOMAIN changes.
2026-07-19 18:34:13 +02:00
1c2f4a1c81 fix(forgejo): set SSH_DOMAIN so the UI's clone URL is actually usable
Forgejo's displayed SSH clone URL comes from SSH_DOMAIN, which defaults
to "localhost" (not DOMAIN) when unset — completely unusable, and
DOMAIN itself would've been wrong anyway since it resolves through
Caddy's wildcard CNAME, which SSH can't traverse. Point it at the
dedicated forgejo.home.brianpooe.com A record instead.
2026-07-19 15:17:25 +02:00
fc116653f7 docs(forgejo): document the forgejo.home.brianpooe.com DNS alias
Added a Technitium A record pointing at the Forgejo LXC directly
(SSH can't go through the Caddy wildcard CNAME), so git-over-SSH URLs
read ssh://git@forgejo.home.brianpooe.com/... instead of the raw IP.
All 5 hosts' remotes/SSH config updated to match.
2026-07-19 15:13:56 +02:00
9dfde294cb feat(forgejo): move git-over-SSH to standard port 22
Relocated LXC 114's own admin sshd to port 2200 (systemd ssh.socket
override) to free port 22 for Forgejo's git-SSH, dropping the ugly
:2222 from every clone URL. Updated the 5 hosts' remotes/SSH config
and the corresponding UniFi rules to match.
2026-07-19 15:09:55 +02:00
0ff2df7033 docs(network): add CADDY/IoT -> Forgejo git-over-SSH rules
Switched all internal hosts from HTTPS+token to SSH for git access to
Forgejo, using a shared deploy key. SSH bypasses Caddy (proxying raw
TCP isn't possible), so raspberrypi and Home Assistant each needed a
direct TRUSTED-zone rule to 10.0.10.18:2222.
2026-07-19 15:00:08 +02:00
a7de8cec3a docs(network): document IoT->Caddy path for HA's Forgejo git access
Home Assistant now pulls homelab-blueprint from Forgejo through Caddy,
which needed both a UniFi zone rule (IoT -> CADDY) and a ufw allow on
the Caddy host itself for the IoT subnet — two independent enforcement
layers, both previously missing/misconfigured.
2026-07-19 14:36:00 +02:00
73ff919b9f feat(forgejo): remove gitea-mirror, all repos now first-class
GitHub mirroring is no longer needed now that Forgejo is the primary
remote. Drops the gitea-mirror service, its volume, env vars, Caddy
site, and firewall rule; docs updated to match.
2026-07-19 13:21:05 +02:00
8 changed files with 215 additions and 164 deletions

View file

@ -32,7 +32,7 @@ help:
@echo " deploy-paperless - Deploy Paperless-ngx"
@echo " deploy-vaultwarden - Deploy Vaultwarden"
@echo " deploy-koffan - Deploy Koffan (grocery list)"
@echo " deploy-forgejo - Deploy Forgejo (self-hosted Git + GitHub mirroring)"
@echo " deploy-forgejo - Deploy Forgejo (self-hosted Git)"
@echo ""
@echo "Utility Targets:"
@echo " status - Show status of all deployed stacks"

View file

@ -28,7 +28,7 @@ Each stack guide in `docker-compose-files/<stack>/README.md` explains appdata pl
| [Beszel Hub](docker-compose-files/beszel-hub/README.md) | Beszel monitoring dashboard |
| [Beszel Socket Proxy](docker-compose-files/socket-proxy/README.md) | Restricted Docker API access for Beszel |
| [Caddy](docker-compose-files/caddy/README.md) | Reverse proxy with Cloudflare DNS-01 |
| [Forgejo](docker-compose-files/forgejo/README.md) | Self-hosted Git service with automatic GitHub mirroring |
| [Forgejo](docker-compose-files/forgejo/README.md) | Self-hosted Git service |
| [Gramps Web](docker-compose-files/gramps-web/README.md) | Genealogy web application |
| [Home Assistant](docker-compose-files/homeassistant/README.md) | Home Assistant, Mosquitto, and Zigbee2MQTT |
| [Immich](docker-compose-files/immich/README.md) | Photo and video management with GPU acceleration |

View file

@ -173,18 +173,20 @@ koffan.{{CADDY_BASE_DOMAIN:-home.example.com}} {
reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.12:8080
}
# Forgejo (LXC 114, .18). Git-over-SSH (port 2222) is not proxyable through
# Caddy — that port is exposed directly on the LXC for SSH clone/push.
git.{{CADDY_BASE_DOMAIN:-home.example.com}} {
# Forgejo (LXC 114, .18) -- publicly exposed at git.brianpooe.com (proxied via
# the VPS over NetBird for outside visitors) as well as reachable directly on
# the LAN via split-horizon DNS. One canonical hostname everywhere, so this
# is NOT under CADDY_BASE_DOMAIN like other internal-only apps.
# Git-over-SSH is not proxyable through Caddy -- forgejo.brianpooe.com
# resolves straight to the LXC (10.0.10.18) instead, bypassing Caddy entirely.
# Root redirects to the .profile landing page instead of the repo explore
# list -- see the Forgejo README's "Public Exposure" section.
git.brianpooe.com {
import common
redir / /brianpooe 302
reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.18:3000
}
git-mirror.{{CADDY_BASE_DOMAIN:-home.example.com}} {
import common
reverse_proxy {{CADDY_OFFICE_PREFIX:-10.0.10}}.18:4321
}
# Hermes Agent web dashboard (LXC 113, not docker — systemd service)
hermes.{{CADDY_BASE_DOMAIN:-home.example.com}} {
import common

View file

@ -1,6 +1,10 @@
# Forgejo
FORGEJO_VERSION=10
FORGEJO_HTTP_PORT=3000
# Defaults to 2222 because the LXC's own system sshd already owns port 22.
# To use the standard port 22 for git-over-SSH instead, first move the LXC's
# own admin sshd off 22 (e.g. via a systemd ssh.socket override setting
# ListenStream to another port), then set FORGEJO_SSH_PORT=22 here.
FORGEJO_SSH_PORT=2222
FORGEJO_BIND_IP=0.0.0.0
FORGEJO_MEM_LIMIT=1G
@ -9,6 +13,18 @@ FORGEJO_MEM_LIMIT=1G
FORGEJO_ROOT_URL=https://git.home.example.com
FORGEJO_DOMAIN=git.home.example.com
# Hostname shown in the web UI's SSH clone URL. Must resolve directly to
# this LXC (not through Caddy — SSH can't be proxied), so it needs its own
# DNS A record rather than reusing FORGEJO_DOMAIN's wildcard CNAME to Caddy.
#
# If this instance is publicly exposed, FORGEJO_DOMAIN/FORGEJO_SSH_DOMAIN
# should be the same public-facing names used everywhere (LAN and internet),
# resolved via split-horizon DNS: an internal DNS server answers them
# directly for LAN clients (its own zone/record, not the wildcard CNAME
# above), public DNS points them at your public edge for everyone else. See
# the Forgejo README's "Public Exposure" section for the full pattern.
FORGEJO_SSH_DOMAIN=forgejo.home.example.com
# Generate with: openssl rand -base64 32
FORGEJO_DB_PASSWORD=<random-secret>
@ -19,35 +35,13 @@ FORGEJO_DATA_ROOT=/mnt/unas/forgejo_data
# false = open self-signup; leave true and create accounts with
# `docker exec forgejo forgejo admin user create ...` instead.
FORGEJO_DISABLE_REGISTRATION=true
# If this instance is ever exposed publicly with the intent of letting
# outsiders browse public repos anonymously (GitHub-style), this MUST stay
# false. true forces every page -- including /explore and any .profile
# landing page -- through /user/login for anonymous visitors, regardless of
# FORGEJO_LANDING_PAGE. It silently defeats public repo browsing.
FORGEJO_REQUIRE_SIGNIN_VIEW=false
# --- gitea-mirror: auto-syncs GitHub repos into Forgejo ---
GITEA_MIRROR_VERSION=latest
GITEA_MIRROR_PORT=4321
GITEA_MIRROR_BIND_IP=0.0.0.0
GITEA_MIRROR_MEM_LIMIT=256m
# Full URL where gitea-mirror's own UI is reachable
GITEA_MIRROR_PUBLIC_URL=https://git-mirror.home.example.com
# Generate with: openssl rand -base64 32
GITEA_MIRROR_AUTH_SECRET=<random-secret>
# GitHub source — a classic PAT with repo (+ read:org if mirroring orgs) scope
GITHUB_USERNAME=<your-github-username>
GITHUB_TOKEN=<github-personal-access-token>
GITEA_MIRROR_SKIP_FORKS=false
GITEA_MIRROR_PRIVATE_REPOS=true
GITEA_MIRROR_STARRED=false
GITEA_MIRROR_ORGANIZATIONS=false
GITEA_MIRROR_ISSUES=false
GITEA_MIRROR_WIKI=false
# Forgejo destination — GITEA_MIRROR_FORGEJO_TOKEN can only be created after
# Forgejo is up and you've logged in once (Settings -> Applications ->
# Generate New Token, scopes: repo, admin:org). See README "GitHub mirroring".
GITEA_MIRROR_FORGEJO_USERNAME=<your-forgejo-username>
GITEA_MIRROR_FORGEJO_TOKEN=<forgejo-api-token>
GITEA_MIRROR_ORG=github-mirrors
GITEA_MIRROR_ORG_VISIBILITY=private
GITEA_MIRROR_INTERVAL=8h
# login = anonymous visitors land on the sign-in page (LAN-only/private use).
# explore = anonymous visitors land on the public repo listing, like GitHub's
# logged-out homepage. Use explore if this instance is publicly exposed.
FORGEJO_LANDING_PAGE=login

View file

@ -1,8 +1,6 @@
# Forgejo
Self-hosted Git service (soft fork of Gitea) with Postgres, plus
[gitea-mirror](https://github.com/RayLabsHQ/gitea-mirror) to automatically
pull all your GitHub repositories in as mirrors.
Self-hosted Git service (soft fork of Gitea) with Postgres.
## Persistent Data
@ -29,9 +27,6 @@ The share binds use `create_host_path: false`, so the stack refuses to start
if the share is not mounted, rather than silently writing into an empty
directory that later needs merging back in.
`gitea_mirror_data` (local named volume) holds gitea-mirror's own small
SQLite state DB — negligible size, not worth splitting out.
## Installation
1. Create `.env` if it does not exist:
@ -42,8 +37,7 @@ SQLite state DB — negligible size, not worth splitting out.
2. Set the required values in `docker-compose-files/forgejo/.env` — at
minimum `FORGEJO_ROOT_URL`, `FORGEJO_DOMAIN`, `FORGEJO_DB_PASSWORD`, and
`FORGEJO_DATA_ROOT`. Leave the `GITEA_MIRROR_*` GitHub/Forgejo-token vars
for step 6 — Forgejo needs to exist before those tokens can be generated.
`FORGEJO_DATA_ROOT`.
3. Add the UNAS share and bind-mount it into the LXC, following
[nfs-mounts.md](../../docs/unas/nfs-mounts.md) (add `forgejo_data` to the
@ -67,53 +61,93 @@ SQLite state DB — negligible size, not worth splitting out.
```bash
./substitute_env.sh docker-compose-files/forgejo/template.yaml docker-compose.forgejo.yml .env docker-compose-files/forgejo/.env
docker compose -f docker-compose.forgejo.yml config --quiet
docker compose -f docker-compose.forgejo.yml up -d forgejo-db forgejo
docker compose -f docker-compose.forgejo.yml up -d
```
`gitea-mirror` is intentionally left out of the first `up` — see step 6.
5. Open `http://<host-ip>:3000`, create the first (admin) account, and
confirm repo creation/push/clone works before wiring up mirroring.
## GitHub Mirroring
`gitea-mirror` is a separate small app, not a Forgejo built-in — Forgejo
itself has no config option to bulk-pull an entire GitHub account. Once you
have a Forgejo login:
1. In Forgejo: **Settings → Applications → Generate New Token**, scopes
`repo` + `admin:org` (needed to auto-create the `github-mirrors`
destination org). Put the token in `GITEA_MIRROR_FORGEJO_TOKEN`.
2. Create a GitHub classic PAT with `repo` scope (add `read:org` if you set
`GITEA_MIRROR_ORGANIZATIONS=true`). Put it in `GITHUB_TOKEN`.
3. Start the mirror service:
```bash
docker compose -f docker-compose.forgejo.yml up -d gitea-mirror
```
With `AUTO_IMPORT_REPOS=true`, `AUTO_MIRROR_REPOS=true`, and
`SCHEDULE_ENABLED=true` (all defaulted on in the template), it imports every
repo from `GITHUB_USERNAME` on first startup and re-syncs every
`GITEA_MIRROR_INTERVAL` (default `8h`) after that — no manual per-repo setup.
Fine-tune what gets pulled via `GITEA_MIRROR_PRIVATE_REPOS`,
`GITEA_MIRROR_STARRED`, `GITEA_MIRROR_ORGANIZATIONS`, `GITEA_MIRROR_SKIP_FORKS`.
`CLEANUP_DELETE_IF_NOT_IN_GITHUB` stays `false` by default — a repo you
delete on GitHub won't get deleted in Forgejo automatically. Flip that on
deliberately if you want mirrors pruned to match GitHub exactly.
Its own UI is at `http://<host-ip>:4321` for monitoring sync status and
tweaking mirror settings beyond what's templated here.
confirm repo creation/push/clone works.
## Reverse Proxy
Add blocks for `git.<domain>` (port `FORGEJO_HTTP_PORT`) and optionally
`git-mirror.<domain>` (port `GITEA_MIRROR_PORT`) to
Add a block for `git.<domain>` (port `FORGEJO_HTTP_PORT`) to
`docker-compose-files/caddy/Caddyfile_template`, then `make reload-caddy`.
Git-over-SSH (`FORGEJO_SSH_PORT`) is not proxyable through Caddy — expose that
port directly if you want SSH clone/push from outside the LAN.
`FORGEJO_SSH_PORT` defaults to `2222` because the LXC's own admin sshd
already owns port 22. This instance was later moved to the standard port —
the LXC's admin sshd got relocated to 2200 first (systemd `ssh.socket`
override), freeing 22 for Forgejo's git-SSH. See `.env.sample` for the
one-time steps if you want to do the same elsewhere.
Since SSH bypasses Caddy, this instance instead gets a plain Technitium `A`
record — `forgejo.brianpooe.com``10.0.10.18` — so clone URLs read
`ssh://git@forgejo.brianpooe.com/<owner>/<repo>.git` instead of the raw
LXC IP. Add this in the Technitium admin UI (or via its API) as a normal
zone record; it isn't behind the `*.home.brianpooe.com` → Caddy wildcard.
## Public Exposure
This instance is publicly reachable at `git.brianpooe.com` (HTTPS) and
`ssh://git@forgejo.brianpooe.com` (SSH), proxied through the Hetzner VPS —
see `~/netbird-vps/docs/netbird-acl-playbook.md` and `public-app-sso.md` on
the VPS for the edge side. Unlike other public apps (koffan, IT-Tools), this
one is deliberately **not** behind the oauth2-proxy/Zitadel SSO gate: public
repos need to be browsable/clonable with no login, like GitHub. Forgejo
enforces its own per-repo visibility regardless of network path, so private
repos stay private without an extra gate.
One canonical hostname everywhere — `FORGEJO_DOMAIN`/`FORGEJO_ROOT_URL` are
`git.brianpooe.com`, not a `.home.` subdomain — resolved via split-horizon
DNS: Technitium answers it directly on the LAN (own zone, `A` record ->
`10.0.15.5`), public DNS (`*.brianpooe.com` wildcard) sends it to the VPS.
Same pattern for `forgejo.brianpooe.com` (`A` record -> `10.0.10.18`
directly, bypassing Caddy, since SSH can't be proxied).
Two settings matter for a public instance and are easy to get wrong:
- `FORGEJO_REQUIRE_SIGNIN_VIEW` **must be `false`**. When `true` it forces
every page (including `/explore` and the `.profile` landing page) through
`/user/login` for anonymous visitors, regardless of `LANDING_PAGE` — it
silently defeats "outsiders can browse public repos."
- `FORGEJO_LANDING_PAGE=explore` so anonymous visitors land on the public
repo listing instead of a login page. On top of that, the Caddy vhost for
`git.brianpooe.com` (`docker-compose-files/caddy/Caddyfile_template`) has
`redir / /brianpooe 302`, so the *root* domain goes straight to the
`.profile` landing page instead of `/explore``/explore` is still one
click away via the nav sidebar. Update/remove that redirect if the
account name or desired landing target ever changes.
**Changing `FORGEJO_DOMAIN`/`FORGEJO_ROOT_URL` also requires updating the
Zitadel OIDC app's redirect URI**, or sign-in breaks with
`invalid_request: redirect_uri is missing in the client configuration`.
Forgejo's OIDC callback is always `<ROOT_URL>/user/oauth2/zitadel/callback`
— this is Forgejo's own direct Zitadel app (project **Homelab**, app
**Forgejo**), separate from the shared oauth2-proxy app used by
koffan/it-tools. Update it via the Zitadel console or, from the VPS:
```bash
PAT=$(cat ~/netbird-vps/zitadel/zitadel-data/admin.pat)
curl -H "Authorization: Bearer $PAT" -H "Content-Type: application/json" \
-X PUT "https://zitadel.brianpooe.com/management/v1/projects/345934587441971203/apps/382370882418376707/oidc_config" -d '{
"redirectUris": ["https://git.brianpooe.com/user/oauth2/zitadel/callback"],
"postLogoutRedirectUris": ["https://git.brianpooe.com"],
"responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
"grantTypes": ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE"],
"appType": "OIDC_APP_TYPE_WEB",
"authMethodType": "OIDC_AUTH_METHOD_TYPE_BASIC",
"accessTokenType": "OIDC_TOKEN_TYPE_BEARER"
}'
```
Public git-over-SSH reaches the LXC via a raw TCP forward on the VPS
(`~/netbird-vps/ssh-forward/`, a `socat` container in `network_mode: host`
listening on the VPS's now-freed port 22) over a NetBird route added
specifically for this (`10.0.10.18/32` on the `homelab-lan` network, routed
through the `pi-caddy` peer — see the VPS's `netbird-acl-playbook.md`
"Model 2" pattern). The VPS's own admin sshd was relocated to port 2200 to
free port 22 for this forward.
## Notes
- Pin `FORGEJO_VERSION` to a specific major (as shipped, `10`) rather than
@ -128,7 +162,6 @@ port directly if you want SSH clone/push from outside the LAN.
```bash
# Tail logs
docker compose -f docker-compose.forgejo.yml logs -f forgejo
docker compose -f docker-compose.forgejo.yml logs -f gitea-mirror
# Create an additional user without open registration
docker exec -it forgejo forgejo admin user create --username <name> --email <email> --random-password

View file

@ -1,5 +1,4 @@
# Forgejo self-hosted Git service, backed by Postgres, with automatic
# mirroring of GitHub repositories via gitea-mirror.
# Forgejo self-hosted Git service, backed by Postgres.
#
# Storage split (see docs/unas/nfs-mounts.md):
# - forgejo_data (local named volume): app config, SSH host keys, session
@ -68,6 +67,12 @@ services:
FORGEJO__database__PASSWD: "{{FORGEJO_DB_PASSWORD}}"
FORGEJO__server__ROOT_URL: "{{FORGEJO_ROOT_URL}}"
FORGEJO__server__DOMAIN: "{{FORGEJO_DOMAIN}}"
# SSH_DOMAIN drives the SSH clone URL shown in the web UI. It defaults
# to "localhost" (not DOMAIN), which is unusable here regardless: SSH
# can't be proxied through Caddy, so FORGEJO_DOMAIN's wildcard CNAME
# to the Caddy host would be wrong too. Point it at a dedicated DNS
# record resolving directly to this LXC instead (see README).
FORGEJO__server__SSH_DOMAIN: "{{FORGEJO_SSH_DOMAIN}}"
FORGEJO__server__SSH_PORT: "{{FORGEJO_SSH_PORT:-2222}}"
# LANDING_PAGE controls what anonymous visitors see at "/" — set to
# login so they land on sign-in instead of the marketing homepage.
@ -150,79 +155,6 @@ services:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
# Auto-syncs GitHub repos into Forgejo as mirrors. Forgejo itself has no
# "pull all my repos" config option — this is a separate open-source tool
# (https://github.com/RayLabsHQ/gitea-mirror) that provides exactly that,
# driven entirely by env vars below plus a one-time Forgejo API token (see
# README "GitHub mirroring" section — the token can't exist until Forgejo
# has an admin account, so it can't be templated here).
gitea-mirror:
image: ghcr.io/raylabshq/gitea-mirror:{{GITEA_MIRROR_VERSION:-latest}}
container_name: gitea-mirror
restart: unless-stopped
depends_on:
forgejo:
condition: service_healthy
environment:
NODE_ENV: production
DATABASE_URL: file:data/gitea-mirror.db
HOST: 0.0.0.0
PORT: "4321"
BASE_URL: "/"
BETTER_AUTH_SECRET: "{{GITEA_MIRROR_AUTH_SECRET}}"
BETTER_AUTH_URL: "{{GITEA_MIRROR_PUBLIC_URL}}"
PUBLIC_BETTER_AUTH_URL: "{{GITEA_MIRROR_PUBLIC_URL}}"
# GitHub source
GITHUB_USERNAME: "{{GITHUB_USERNAME}}"
GITHUB_TOKEN: "{{GITHUB_TOKEN}}"
SKIP_FORKS: "{{GITEA_MIRROR_SKIP_FORKS:-false}}"
PRIVATE_REPOSITORIES: "{{GITEA_MIRROR_PRIVATE_REPOS:-true}}"
MIRROR_STARRED: "{{GITEA_MIRROR_STARRED:-false}}"
MIRROR_ORGANIZATIONS: "{{GITEA_MIRROR_ORGANIZATIONS:-false}}"
MIRROR_ISSUES: "{{GITEA_MIRROR_ISSUES:-false}}"
MIRROR_WIKI: "{{GITEA_MIRROR_WIKI:-false}}"
# Forgejo destination — internal service name, no need to hairpin
# through the reverse proxy.
GITEA_URL: "http://forgejo:3000"
GITEA_TOKEN: "{{GITEA_MIRROR_FORGEJO_TOKEN}}"
GITEA_USERNAME: "{{GITEA_MIRROR_FORGEJO_USERNAME}}"
GITEA_ORGANIZATION: "{{GITEA_MIRROR_ORG:-github-mirrors}}"
GITEA_ORG_VISIBILITY: "{{GITEA_MIRROR_ORG_VISIBILITY:-private}}"
# Auto-import every repo on startup and keep mirroring on a schedule —
# this is the "pull all my GitHub repos via config" behavior.
AUTO_IMPORT_REPOS: "true"
AUTO_MIRROR_REPOS: "true"
SCHEDULE_ENABLED: "true"
GITEA_MIRROR_INTERVAL: "{{GITEA_MIRROR_INTERVAL:-8h}}"
CLEANUP_DELETE_IF_NOT_IN_GITHUB: "false"
CLEANUP_DRY_RUN: "true"
networks:
- forgejo_network
ports:
- "{{GITEA_MIRROR_BIND_IP:-0.0.0.0}}:{{GITEA_MIRROR_PORT:-4321}}:4321"
volumes:
- /etc/localtime:/etc/localtime:ro
- gitea_mirror_data:/app/data
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4321/api/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
deploy:
resources:
limits:
memory: "{{GITEA_MIRROR_MEM_LIMIT:-256m}}"
reservations:
memory: 64M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
networks:
forgejo_network:
driver: bridge
@ -231,4 +163,3 @@ networks:
volumes:
forgejo_db_data:
forgejo_data:
gitea_mirror_data:

View file

@ -99,7 +99,7 @@ Every policy in this table uses:
| `ALLOW CADDY to Bento PDF` | `10.0.10.6` | `8080` | Bento PDF |
| `ALLOW CADDY to Koffan` | `10.0.10.12` | `8080` | Koffan (LXC 111) |
| `ALLOW CADDY to Hermes` | `10.0.10.19` | `9119` | Hermes Agent dashboard (LXC 113) |
| `ALLOW CADDY to Forgejo` | `10.0.10.18` | `3000,4321` | Forgejo (LXC 114) and gitea-mirror |
| `ALLOW CADDY to Forgejo` | `10.0.10.18` | `3000,22` | Forgejo web UI (LXC 114) and git-over-SSH for hosts pulling from raspberrypi's zone |
---
@ -122,6 +122,87 @@ Every policy in this table uses:
---
## 3a. IoT → CADDY Policies (reverse direction)
The Home Assistant host itself also needs to reach Caddy directly — it
pulls `homelab-blueprint` from Forgejo (`git.brianpooe.com`, proxied
through Caddy) to deploy its own docker-compose stack.
**Two independent layers enforce traffic into the Caddy host — both must
allow it, and both were found misconfigured while wiring this up:**
1. **UniFi zone-based firewall** (router-level, VLAN → VLAN):
| Policy name | Source zone | Source IP | Src. Port | Destination zone | Destination IP | Destination port(s) | Purpose |
|---|---|---|---|---|---|---|---|
| `Allow HA to Caddy` | `IoT` | `10.0.30.6` (Home Assistant IP) | `Any` | `CADDY` | `10.0.15.5` (Caddy IP) | `443` | git fetch/pull of homelab-blueprint from Forgejo |
Src. Port must be `Any`, not restricted to HA's own listening ports
(8080/8123) — this is outbound traffic *from* HA, so the source port is
a random ephemeral port, not the port HA listens on. Setting Src. Port
to a "Home Assistant" port object here was the original bug: the rule
never matched, so traffic fell through to `Block All Traffic` below it.
2. **`ufw` on the Caddy host itself** (10.0.15.5) — a second, separate
enforcement point the UniFi rule doesn't touch. It allow-lists inbound
80/443 per source subnet and did not include the IoT VLAN
(`10.0.30.0/24`):
```bash
sudo ufw allow from 10.0.30.0/24 to any port 80,443 proto tcp
```
## 3b. CADDY / IoT → Forgejo git-over-SSH (port 22)
Both raspberrypi (CADDY zone) and Home Assistant (IoT zone) clone
`homelab-blueprint` over SSH directly against the Forgejo LXC
(`10.0.10.18:22`), bypassing Caddy entirely — SSH can't be proxied
through Caddy (HTTP/S only), so this traffic goes straight to the TRUSTED
zone. `10.0.10.18` has no host-level firewall (`ufw` not installed,
`iptables INPUT` policy `ACCEPT`), so only the UniFi zone rule matters
here — no second enforcement layer to fight, unlike the Caddy host above.
Forgejo's git-SSH runs on the standard port 22 on this LXC, not the usual
`2222` (see `docker-compose-files/forgejo/README.md`) — the LXC's own
admin sshd was relocated to port **2200** (systemd `ssh.socket` override)
to free 22 up. Direct admin SSH to this LXC now needs `-p 2200`; `pct exec
114` from the Proxmox host is unaffected and still works normally.
| Policy name | Source zone | Source IP | Destination zone | Destination IP | Destination port(s) | Purpose |
|---|---|---|---|---|---|---|
| `ALLOW CADDY to Forgejo` (existing rule, port added) | `CADDY` | `10.0.15.5` | `TRUSTED` | `10.0.10.18` | `3000,22` | Forgejo web UI + git-over-SSH from raspberrypi |
| `Allow HA to Forgejo SSH` | `IoT` | `10.0.30.6` | `TRUSTED` | `10.0.10.18` | `22` | git-over-SSH from homeassistant |
`ufw status` on that host is the fastest way to check which subnets can
already reach it before assuming a UniFi rule is the problem.
## 3c. Public Exposure of Forgejo (Hetzner VPS)
Forgejo is also reachable from the public internet, at `git.brianpooe.com`
(HTTPS, public repos only — anonymous browsing/cloning, like GitHub) and
`ssh://git@forgejo.brianpooe.com` (SSH, requires a registered account key —
self-registration is disabled, so this is effectively you-only). Full detail
lives in `docker-compose-files/forgejo/README.md` ("Public Exposure") and,
on the VPS itself, `~/netbird-vps/docs/netbird-acl-playbook.md` and
`public-app-sso.md`. Summary of what changed on the LAN side of that path:
- **DNS is split-horizon**, not a new firewall rule: `git.brianpooe.com` and
`forgejo.brianpooe.com` are each their own single-name Technitium zone
(`A` record direct to `10.0.15.5` and `10.0.10.18` respectively) so LAN
clients resolve them locally while the public `*.brianpooe.com` wildcard
sends everyone else to the VPS. No UniFi policy changes were needed for
the HTTPS path — it already flows through the existing `ALLOW CADDY to
Forgejo` rule above.
- **The public SSH path is a new NetBird route**, not a UniFi rule: the VPS
NetBird peer previously only routed to the Pi Caddy (`10.0.15.5/32`).
A second `10.0.10.18/32` resource (routing peer `pi-caddy`, "Model 2" in
the playbook) was added so the VPS can reach Forgejo's git-SSH directly.
The VPS forwards its own (now-freed) public port 22 to `10.0.10.18:22`
via a `socat` container (`~/netbird-vps/ssh-forward/`) — the VPS's own
admin sshd was relocated to port 2200 to make room.
---
## 4. Public DNS Block Policies (per zone)
Without these, devices can bypass Technitium by querying public resolvers

View file

@ -5,6 +5,16 @@ Hetzner VPS as the edge. Currently on this path: Koffan
(`koffan.brianpooe.com`) and IT-Tools (`it-tools.brianpooe.com`); the setup is
built to be reused for any future app.
> [!NOTE]
> Forgejo (`git.brianpooe.com`) also runs through this VPS edge but is
> deliberately **not** on this SSO path — public repos need to be
> browsable/clonable with no login, like GitHub. It gets its own plain
> `reverse_proxy` vhost (`crowdsec` + `rate_limit`, no `forward_auth`) in
> `caddy/Caddyfile.template`. Forgejo enforces its own per-repo visibility
> regardless of network path, so private repos stay private without an SSO
> gate. Its public git-over-SSH path is separate again — a raw TCP forward,
> not Caddy at all — see the VPS's `docs/netbird-acl-playbook.md`.
> [!NOTE]
> This intentionally does **not** use NetBird's built-in reverse-proxy
> ("Services") feature. Self-hosted NetBird requires running your own proxy