Ableger

Schnittstellen

Technische Dokumentation. Zwei getrennte Themen in einem Dokument, weil beide auf demselben Mechanismus (Gitea-OAuth2) aufbauen.

  1. Decap CMS gegen Gitea anmelden (MUSS)
  2. Privater Modus (KANN) - stellt (einen Teil des Projekts) hinter eine Authentifizierung.

Teil 1: Decap CMS gegen Gitea anmelden (MUSS)

Diese Werte füllen install.env (daraus generiert scripts/generate-config.sh bei jedem Deploy admin/config.yml, siehe Getting Started) und richten zwei serverseitige Voraussetzungen ein, die Decap CMS gegen Gitea zusätzlich braucht. Jeder Fork braucht seine eigene OAuth-Anwendung.

Zwei Dinge sind nicht offensichtlich: Decaps Gitea-Backend nutzt einen reinen PKCE-Flow ohne Client-Secret, und es ruft Giteas API direkt aus dem Browser auf (Cross-Origin). Beides ist unten in den Schritten berücksichtigt.

Schritt 1: OAuth2-Anwendung anlegen

  1. In Gitea anmelden mit dem Konto, das die Anwendung besitzen soll.

  2. Oben rechts auf den Avatar → Einstellungen (Settings) → Reiter Anwendungen (Applications).

  3. Im Bereich OAuth2-Anwendungen verwalten → Neue Anwendung erstellen.

  4. Anwendungsname: frei wählbar, z. B. Ableger Decap CMS.

  5. Weiterleitungs-URI / Redirect URI: exakt

    https://ableger.example.org/admin/
    

