🔧 Migrate app.ini for Gitea 28 and match live settings 🔧

Audited the template against Gitea 28.0.0 (source plus the binary's
own startup log) and fixed everything it flags. Existing configs are
migrated on startup without touching user values.

- rootfs/tmp/etc/gitea/app.ini: ALLOWED_HOST_LIST "*" -> private,loopback with explicit EGRESS_MODE=lax; [migration] -> [migrations] with ALLOWED_HOST_LIST; drop deprecated/dead keys and the non-existent [notify.email]; add WORK_PATH and LOCAL_ROOT_URL; DISABLE_REGISTRATION=true
- rootfs/usr/local/etc/docker/init.d/08-gitea.sh: add __gitea_ini_get/__gitea_ini_unset and migrate existing configs (invalid "*" host list, [webhook] key moved to [security], [migrations] host list, EGRESS_MODE)
- TODO.AI.md: record the audit, the verification, and the behaviour changes needing owner confirmation

Claude-Session: https://claude.ai/code/session_01Kae22tmhzhmfHyqc34PZbx
This commit is contained in:
casjay committed 2026-10-03 20:24:39 -04:00
1 parent 246fb4494c
commit 13e9ea9e16
3 files changed
+65 -17

No files matched your search

+21
View File
@@ -7,6 +7,27 @@
and the container exits 1 (empty days value passed to `openssl req -days`). The file is 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. generated, so the fix belongs in the upstream gen-dockerfile template, then regenerate.
## Fixed — app.ini migrated for Gitea 28.0.0 (template + startup)
- Audited against the Gitea source and the 28.0.0 binary's own startup log. Findings: `[security]
ALLOWED_HOST_LIST = *` is rejected (logged `[E]`); `[migration]` is not a section (real name
`[migrations]`, so SKIP_TLS_VERIFY and the host settings under it were silently ignored);
`ALLOWED_DOMAINS`/`BLOCKED_DOMAINS`/`ALLOW_LOCALNETWORKS` deprecated since v28 in favour of
`ALLOWED_HOST_LIST`/`BLOCKED_HOST_LIST`; dead keys `[security] COOKIE_USERNAME`/`COOKIE_SECURE`,
`[server] LETSENCRYPT_ACCEPTTOS`, `[git.timeout] DEFAULT/CLONE/PULL`, `[cors] SCHEME/ALLOW_SUBDOMAIN`;
`[notify.email]` is not a section (`ENABLE_NOTIFY_MAIL` is in `[service]`).
- Template: fixed/removed all of the above; `ALLOWED_HOST_LIST = private,loopback` and explicit
`EGRESS_MODE = lax` in `[security]` and `[migrations]`; added `WORK_PATH`, `LOCAL_ROOT_URL`; matched live
values `DISABLE_REGISTRATION = true`, `ALLOW_LOCALNETWORKS` intent (now via ALLOWED_HOST_LIST).
- `08-gitea.sh` migrates existing configs on start: `*` becomes `private,loopback` (a real value in the
deprecated `[webhook]` key is moved to `[security]`), `[migrations]` host list and `EGRESS_MODE` are
added only when missing. Dead keys are left in place (ignored by Gitea).
- Verified with the real image: a fresh install and an upgrade of a real customised config both start with
zero `[E]` and, for the upgrade, zero warnings. Kept on purpose: `DISABLE_QUERY_AUTH_TOKEN = false` (an
earlier deliberate pin) so a fresh install still logs its one warning.
- Behaviour now actually in effect (was silently ignored before): `[migrations] SKIP_TLS_VERIFY = true`
and migrations/mirrors may reach private and loopback hosts. Needs the owner's confirmation.
## Fixed — startup overwrote user config when `.initialized` marker was missing (init.d seed copy) ## Fixed — startup overwrote user config when `.initialized` marker was missing (init.d seed copy)
- `__run_precopy` in `08-gitea.sh`, `05-dockerd.sh` and `zz-act_runner.sh` did - `__run_precopy` in `08-gitea.sh`, `05-dockerd.sh` and `zz-act_runner.sh` did
+8 -17
View File
@@ -2,6 +2,7 @@
APP_NAME = REPLACE_SERVER_SITE_TITLE APP_NAME = REPLACE_SERVER_SITE_TITLE
RUN_USER = REPLACE_SERVICE_USER RUN_USER = REPLACE_SERVICE_USER
RUN_MODE = prod RUN_MODE = prod
WORK_PATH = REPLACE_DATA_DIR
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[server] [server]
@@ -9,6 +10,7 @@ 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
LOCAL_ROOT_URL = http://localhost:80/
DOMAIN = REPLACE_SERVER_NAME DOMAIN = REPLACE_SERVER_NAME
APP_DATA_PATH = REPLACE_DATA_DIR APP_DATA_PATH = REPLACE_DATA_DIR
OFFLINE_MODE = false OFFLINE_MODE = false
@@ -19,7 +21,6 @@ ACME_URL =
ACME_ACCEPTTOS = true ACME_ACCEPTTOS = true
ACME_EMAIL = ssl-admin@REPLACE_SERVER_NAME ACME_EMAIL = ssl-admin@REPLACE_SERVER_NAME
ACME_DIRECTORY = REPLACE_DATA_DIR/certs ACME_DIRECTORY = REPLACE_DATA_DIR/certs
LETSENCRYPT_ACCEPTTOS = true
DISABLE_SSH = false DISABLE_SSH = false
START_SSH_SERVER = true START_SSH_SERVER = true
SSH_LISTEN_HOST = 0.0.0.0 SSH_LISTEN_HOST = 0.0.0.0
@@ -167,11 +168,8 @@ MAX_GIT_DIFF_FILES = 100
GC_ARGS = GC_ARGS =
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[git.timeout] [git.timeout]
DEFAULT = 360
MIGRATE = 600 MIGRATE = 600
MIRROR = 300 MIRROR = 300
CLONE = 300
PULL = 300
GC = 60 GC = 60
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[picture] [picture]
@@ -191,8 +189,6 @@ PASSWORD_HASH_ALGO = argon2
MIN_PASSWORD_LENGTH = 8 MIN_PASSWORD_LENGTH = 8
PASSWORD_COMPLEXITY = on PASSWORD_COMPLEXITY = on
LOGIN_REMEMBER_DAYS = 1825 LOGIN_REMEMBER_DAYS = 1825
COOKIE_USERNAME = git_REPLACE_RANDOM_COOKIE_KEY
COOKIE_SECURE = false
CSRF_COOKIE_HTTP_ONLY = true CSRF_COOKIE_HTTP_ONLY = true
REVERSE_PROXY_LIMIT = 1 REVERSE_PROXY_LIMIT = 1
REVERSE_PROXY_TRUSTED_PROXIES = * REVERSE_PROXY_TRUSTED_PROXIES = *
@@ -200,7 +196,8 @@ REVERSE_PROXY_AUTHENTICATION_USER = X-WEBAUTH-USER
REVERSE_PROXY_AUTHENTICATION_EMAIL = X-WEBAUTH-EMAIL REVERSE_PROXY_AUTHENTICATION_EMAIL = X-WEBAUTH-EMAIL
REVERSE_PROXY_AUTHENTICATION_FULL_NAME = X-WEBAUTH-FULLNAME REVERSE_PROXY_AUTHENTICATION_FULL_NAME = X-WEBAUTH-FULLNAME
X_FRAME_OPTIONS = SAMEORIGIN X_FRAME_OPTIONS = SAMEORIGIN
ALLOWED_HOST_LIST = * ALLOWED_HOST_LIST = private,loopback
EGRESS_MODE = lax
; explicit: defaults to true starting Gitea 1.23 and deprecates ?token=/?access_token= ; explicit: defaults to true starting Gitea 1.23 and deprecates ?token=/?access_token=
; auth entirely in 1.24 — pin false so query-token API calls keep authenticating on ; auth entirely in 1.24 — pin false so query-token API calls keep authenticating on
; upgrade instead of silently downgrading to anonymous; migrate callers to ; upgrade instead of silently downgrading to anonymous; migrate callers to
@@ -218,11 +215,10 @@ DEFAULT_PAGING_NUM = 50
DEFAULT_GIT_TREES_PER_PAGE = 1000 DEFAULT_GIT_TREES_PER_PAGE = 1000
DEFAULT_MAX_BLOB_SIZE = 10485760 DEFAULT_MAX_BLOB_SIZE = 10485760
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[migration] [migrations]
SKIP_TLS_VERIFY = true SKIP_TLS_VERIFY = true
ALLOWED_DOMAINS = ALLOWED_HOST_LIST = private,loopback
BLOCKED_DOMAINS = EGRESS_MODE = lax
ALLOW_LOCALNETWORKS = false
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[git.config] [git.config]
http.sslVerify = false http.sslVerify = false
@@ -236,7 +232,7 @@ PROXY_URL =
PROXY_HOSTS = PROXY_HOSTS =
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[service] [service]
DISABLE_REGISTRATION = false DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = false REQUIRE_SIGNIN_VIEW = false
REGISTER_EMAIL_CONFIRM = REPLACE_GITEA_EMAIL_CONFIRM REGISTER_EMAIL_CONFIRM = REPLACE_GITEA_EMAIL_CONFIRM
ENABLE_NOTIFY_MAIL = REPLACE_GITEA_EMAIL_CONFIRM ENABLE_NOTIFY_MAIL = REPLACE_GITEA_EMAIL_CONFIRM
@@ -277,9 +273,6 @@ SEND_AS_PLAIN_TEXT = false
[email.incoming] [email.incoming]
ENABLED = false ENABLED = false
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[notify.email]
ENABLED = REPLACE_GITEA_EMAIL_CONFIRM
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[openid] [openid]
ENABLE_OPENID_SIGNIN = true ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true ENABLE_OPENID_SIGNUP = true
@@ -327,9 +320,7 @@ TOKEN =
; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[cors] [cors]
ENABLED = true ENABLED = true
SCHEME = REPLACE_SERVER_PROTO
ALLOW_DOMAIN = * ALLOW_DOMAIN = *
ALLOW_SUBDOMAIN = true
METHODS = GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS METHODS = GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
MAX_AGE = 10m MAX_AGE = 10m
ALLOW_CREDENTIALS = true ALLOW_CREDENTIALS = true
@@ -137,6 +137,27 @@ __gitea_ini_set() {
' "$ini_file" >"$tmp_file" && cat "$tmp_file" >"$ini_file" ' "$ini_file" >"$tmp_file" && cat "$tmp_file" >"$ini_file"
rm -f "$tmp_file" rm -f "$tmp_file"
} }
# Remove KEY from [SECTION] of an ini file, leaving everything else untouched
__gitea_ini_unset() {
local ini_file="$1" section="$2" key="$3" tmp_file
[ -f "$ini_file" ] || return 1
tmp_file="$(mktemp)" || return 1
awk -v s="[$section]" -v k="$key" '
/^\[/ { in_s = ($0 == s) }
in_s && $0 ~ "^" k "[[:space:]]*=" { next }
{ print }
' "$ini_file" >"$tmp_file" && cat "$tmp_file" >"$ini_file"
rm -f "$tmp_file"
}
# Print the value of KEY in [SECTION], empty when absent
__gitea_ini_get() {
local ini_file="$1" section="$2" key="$3"
[ -f "$ini_file" ] || return 1
awk -v s="[$section]" -v k="$key" '
/^\[/ { in_s = ($0 == s) }
in_s && $0 ~ "^" k "[[:space:]]*=" { v = substr($0, index($0, "=") + 1); gsub(/^[[:space:]]+|[[:space:]]+$/, "", v); print v; exit }
' "$ini_file"
}
# Set KEY = VALUE inside [SECTION] only when the key is not already present (keeps user choices) # Set KEY = VALUE inside [SECTION] only when the key is not already present (keeps user choices)
__gitea_ini_default() { __gitea_ini_default() {
local ini_file="$1" section="$2" key="$3" value="$4" local ini_file="$1" section="$2" key="$3" value="$4"
@@ -478,6 +499,21 @@ __update_conf_files() {
__gitea_ini_default "$_ini_file" cron.delete_old_actions OLDER_THAN 8760h __gitea_ini_default "$_ini_file" cron.delete_old_actions OLDER_THAN 8760h
__gitea_ini_default "$_ini_file" cron.delete_old_system_notices ENABLED true __gitea_ini_default "$_ini_file" cron.delete_old_system_notices ENABLED true
__gitea_ini_default "$_ini_file" cron.delete_old_system_notices OLDER_THAN 2160h __gitea_ini_default "$_ini_file" cron.delete_old_system_notices OLDER_THAN 2160h
# Gitea 28 migration: a catch-all "*" host is rejected and logged as an error, so private and
# loopback are listed explicitly. A real value from the deprecated [webhook] key is moved to [security]
_hosts_sec="$(__gitea_ini_get "$_ini_file" security ALLOWED_HOST_LIST)"
_hosts_wh="$(__gitea_ini_get "$_ini_file" webhook ALLOWED_HOST_LIST)"
[ "$_hosts_sec" = "*" ] && _hosts_sec=""
[ "$_hosts_wh" = "*" ] && _hosts_wh=""
_hosts_sec="${_hosts_sec:-${_hosts_wh:-private,loopback}}"
__gitea_ini_set "$_ini_file" security ALLOWED_HOST_LIST "$_hosts_sec"
__gitea_ini_unset "$_ini_file" webhook ALLOWED_HOST_LIST
unset _hosts_sec _hosts_wh
# [migrations] (plural) is the real section; the old [migration] block was ignored by Gitea
__gitea_ini_default "$_ini_file" migrations ALLOWED_HOST_LIST "private,loopback"
# Stating the egress mode explicitly keeps Gitea's default (lax) and silences its startup warning
__gitea_ini_default "$_ini_file" security EGRESS_MODE lax
__gitea_ini_default "$_ini_file" migrations EGRESS_MODE lax
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)