Compare commits
86
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d82b2b278 | ||
|
|
39387b5e0f | ||
|
|
e67ac50c61 | ||
|
|
3ebbeba672 | ||
|
|
07394769ca | ||
|
|
ae939c4085 | ||
|
|
28996eff57 | ||
|
|
93efe0d607 | ||
|
|
79861c72f3 | ||
|
|
2422ce1385 | ||
|
|
6fc6c3b84d | ||
|
|
e6522eadec | ||
|
|
2dcfaafc87 | ||
|
|
33e4f64d69 | ||
|
|
cbfc28c2dd | ||
|
|
a212be09c3 | ||
|
|
8a9241f0ff | ||
|
|
5893346625 | ||
|
|
5847b81dfd | ||
|
|
52207598b9 | ||
|
|
c57f760fdc | ||
|
|
57fd74f5af | ||
|
|
4ed7a9d2d5 | ||
|
|
9ba1d7e9db | ||
|
|
4b5ca9f6ea | ||
|
|
2517b31336 | ||
|
|
b671c1b2ec | ||
|
|
d0c444e63f | ||
|
|
0f89a3d534 | ||
|
|
d84b958937 | ||
|
|
866d895357 | ||
|
|
2005534b12 | ||
|
|
a3642c5159 | ||
|
|
8aaaf993ac | ||
|
|
09a24c2f16 | ||
|
|
0be27b15e9 | ||
|
|
9714bfca2e | ||
|
|
93288be7e2 | ||
|
|
e3874b2ebe | ||
|
|
343c2ef68b | ||
|
|
164d5e8891 | ||
|
|
24fe5a3177 | ||
|
|
70f3bfbd85 | ||
|
|
2c93a2c7fd | ||
|
|
5c3a38b9f0 | ||
|
|
24b62b7415 | ||
|
|
52604b3ba6 | ||
|
|
3d9df0793a | ||
|
|
14541062fc | ||
|
|
d954c605b0 | ||
|
|
5edfed7735 | ||
|
|
910a49f12f | ||
|
|
22990b6583 | ||
|
|
b4ac5a4de0 | ||
|
|
daf0ed11e4 | ||
|
|
0fe0c74235 | ||
|
|
2e7e073b63 | ||
|
|
f27fdad711 | ||
|
|
e965c2bd76 | ||
|
|
c7cc7176f0 | ||
|
|
ad5440a58b | ||
|
|
a55f6c430a | ||
|
|
3b0689dfd9 | ||
|
|
83d62b05fd | ||
|
|
423b295acf | ||
|
|
25dc4251de | ||
|
|
0a32ab7844 | ||
|
|
24e59a280c | ||
|
|
253ee7587b | ||
|
|
505a342417 | ||
|
|
ddfae32987 | ||
|
|
1b4036a0c2 | ||
|
|
a49f8c3533 | ||
|
|
c9d1eac7f2 | ||
|
|
7ed376e9b7 | ||
|
|
435992a4f0 | ||
|
|
9d3801494a | ||
|
|
eb794e61f9 | ||
|
|
5ace213bd4 | ||
|
|
1d386e6a58 | ||
|
|
778d06b0b8 | ||
|
|
63faa9b1bd | ||
|
|
4199f42f70 | ||
|
|
2c51ab5faa | ||
|
|
6dff335ac7 | ||
|
|
85b13d07a7 |
@@ -623,6 +623,25 @@ in `services/authelia.sh`) — prompts for a new duration (`12h`, `7d`,
|
||||
Sessions persist through reboots regardless of duration (Redis stores
|
||||
session state in a volume).
|
||||
|
||||
**`inactivity` must track `remember_me`, or a long remember_me is a lie.**
|
||||
`inactivity` is a separate session field — how long a session can sit idle
|
||||
before Authelia ends it — and it is NOT extended or bypassed by the
|
||||
"Remember me" checkbox; the two are independent. Confirmed live: a user
|
||||
set `remember_me: 1y` expecting "won't be asked to log in again for a
|
||||
year," but the install default left `inactivity` at a much shorter value
|
||||
(2h at the time), so ordinary daily gaps between visits (overnight, a
|
||||
workday) ended the session on inactivity grounds well before remember_me
|
||||
ever came into play — the 1y setting was doing nothing. Fixed at both ends
|
||||
so this can't recur silently: `install_authelia()`'s own template now sets
|
||||
`inactivity: 7d`, matching its `remember_me: 7d` default instead of a
|
||||
shorter one, and `_authelia_set_remember_me()` now writes the SAME new
|
||||
duration into both keys on every change, not just `remember_me` alone. If
|
||||
you ever hand-edit `session:` instead of using the menu option, keep
|
||||
`inactivity` and `remember_me` equal — a mismatch here is exactly the bug
|
||||
above, not a valid intentional configuration. `expiration` (the cap for a
|
||||
session that never checked "Remember me") is a legitimately different,
|
||||
shorter-by-design setting and is untouched by any of this.
|
||||
|
||||
**The config key is `remember_me`, not `remember_me_duration`.** Authelia
|
||||
renamed it in 4.38; this repo pins `4.39.20`. A stale `remember_me_duration`
|
||||
key doesn't error, Authelia just silently ignores it — confirmed against
|
||||
@@ -635,7 +654,7 @@ touch this by hand instead of the menu option, the current schema is:
|
||||
session:
|
||||
secret: 'your-existing-secret'
|
||||
expiration: 1h
|
||||
inactivity: 5m
|
||||
inactivity: 1y
|
||||
remember_me: 1y
|
||||
cookies:
|
||||
- domain: 'example.com'
|
||||
|
||||
+81
-20
@@ -242,7 +242,8 @@ install_authelia() {
|
||||
echo " 7) Reconfigure from scratch (regenerates secrets/users — breaks"
|
||||
echo " existing sessions for every domain already on this instance)"
|
||||
echo " 8) Show who has universal vs. service-scoped access"
|
||||
echo " 9) Change \"Remember me\" session duration (stay logged in longer)"
|
||||
echo " 9) Change \"Remember me\" session duration (stay logged in longer — also"
|
||||
echo " raises the inactivity timeout to match, so it can't cut it short)"
|
||||
echo " 10) Protect an existing site with this instance (pick a local Caddy site,"
|
||||
echo " or type one on a different box — gates it with a login, same as any"
|
||||
echo " other service already protected this way)"
|
||||
@@ -501,7 +502,12 @@ access_control:
|
||||
session:
|
||||
name: authelia_session
|
||||
expiration: 12h
|
||||
inactivity: 2h
|
||||
# Matches remember_me below, not a shorter default — an idle timeout
|
||||
# shorter than remember_me silently cuts a "remembered" session short
|
||||
# regardless of its own duration. See _authelia_set_remember_me()'s
|
||||
# comment for the live case this caused. Change both together (that
|
||||
# function does exactly this) rather than one at a time.
|
||||
inactivity: 7d
|
||||
remember_me: 7d
|
||||
cookies:
|
||||
- domain: ${AUTHELIA_DOMAIN}
|
||||
@@ -1356,6 +1362,29 @@ _authelia_gen_temp_password() {
|
||||
| fold -w1 | shuf | tr -d '\n'
|
||||
}
|
||||
|
||||
# Lets the admin type a specific password instead of always getting an
|
||||
# auto-generated one — same masked-input, "[Enter = auto-generate]"
|
||||
# convention services/backup.sh/borg-backup.sh/koha.sh already use for their
|
||||
# own passwords, rather than inventing a separate typed-vs-generated menu
|
||||
# choice here. Sets two out-params (not `local` — read them after the call
|
||||
# returns, same convention as OIDC_CLIENT_SECRET_PLAIN elsewhere in this
|
||||
# file): AUTHELIA_CHOSEN_PASSWORD (the plaintext, never written to disk —
|
||||
# only its argon2 hash is) and AUTHELIA_PASSWORD_AUTO_GENERATED (so callers
|
||||
# can word their own "here's the password" message correctly either way).
|
||||
_authelia_prompt_password() {
|
||||
AUTHELIA_CHOSEN_PASSWORD=""
|
||||
AUTHELIA_PASSWORD_AUTO_GENERATED=false
|
||||
local _pw=""
|
||||
if [ "$UNATTENDED" != true ]; then
|
||||
read -rsp " Password [Enter = auto-generate]: " _pw; echo
|
||||
fi
|
||||
if [ -z "$_pw" ]; then
|
||||
_pw="$(_authelia_gen_temp_password)"
|
||||
AUTHELIA_PASSWORD_AUTO_GENERATED=true
|
||||
fi
|
||||
AUTHELIA_CHOSEN_PASSWORD="$_pw"
|
||||
}
|
||||
|
||||
# Adds a new user to an EXISTING Authelia instance's users.yml — the scripted
|
||||
# version of the manual "generate a hash, paste a users.yml block, restart"
|
||||
# steps this file's own generated README already documents. Non-destructive:
|
||||
@@ -1376,8 +1405,9 @@ add_authelia_user() {
|
||||
|
||||
echo ""
|
||||
echo " Add a new user to this Authelia instance."
|
||||
echo " They log in with their username (not email). A temporary password"
|
||||
echo " is generated below — hand it to them directly. \"Forgot Password\""
|
||||
echo " They log in with their username (not email). You'll set a password"
|
||||
echo " next — type your own or leave it blank to auto-generate one — shown"
|
||||
echo " once here either way, never stored in plaintext. \"Forgot Password\""
|
||||
echo " and Authelia's own Settings → Change Password both require working"
|
||||
echo " SMTP (both email a one-time code), so until that's fixed, use this"
|
||||
echo " menu's \"Edit an existing user\" → \"Reset password\" for future resets."
|
||||
@@ -1399,9 +1429,9 @@ add_authelia_user() {
|
||||
local NEW_ADMIN_YN=""
|
||||
prompt_yn " Grant admin group membership too? (y/n):" "n" NEW_ADMIN_YN
|
||||
|
||||
log_info "Generating temporary password + hash..."
|
||||
local TEMP_PASS NEW_HASH
|
||||
TEMP_PASS="$(_authelia_gen_temp_password)"
|
||||
_authelia_prompt_password
|
||||
local TEMP_PASS="$AUTHELIA_CHOSEN_PASSWORD" NEW_HASH
|
||||
log_info "Generating password hash..."
|
||||
NEW_HASH=$(docker run --rm authelia/authelia:4.39.20 \
|
||||
authelia crypto hash generate argon2 --password "$TEMP_PASS" 2>/dev/null \
|
||||
| grep -oP '(?<=Digest: ).*')
|
||||
@@ -1439,8 +1469,12 @@ ${GROUPS_BLOCK}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " New user: ${NEW_USERNAME}"
|
||||
echo " Temp password: ${TEMP_PASS}"
|
||||
echo " New user: ${NEW_USERNAME}"
|
||||
if [ "$AUTHELIA_PASSWORD_AUTO_GENERATED" = true ]; then
|
||||
echo " Temp password: ${TEMP_PASS}"
|
||||
else
|
||||
echo " Password: ${TEMP_PASS} (the one you just typed)"
|
||||
fi
|
||||
echo " Give this to them directly (it's shown once, nothing stores it in"
|
||||
echo " plaintext). They can log in with it as-is and keep using it, or"
|
||||
echo " change it themselves from Authelia's Settings page — but that page"
|
||||
@@ -2355,6 +2389,19 @@ _authelia_report_access_scope() {
|
||||
# earlier version of this very file's own README section) uses the old
|
||||
# name, which Authelia would just silently ignore rather than error on.
|
||||
#
|
||||
# Also writes the SAME value into `inactivity` — a separate session field
|
||||
# (default 2h, set alongside remember_me in install_authelia()'s own
|
||||
# template) that ends a session after that much idle time regardless of
|
||||
# remember_me, since it isn't disabled or extended by the "Remember me"
|
||||
# checkbox. Confirmed live: a user who'd set remember_me to 1y still got
|
||||
# logged out after ordinary daily gaps (overnight, a workday) because
|
||||
# inactivity was still sitting at its 2h default — remember_me alone does
|
||||
# NOT deliver "won't be asked to log in again for the duration I set"
|
||||
# without this. Tying the two together is what actually delivers that.
|
||||
# `expiration` (the session cap when "Remember me" is NOT checked) is left
|
||||
# alone — a shorter default there for an un-remembered session is correct,
|
||||
# separate behavior, not the same gap.
|
||||
#
|
||||
# This only controls AUTHELIA's own session — it does not touch how long
|
||||
# a native-OIDC app's (Gitea/Mealie/ActualBudget) own session/token lasts
|
||||
# after logging in via Authelia. A long remember_me makes re-authenticating
|
||||
@@ -2370,7 +2417,10 @@ _authelia_set_remember_me() {
|
||||
echo ""
|
||||
echo " Current \"remember me\" duration: ${current:-not set}"
|
||||
echo " How long a session lasts when someone checks \"Remember me\" at login —"
|
||||
echo " applies to every domain this Authelia instance protects."
|
||||
echo " applies to every domain this Authelia instance protects. Also sets"
|
||||
echo " \"inactivity\" (idle timeout) to the same value, so a gap between visits"
|
||||
echo " shorter than this can't log you out early — otherwise inactivity's own"
|
||||
echo " separate, much shorter default cuts a long remember_me short."
|
||||
echo " Examples: 12h, 7d, 1M (month), 1y. Set to -1 to disable Remember Me entirely."
|
||||
local new_duration=""
|
||||
prompt_text " New duration [${current:-7d}]:" "${current:-7d}" new_duration
|
||||
@@ -2384,8 +2434,13 @@ _authelia_set_remember_me() {
|
||||
else
|
||||
sed -i "/^session:\$/a\\ remember_me: '${new_duration}'" "$config_file"
|
||||
fi
|
||||
if grep -qE '^ inactivity:' "$config_file"; then
|
||||
sed -i "s/^ inactivity:.*/ inactivity: '${new_duration}'/" "$config_file"
|
||||
else
|
||||
sed -i "/^ remember_me:/a\\ inactivity: '${new_duration}'" "$config_file"
|
||||
fi
|
||||
chown 1000:1000 "$config_file" 2>/dev/null || true
|
||||
log_success "\"Remember me\" duration set to ${new_duration}."
|
||||
log_success "\"Remember me\" duration and inactivity timeout both set to ${new_duration}."
|
||||
|
||||
local restart_auth=""
|
||||
prompt_yn " Restart Authelia to apply? (y/n):" "y" restart_auth
|
||||
@@ -2397,9 +2452,10 @@ _authelia_set_remember_me() {
|
||||
|
||||
echo ""
|
||||
log_info "Takes effect for NEW logins where \"Remember me\" is checked at Authelia's"
|
||||
log_info "login page — existing sessions keep whatever expiration they already had."
|
||||
log_info "The checkbox itself is already on the login form by default; this only"
|
||||
log_info "changes how long checking it actually keeps you signed in."
|
||||
log_info "login page — existing sessions keep whatever expiration/inactivity they"
|
||||
log_info "already had. The checkbox itself is already on the login form by default;"
|
||||
log_info "this only changes how long checking it actually keeps you signed in, and"
|
||||
log_info "stops the separate inactivity timeout from cutting that short."
|
||||
}
|
||||
|
||||
# Export/import accounts (+ optionally 2FA/session state) — for migrating to
|
||||
@@ -2697,7 +2753,7 @@ _authelia_manage_one_user() {
|
||||
echo ""
|
||||
echo " Editing user: $TARGET (admin: $IS_ADMIN, 2FA-exempt: $IS_EXEMPT)"
|
||||
echo " 1) Edit email / display name"
|
||||
echo " 2) Reset password"
|
||||
echo " 2) Set/reset password (type your own, or auto-generate)"
|
||||
echo " 3) Reset 2FA device (they register a new one on next login)"
|
||||
if [ "$IS_EXEMPT" = "yes" ]; then
|
||||
echo " 4) Restore the 2FA requirement for this user"
|
||||
@@ -2729,9 +2785,9 @@ _authelia_manage_one_user() {
|
||||
log_success "Updated $TARGET's email/display name."
|
||||
;;
|
||||
2)
|
||||
log_info "Generating a new temporary password + hash..."
|
||||
local NEW_TEMP_PASS NEW_HASH
|
||||
NEW_TEMP_PASS="$(_authelia_gen_temp_password)"
|
||||
_authelia_prompt_password
|
||||
local NEW_TEMP_PASS="$AUTHELIA_CHOSEN_PASSWORD" NEW_HASH
|
||||
log_info "Generating password hash..."
|
||||
NEW_HASH=$(docker run --rm authelia/authelia:4.39.20 \
|
||||
authelia crypto hash generate argon2 --password "$NEW_TEMP_PASS" 2>/dev/null \
|
||||
| grep -oP '(?<=Digest: ).*')
|
||||
@@ -2740,8 +2796,13 @@ _authelia_manage_one_user() {
|
||||
else
|
||||
_authelia_set_user_field "$USERS_FILE" "$START" "$END" "password" " password: \"${NEW_HASH}\""
|
||||
chown 1000:1000 "$USERS_FILE" 2>/dev/null || true
|
||||
log_success "Password reset for $TARGET."
|
||||
echo " New password: ${NEW_TEMP_PASS}"
|
||||
if [ "$AUTHELIA_PASSWORD_AUTO_GENERATED" = true ]; then
|
||||
log_success "Password reset for $TARGET (auto-generated)."
|
||||
echo " New password: ${NEW_TEMP_PASS}"
|
||||
else
|
||||
log_success "Password set for $TARGET."
|
||||
echo " Password: ${NEW_TEMP_PASS} (the one you just typed)"
|
||||
fi
|
||||
echo " Give this to them directly — shown once, not stored in plaintext anywhere."
|
||||
fi
|
||||
;;
|
||||
|
||||
+321
-4
@@ -453,6 +453,11 @@ _gitea_remove_sync_timer() {
|
||||
# reconfigure of an existing one. Always asked (matches pstn-trunk.sh's
|
||||
# international-calling step reasoning: a live-editable extra, not a
|
||||
# structural setting tied exclusively to fresh installs).
|
||||
#
|
||||
# Sets _GITEA_SYNC_FLAG as an out-param (not `local` — read it after the
|
||||
# call returns, same convention as CADDY_SERVICE_CONFIGURED) so the caller
|
||||
# can decide whether the real-time webhook offer even makes sense for the
|
||||
# direction just chosen.
|
||||
_gitea_run_sync_direction_step() {
|
||||
local DIR="$1"
|
||||
|
||||
@@ -463,12 +468,13 @@ _gitea_run_sync_direction_step() {
|
||||
echo " 3) Both directions"
|
||||
local _DIR_CHOICE=""
|
||||
prompt_text " Choice [1]:" "1" _DIR_CHOICE
|
||||
local FLAG="" DIR_DESC=""
|
||||
local DIR_DESC=""
|
||||
case "$_DIR_CHOICE" in
|
||||
2) FLAG="--push-only"; DIR_DESC="Gitea -> GitHub only" ;;
|
||||
3) FLAG=""; DIR_DESC="both directions" ;;
|
||||
*) FLAG="--pull-only"; DIR_DESC="GitHub -> Gitea only" ;;
|
||||
2) _GITEA_SYNC_FLAG="--push-only"; DIR_DESC="Gitea -> GitHub only" ;;
|
||||
3) _GITEA_SYNC_FLAG=""; DIR_DESC="both directions" ;;
|
||||
*) _GITEA_SYNC_FLAG="--pull-only"; DIR_DESC="GitHub -> Gitea only" ;;
|
||||
esac
|
||||
local FLAG="$_GITEA_SYNC_FLAG"
|
||||
log_info "Sync direction: $DIR_DESC"
|
||||
|
||||
_gitea_remove_sync_timer
|
||||
@@ -519,6 +525,250 @@ _gitea_run_sync_direction_step() {
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
# ── Real-time sync: a GitHub webhook receiver, not just the timer above ────
|
||||
# The timer above polls on a fixed schedule (default 6h) — fine for a slow
|
||||
# backup cadence, but a genuine "GitHub -> Gitea in real time" ask needs
|
||||
# GitHub to tell Gitea the moment something changes instead of Gitea finding
|
||||
# out up to one interval late. GitHub's own webhook (repo Settings ->
|
||||
# Webhooks) is the standard way to do that: it POSTs a JSON payload the
|
||||
# instant someone pushes. This writes a tiny stdlib-only Python HTTP server
|
||||
# to receive it — python3 is already a hard dependency of this directory's
|
||||
# gitea-github-sync.sh itself (used there for JSON parsing), so this adds
|
||||
# no new dependency — running under its own persistent systemd service,
|
||||
# and wires it up to Caddy the same way every other web-facing piece of
|
||||
# this install does.
|
||||
#
|
||||
# Deliberately NOT a Docker container: it just shells out to the existing
|
||||
# gitea-github-sync.sh sitting right next to it in $DIR, the same way the
|
||||
# timer's own systemd service does — no image to build/pull for what's
|
||||
# fundamentally a few lines of stdlib HTTP handling.
|
||||
_gitea_write_webhook_receiver() {
|
||||
local DIR="$1"
|
||||
cat > "$DIR/gitea-github-webhook.py" << 'PYEOF'
|
||||
#!/usr/bin/env python3
|
||||
"""Gitea <-> GitHub webhook receiver — triggers an immediate, single-repo
|
||||
mirror sync (gitea-github-sync.sh --repo owner/name --pull-only) the moment
|
||||
GitHub POSTs a push event, instead of waiting for the scheduled timer.
|
||||
|
||||
Written by services/gitea.sh — re-run 'sudo ./setup.sh gitea' (Update mode
|
||||
is fine) to regenerate this file rather than hand-editing it; a hand edit
|
||||
survives until the next Update-mode rerun overwrites it again.
|
||||
|
||||
WEBHOOK_SECRET is read from .env in this same directory at every request,
|
||||
never taken from the environment/systemd unit — /etc/systemd/system/*.service
|
||||
files are world-readable, and .env (chmod 600) is already where every other
|
||||
token in this directory lives.
|
||||
"""
|
||||
import hashlib
|
||||
import hmac
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
SYNC_DIR = os.environ.get("GITEA_SYNC_DIR", os.path.dirname(os.path.abspath(__file__)))
|
||||
ENV_PATH = os.path.join(SYNC_DIR, ".env")
|
||||
PORT = int(os.environ.get("WEBHOOK_PORT", "3020"))
|
||||
|
||||
|
||||
def _load_env_value(key):
|
||||
try:
|
||||
with open(ENV_PATH, "r") as f:
|
||||
for line in f:
|
||||
line = line.split("#", 1)[0].strip()
|
||||
if not line.startswith(key + "="):
|
||||
continue
|
||||
return line[len(key) + 1:].strip().strip("'").strip('"')
|
||||
except OSError:
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
class Handler(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt, *args):
|
||||
sys.stderr.write("%s - %s\n" % (self.address_string(), fmt % args))
|
||||
|
||||
def _reply(self, code, body=b""):
|
||||
self.send_response(code)
|
||||
self.end_headers()
|
||||
if body:
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self):
|
||||
self._reply(200, b"gitea-github-webhook: listening\n")
|
||||
|
||||
def do_POST(self):
|
||||
secret = _load_env_value("WEBHOOK_SECRET").encode()
|
||||
if not secret:
|
||||
self._reply(503, b"WEBHOOK_SECRET not configured")
|
||||
return
|
||||
|
||||
length = int(self.headers.get("Content-Length", 0) or 0)
|
||||
body = self.rfile.read(length) if length else b""
|
||||
|
||||
sig = self.headers.get("X-Hub-Signature-256", "")
|
||||
expected = "sha256=" + hmac.new(secret, body, hashlib.sha256).hexdigest()
|
||||
if not sig or not hmac.compare_digest(sig, expected):
|
||||
self._reply(401, b"bad signature")
|
||||
return
|
||||
|
||||
event = self.headers.get("X-GitHub-Event", "")
|
||||
if event == "ping":
|
||||
self._reply(200, b"pong")
|
||||
return
|
||||
if event != "push":
|
||||
self._reply(204)
|
||||
return
|
||||
|
||||
try:
|
||||
payload = json.loads(body or b"{}")
|
||||
full_name = payload["repository"]["full_name"]
|
||||
except (json.JSONDecodeError, KeyError, TypeError):
|
||||
self._reply(400, b"couldn't find repository.full_name in payload")
|
||||
return
|
||||
|
||||
self._reply(202, b"sync queued\n")
|
||||
sync_script = os.path.join(SYNC_DIR, "gitea-github-sync.sh")
|
||||
sync_env = dict(os.environ, SYNC_ENV=ENV_PATH)
|
||||
subprocess.Popen(
|
||||
["bash", sync_script, "--repo", full_name, "--pull-only"],
|
||||
cwd=SYNC_DIR,
|
||||
env=sync_env,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
server = http.server.ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
|
||||
server.serve_forever()
|
||||
PYEOF
|
||||
chmod +x "$DIR/gitea-github-webhook.py"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$DIR/gitea-github-webhook.py"
|
||||
}
|
||||
|
||||
_gitea_write_webhook_service() {
|
||||
local DIR="$1" RUN_USER="$2" RUN_HOME="$3" PORT="$4"
|
||||
local _service="/etc/systemd/system/gitea-github-webhook.service"
|
||||
|
||||
cat > "$_service" << UNIT
|
||||
[Unit]
|
||||
Description=Gitea-GitHub Webhook Receiver (real-time mirror sync trigger)
|
||||
After=network-online.target docker.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=${RUN_USER}
|
||||
Environment=HOME=${RUN_HOME}
|
||||
Environment=GITEA_SYNC_DIR=${DIR}
|
||||
Environment=WEBHOOK_PORT=${PORT}
|
||||
ExecStart=/usr/bin/python3 ${DIR}/gitea-github-webhook.py
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now gitea-github-webhook.service
|
||||
}
|
||||
|
||||
_gitea_remove_webhook_service() {
|
||||
systemctl disable --now gitea-github-webhook.service 2>/dev/null || true
|
||||
rm -f /etc/systemd/system/gitea-github-webhook.service
|
||||
systemctl daemon-reload 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Offers the webhook receiver above as an addition to (not a replacement
|
||||
# for) the timer set up in _gitea_run_sync_direction_step — the timer keeps
|
||||
# covering the Gitea -> GitHub direction (and acts as a safety net for any
|
||||
# push GitHub's webhook delivery ever misses), the webhook just gets the
|
||||
# GitHub -> Gitea direction down from "up to one interval late" to seconds.
|
||||
# Always asked on every install/reconfigure, same "live-editable extra"
|
||||
# pattern as the direction+autosync step itself — see that function's own
|
||||
# comment. Skipped (and any existing webhook torn down) outright when the
|
||||
# chosen direction is push-only, since GitHub has nothing to notify about
|
||||
# in that direction.
|
||||
_gitea_offer_realtime_webhook() {
|
||||
local DIR="$1" SYNC_FLAG="$2"
|
||||
|
||||
if [[ "$SYNC_FLAG" == "--push-only" ]]; then
|
||||
_gitea_remove_webhook_service
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
local USE_WEBHOOK=""
|
||||
prompt_yn " Also add a GitHub webhook for near-instant sync (push on GitHub -> synced here in seconds, instead of waiting for the timer above)? (y/n):" "n" USE_WEBHOOK
|
||||
if [[ ! "$USE_WEBHOOK" =~ ^[Yy]$ ]]; then
|
||||
_gitea_remove_webhook_service
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Reuse an existing secret/port across reruns — rotating either one
|
||||
# silently breaks a webhook GitHub already has configured against the
|
||||
# old value, the same reasoning services/asterisk.sh's TURN port-range
|
||||
# persistence follows for a live coturn install.
|
||||
local WEBHOOK_SECRET WEBHOOK_PORT
|
||||
WEBHOOK_SECRET="$(grep '^WEBHOOK_SECRET=' "$DIR/.env" 2>/dev/null | cut -d= -f2- | tr -d "'\"")"
|
||||
WEBHOOK_PORT="$(grep '^WEBHOOK_PORT=' "$DIR/.env" 2>/dev/null | cut -d= -f2- | tr -d "'\"")"
|
||||
[[ -z "$WEBHOOK_SECRET" ]] && WEBHOOK_SECRET="$(generate_password 40)"
|
||||
if [[ -z "$WEBHOOK_PORT" ]]; then
|
||||
WEBHOOK_PORT=3020
|
||||
find_free_port WEBHOOK_PORT "$WEBHOOK_PORT"
|
||||
fi
|
||||
|
||||
if grep -q '^WEBHOOK_SECRET=' "$DIR/.env" 2>/dev/null; then
|
||||
sed -i "s|^WEBHOOK_SECRET=.*|WEBHOOK_SECRET='${WEBHOOK_SECRET}'|" "$DIR/.env"
|
||||
else
|
||||
echo "WEBHOOK_SECRET='${WEBHOOK_SECRET}'" >> "$DIR/.env"
|
||||
fi
|
||||
if grep -q '^WEBHOOK_PORT=' "$DIR/.env" 2>/dev/null; then
|
||||
sed -i "s|^WEBHOOK_PORT=.*|WEBHOOK_PORT='${WEBHOOK_PORT}'|" "$DIR/.env"
|
||||
else
|
||||
echo "WEBHOOK_PORT='${WEBHOOK_PORT}'" >> "$DIR/.env"
|
||||
fi
|
||||
chmod 600 "$DIR/.env"
|
||||
chown "$ACTUAL_USER:$ACTUAL_USER" "$DIR/.env"
|
||||
|
||||
_gitea_write_webhook_receiver "$DIR"
|
||||
_gitea_write_webhook_service "$DIR" "$ACTUAL_USER" "$ACTUAL_HOME" "$WEBHOOK_PORT"
|
||||
log_success "Webhook receiver running on port ${WEBHOOK_PORT} (systemctl status gitea-github-webhook)."
|
||||
|
||||
# Bare port -> host.docker.internal:PORT, same convention as every other
|
||||
# host-process (non-container) upstream in this repo — see the
|
||||
# configure_caddy_for_service usage note in CLAUDE.md.
|
||||
configure_caddy_for_service "Gitea GitHub Webhook" "$WEBHOOK_PORT" "gitea-webhook"
|
||||
if [[ "$CADDY_SERVICE_CONFIGURED" == true ]]; then
|
||||
if command -v ufw &>/dev/null; then
|
||||
if [[ "$CADDY_SERVICE_MODE" == "local" ]]; then
|
||||
ufw delete allow "${WEBHOOK_PORT}/tcp" 2>/dev/null || true
|
||||
ufw_allow_from_caddy_net "${WEBHOOK_PORT}"
|
||||
else
|
||||
ufw allow "${WEBHOOK_PORT}/tcp" comment "Gitea GitHub webhook" >/dev/null 2>&1 || true
|
||||
ensure_ufw_enabled
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
log_success "Now add the webhook on GitHub, for every repo you want instant sync from:"
|
||||
log_info " Repo -> Settings -> Webhooks -> Add webhook"
|
||||
log_info " Payload URL: https://${CADDY_SERVICE_DOMAIN}/"
|
||||
log_info " Content type: application/json"
|
||||
log_info " Secret: ${WEBHOOK_SECRET}"
|
||||
log_info " Events: Just the push event"
|
||||
log_info "The timer above still covers every other repo, and this one too, on its"
|
||||
log_info "own schedule — the webhook is an addition, not a replacement for it."
|
||||
else
|
||||
log_warning "Webhook receiver is running (0.0.0.0:${WEBHOOK_PORT}) but nothing is exposing"
|
||||
log_warning "it to the internet, so GitHub can't reach it yet — re-run this installer and"
|
||||
log_warning "configure Caddy for it, or point your own reverse proxy at"
|
||||
log_warning "127.0.0.1:${WEBHOOK_PORT} (or the container-reachable host IP) by hand."
|
||||
log_info " Secret (for whenever you do expose it): ${WEBHOOK_SECRET}"
|
||||
fi
|
||||
}
|
||||
|
||||
install_gitea() {
|
||||
log_info "Setting up self-hosted Gitea..."
|
||||
|
||||
@@ -537,6 +787,8 @@ install_gitea() {
|
||||
echo "[DRY-RUN] Would ask sync direction (GitHub->Gitea / Gitea->GitHub / both) and whether"
|
||||
echo "[DRY-RUN] to install a systemd timer for automatic sync, or print manual instructions"
|
||||
echo "[DRY-RUN] Would offer to run a sync now (dry-run preview or for real), off-schedule"
|
||||
echo "[DRY-RUN] Would offer a GitHub webhook receiver for near-instant GitHub->Gitea sync"
|
||||
echo "[DRY-RUN] (systemd service + Caddy front door), unless direction is push-only"
|
||||
echo "[DRY-RUN] Would offer \"Sign in with Authelia\" (OIDC) if Authelia is installed"
|
||||
echo "[DRY-RUN] Would offer zero-click Authelia login (reverse-proxy auth) if Authelia"
|
||||
echo "[DRY-RUN] and local Caddy are both installed — rewires Gitea onto caddy_net"
|
||||
@@ -567,6 +819,7 @@ install_gitea() {
|
||||
&& log_success "Gitea refreshed and restarted." \
|
||||
|| log_warning "Restart failed — check: docker compose -f $DIR/docker-compose.yml logs"
|
||||
_gitea_run_sync_direction_step "$DIR"
|
||||
_gitea_offer_realtime_webhook "$DIR" "$_GITEA_SYNC_FLAG"
|
||||
_gitea_offer_authelia_sso "$DIR"
|
||||
_gitea_offer_reverse_proxy_auth "$DIR"
|
||||
_gitea_offer_actions_runner "$DIR"
|
||||
@@ -732,6 +985,7 @@ ENV
|
||||
fi
|
||||
|
||||
_gitea_run_sync_direction_step "$DIR"
|
||||
_gitea_offer_realtime_webhook "$DIR" "$_GITEA_SYNC_FLAG"
|
||||
|
||||
# ── Caddy — no forward_auth gate here. Gitea has its own built-in login,
|
||||
# unlike the no-auth-at-all apps elsewhere in this repo that need Caddy
|
||||
@@ -794,6 +1048,69 @@ Config (which repos, private/forks handling) lives at
|
||||
\`~/.config/gitea-github-sync/config\` — edit directly, or re-run
|
||||
\`bash gitea-github-sync.sh --init\` to redo it interactively.
|
||||
|
||||
## Real-time sync via GitHub webhook (optional)
|
||||
|
||||
The setup above only covers the GitHub -> Gitea direction; it doesn't apply
|
||||
if you chose Gitea -> GitHub only (GitHub has nothing to notify about in
|
||||
that direction). Adds a small Python HTTP server
|
||||
(\`gitea-github-webhook.py\`, in this directory) run as its own systemd
|
||||
service (\`gitea-github-webhook.service\`) that GitHub POSTs to the instant
|
||||
someone pushes — it verifies the request's HMAC signature against
|
||||
\`WEBHOOK_SECRET\` in \`.env\`, then runs \`gitea-github-sync.sh --repo
|
||||
owner/name --pull-only\` for just that one repo. The scheduled timer above
|
||||
still runs on its own interval regardless — the webhook is an addition
|
||||
that gets the GitHub -> Gitea direction down to seconds, not a replacement
|
||||
for it (and still catches anything a missed webhook delivery would have
|
||||
picked up next interval anyway).
|
||||
|
||||
Not set up yet, or want to change the port/secret? Re-run
|
||||
\`sudo ./setup.sh gitea\` (Update mode is fine) and answer yes to "Also add
|
||||
a GitHub webhook...". That only stands up the *receiver* on this box — you
|
||||
still add the actual webhook on GitHub's side afterward, using the payload
|
||||
URL and secret the installer printed (also readable back from \`.env\` as
|
||||
\`WEBHOOK_PORT\` / \`WEBHOOK_SECRET\` if you need them again).
|
||||
|
||||
**Option A — one repo at a time.** Fastest, but only covers repos you do
|
||||
this for individually:
|
||||
repo -> Settings -> Webhooks -> Add webhook
|
||||
- Payload URL: the URL the installer printed
|
||||
- Content type: \`application/json\`
|
||||
- Secret: your \`WEBHOOK_SECRET\`
|
||||
- Events: "Just the push event"
|
||||
|
||||
**Option B — every repo on your account, current AND future, from one
|
||||
setup.** A plain repo webhook (Option A) is always per-repo, no way around
|
||||
that — but a personal GitHub App installed with "All repositories" access
|
||||
covers every repo automatically, including ones you create afterward. No
|
||||
receiver/code change needed for this: an App's webhook uses the exact same
|
||||
HMAC-secret mechanism as a repo webhook, so the same \`WEBHOOK_SECRET\`
|
||||
works for both.
|
||||
|
||||
1. GitHub -> Settings -> Developer settings -> GitHub Apps -> New GitHub App
|
||||
2. Webhook URL: same payload URL as Option A. Webhook secret: your
|
||||
\`WEBHOOK_SECRET\`. (Homepage URL is a separate, purely cosmetic field —
|
||||
point it at anything, e.g. your GitHub profile; GitHub never sends
|
||||
anything there, unlike Webhook URL.)
|
||||
3. Permissions -> Repository permissions -> Contents: Read-only (required
|
||||
to unlock the Push event checkbox)
|
||||
4. Subscribe to events: Push only
|
||||
5. Where can this GitHub App be installed: "Only on this account"
|
||||
6. Create it, then Install App -> choose "All repositories" -> Install
|
||||
|
||||
If you'd already added Option A webhooks on a few repos, they're now
|
||||
redundant (not harmful, just two triggers per push) — remove them once
|
||||
the App is confirmed working.
|
||||
|
||||
**Verify either option** — push to a repo, then watch it arrive:
|
||||
|
||||
\`\`\`bash
|
||||
systemctl status gitea-github-webhook # is it running?
|
||||
journalctl -u gitea-github-webhook -f # watch it receive + trigger syncs
|
||||
\`\`\`
|
||||
|
||||
GitHub also shows delivery attempts and response codes: repo (or App) ->
|
||||
Settings -> Webhooks -> the webhook -> Recent Deliveries.
|
||||
|
||||
## Sign in with Authelia (optional)
|
||||
|
||||
If Authelia is installed, re-run \`sudo ./setup.sh gitea\` (Update mode is
|
||||
|
||||
+2781
-168
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user