🐛 Fix passkeys behind TLS reverse proxy 🐛

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
This commit is contained in:
casjay committed 2026-10-03 11:23:03 -04:00
1 parent 6e24727c29
commit cb16b1659b
4 files changed
+54 -11

No files matched your search

+10 -2
View File
@@ -64,7 +64,8 @@ services:
TZ: ${TZ:-America/New_York} TZ: ${TZ:-America/New_York}
CONTAINER_NAME: casjaysdevdocker-gitea-latest CONTAINER_NAME: casjaysdevdocker-gitea-latest
HOSTNAME: ${BASE_HOST_NAME:-git.example.com} 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 DATABASE_DIR_SQLITE: /data/db/sqlite
volumes: volumes:
- ./volumes/data:/data:z - ./volumes/data:/data:z
@@ -99,7 +100,8 @@ networks:
| `GITEA_HOSTNAME` | _(empty)_ | Alias for `GITEA_SERVER` | | `GITEA_HOSTNAME` | _(empty)_ | Alias for `GITEA_SERVER` |
| `FULL_DOMAIN_NAME` | _(empty)_ | Fallback FQDN used when neither `GITEA_SERVER` nor `GITEA_HOSTNAME` is set | | `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`) | | `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_PORT` | `80` | Internal port Gitea listens on |
| `GITEA_NAME` | `SelfHosted GIT Server` | Site title shown in the UI | | `GITEA_NAME` | `SelfHosted GIT Server` | Site title shown in the UI |
| `GITEA_TZ` | `$TZ` | Override timezone for Gitea specifically | | `GITEA_TZ` | `$TZ` | Override timezone for Gitea specifically |
@@ -134,6 +136,12 @@ networks:
| `GITEA_SQL_NAME` | _(empty)_ | Database name (external DB only) | | `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`) | | `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** **act\_runner**
| Variable | Default | Description | | Variable | Default | Description |
+16 -6
View File
@@ -1,12 +1,22 @@
# TODO.AI.md # 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 - With `SSL_ENABLED=yes` (e.g. `CONTAINER_WEB_SERVER_PROTOCOL=https` or `/config/enable/ssl`) and
`SERVICE_PROTOCOL`, which defaults to `https`. With no cert configured Gitea logs no cert present, startup logs `valid for 0 year[s]` then `req: Can't parse "-nodes" as a number`
`Failed to start server: open : no such file or directory` and never listens on :80, so and the container exits 1 (empty days value passed to `openssl req -days`). The file is
act_runner's prerun wait loop stalls. Workaround: `-e GITEA_PROTO=http`. Needs a decision: generated, so the fix belongs in the upstream gen-dockerfile template, then regenerate.
listen protocol vs ROOT_URL protocol are likely meant to differ behind a TLS proxy.
## 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` ## Open — start-runners uses `set -e` instead of `set -eo pipefail`
+1 -1
View File
@@ -5,7 +5,7 @@ RUN_MODE = prod
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[server] [server]
PROTOCOL = REPLACE_SERVER_PROTO PROTOCOL = http
HTTP_ADDR = 0.0.0.0 HTTP_ADDR = 0.0.0.0
HTTP_PORT = 80 HTTP_PORT = 80
ROOT_URL = REPLACE_SERVER_PROTO://REPLACE_SERVER_NAME ROOT_URL = REPLACE_SERVER_PROTO://REPLACE_SERVER_NAME
+27 -2
View File
@@ -123,7 +123,20 @@ if [ -n "$SERVICE_NAME" ] && [ -f "/run/init.d/$SERVICE_NAME.pid" ]; then
fi fi
# - - - - - - - - - - - - - - - - - - - - - - - - - # - - - - - - - - - - - - - - - - - - - - - - - - -
# Custom functions # 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 # Script to execute
START_SCRIPT="/usr/local/etc/docker/exec/$SERVICE_NAME" 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")}}}" HOSTNAME="${GITEA_SERVER:-${GITEA_HOSTNAME:-${FULL_DOMAIN_NAME:-$(hostname -f 2>/dev/null || echo "$HOSTNAME")}}}"
SERVER_NAME="${DOMAIN:-$HOSTNAME}" SERVER_NAME="${DOMAIN:-$HOSTNAME}"
SERVER_PROTO="${SERVICE_PROTOCOL:-http}" 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 # Feed SERVER_NAME back into FULL_DOMAIN_NAME so __initialize_replace_variables
# uses DOMAIN (if set) rather than falling back to the raw HOSTNAME. # uses DOMAIN (if set) rather than falling back to the raw HOSTNAME.
export FULL_DOMAIN_NAME="$SERVER_NAME" export FULL_DOMAIN_NAME="$SERVER_NAME"
@@ -427,7 +444,15 @@ __update_conf_files() {
for _ini_file in "$CONF_DIR/app.ini"; do for _ini_file in "$CONF_DIR/app.ini"; do
[ -f "$_ini_file" ] || continue [ -f "$_ini_file" ] || continue
# Sync ROOT_URL, DOMAIN, and SSH_DOMAIN — prefer DOMAIN env var over raw hostname # 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|^DOMAIN[[:space:]]*=.*|DOMAIN = ${SERVER_NAME}|" "$_ini_file"
sed -i "s|^SSH_DOMAIN[[:space:]]*=.*|SSH_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) # Remove deprecated [cors].X_FRAME_OPTIONS (moved to [security] in Gitea v1.26)