Muss zeichengenau mit der später tatsächlich aufgerufenen URL übereinstimmen (inkl. https://, mit abschließendem Slash). 6. „Confidential Client“ deaktivieren / nicht ankreuzen, falls angeboten. Decaps Gitea-Backend ist ein Public Client (PKCE, RFC 6749) und schickt beim Token-Austausch nie ein Client-Secret (bei „Confidential Client“ verlangt Gitea eines und der Login schlägt mit invalid empty client secret fehl). 7. Erstellen klicken. Gitea zeigt jetzt Client-ID (und ggf. ein Client-Secret, das nicht gebraucht wird) an.

Achtung

Das Client-Secret nirgends im Repository eintragen. install.env (wie das daraus generierte admin/config.yml) ist eine öffentlich einsehbare Datei (Grundprinzip Öffentlichkeit). Sie enthält bewusst nur die Client-ID (OAUTH_CLIENT_ID), kein Secret.

Schritt 2: Client-ID in install.env eintragen

Repo-Name, Gitea-Domain und Branch (main, fest in admin/config.template.yml) sind normalerweise bereits eingetragen. Es fehlt nur die Client-ID:

OAUTH_CLIENT_ID="<Client-ID aus Schritt 1>"

Schritt 3: CORS in Gitea freischalten

Decap CMS ruft Giteas API (api_root, login/oauth/access_token) direkt aus dem Browser der Website-Domain auf, eine andere Origin als Gitea selbst. Browser blockieren das ohne explizite Freigabe.

In Giteas app.ini (bei Uberspace: ~/gitea/custom/conf/app.ini) ergänzen und den Gitea-Dienst neu starten:

[cors]
ENABLED = true
ALLOW_DOMAIN = https://ableger.example.org
METHODS = GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
ALLOW_CREDENTIALS = false
HEADERS = Content-Type,User-Agent,Authorization
MAX_AGE = 10m

Bei Uberspace: supervisorctl restart gitea. ALLOW_DOMAIN bewusst auf genau diese eine Origin beschränkt, kein Wildcard (*).

Prüfen

  1. /admin/ aufrufen → Login with Gitea klicken.
  2. Gitea fragt nach Autorisierung der Anwendung → bestätigen.
  3. Weiterleitung zurück zu /admin/, jetzt mit sichtbarer Decap-CMS-Oberfläche und den Collections „Methoden“ und „Varianten“.

Troubleshooting

  • „redirect_uri does not match“: Redirect-URI in Gitea (Schritt 1.5) stimmt nicht exakt mit der tatsächlich aufgerufenen /admin/-URL überein (http vs. https, mit/ohne www, fehlender/zusätzlicher Slash).
  • „TypeError: Failed to fetch“ nach der Autorisierung, Konsole zeigt einen CORS-Fehler: Schritt 3 (CORS) fehlt oder ALLOW_DOMAIN passt nicht zur tatsächlichen Website-Domain.
  • „Error: invalid empty client secret“: Die OAuth2-Anwendung ist als „Confidential Client“ statt „Public Client“ registriert (Schritt 1.6). In Giteas Datenbank ist das die Spalte confidential_client in der Tabelle oauth2_application – muss 0 sein.
  • Login-Button erscheint gar nicht: admin/index.html fehlt oder lädt nicht (Skript-URL/CDN-Erreichbarkeit prüfen).

Teil 2: Privater Modus (KANN)

Richtet Website-Ansicht und Redaktion (/admin/) hinter zwei unterschiedlichen Gitea-Teams ein. Die Website ist dann nur für angemeldete Konten sichtbar. Siehe Konzept und Grundprinzipien, „Grundprinzipien“ zur konzeptionellen Einordnung als bewusste, abschaltbare Ausnahme.

Voraussetzung: Organisation statt persönliches Konto

Gitea-Teams existieren nur innerhalb einer Organisation. Beide Repos (Website, Methoden) müssen dafür in einer Organisation liegen (Gitea-Bordfunktion: Repo-Einstellungen → Transfer Ownership, falls sie zunächst unter einem persönlichen Konto entstanden). Siehe auch Getting Started, Voraussetzungen.

Schritt 1: Zwei Teams anlegen

In der Organisation, unter Teams:

TeamRepo-Rechte auf methodenRepo-Rechte auf website
writeWritekeine
readRead (optional, das Repo ist ohnehin öffentlich)keine

Bewusst keine Rechte auf das Website-Repo für beide Teams. Der CMS-/Website-Zugang soll unabhängig vergeben werden können. Das automatische Owners-Team hat wie gewohnt vollen Zugriff auf beides.

Schritt 2: Zwei OAuth2-Anwendungen registrieren

Anders als bei Decap (Teil 1) läuft oauth2-proxy serverseitig und kann ein Secret sicher halten – Confidential Client aktiviert, nicht deaktiviert.

AnwendungRedirect URIDeckt ab
Website-Zugriff (Leser)https://ableger.example.org/oauth2/callbackalles außer /admin/
Redaktion-Zugriff (Autor*innen)https://ableger.example.org/admin/oauth2/callbacknur /admin/

Unterschiedliche Redirect-URIs, weil jede Instanz einen eigenen Callback-Pfad bekommt (siehe Schritt 4, proxy_prefix). Client-ID und Client-Secret notieren. Das Secret landet nur in der Server-Konfiguration (Schritt 4) und niemals im Repo.

Schritt 3: oauth2-proxy installieren

cd /tmp
curl -sL -o oauth2-proxy.tar.gz https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.15.3/oauth2-proxy-v7.15.3.linux-amd64.tar.gz
tar -xzf oauth2-proxy.tar.gz
mv oauth2-proxy-v7.15.3.linux-amd64/oauth2-proxy ~/bin/oauth2-proxy
chmod +x ~/bin/oauth2-proxy
rm -rf oauth2-proxy.tar.gz oauth2-proxy-v7.15.3.linux-amd64

Reguläre Linux-amd64-Variante, kein Extended-Build nötig.

Schritt 4: Zwei Instanzen konfigurieren

~/etc/oauth2-proxy/read.cfg und ~/etc/oauth2-proxy/write.cfg, je chmod 600 (enthalten Secrets). Kernwerte:

provider = "github"                 # Gitea uses the GitHub provider implementation, there is no dedicated Gitea provider
provider_display_name = "Gitea"
login_url = "https://git.example.org/login/oauth/authorize"
redeem_url = "https://git.example.org/login/oauth/access_token"
validate_url = "https://git.example.org/api/v1/user/emails"
client_id = "..."
client_secret = "..."
redirect_url = "https://ableger.example.org/oauth2/callback"      # write.cfg: .../admin/oauth2/callback
github_org = "<Organisationsname>"
github_team = "read,write,Owners"   # write.cfg: "write,Owners" - see note below
scope = "read:org"
email_domains = [ "*" ]
upstreams = [ "file:///var/www/virtual/<account>/ableger.example.org/#/" ]   # write.cfg: "http://127.0.0.1:4182/#/", see "Admin-Status-Bereich" below
http_address = "0.0.0.0:4180"       # write.cfg: 4181 - see "wrong interface" note below
cookie_name = "_oauth2_read"        # write.cfg: "_oauth2_write" - MUST differ
cookie_secret = "<32 zufällige Zeichen>"
cookie_secure = true
reverse_proxy = true
proxy_prefix = "/oauth2"            # write.cfg: "/admin/oauth2" - keeps the callback inside /admin/

github_team ist bewusst keine reine Trennung, sondern eine Hierarchie: write und Owners dürfen auch das Leser-Gate passieren (wer schreiben darf, soll erst recht lesen dürfen), Owners zusätzlich auch das Redaktions-Gate. Sonst müsste jede Autor*in doppelt gepflegt werden. github_team nimmt eine kommagetrennte Liste (ODER-Verknüpfung).

Drei Stolpersteine

  • cookie_secret muss exakt 16/24/32 Bytes als Literal sein, nicht eine Base64-Kodierung dieser Länge. openssl rand -base64 32 erzeugt einen 44 Zeichen langen String – oauth2-proxy meldet dann "cookie_secret must be ... but is 44 bytes" und startet gar nicht. Fix: openssl rand -hex 16 (32 Hex-Zeichen = 32 Byte als Literal).
  • file://-Upstreams brauchen einen #/-Fragment-Teil, sonst wird das Wurzelverzeichnis nicht auf den URL-Pfad / gemappt – jede angemeldete Anfrage auf / liefert sonst 404 page not found, obwohl index.html existiert.
  • http_address muss auf 0.0.0.0 binden, nicht 127.0.0.1. Uberspaces web backend-Mechanismus meldet sonst NOT OK, wrong interface (127.0.0.1).

Bekannte, offene Einschränkung: unsaubere Fehlerseite bei abgelehntem Login

Bei einem korrekt abgelehnten Login (Person authentifiziert, aber nicht im nötigen Team) zeigt oauth2-proxy 500 statt einem sauberen 403. Die Ablehnung selbst funktioniert zuverlässig (im Log nachvollziehbar: Missing Team:"..." from Org:"..." in teams: [...]), die Fehlerseite ist aber nicht sauber. Nicht sicherheitsrelevant, aber die Standardseite liest sich wie ein kaputtes Deployment statt wie eine normale Zugriffsablehnung.

Ursache (verifiziert in oauthproxy.go, Funktion OAuthCallback, v7.15.3): Jeder Fehler aus enrichSessionState, darunter auch eine berechtigte Team-Ablehnung wird pauschal mit http.StatusInternalServerError beantwortet. Hart codiertes Upstream-Verhalten, keine Konfigurationsoption ändert das.

Mögliche Lösung: oauth2-proxy unterstützt eigene HTML-Vorlagen über --custom-templates-dir. Eine eigene error.html, basierend auf der eingebauten pkg/app/pagewriter/error.html, könnte die Standardmeldung durch einen ruhigeren, ins eigene Layout eingepassten Text ersetzen. Der HTTP-Statuscode selbst bliebe technisch 500.

Schritt 5: Als Supervisor-Dienst einrichten

oauth2-proxy muss dauerhaft im Hintergrund laufen und jeden Aufruf der Website bzw. von /admin/ abfangen – nicht nur einmalig von Hand gestartet werden, sondern auch einen Server-Neustart oder einen Absturz überstehen. Ein Supervisor-Dienst übernimmt genau das: Start beim Boot, automatischer Neustart bei einem Absturz, zentral verwaltbar über supervisorctl. Gleiches Muster wie Gitea selbst.

# ~/etc/services.d/oauth2-proxy-read.ini
[program:oauth2-proxy-read]
directory=%(ENV_HOME)s
command=%(ENV_HOME)s/bin/oauth2-proxy --config=%(ENV_HOME)s/etc/oauth2-proxy/read.cfg
startsecs=10
autorestart=yes

Analog oauth2-proxy-write.ini mit write.cfg. Danach:

supervisorctl reread
supervisorctl update
supervisorctl status   # beide sollten RUNNING zeigen

Schritt 6: Routing umstellen

Dieser Schritt ändert die Erreichbarkeit der live Website. Er sollte also erst ausgeführt werden, wenn Schritt 1–5 durchgetestet sind und mindestens eine Person testweise in read/write ist, um sich nicht auszusperren.

uberspace web backend set 'ableger.example.org/admin/' --http --port 4181
uberspace web backend set 'ableger.example.org/' --http --port 4180

Der spezifischere Pfad (/admin/) zuerst, danach der allgemeine Pfad – Uberspace matcht nach längstem Pfad-Präfix.

Rollback (zurück zu statischer Auslieferung ohne Zugangsbeschränkung):

uberspace web backend set 'ableger.example.org/admin/' --apache
uberspace web backend set 'ableger.example.org/' --apache

Prüfen

Vor Schritt 6, direkt auf dem Server:

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4180/   # 403 mit Decap-losem "Sign in with Gitea" ist korrekt

Nach Schritt 6, im Browser: https://ableger.example.org/ und .../admin/ sollten je zur Gitea-Anmeldung führen, nach Login mit einer Person im passenden Team die jeweilige Oberfläche zeigen und andernfalls ablehnen.

Admin-Status-Bereich

Der Status-Bereich (/admin/status/) läuft komplett hinter der bestehenden write-Instanz, keine eigene Anmeldeprüfung nötig. Details zum Deploy-Hook-Teil: Getting Started, Schritt 10.

Warnung

oauth2-proxy 7.15.3 löst mehrere, per URL-Fragment pfadunterschiedene Upstreams (file:// + http://) in der einfachen upstreams = [...]-Listenform nicht wie in der Dokumentation beschrieben auf. Beide Einträge landen auf Pfad /, Start scheitert mit upstream ids/paths must be unique. Für echtes Pfad-Routing über mehrere Upstreams bräuchte es die strukturierte „Alpha Config“ (YAML, eigenes Schema) statt der flachen .cfg-Form. Einfacher umgangen: admin-status-server.rb liefert selbst auch die statischen Dateien (WEBrick DocumentRoot) – write.cfg braucht dadurch aber nur einen Upstream (http://127.0.0.1:4182/#/).