Homelab-Dokumentation und Konfigurationsvorlagen

Dokumentation aus dem Wiki uebernommen und um die Dienste auf dem Server
erweitert. Alle Anleitungen sind so geschrieben, dass sich der jeweilige
Dienst allein daraus neu aufsetzen laesst.

docs/      Dienste im Heimnetz (aus dem Wiki, anonymisiert)
services/  Dienste auf dem Server mit Konfigurationsvorlagen
           - matrix-synapse: Homeserver, Postgres, Well-Known-Delegation
           - coturn:         TURN-Relay fuer Anrufe
           - element-web:    Web-Client
           - wikijs, gitea, nginx

Durchgehend anonymisiert: echte Domain durch example.com ersetzt, IP-Adressen
und E-Mail-Adressen durch Platzhalter. Konfigurationsdateien liegen nur als
.example mit Platzhaltern statt echter Secrets vor.

Die Anleitungen halten die Stolpersteine fest, die beim Aufbau tatsaechlich
aufgetreten sind, unter anderem:
- Synapse verlangt LC_COLLATE=C, sonst startet es nicht
- server_name ist nachtraeglich nicht aenderbar -> Delegation noetig
- register_new_matrix_user liest conf.d nicht, Secret muss per -k kommen
- coturn braucht external-ip, sonst kommen keine Medien durch
- Gruppenmitgliedschaft fuer den Zertifikatszugriff wirkt erst beim Neustart
- nginx vererbt add_header nicht in location-Bloecke mit eigenen Direktiven
This commit is contained in:
x3
2026-08-31 17:56:02 +00:00
commit 4a55944aa8
32 changed files with 1848 additions and 0 deletions
+240
View File
@@ -0,0 +1,240 @@
# TURN-Server (coturn)
TURN-Relay für Sprach- und Videoanrufe in Matrix/Element.
| Punkt | Wert |
|---|---|
| Software | `coturn` |
| Öffentlich | `turn.example.com` |
| Ports | 3478 TCP/UDP (STUN/TURN), 5349 TCP/UDP (TURNS), 4915265535 UDP (Relay) |
| Authentifizierung | Zeitbegrenzt über gemeinsames Secret |
## Wozu überhaupt?
WebRTC verbindet zwei Teilnehmer möglichst direkt. Sitzen beide hinter NAT
oder einer restriktiven Firewall, klappt das nicht der Anruf klingelt, aber
es kommt kein Ton an. Der TURN-Server leitet den Medienstrom dann weiter.
Er ist damit ein **Relay für fremden Datenverkehr**. Ohne Absicherung ist das
ein offener Proxy, mit dem sich das interne Netz des Servers erreichen lässt.
Der Abschnitt „Absicherung“ unten ist deshalb nicht optional.
## Kein festes Passwort
coturn und Synapse teilen sich ein Secret (`static-auth-secret` bzw.
`turn_shared_secret`). Fragt ein Client Zugangsdaten an, erzeugt Synapse daraus
per HMAC ein kurzlebiges Paar:
- Benutzername: `<ablaufzeitpunkt>:<benutzer-id>`
- Passwort: HMAC-SHA1 über den Benutzernamen mit dem Secret, Base64-kodiert
coturn rechnet dasselbe nach. Es gibt also **keine angelegten Benutzer** beide
Seiten müssen nur exakt dasselbe Secret kennen.
## 1. Installation
```bash
sudo apt-get install -y coturn
# Das Debian-Paket startet ohne diesen Schalter nicht.
sudo sed -i 's/^#\?TURNSERVER_ENABLED=.*/TURNSERVER_ENABLED=1/' /etc/default/coturn
```
## 2. Secret erzeugen
```bash
openssl rand -hex 32
```
Derselbe Wert muss in `/etc/turnserver.conf` (`static-auth-secret`) **und** in
der Synapse-Konfiguration (`turn_shared_secret`) stehen.
## 3. Konfiguration
`turnserver.conf.example` nach `/etc/turnserver.conf` kopieren und anpassen:
```bash
sudo cp turnserver.conf.example /etc/turnserver.conf
sudo chown root:turnserver /etc/turnserver.conf
sudo chmod 640 /etc/turnserver.conf
```
Zwingend zu ersetzen:
- `static-auth-secret` der oben erzeugte Wert
- `external-ip` die **öffentliche** IP des Servers
- `realm` / `server-name` die eigene Domain
### external-ip nicht vergessen
Ohne `external-ip` kündigt coturn hinter NAT seine private Adresse als
Relay-Kandidat an. Der Anruf baut sich dann scheinbar auf, aber es fließen keine
Medien. Zu ermitteln mit:
```bash
curl -s -4 ifconfig.me
```
## 4. Zertifikat lesbar machen
`turns:` auf Port 5349 braucht Zertifikat und Key. coturn läuft als Benutzer
`turnserver` und kommt an Let's-Encrypt-Material standardmäßig nicht heran.
Statt die Rechte global aufzuweichen, eine eigene Gruppe verwenden:
```bash
sudo groupadd -f ssl-cert
sudo usermod -aG ssl-cert turnserver
sudo chgrp -R ssl-cert /etc/letsencrypt/live /etc/letsencrypt/archive
sudo chmod -R g+rX /etc/letsencrypt/live /etc/letsencrypt/archive
sudo find /etc/letsencrypt/archive -name 'privkey*.pem' -exec chmod 640 {} \;
```
> Gruppenmitgliedschaften greifen erst beim **Start eines Prozesses**. Läuft
> coturn schon, ist danach ein `systemctl restart coturn` nötig ein `reload`
> reicht nicht.
Prüfen, ob es wirklich klappt:
```bash
sudo -u turnserver head -c 30 /etc/letsencrypt/live/matrix.example.com/privkey.pem
```
### Erneuerung berücksichtigen
coturn liest das Zertifikat nur beim Start. Nach einer Erneuerung würde es mit
dem alten weiterarbeiten, bis es irgendwann abläuft. Deshalb ein Deploy-Hook
nach `/etc/letsencrypt/renewal-hooks/deploy/10-reload-services.sh`:
```sh
#!/bin/sh
set -e
chgrp -R ssl-cert /etc/letsencrypt/live /etc/letsencrypt/archive || true
chmod -R g+rX /etc/letsencrypt/live /etc/letsencrypt/archive || true
find /etc/letsencrypt/archive -name 'privkey*.pem' -exec chmod 640 {} \; || true
systemctl reload nginx || true
systemctl restart coturn || true
```
Ausführbar machen: `sudo chmod +x …/10-reload-services.sh`
## 5. Absicherung
Die `denied-peer-ip`-Zeilen in der Beispielkonfiguration sperren alle privaten
und reservierten Netze. **Ohne sie kann jeder, der Zugangsdaten bekommt, über
den TURN-Server auf `localhost` und das interne Netz zugreifen** inklusive
Diensten, die nur lokal lauschen.
Weitere gesetzte Schalter:
| Schalter | Wirkung |
|---|---|
| `no-tcp-relay` | Kein TCP-Relay (RFC 6062). WebRTC braucht es nicht. |
| `no-multicast-peers` | Kein Relay an Multicast-Adressen |
| `no-cli` | Telnet-Verwaltungsschnittstelle aus |
| `no-tlsv1`, `no-tlsv1_1`, `no-sslv3` | Veraltete Protokolle aus |
| `user-quota`, `total-quota` | Begrenzt den Missbrauch |
## 6. Start und Prüfung
```bash
sudo systemctl enable --now coturn
sudo systemctl status coturn
```
Lauschen sollte er auf beiden Ports:
```bash
ss -tuln | grep -E '3478|5349'
```
Im Log darf **nicht** stehen „cannot start TLS and DTLS listeners“. Das heißt,
dass coturn das Zertifikat nicht lesen kann:
```bash
sudo journalctl -u coturn | grep -iE 'certificate|private key|TLS'
```
### Relay tatsächlich testen
Ein Blick auf offene Ports genügt nicht erst eine echte Allocation beweist,
dass Authentifizierung und Relay funktionieren. Zugangsdaten selbst berechnen:
```bash
python3 - 'HIER-DAS-TURN-SECRET' <<'PY'
import hashlib, hmac, base64, time, sys
secret = sys.argv[1]
user = f"{int(time.time()) + 3600}:testuser"
pw = base64.b64encode(
hmac.new(secret.encode(), user.encode(), hashlib.sha1).digest()).decode()
print(user); print(pw)
PY
```
Damit eine Allocation anfordern:
```bash
turnutils_uclient -u '<benutzername>' -w '<passwort>' -y -n 2 turn.example.com -p 3478
```
Erfolg sieht so aus gleich viele gesendete und empfangene Nachrichten, keine
verlorenen Pakete:
```
start_mclient: tot_send_msgs=8, tot_recv_msgs=8
Total lost packets 0 (0.000000%)
```
Gegenprobe mit falschem Passwort muss `Cannot complete Allocation` liefern.
> `turnutils_uclient -T …` schlägt mit `error 442` fehl. Das ist **kein**
> Fehler: `-T` fordert ein TCP-Relay an, das per `no-tcp-relay` gesperrt ist.
TLS-Listener separat prüfen:
```bash
openssl s_client -connect turn.example.com:5349 -servername turn.example.com </dev/null
```
Der Domainname muss im Zertifikat stehen:
```bash
openssl s_client -connect turn.example.com:5349 </dev/null 2>/dev/null \
| openssl x509 -noout -ext subjectAltName
```
## 7. Verbindung zu Synapse
In `/etc/matrix-synapse/conf.d/homelab.yaml` stehen `turn_uris` und
`turn_shared_secret`. Nach dem Neustart lässt sich prüfen, was ein Client
wirklich ausgeliefert bekommt:
```bash
curl -s -H "Authorization: Bearer <access-token>" \
https://matrix.example.com/_matrix/client/v3/voip/turnServer
```
Die zurückgegebenen Zugangsdaten sollten mit `turnutils_uclient` funktionieren
das ist der eigentliche Ende-zu-Ende-Nachweis.
## Firewall
Freizugeben sind:
- 3478 TCP **und** UDP
- 5349 TCP **und** UDP
- 4915265535 UDP (Relay-Bereich)
Der Relay-Bereich wird oft vergessen. Ohne ihn kommt die Allocation zustande,
aber es fließen keine Medien.
## Stolpersteine
| Symptom | Ursache |
|---|---|
| Port 5349 fehlt, Log meldet „certificate file is not set properly“ | `turnserver` darf den Key nicht lesen, oder Dienst lief schon vor der Gruppenänderung |
| Anruf klingelt, kein Ton/Bild | `external-ip` fehlt oder Relay-Ports gesperrt |
| `error 442` im Test | `-T` verwendet; TCP-Relay ist absichtlich aus |
| Authentifizierung schlägt fehl | Secret in coturn und Synapse verschieden |
| Nach Zertifikatserneuerung bricht TURNS ab | Deploy-Hook fehlt |
+62
View File
@@ -0,0 +1,62 @@
# ===========================================================================
# coturn fuer Matrix/Element-Anrufe (turn.example.com)
# ===========================================================================
listening-port=3478
tls-listening-port=5349
# Auf allen Adressen lauschen, aber die oeffentliche IP in Kandidaten melden.
# Ohne external-ip kuendigt der Server hinter NAT die private IP an und
# Verbindungen scheitern.
external-ip=HIER-DIE-OEFFENTLICHE-IP
realm=example.com
server-name=turn.example.com
# Zeitbegrenzte Anmeldedaten. Synapse leitet daraus mit demselben Secret
# kurzlebige Zugangsdaten fuer die Clients ab -- es gibt keine festen Nutzer.
use-auth-secret
static-auth-secret=HIER-DAS-TURN-SECRET
# TLS-Material (SAN turn.example.com liegt im Matrix-Zertifikat)
cert=/etc/letsencrypt/live/matrix.example.com/fullchain.pem
pkey=/etc/letsencrypt/live/matrix.example.com/privkey.pem
# Veraltete/schwache Verfahren abschalten
no-tlsv1
no-tlsv1_1
no-sslv3
fingerprint
# Relay-Portbereich
min-port=49152
max-port=65535
# --------------------------- Absicherung ----------------------------------
# Ohne diese Sperren laesst sich der TURN-Server als Sprungbrett in das
# eigene interne Netz und auf localhost missbrauchen.
no-multicast-peers
no-cli
no-tcp-relay
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.0.0.0-192.0.0.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=198.18.0.0-198.19.255.255
denied-peer-ip=240.0.0.0-255.255.255.255
denied-peer-ip=::1
denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff
denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255
denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff
# Missbrauch begrenzen
user-quota=12
total-quota=1200
syslog
+141
View File
@@ -0,0 +1,141 @@
# Element Web
Web-Client für den eigenen Matrix-Server, ausgeliefert als statische Dateien
über nginx.
| Punkt | Wert |
|---|---|
| Software | `element-web` (Release-Tarball) |
| Öffentlich | `https://element.example.com` |
| Verzeichnis | `/var/www/element` |
## Warum eine eigene Domain?
Element **darf nicht** unter derselben Domain laufen wie die Client-API des
Homeservers. Beide teilen sich sonst denselben Origin, und eine Schwachstelle im
Client könnte direkt auf die API zugreifen. Deshalb:
- `matrix.example.com` → nur Synapse
- `element.example.com` → nur der Web-Client
## 1. Herunterladen und Signatur prüfen
```bash
VER=$(curl -s https://api.github.com/repos/element-hq/element-web/releases/latest \
| python3 -c "import json,sys; print(json.load(sys.stdin)['tag_name'])")
echo "$VER"
curl -fLO "https://github.com/element-hq/element-web/releases/download/${VER}/element-${VER}.tar.gz"
curl -fLO "https://github.com/element-hq/element-web/releases/download/${VER}/element-${VER}.tar.gz.asc"
```
Die Signatur zu prüfen ist hier keine Formsache: das Paket läuft später im
Browser mit vollem Zugriff auf die Sitzung.
```bash
curl -fsSL -o element-release-key.gpg https://packages.element.io/element-release-key.gpg
gpg --import element-release-key.gpg
gpg --verify element-${VER}.tar.gz.asc element-${VER}.tar.gz
```
Erwartet wird `Good signature from "Element Releases <releases@riot.im>"` mit dem
Fingerprint `712B FBEE 92DC A452 52DB 17D7 C7BE 97EF A179 B100`.
Die Meldung „This key is not certified with a trusted signature“ ist dabei
normal sie besagt nur, dass der Schlüssel lokal nicht als vertrauenswürdig
markiert wurde. Entscheidend ist, dass der Fingerprint stimmt.
## 2. Ausrollen
```bash
sudo tar xzf element-${VER}.tar.gz -C /var/www
sudo rm -rf /var/www/element
sudo mv /var/www/element-${VER} /var/www/element
sudo chown -R www-data:www-data /var/www/element
```
## 3. Konfiguration
`config.json.example` nach `/var/www/element/config.json` kopieren und anpassen.
Wichtig ist der Unterschied der beiden Felder:
- `base_url` → wo die API tatsächlich liegt (`https://matrix.example.com`)
- `server_name` → was dem Benutzer angezeigt wird (`example.com`)
```json
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.example.com",
"server_name": "example.com"
}
},
"disable_guests": true
}
```
## 4. nginx
`nginx/element.example.com.conf` und `nginx/element-security.conf` kopieren:
```bash
sudo cp nginx/element-security.conf /etc/nginx/snippets/
sudo cp nginx/element.example.com.conf /etc/nginx/sites-available/
sudo ln -s /etc/nginx/sites-available/element.example.com.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
### Zwei Fallstricke in der nginx-Konfiguration
**Caching.** `index.html`, `config.json` und `version` dürfen nicht dauerhaft
gecacht werden. Sonst zeigt der Browser nach einem Update weiter die alte
Version oder alte Serverdaten. Die Dateien unter `/bundles/` tragen dagegen
einen Hash im Namen und dürfen unbegrenzt gecacht werden.
**Header-Vererbung.** nginx vererbt `add_header` **nicht**, sobald ein
`location`-Block eigene `add_header`-Direktiven enthält. Da `location /` per
`try_files` auf `/index.html` verweist und dieser Block ein eigenes
`Cache-Control` setzt, würden die Sicherheitsheader dort stillschweigend
verschwinden. Deshalb liegen sie in einem Snippet, das in **jedem** betroffenen
Block eingebunden wird.
Kontrolle:
```bash
curl -s -o /dev/null -D - https://element.example.com/ | grep -iE '^x-|^strict|^referrer'
```
Es müssen `x-frame-options`, `x-content-type-options`, `referrer-policy` und
`strict-transport-security` auftauchen.
## 5. Prüfung
```bash
curl -s -o /dev/null -w "%{http_code}\n" https://element.example.com/
curl -s https://element.example.com/version
curl -s https://element.example.com/config.json
```
Danach im Browser öffnen und mit einem Konto anmelden. Der Anmeldedialog sollte
`example.com` als Server anzeigen, nicht `matrix.example.com`.
## Aktualisieren
Element ist rein statisch ein Update ist ein Austausch des Verzeichnisses.
Die `config.json` liegt im Verzeichnis und muss dabei gesichert werden:
```bash
sudo cp /var/www/element/config.json /tmp/element-config.json
# ... neue Version wie oben entpacken ...
sudo cp /tmp/element-config.json /var/www/element/config.json
sudo chown -R www-data:www-data /var/www/element
```
## Stolpersteine
| Symptom | Ursache |
|---|---|
| Nach Update alte Version im Browser | `index.html`/`version` wurden gecacht |
| Sicherheitsheader fehlen | `add_header`-Vererbung durch eigenen `location`-Block gekippt |
| Anmeldung zeigt falschen Servernamen | `server_name` in `config.json` falsch |
| Konfiguration nach Update weg | `config.json` liegt im ausgetauschten Verzeichnis |
+20
View File
@@ -0,0 +1,20 @@
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.example.com",
"server_name": "example.com"
}
},
"brand": "Element",
"default_country_code": "DE",
"disable_custom_urls": false,
"disable_guests": true,
"disable_3pid_login": true,
"show_labs_settings": true,
"room_directory": {
"servers": ["example.com"]
},
"setting_defaults": {
"use_system_theme": false
}
}
@@ -0,0 +1,7 @@
# Sicherheitsheader fuer Element. Muss in JEDEN location-Block eingebunden
# werden, der selbst add_header verwendet: nginx vererbt add_header nicht,
# sobald ein Block eigene Direktiven setzt.
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy no-referrer always;
add_header Strict-Transport-Security "max-age=31536000" always;
@@ -0,0 +1,51 @@
server {
listen 80;
listen [::]:80;
server_name element.example.com;
location /.well-known/acme-challenge/ { root /var/www/html; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name element.example.com;
ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
root /var/www/element;
index index.html;
# Element laedt Raum-Inhalte in Iframes/Worker; diese Header sind die
# von Element empfohlene Mindestabsicherung.
include /etc/nginx/snippets/element-security.conf;
# config.json und index.html duerfen nicht dauerhaft gecacht werden,
# sonst zeigen Clients nach einem Update alte Serverdaten.
location = /config.json {
include /etc/nginx/snippets/element-security.conf;
add_header Cache-Control "no-cache" always;
}
location = /index.html {
include /etc/nginx/snippets/element-security.conf;
add_header Cache-Control "no-cache" always;
}
location = /version {
include /etc/nginx/snippets/element-security.conf;
add_header Cache-Control "no-cache" always;
}
# Gehashte Bundle-Dateien sind unveraenderlich.
location /bundles/ {
include /etc/nginx/snippets/element-security.conf;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
location / {
try_files $uri $uri/ /index.html;
}
}
+148
View File
@@ -0,0 +1,148 @@
# Gitea
Selbstgehostetes Git-Hosting.
| Punkt | Wert |
|---|---|
| Software | Gitea (Binary nach `/usr/local/bin/gitea`) |
| Öffentlich | `https://git.example.com` |
| Interner Port | `127.0.0.1:3001` |
| Datenbank | SQLite (`/var/lib/gitea/data/gitea.db`) |
| Dienstbenutzer | `git` |
## 1. Benutzer und Verzeichnisse
```bash
sudo adduser --system --shell /bin/bash --gecos 'Gitea' \
--group --disabled-password --home /home/git git
sudo mkdir -p /var/lib/gitea/{custom,data,log} /etc/gitea
sudo chown -R git:git /var/lib/gitea
sudo chmod 750 /var/lib/gitea
# Waehrend der Ersteinrichtung muss Gitea in /etc/gitea schreiben duerfen.
sudo chown root:git /etc/gitea
sudo chmod 770 /etc/gitea
```
## 2. Binary installieren
```bash
VER=$(curl -s https://api.github.com/repos/go-gitea/gitea/releases/latest \
| python3 -c "import json,sys; print(json.load(sys.stdin)['tag_name'].lstrip('v'))")
curl -fLo /tmp/gitea "https://dl.gitea.com/gitea/${VER}/gitea-${VER}-linux-amd64"
sudo install -m 755 /tmp/gitea /usr/local/bin/gitea
gitea --version
```
## 3. systemd-Unit
Nach `/etc/systemd/system/gitea.service`:
```ini
[Unit]
Description=Gitea (Git with a cup of tea)
After=network.target
[Service]
User=git
Group=git
WorkingDirectory=/var/lib/gitea
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
Restart=always
RestartSec=5
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now gitea
```
## 4. Ersteinrichtung
Gitea zunächst über nginx erreichbar machen (siehe `services/nginx/`), dann
`https://git.example.com` im Browser öffnen und den Installationsdialog
ausfüllen. Danach die Rechte auf `/etc/gitea` wieder einschränken:
```bash
sudo chmod 750 /etc/gitea
sudo chmod 640 /etc/gitea/app.ini
```
Das ist wichtig: `app.ini` enthält den `SECRET_KEY` und den
`INTERNAL_TOKEN`. Bleibt die Datei für alle lesbar, kann jeder lokale Benutzer
Sitzungen fälschen.
## 5. Wichtige Werte in `app.ini`
```ini
[server]
HTTP_PORT = 3001
ROOT_URL = https://git.example.com/
DOMAIN = git.example.com
SSH_PORT = 22
[database]
DB_TYPE = sqlite3
PATH = /var/lib/gitea/data/gitea.db
```
`ROOT_URL` muss stimmen, sonst erzeugt Gitea Klon-URLs und Weiterleitungen mit
falschem Hostnamen.
Nach der Ersteinrichtung sollte die offene Registrierung geschlossen werden:
```ini
[service]
DISABLE_REGISTRATION = true
```
## 6. Sicherung
```bash
sudo systemctl stop gitea
sudo tar czf gitea-$(date +%F).tar.gz /var/lib/gitea /etc/gitea/app.ini
sudo systemctl start gitea
```
Die SQLite-Datenbank sollte nicht im laufenden Betrieb kopiert werden.
Alternativ Giteas eigener Dump:
```bash
sudo -u git gitea dump --config /etc/gitea/app.ini
```
## Über die API arbeiten
Repository anlegen, ohne den Browser zu benutzen:
```bash
curl -u '<benutzer>:<passwort-oder-token>' \
-H "Content-Type: application/json" \
-X POST -d '{"name":"projekt","private":true,"default_branch":"main"}' \
https://git.example.com/api/v1/user/repos
```
Issue anlegen:
```bash
curl -u '<benutzer>:<token>' -H "Content-Type: application/json" \
-X POST -d '{"title":"Titel","body":"Beschreibung"}' \
https://git.example.com/api/v1/repos/<benutzer>/<repo>/issues
```
> Besser ein Zugriffstoken (Einstellungen → Anwendungen) statt des Passworts
> verwenden und den Geltungsbereich einschränken.
## Stolpersteine
| Symptom | Ursache |
|---|---|
| Falsche Klon-URLs | `ROOT_URL`/`DOMAIN` stimmen nicht |
| Ersteinrichtung schlägt fehl | `/etc/gitea` für den Benutzer `git` nicht schreibbar |
| `app.ini` weltlesbar | Rechte nach der Einrichtung nicht zurückgesetzt |
| Beschädigte Sicherung | SQLite im laufenden Betrieb kopiert |
+270
View File
@@ -0,0 +1,270 @@
# Matrix-Homeserver (Synapse)
Eigener Matrix-Server mit PostgreSQL, nginx als TLS-Terminierung und
Well-Known-Delegation, damit die Benutzer-IDs kurz bleiben.
| Punkt | Wert |
|---|---|
| Software | Synapse (Paket `matrix-synapse-py3` von packages.matrix.org) |
| Datenbank | PostgreSQL |
| Interner Listener | `127.0.0.1:8008` (kein TLS, das macht nginx) |
| Öffentlich | `https://matrix.example.com` |
| Benutzer-IDs | `@name:example.com` |
## Warum zwei Domains?
Das ist der Punkt, an dem die meisten Anleitungen unnötig kompliziert werden.
Der `server_name` ist der **hintere Teil jeder Benutzer-ID**. Setzt man ihn auf
`matrix.example.com`, heißen alle Benutzer `@name:matrix.example.com` unschön
und nicht mehr änderbar, denn der `server_name` lässt sich nachträglich **nicht**
umstellen, ohne alle Konten und Räume zu verlieren.
Deshalb:
- `server_name: example.com` → IDs lauten `@name:example.com`
- Synapse läuft aber auf `matrix.example.com`
- Zwei kleine JSON-Dokumente unter `https://example.com/.well-known/matrix/…`
verweisen andere Server und Clients dorthin
Die Apex-Domain muss dafür **nur diese zwei Dateien** ausliefern, sonst nichts.
## Voraussetzungen
- Debian/Ubuntu mit root-Rechten
- PostgreSQL
- nginx
- certbot
- DNS-A-Records auf den Server: `example.com`, `matrix.example.com`
## 1. Paketquelle und Installation
```bash
sudo install -d -m 0755 /etc/apt/keyrings
sudo curl -fsSL -o /etc/apt/keyrings/matrix-org-archive-keyring.gpg \
https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg
# <suite> ist der Codename der Distribution, z. B. noble, trixie, resolute
echo "deb [signed-by=/etc/apt/keyrings/matrix-org-archive-keyring.gpg] \
https://packages.matrix.org/debian/ <suite> main" \
| sudo tee /etc/apt/sources.list.d/matrix-org.list
sudo apt-get update
```
Ob es für die eigene Distribution ein Paket gibt, lässt sich vorher prüfen:
```bash
curl -s https://packages.matrix.org/debian/dists/<suite>/main/binary-amd64/Packages \
| grep -E "^Package:|^Version:"
```
Installation. Der `server_name` wird per debconf vorbelegt, damit die
Installation nicht interaktiv nachfragt:
```bash
echo "matrix-synapse-py3 matrix-synapse/server-name string example.com" \
| sudo debconf-set-selections
echo "matrix-synapse-py3 matrix-synapse/report-stats boolean false" \
| sudo debconf-set-selections
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y matrix-synapse-py3
```
> **Wichtig:** Der hier gesetzte `server_name` ist endgültig. Er landet in
> `/etc/matrix-synapse/conf.d/server_name.yaml`.
## 2. Datenbank
Synapse besteht auf `C`-Collation. Eine mit den üblichen `de_DE.UTF-8`-Vorgaben
angelegte Datenbank wird beim Start **abgelehnt**:
```bash
sudo -u postgres psql <<'SQL'
CREATE ROLE synapse_user LOGIN PASSWORD 'HIER-EIN-STARKES-PASSWORT';
CREATE DATABASE synapse
ENCODING 'UTF8'
LC_COLLATE='C'
LC_CTYPE='C'
template=template0
OWNER synapse_user;
SQL
```
Kontrolle unter `Collate`/`Ctype` muss `C` stehen:
```bash
sudo -u postgres psql -c "\l synapse"
```
## 3. Konfiguration
Eigene Einstellungen gehören nach `/etc/matrix-synapse/conf.d/`. Die
`homeserver.yaml` wird vom Paket verwaltet und bei Upgrades überschrieben.
Der von der Installation angelegte SQLite-Block in `homeserver.yaml` muss
entfernt oder auskommentiert werden, sonst kollidiert er mit der conf.d-Datei:
```bash
sudo sed -i '/^database:/,/database: \/var\/lib\/matrix-synapse\/homeserver.db/d' \
/etc/matrix-synapse/homeserver.yaml
```
Dann die Vorlagen aus diesem Verzeichnis kopieren und die Platzhalter füllen:
```bash
sudo cp conf.d/database.yaml.example /etc/matrix-synapse/conf.d/database.yaml
sudo cp conf.d/homelab.yaml.example /etc/matrix-synapse/conf.d/homelab.yaml
sudo chown root:matrix-synapse /etc/matrix-synapse/conf.d/*.yaml
sudo chmod 640 /etc/matrix-synapse/conf.d/*.yaml
```
### Secrets explizit setzen
Fehlen `macaroon_secret_key` und `form_secret`, leitet Synapse sie aus dem
Signing-Key ab und warnt beim Start. Wird der Signing-Key später ersetzt, sind
schlagartig **alle Access-Tokens ungültig**. Deshalb einmalig festlegen:
```bash
sudo tee /etc/matrix-synapse/conf.d/secrets.yaml >/dev/null <<EOF
macaroon_secret_key: "$(openssl rand -base64 48 | tr -d '\n')"
form_secret: "$(openssl rand -base64 48 | tr -d '\n')"
EOF
sudo chown root:matrix-synapse /etc/matrix-synapse/conf.d/secrets.yaml
sudo chmod 640 /etc/matrix-synapse/conf.d/secrets.yaml
```
Registrierungs-Secret separat ablegen, damit es nicht in einer YAML steht:
```bash
openssl rand -hex 32 | sudo tee /etc/matrix-synapse/registration_shared_secret >/dev/null
sudo chown root:matrix-synapse /etc/matrix-synapse/registration_shared_secret
sudo chmod 640 /etc/matrix-synapse/registration_shared_secret
```
Start:
```bash
sudo systemctl restart matrix-synapse
sudo systemctl status matrix-synapse
curl -s http://127.0.0.1:8008/_matrix/client/versions
```
## 4. nginx
`nginx/matrix.conf` nach `/etc/nginx/sites-available/` kopieren, verlinken und
ein Zertifikat holen:
```bash
sudo certbot certonly --webroot -w /var/www/html -d matrix.example.com
sudo ln -s /etc/nginx/sites-available/matrix.example.com \
/etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
Zwei Details in der Konfiguration sind nicht optional:
- `client_max_body_size` muss mindestens so groß sein wie `max_upload_size` in
Synapse, sonst brechen Uploads mit einem nginx-413 ab, bevor Synapse sie sieht.
- `proxy_read_timeout` muss großzügig sein. Clients halten `/sync` als
Long-Poll offen; beim nginx-Standard von 60 Sekunden reißt die Verbindung
ständig ab.
## 5. Well-Known-Delegation
Im vHost der **Apex-Domain** (`example.com`):
```nginx
location = /.well-known/matrix/server {
default_type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '{"m.server": "matrix.example.com:443"}';
}
location = /.well-known/matrix/client {
default_type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '{"m.homeserver": {"base_url": "https://matrix.example.com"}}';
}
```
Der `Access-Control-Allow-Origin`-Header ist beim Client-Dokument
zwingend ohne ihn verweigern Browser-Clients die Abfrage.
Prüfen:
```bash
curl -s https://example.com/.well-known/matrix/server
curl -s https://example.com/.well-known/matrix/client
```
## 6. Admin-Benutzer anlegen
Die Registrierung ist bewusst geschlossen (`enable_registration: false`), sonst
wird der Server binnen Tagen als Spam-Relay missbraucht. Benutzer werden per
Shared Secret angelegt.
`register_new_matrix_user` liest **nur** die mit `-c` übergebene Datei. Liegt
das Secret in `conf.d/`, findet es der Befehl nicht deshalb direkt übergeben:
```bash
SECRET=$(sudo cat /etc/matrix-synapse/registration_shared_secret)
sudo register_new_matrix_user -k "$SECRET" \
-u admin -p 'PASSWORT' -a http://127.0.0.1:8008
```
`-a` macht den Benutzer zum Server-Admin. Kontrolle:
```bash
sudo -u postgres psql -d synapse -c "SELECT name, admin FROM users;"
```
## 7. Funktionsprüfung
```bash
# Login über die öffentliche URL
curl -s -X POST https://matrix.example.com/_matrix/client/v3/login \
-H 'Content-Type: application/json' \
-d '{"type":"m.login.password",
"identifier":{"type":"m.id.user","user":"admin"},
"password":"PASSWORT"}'
# Föderation von außen prüfen (offizieller Tester)
curl -s "https://federationtester.matrix.org/api/report?server_name=example.com" \
| python3 -c "import json,sys; print('FederationOK:', json.load(sys.stdin)['FederationOK'])"
```
`FederationOK: True` ist das Ziel. Schlägt es fehl, liegt es fast immer an der
Well-Known-Datei oder an einer unvollständigen Zertifikatskette.
## Betrieb
```bash
sudo systemctl status matrix-synapse
sudo journalctl -u matrix-synapse -f
```
Datenbank sichern:
```bash
sudo -u postgres pg_dump -Fc synapse > synapse-$(date +%F).dump
```
Mitzusichern sind außerdem:
- `/etc/matrix-synapse/homeserver.signing.key` **ohne diesen Schlüssel ist der
Server für andere Server eine andere Identität.** Nicht ersetzbar.
- `/etc/matrix-synapse/conf.d/` enthält die Secrets
- `/var/lib/matrix-synapse/media` hochgeladene Dateien
## Stolpersteine
| Symptom | Ursache |
|---|---|
| Start bricht mit Collation-Fehler ab | Datenbank ohne `LC_COLLATE='C'` angelegt |
| „Config is missing macaroon_secret_key“ | `secrets.yaml` fehlt |
| Uploads scheitern mit 413 | `client_max_body_size` < `max_upload_size` |
| Clients verlieren dauernd die Verbindung | `proxy_read_timeout` zu klein |
| Föderation schlägt fehl | Well-Known falsch oder Zertifikatskette unvollständig |
| IDs lauten `@name:matrix.example.com` | `server_name` falsch gesetzt nicht mehr korrigierbar |
@@ -0,0 +1,12 @@
# -> /etc/matrix-synapse/conf.d/database.yaml (root:matrix-synapse, 0640)
database:
name: psycopg2
txn_limit: 10000
args:
user: synapse_user
password: HIER-DAS-DATENBANK-PASSWORT
dbname: synapse
host: 127.0.0.1
port: 5432
cp_min: 5
cp_max: 10
@@ -0,0 +1,51 @@
# -> /etc/matrix-synapse/conf.d/homelab.yaml (root:matrix-synapse, 0640)
#
# Liegt bewusst in conf.d: homeserver.yaml wird vom Paket verwaltet und bei
# Upgrades ueberschrieben.
# Synapse lauscht nur lokal; TLS und Port 443 uebernimmt nginx davor.
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
bind_addresses: ['127.0.0.1']
resources:
- names: [client, federation]
compress: false
public_baseurl: https://matrix.example.com/
# Offene Registrierung aus: sonst wird der Server als Spam-Relay missbraucht.
enable_registration: false
registration_shared_secret_path: /etc/matrix-synapse/registration_shared_secret
max_upload_size: 100M
url_preview_enabled: true
# Ohne diese Sperrliste laesst sich der Link-Vorschau-Dienst benutzen, um das
# interne Netz des Servers zu scannen.
url_preview_ip_range_blacklist:
- '127.0.0.0/8'
- '10.0.0.0/8'
- '172.16.0.0/12'
- '192.168.0.0/16'
- '100.64.0.0/10'
- '169.254.0.0/16'
- '::1/128'
- 'fe80::/64'
- 'fc00::/7'
# --------------------------- VoIP / TURN ----------------------------------
turn_uris:
- "turn:turn.example.com:3478?transport=udp"
- "turn:turn.example.com:3478?transport=tcp"
- "turns:turn.example.com:5349?transport=udp"
- "turns:turn.example.com:5349?transport=tcp"
# Muss exakt dem static-auth-secret in der turnserver.conf entsprechen.
turn_shared_secret: "HIER-DAS-TURN-SECRET"
turn_user_lifetime: 86400000
turn_allow_guests: false
trusted_key_servers:
- server_name: "matrix.org"
suppress_key_server_warning: true
@@ -0,0 +1,7 @@
# -> /etc/matrix-synapse/conf.d/secrets.yaml (root:matrix-synapse, 0640)
#
# Mit "openssl rand -base64 48" erzeugen. Fehlen die Werte, leitet Synapse sie
# aus dem Signing-Key ab; ein spaeterer Wechsel des Signing-Keys wuerde dann
# alle Access-Tokens ungueltig machen.
macaroon_secret_key: "HIER-EIN-ZUFALLSWERT"
form_secret: "HIER-EIN-ZUFALLSWERT"
@@ -0,0 +1,41 @@
# Synapse-Client- und Federation-API. Synapse selbst lauscht nur auf
# 127.0.0.1:8008; TLS und Port 443 macht nginx.
server {
listen 80;
listen [::]:80;
server_name matrix.example.com;
location /.well-known/acme-challenge/ { root /var/www/html; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name matrix.example.com;
ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Muss mindestens so gross sein wie max_upload_size in Synapse.
client_max_body_size 100M;
location ~ ^(/_matrix|/_synapse/client) {
proxy_pass http://127.0.0.1:8008;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_http_version 1.1;
# Long-Polling (/sync) darf nicht nach 60s abgeschnitten werden.
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
}
location / {
return 200 'Matrix homeserver. Web client: https://element.example.com\n';
default_type text/plain;
}
}
+128
View File
@@ -0,0 +1,128 @@
# nginx und TLS
nginx ist der einzige Dienst, der von außen erreichbar ist. Alle Anwendungen
lauschen nur auf `127.0.0.1` und werden von hier weitergereicht.
## Aufteilung
| Domain | Ziel |
|---|---|
| `example.com` | statische Seite + Matrix-Well-Known |
| `matrix.example.com` | Synapse auf `127.0.0.1:8008` |
| `element.example.com` | statische Dateien `/var/www/element` |
| `wiki.example.com` | Wiki.js auf `127.0.0.1:3000` |
| `git.example.com` | Gitea auf `127.0.0.1:3001` |
| `turn.example.com` | **kein** vHost coturn nutzt das Zertifikat direkt |
`turn.example.com` läuft bewusst nicht über nginx. coturn spricht kein HTTP; es
braucht nur ein gültiges Zertifikat für Port 5349. Der Name muss trotzdem in
einem Zertifikat stehen, deshalb wird er beim Ausstellen mit angegeben.
## Zertifikate
Ein Zertifikat für mehrere Namen:
```bash
sudo certbot certonly --webroot -w /var/www/html \
-d matrix.example.com -d element.example.com -d turn.example.com
```
Für die Erstausstellung muss Port 80 die ACME-Anfrage beantworten können:
```nginx
server {
listen 80;
server_name matrix.example.com element.example.com turn.example.com;
root /var/www/html;
location /.well-known/acme-challenge/ { allow all; }
}
```
Enthaltene Namen prüfen:
```bash
sudo openssl x509 -in /etc/letsencrypt/live/matrix.example.com/fullchain.pem \
-noout -ext subjectAltName
```
## Erneuerung
certbot erneuert automatisch. Dienste, die das Material **selbst** lesen (hier
coturn), merken das nicht sie halten es seit dem Start im Speicher. Deshalb
ein Deploy-Hook nach
`/etc/letsencrypt/renewal-hooks/deploy/10-reload-services.sh`:
```sh
#!/bin/sh
set -e
chgrp -R ssl-cert /etc/letsencrypt/live /etc/letsencrypt/archive || true
chmod -R g+rX /etc/letsencrypt/live /etc/letsencrypt/archive || true
find /etc/letsencrypt/archive -name 'privkey*.pem' -exec chmod 640 {} \; || true
systemctl reload nginx || true
systemctl restart coturn || true
```
Trockenlauf:
```bash
sudo certbot renew --dry-run
```
## Zertifikate für Nicht-HTTP-Dienste freigeben
Standardmäßig darf nur `root` den privaten Schlüssel lesen. Statt die Rechte
global zu öffnen, eine Gruppe verwenden und nur die betroffenen Dienstbenutzer
aufnehmen:
```bash
sudo groupadd -f ssl-cert
sudo usermod -aG ssl-cert turnserver
sudo chgrp -R ssl-cert /etc/letsencrypt/live /etc/letsencrypt/archive
sudo chmod -R g+rX /etc/letsencrypt/live /etc/letsencrypt/archive
sudo find /etc/letsencrypt/archive -name 'privkey*.pem' -exec chmod 640 {} \;
```
Gruppenmitgliedschaften wirken erst beim Start eines Prozesses danach ist ein
`restart` nötig, kein `reload`.
## Zwei wiederkehrende Fallstricke
**`add_header` wird nicht vererbt.** Sobald ein `location`-Block eine eigene
`add_header`-Direktive enthält, verliert er *alle* geerbten. Sicherheitsheader
gehören deshalb in ein Snippet, das in jeden betroffenen Block eingebunden wird:
```nginx
include /etc/nginx/snippets/element-security.conf;
```
**Zeitüberschreitungen bei Long-Polling.** Matrix-Clients halten `/sync` offen.
Mit dem Standardwert von 60 Sekunden bricht die Verbindung ständig ab:
```nginx
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_buffering off;
```
## Prüfen
```bash
sudo nginx -t
sudo systemctl reload nginx
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/
curl -s -o /dev/null -D - https://element.example.com/ | grep -iE '^x-|^strict'
```
## Firewall
Freizugeben: 80 und 443 TCP, dazu für coturn 3478 und 5349 (je TCP und UDP)
sowie 4915265535 UDP.
```bash
sudo ufw allow 80,443/tcp
sudo ufw allow 3478,5349/tcp
sudo ufw allow 3478,5349/udp
sudo ufw allow 49152:65535/udp
```
+131
View File
@@ -0,0 +1,131 @@
# Wiki.js
Wiki mit PostgreSQL-Backend, betrieben als systemd-Dienst hinter nginx.
| Punkt | Wert |
|---|---|
| Software | Wiki.js 2.x (Node.js) |
| Öffentlich | `https://wiki.example.com` |
| Interner Port | `127.0.0.1:3000` |
| Datenbank | PostgreSQL (`wiki`) |
| Verzeichnis | `/opt/wikijs` |
## 1. Voraussetzungen
```bash
sudo apt-get install -y nodejs npm postgresql
node --version # Wiki.js 2.x braucht Node 18 oder neuer
```
## 2. Datenbank
```bash
sudo -u postgres psql <<'SQL'
CREATE ROLE wikijs LOGIN PASSWORD 'HIER-EIN-STARKES-PASSWORT';
CREATE DATABASE wiki OWNER wikijs;
SQL
```
## 3. Installation
```bash
sudo mkdir -p /opt/wikijs && cd /opt/wikijs
sudo curl -fLO https://github.com/Requarks/wiki/releases/latest/download/wiki-js.tar.gz
sudo tar xzf wiki-js.tar.gz
sudo rm wiki-js.tar.gz
sudo cp config.sample.yml config.yml
```
`config.yml` anpassen:
```yaml
port: 3000
bindIP: 127.0.0.1 # nur lokal; TLS macht nginx
db:
type: postgres
host: localhost
port: 5432
user: wikijs
pass: HIER-DAS-DATENBANK-PASSWORT
db: wiki
```
`bindIP: 127.0.0.1` ist wichtig sonst ist Wiki.js unter Port 3000 direkt aus
dem Internet erreichbar, am Reverse Proxy und dessen TLS vorbei.
## 4. systemd-Unit
Nach `/etc/systemd/system/wikijs.service`:
```ini
[Unit]
Description=Wiki.js
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User=wikijs
Group=wikijs
WorkingDirectory=/opt/wikijs
ExecStart=/usr/bin/node server
Restart=on-failure
RestartSec=10
Environment=NODE_ENV=production
NoNewPrivileges=true
ProtectSystem=full
ReadWritePaths=/opt/wikijs
[Install]
WantedBy=multi-user.target
```
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now wikijs
sudo journalctl -u wikijs -f
```
## 5. Ersteinrichtung
`https://wiki.example.com` öffnen und Administrationskonto anlegen. Danach unter
**Administration → Groups → Guests** prüfen, welche Rechte nicht angemeldete
Besucher haben standardmäßig dürfen sie lesen.
## 6. Sicherung
Der gesamte Inhalt liegt in PostgreSQL:
```bash
sudo -u postgres pg_dump -Fc wiki > wiki-$(date +%F).dump
```
Zusätzlich `config.yml` sichern. Hochgeladene Dateien liegen je nach
Einstellung in der Datenbank oder unter `/opt/wikijs/data`.
## Inhalte exportieren
Für eine Übernahme in ein Git-Repository lassen sich die Seiten direkt aus der
Datenbank holen:
```bash
sudo -u postgres psql -d wiki -t -A -F'|' \
-c "SELECT id, path, title FROM pages ORDER BY id;"
sudo -u postgres psql -d wiki -t -A \
-c "SELECT content FROM pages WHERE id=<id>;" > seite.md
```
Wiki.js kann Inhalte auch fortlaufend nach Git spiegeln
(**Administration → Storage → Git**). Das ist der bessere Weg, wenn die
Dokumentation dauerhaft versioniert werden soll.
## Stolpersteine
| Symptom | Ursache |
|---|---|
| Wiki direkt über Port 3000 erreichbar | `bindIP` nicht auf `127.0.0.1` gesetzt |
| Start bricht ab | Datenbank nicht erreichbar oder falsches Passwort |
| Nach Update fehlen Inhalte | `config.yml` beim Austausch überschrieben |
| Wiki öffentlich lesbar | Rechte der Gruppe „Guests“ nicht eingeschränkt |