Schnittstellen
Technische Dokumentation. Zwei getrennte Themen in einem Dokument, weil beide auf demselben Mechanismus (Gitea-OAuth2) aufbauen.
- Decap CMS gegen Gitea anmelden (MUSS)
- 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
In Gitea anmelden mit dem Konto, das die Anwendung besitzen soll.
Oben rechts auf den Avatar → Einstellungen (Settings) → Reiter Anwendungen (Applications).
Im Bereich OAuth2-Anwendungen verwalten → Neue Anwendung erstellen.
Anwendungsname: frei wählbar, z. B.
Ableger Decap CMS.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.
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
/admin/aufrufen → Login with Gitea klicken.- Gitea fragt nach Autorisierung der Anwendung → bestätigen.
- 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_DOMAINpasst 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_clientin der Tabelleoauth2_application– muss0sein. - Login-Button erscheint gar nicht:
admin/index.htmlfehlt 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:
| Team | Repo-Rechte auf methoden | Repo-Rechte auf website |
|---|---|---|
write | Write | keine |
read | Read (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.
| Anwendung | Redirect URI | Deckt ab |
|---|---|---|
| Website-Zugriff (Leser) | https://ableger.example.org/oauth2/callback | alles außer /admin/ |
| Redaktion-Zugriff (Autor*innen) | https://ableger.example.org/admin/oauth2/callback | nur /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_secretmuss exakt 16/24/32 Bytes als Literal sein, nicht eine Base64-Kodierung dieser Länge.openssl rand -base64 32erzeugt 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 sonst404 page not found, obwohlindex.htmlexistiert.http_addressmuss auf0.0.0.0binden, nicht127.0.0.1. Uberspacesweb backend-Mechanismus meldet sonstNOT 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.
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/#/).