Updated code to configure Forgejo to have two DNS names, one for git and for ssh.
Some checks failed
deploy / deploy (push) Has been cancelled

This commit is contained in:
Russell Seymour 2026-08-25 17:20:13 +01:00
parent 1250c9cef6
commit d59f463d3d
Signed by: russells
GPG key ID: 78116D73A1E3EC41
9 changed files with 97 additions and 10 deletions

View file

@ -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`,

View file

@ -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

View file

@ -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/`).

View file

@ -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

View file

@ -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

View file

@ -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`

View file

@ -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

View file

@ -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.

View file

@ -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