From cb16b1659b50a0606a2cff82e0ec1b5d062d1170 Mon Sep 17 00:00:00 2001 From: casjay Date: Sat, 3 Oct 2026 11:23:03 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fix=20passkeys=20behind=20TLS=20?= =?UTF-8?q?reverse=20proxy=20=F0=9F=90=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gitea now always listens on plain http while ROOT_URL carries the public https origin, per Gitea's reverse-proxy docs. Fixes fresh containers failing to start (PROTOCOL = https with no cert) and passkey origin mismatches behind a proxy. - rootfs/usr/local/etc/docker/init.d/08-gitea.sh: add __gitea_ini_set helper; stamp [server] PROTOCOL = http, ROOT_URL from new GITEA_ROOT_URL (trailing slash), and [session] COOKIE_SECURE from ROOT_URL scheme on every startup - rootfs/tmp/etc/gitea/app.ini: default PROTOCOL to http instead of the public protocol placeholder - README.md: document GITEA_PROTO as the public protocol (default https), GITEA_ROOT_URL, and reverse-proxy/passkey requirements; compose example uses https - TODO.AI.md: record the fix and log the generated __create_ssl_cert crash Claude-Session: https://claude.ai/code/session_01Kae22tmhzhmfHyqc34PZbx --- README.md | 12 ++++++-- TODO.AI.md | 22 ++++++++++---- rootfs/tmp/etc/gitea/app.ini | 2 +- .../usr/local/etc/docker/init.d/08-gitea.sh | 29 +++++++++++++++++-- 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a17a3dd..5ebc6a8 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,8 @@ services: TZ: ${TZ:-America/New_York} CONTAINER_NAME: casjaysdevdocker-gitea-latest HOSTNAME: ${BASE_HOST_NAME:-git.example.com} - GITEA_PROTO: http + # public protocol served by the proxy; must be https for passkeys + GITEA_PROTO: https DATABASE_DIR_SQLITE: /data/db/sqlite volumes: - ./volumes/data:/data:z @@ -99,7 +100,8 @@ networks: | `GITEA_HOSTNAME` | _(empty)_ | Alias for `GITEA_SERVER` | | `FULL_DOMAIN_NAME` | _(empty)_ | Fallback FQDN used when neither `GITEA_SERVER` nor `GITEA_HOSTNAME` is set | | `DOMAIN` | _(empty)_ | Overrides the domain used in email addresses (takes precedence over `GITEA_SERVER`) | -| `GITEA_PROTO` | `http` | Protocol used in ROOT\_URL (`http` or `https`) | +| `GITEA_PROTO` | `https` | Public (browser-facing) protocol used in ROOT\_URL (`http` or `https`); use `https` behind a TLS reverse proxy | +| `GITEA_ROOT_URL` | `$GITEA_PROTO://$DOMAIN/` | Full public URL; set it when the proxy uses a non-standard port or sub-path | | `GITEA_PORT` | `80` | Internal port Gitea listens on | | `GITEA_NAME` | `SelfHosted GIT Server` | Site title shown in the UI | | `GITEA_TZ` | `$TZ` | Override timezone for Gitea specifically | @@ -134,6 +136,12 @@ networks: | `GITEA_SQL_NAME` | _(empty)_ | Database name (external DB only) | | `DATABASE_DIR_SQLITE` | `$DATA_DIR/db/sqlite` | Override the SQLite database directory (mount a separate volume here to keep the DB outside `/data`) | +**Behind a TLS reverse proxy (passkeys/WebAuthn)** — passkeys only work when `ROOT_URL` matches the +browser's address exactly and `DOMAIN` is the bare hostname. Set `DOMAIN` (or `GITEA_SERVER`) to the +public hostname and keep `GITEA_PROTO=https`; Gitea keeps listening on plain http, marks session cookies +`Secure`, and the proxy must forward `Host` and `X-Forwarded-Proto`. Use `GITEA_ROOT_URL` for a +non-standard port or sub-path. + **act\_runner** | Variable | Default | Description | diff --git a/TODO.AI.md b/TODO.AI.md index e662a61..c8c567e 100644 --- a/TODO.AI.md +++ b/TODO.AI.md @@ -1,12 +1,22 @@ # TODO.AI.md -## Open — fresh container: Gitea fails to listen (PROTOCOL = https with no cert) +## Open — generated `__create_ssl_cert` crashes the container (functions/entrypoint.sh) -- Commit `7741c06` set `[server] PROTOCOL = REPLACE_SERVER_PROTO`; `08-gitea.sh` derives it from - `SERVICE_PROTOCOL`, which defaults to `https`. With no cert configured Gitea logs - `Failed to start server: open : no such file or directory` and never listens on :80, so - act_runner's prerun wait loop stalls. Workaround: `-e GITEA_PROTO=http`. Needs a decision: - listen protocol vs ROOT_URL protocol are likely meant to differ behind a TLS proxy. +- With `SSL_ENABLED=yes` (e.g. `CONTAINER_WEB_SERVER_PROTOCOL=https` or `/config/enable/ssl`) and + no cert present, startup logs `valid for 0 year[s]` then `req: Can't parse "-nodes" as a number` + and the container exits 1 (empty days value passed to `openssl req -days`). The file is + generated, so the fix belongs in the upstream gen-dockerfile template, then regenerate. + +## Fixed — reverse-proxy/passkey support: ROOT_URL decoupled from listen protocol (08-gitea.sh) + +- Commit `7741c06` made `[server] PROTOCOL` follow `SERVICE_PROTOCOL` (default `https`), so a fresh + container had no cert and Gitea never listened. Per Gitea's reverse-proxy docs Gitea listens on + plain http and TLS lives on the proxy: `PROTOCOL` is now stamped `http` every startup, + `ROOT_URL` is stamped from `GITEA_ROOT_URL` (default `$SERVICE_PROTOCOL://$SERVER_NAME/`), and + `[session] COOKIE_SECURE` follows ROOT_URL's scheme. Passkeys use ROOT_URL as origin and DOMAIN + as RP ID (`modules/auth/webauthn`). +- Verified in a fresh container: Gitea up on http, no cert created, ROOT_URL + `https://git.example.test/`, cookies `Secure`, passkey assertion `rpId: git.example.test`. ## Open — start-runners uses `set -e` instead of `set -eo pipefail` diff --git a/rootfs/tmp/etc/gitea/app.ini b/rootfs/tmp/etc/gitea/app.ini index 1b78bc4..d281661 100644 --- a/rootfs/tmp/etc/gitea/app.ini +++ b/rootfs/tmp/etc/gitea/app.ini @@ -5,7 +5,7 @@ RUN_MODE = prod ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; [server] -PROTOCOL = REPLACE_SERVER_PROTO +PROTOCOL = http HTTP_ADDR = 0.0.0.0 HTTP_PORT = 80 ROOT_URL = REPLACE_SERVER_PROTO://REPLACE_SERVER_NAME diff --git a/rootfs/usr/local/etc/docker/init.d/08-gitea.sh b/rootfs/usr/local/etc/docker/init.d/08-gitea.sh index 4db9ae5..a4714f0 100755 --- a/rootfs/usr/local/etc/docker/init.d/08-gitea.sh +++ b/rootfs/usr/local/etc/docker/init.d/08-gitea.sh @@ -123,7 +123,20 @@ if [ -n "$SERVICE_NAME" ] && [ -f "/run/init.d/$SERVICE_NAME.pid" ]; then fi # - - - - - - - - - - - - - - - - - - - - - - - - - # Custom functions - +# Set KEY = VALUE inside [SECTION] of an ini file, replacing an existing key or adding it to the section +__gitea_ini_set() { + local ini_file="$1" section="$2" key="$3" value="$4" tmp_file + [ -f "$ini_file" ] || return 1 + tmp_file="$(mktemp)" || return 1 + awk -v s="[$section]" -v k="$key" -v v="$value" ' + BEGIN { in_s = 0; done = 0 } + /^\[/ { if (in_s && !done) { print k " = " v; done = 1 } in_s = ($0 == s) } + in_s && $0 ~ "^" k "[[:space:]]*=" { if (!done) { print k " = " v; done = 1 } next } + { print } + END { if (!done) { if (!in_s) print s; print k " = " v } } + ' "$ini_file" >"$tmp_file" && cat "$tmp_file" >"$ini_file" + rm -f "$tmp_file" +} # - - - - - - - - - - - - - - - - - - - - - - - - - # Script to execute START_SCRIPT="/usr/local/etc/docker/exec/$SERVICE_NAME" @@ -277,6 +290,10 @@ GITEA_SQL_TYPE="${ENV_GITEA_SQL_TYPE:-${GITEA_SQL_TYPE:-sqlite3}}" HOSTNAME="${GITEA_SERVER:-${GITEA_HOSTNAME:-${FULL_DOMAIN_NAME:-$(hostname -f 2>/dev/null || echo "$HOSTNAME")}}}" SERVER_NAME="${DOMAIN:-$HOSTNAME}" SERVER_PROTO="${SERVICE_PROTOCOL:-http}" +# SERVICE_PROTOCOL is the public (browser-facing) protocol used for ROOT_URL; passkeys/WebAuthn +# require ROOT_URL to match the browser origin exactly. Gitea itself always listens on plain http; +# TLS is terminated by the reverse proxy, which must forward Host and X-Forwarded-Proto. +GITEA_ROOT_URL="${GITEA_ROOT_URL:-${SERVICE_PROTOCOL:-https}://${SERVER_NAME}/}" # Feed SERVER_NAME back into FULL_DOMAIN_NAME so __initialize_replace_variables # uses DOMAIN (if set) rather than falling back to the raw HOSTNAME. export FULL_DOMAIN_NAME="$SERVER_NAME" @@ -427,7 +444,15 @@ __update_conf_files() { for _ini_file in "$CONF_DIR/app.ini"; do [ -f "$_ini_file" ] || continue # Sync ROOT_URL, DOMAIN, and SSH_DOMAIN — prefer DOMAIN env var over raw hostname - sed -i "s|^ROOT_URL[[:space:]]*=.*|ROOT_URL = ${SERVICE_PROTOCOL:-https}://${SERVER_NAME}|" "$_ini_file" + # ROOT_URL is the public origin (passkeys/WebAuthn match it exactly); always end with / + sed -i "s|^ROOT_URL[[:space:]]*=.*|ROOT_URL = ${GITEA_ROOT_URL%/}/|" "$_ini_file" + # Listen protocol is independent of ROOT_URL: always http, TLS lives on the reverse proxy + __gitea_ini_set "$_ini_file" server PROTOCOL http + # Mark the session cookie Secure whenever the public URL is https + case "$GITEA_ROOT_URL" in + https://*) __gitea_ini_set "$_ini_file" session COOKIE_SECURE true ;; + *) __gitea_ini_set "$_ini_file" session COOKIE_SECURE false ;; + esac sed -i "s|^DOMAIN[[:space:]]*=.*|DOMAIN = ${SERVER_NAME}|" "$_ini_file" sed -i "s|^SSH_DOMAIN[[:space:]]*=.*|SSH_DOMAIN = ${SERVER_NAME}|" "$_ini_file" # Remove deprecated [cors].X_FRAME_OPTIONS (moved to [security] in Gitea v1.26)