From d59f463d3d519d26348fc79e11f5b41fa3f29617 Mon Sep 17 00:00:00 2001 From: Russell Seymour Date: Tue, 25 Aug 2026 17:20:13 +0100 Subject: [PATCH] Updated code to configure Forgejo to have two DNS names, one for git and for ssh. --- README.md | 8 +++++++ .../ansible/inventory/host_vars/forgejo.yml | 10 ++++++-- docs/forgejo-proxmox.md | 24 +++++++++++++++---- src/forgejo/ansible/proxmox/README.md | 15 ++++++++++++ .../ansible/proxmox/templates/app.ini.j2 | 10 ++++++-- src/forgejo/ansible/proxmox/vars.yml | 19 +++++++++++++++ src/forgejo/ansible/unraid/.env.example | 3 +++ src/forgejo/ansible/unraid/docker-compose.yml | 7 +++++- src/forgejo/common/vars.yml | 11 +++++++++ 9 files changed, 97 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0fcab00..8bccda3 100644 --- a/README.md +++ b/README.md @@ -771,6 +771,14 @@ the native install needs, `terraform apply -target=module.forgejo`, Ansible deliberately does not do — moving repository data, and the DNS/SSH-port cutover. +Note Forgejo needs **two** DNS names, not one: `FORGEJO_DOMAIN` for the web +side, pointing at whatever terminates HTTPS in front of it, and +`FORGEJO_SSH_DOMAIN` for git-over-SSH, pointing at the container itself. SSH +has no SNI, so a reverse proxy can't route it by hostname the way it routes +HTTP — the split is forced by the protocol. See +`src/forgejo/ansible/proxmox/README.md` for the alternative (a layer-4 TCP +proxy) and why it isn't used here. + ## Deploying one platform, one host, one stack `playbooks/deploy.yml` runs everything; `playbooks/unraid.yml`, diff --git a/build/config/ansible/inventory/host_vars/forgejo.yml b/build/config/ansible/inventory/host_vars/forgejo.yml index 30791b0..8e86d87 100644 --- a/build/config/ansible/inventory/host_vars/forgejo.yml +++ b/build/config/ansible/inventory/host_vars/forgejo.yml @@ -18,8 +18,14 @@ # off nas2, which is no longer a machine that can be read from. # # Also note SSH clone URLs move from port 2222 to 22 (see FORGEJO_SSH_PORT in -# src/forgejo/ansible/proxmox/vars.yml): existing remotes need editing, and -# git.turtlesystems.uk has to resolve to this container. +# src/forgejo/ansible/proxmox/vars.yml): existing remotes need editing. +# +# They also move to a different *hostname*. HTTP for git.turtlesystems.uk is +# proxied by Caddy, which is a different machine and has no sshd, so SSH is +# advertised as src.turtlesystems.uk instead (FORGEJO_SSH_DOMAIN, same file) — +# and it is that name, not the web one, that has to resolve to `ansible_host` +# below. SSH has no SNI, so this split is forced by the protocol rather than +# being a Caddy shortcoming; the reasoning is in vars.yml. # The address Terraform assigns this guest (build/config/terraform/main.tf). # Pinned here rather than left to DNS: the API-backed dynamic inventory diff --git a/docs/forgejo-proxmox.md b/docs/forgejo-proxmox.md index 2a65bab..f8a0dc7 100644 --- a/docs/forgejo-proxmox.md +++ b/docs/forgejo-proxmox.md @@ -192,11 +192,25 @@ gives back every file and no issues, pull requests, users or permissions. Last, once the above is verified: -- Point `git.turtlesystems.uk` at this container. -- **SSH clone URLs move from port 2222 to 22.** The Unraid stack published - 2222 to dodge the host's own sshd; this container has its own IP and uses - its own sshd, with Forgejo managing the `git` user's `authorized_keys`. - Every existing remote needs editing — there is no redirect for this. +- Point `git.turtlesystems.uk` at whatever terminates HTTPS for it — the Caddy + reverse proxy at 192.168.50.51, which proxies through to this container's + `FORGEJO_HTTP_PORT`. +- Point `src.turtlesystems.uk` at this container (192.168.50.52). This is the + name in `FORGEJO_SSH_DOMAIN`, and the one SSH clone URLs use. +- **SSH clone URLs move from port 2222 to 22, and onto their own hostname.** + The Unraid stack published 2222 to dodge the host's own sshd; this container + has its own IP and uses its own sshd, with Forgejo managing the `git` user's + `authorized_keys`. Every existing remote needs editing — there is no + redirect for this. + + The two hostnames are not redundancy. An SSH client sends a version banner + and nothing identifying the host it meant — there is no equivalent of TLS + SNI — so a reverse proxy cannot route SSH by name the way it routes HTTP. + The web name therefore has to point at the proxy and the SSH name at + Forgejo. Collapsing them into one would mean putting a layer-4 TCP proxy on + the proxy's port 22 (Caddy can, via the third-party `caddy-l4` module, at + the cost of a custom binary and of losing client IPs on SSH), not + configuring the hostname more cleverly. - The Forgejo Actions runner registration is tied to the instance. Re-register it against the new host, or CI stops running (`.forgejo/workflows/`). diff --git a/src/forgejo/ansible/proxmox/README.md b/src/forgejo/ansible/proxmox/README.md index 491fd72..2adc59d 100644 --- a/src/forgejo/ansible/proxmox/README.md +++ b/src/forgejo/ansible/proxmox/README.md @@ -79,6 +79,21 @@ editing `FORGEJO_RELEASE` and redeploying. the `git` user's `authorized_keys` rather than running its own server. This changes the SSH clone URLs Forgejo advertises, so existing remotes need updating after a migration. +- **SSH is on its own hostname** (`FORGEJO_SSH_DOMAIN` = + `src.turtlesystems.uk`), separate from the web one (`FORGEJO_DOMAIN` = + `git.turtlesystems.uk`, which resolves to the Caddy reverse proxy). Not a + redundancy and not a Caddy limitation: SSH has no SNI — the client sends a + version banner and nothing naming the host it meant — so no reverse proxy + can route SSH by hostname the way it routes HTTP. The web name has to point + at whatever terminates TLS and the SSH name at whatever runs sshd, and here + those are different machines. One name is possible only by putting a + layer-4 TCP proxy on the proxy's port 22 (Caddy's third-party `caddy-l4` + module does this) and accepting a custom Caddy build plus every SSH + connection appearing to originate from the proxy. + + Both names must therefore exist in DNS, and the failure mode of getting + this wrong is quiet: web logins work, and only `git push` breaks, with a + connection refused against a host that has no sshd. - **`INSTALL_LOCK = true`.** `app.ini` is rendered in full from Vault, so the web installer is skipped entirely. Nothing is left for Forgejo to generate on first boot — which is why `LFS_JWT_SECRET` has to exist in Vault here diff --git a/src/forgejo/ansible/proxmox/templates/app.ini.j2 b/src/forgejo/ansible/proxmox/templates/app.ini.j2 index b9e98d7..8533703 100644 --- a/src/forgejo/ansible/proxmox/templates/app.ini.j2 +++ b/src/forgejo/ansible/proxmox/templates/app.ini.j2 @@ -28,8 +28,14 @@ HTTP_PORT = {{ app_config.FORGEJO_HTTP_PORT }} APP_DATA_PATH = {{ app_config.FORGEJO_DATA_PATH }} {# The container's own sshd serves git over SSH, so Forgejo only advertises the address and manages the git user's authorized_keys. On Unraid this was - Forgejo's built-in server behind a published Docker port. #} -SSH_DOMAIN = {{ app_config.FORGEJO_DOMAIN }} + Forgejo's built-in server behind a published Docker port. + + FORGEJO_SSH_DOMAIN, not FORGEJO_DOMAIN: SSH reaches this container + directly while HTTP goes through a reverse proxy, so the two names resolve + to different machines. Rendering the web domain here advertises clone URLs + pointing at the proxy, which has no sshd — see ../vars.yml for why the + protocol leaves no other option. #} +SSH_DOMAIN = {{ app_config.FORGEJO_SSH_DOMAIN }} SSH_PORT = {{ app_config.FORGEJO_SSH_PORT }} START_SSH_SERVER = false DISABLE_SSH = false diff --git a/src/forgejo/ansible/proxmox/vars.yml b/src/forgejo/ansible/proxmox/vars.yml index 4cefc97..2f3dc9a 100644 --- a/src/forgejo/ansible/proxmox/vars.yml +++ b/src/forgejo/ansible/proxmox/vars.yml @@ -27,6 +27,25 @@ env_defaults: # SSH clone URLs Forgejo advertises — see README.md in this directory. FORGEJO_SSH_PORT: "22" + # Git over SSH lands on this container directly; the web side does not. HTTP + # is proxied by Caddy at 192.168.50.51, which is what git.turtlesystems.uk + # (FORGEJO_DOMAIN) resolves to — and Caddy speaks HTTP, so port 22 there is + # closed. Pointing the SSH clone URLs at the web domain therefore advertises + # a host with no sshd on it, which is exactly what this override exists to + # avoid. + # + # Splitting the names is not a workaround for a Caddy limitation; it is + # forced by the protocol. An SSH client sends a version banner and nothing + # else — no equivalent of TLS SNI — so no proxy can tell which backend a + # connection was meant for. The alternatives are a layer-4 TCP proxy on + # port 22 (Caddy can do this via the third-party `caddy-l4` module, at the + # cost of a custom binary and of every SSH connection appearing to come from + # the proxy's address) or giving Forgejo its own name, as here. + # + # Keep in step with the A record: src.turtlesystems.uk must resolve to this + # container's `ansible_host` in host_vars/forgejo.yml (192.168.50.52). + FORGEJO_SSH_DOMAIN: src.turtlesystems.uk + # No `unraid_shared` Docker network here to resolve `shared-postgres` # through, so the shared Postgres is reached over the LAN, at the address of # the LXC running it. Keep in step with `ip_address` on the `postgres` diff --git a/src/forgejo/ansible/unraid/.env.example b/src/forgejo/ansible/unraid/.env.example index 14bc60d..bef1b66 100644 --- a/src/forgejo/ansible/unraid/.env.example +++ b/src/forgejo/ansible/unraid/.env.example @@ -9,6 +9,9 @@ FORGEJO_HTTP_PORT=3000 FORGEJO_SSH_PORT=2222 FORGEJO_DOMAIN=git.example.internal FORGEJO_ROOT_URL=http://git.example.internal:3000/ +# The host SSH clone URLs point at. Same as FORGEJO_DOMAIN unless a reverse +# proxy fronts the web side, in which case that name has no sshd behind it. +FORGEJO_SSH_DOMAIN=git.example.internal DB_HOST=shared-postgres DB_NAME=forgejo DB_USER=forgejo diff --git a/src/forgejo/ansible/unraid/docker-compose.yml b/src/forgejo/ansible/unraid/docker-compose.yml index 04b0118..db353ba 100644 --- a/src/forgejo/ansible/unraid/docker-compose.yml +++ b/src/forgejo/ansible/unraid/docker-compose.yml @@ -27,7 +27,12 @@ services: FORGEJO__database__PASSWD: ${DB_PASSWORD} FORGEJO__server__DOMAIN: ${FORGEJO_DOMAIN} FORGEJO__server__ROOT_URL: ${FORGEJO_ROOT_URL} - FORGEJO__server__SSH_DOMAIN: ${FORGEJO_DOMAIN} + # Its own variable rather than ${FORGEJO_DOMAIN}: the SSH clone URLs need + # a name that resolves to whatever is actually listening for SSH, which + # is not the web domain wherever a reverse proxy terminates HTTP. Equal + # to FORGEJO_DOMAIN by default in ../../common/vars.yml, so this is a + # no-op until a platform overrides it. + FORGEJO__server__SSH_DOMAIN: ${FORGEJO_SSH_DOMAIN} FORGEJO__server__SSH_PORT: ${FORGEJO_SSH_PORT} # Fixed rather than left to be auto-generated, so recreating the # container from a fresh volume doesn't invalidate sessions/tokens. diff --git a/src/forgejo/common/vars.yml b/src/forgejo/common/vars.yml index 17c31b9..78464ea 100644 --- a/src/forgejo/common/vars.yml +++ b/src/forgejo/common/vars.yml @@ -9,6 +9,17 @@ env_defaults: FORGEJO_SSH_PORT: "2222" FORGEJO_DOMAIN: git.turtlesystems.uk FORGEJO_ROOT_URL: "https://git.turtlesystems.uk" + + # The hostname Forgejo puts in the SSH clone URLs it advertises, kept + # separate from FORGEJO_DOMAIN because the two need not resolve to the same + # machine. SSH carries no hostname in its handshake — there is no SNI — so a + # reverse proxy cannot route it by name the way it routes HTTP. A deployment + # that terminates TLS on a proxy in front of Forgejo therefore has the web + # domain pointing at the proxy and the SSH domain pointing at Forgejo + # itself. Defaulted to FORGEJO_DOMAIN here, which is correct whenever + # nothing sits in front; the platform that needs them to differ overrides it + # (see ../ansible/proxmox/vars.yml). + FORGEJO_SSH_DOMAIN: git.turtlesystems.uk DB_NAME: forgejo DB_USER: forgejo # DB_PASSWORD, SECRET_KEY, INTERNAL_TOKEN, JWT_SECRET are secrets — sourced