VPS einrichten – erste Schritte¶
Bevor Kontext auf einen neuen Server kommt (Installation), wird der Zugang
abgesichert: Anmeldung nur noch mit SSH-Schlüssel, kein Passwort-Login mehr. Die Beispiele
gehen von einem frischen Debian/Ubuntu-VPS aus (etwa bei netcup) mit der IP 203.0.113.10.
Was wo passiert:
| Schritt | Wo |
|---|---|
| 1. Root-Passwort setzen | Kundenbereich des Anbieters |
| 2. Schlüssel auf den Server | eigener Rechner |
3. Kurzname in ~/.ssh/config |
eigener Rechner |
| 4. Passwort-Login abschalten | Server |
| 5. Mail-Sperre aufheben | Kundenbereich des Anbieters |
| 6. Podman installieren | Server |
| 7. DNS bei Cloudflare | Cloudflare und Server |
| 8. Firewall | Server |
| 9. Kontext mit Quadlet starten | Server |
| 10. Stalwart ohne Oberfläche einrichten | Server |
| 11. Backup auf Cloudflare R2 | Cloudflare und Server |
| 12. Mail umstellen | Cloudflare und Server |
| 13. Zugangsdaten sichern | Passwortmanager |
| 14. Automatische Updates | Server |
1. Root-Passwort setzen¶
Ein neuer VPS lässt sich oft noch nicht anmelden: ssh root@… endet mit Permission denied.
Dann ist für root noch kein (oder ein unbekanntes) Passwort gesetzt. Bei netcup im
Server Control Panel unter Server → Zugang ein neues Root-Passwort setzen, danach geht es:
ssh root@203.0.113.10
Klappt es immer noch nicht, zeigt ssh -v root@203.0.113.10 in der Zeile
Authentications that can continue:, was der Server erlaubt. Steht dort nur publickey, ist
der Passwort-Login schon aus. Als Notzugang bleibt die Konsole des Anbieters (bei netcup
Bildschirm); sie ist ein lokaler Login und braucht kein SSH.
2. Schlüssel auf den Server¶
Auf dem eigenen Rechner. Noch keinen Schlüssel? Einen anlegen:
ssh-keygen -t ed25519 -f ~/.ssh/privat_ed25519
Den öffentlichen Teil auf den Server kopieren – ssh-copy-id fragt ein letztes Mal nach dem
Root-Passwort und hängt den Schlüssel an ~/.ssh/authorized_keys auf dem Server an:
ssh-copy-id -i ~/.ssh/privat_ed25519.pub root@203.0.113.10
Ohne ssh-copy-id (Windows, PowerShell):
type $env:USERPROFILE\.ssh\privat_ed25519.pub | ssh root@203.0.113.10 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
Keybund
Mit Keybund geht das auch ohne Terminal: Server anlegen, Schlüssel zuordnen, Add key, dann Test.
3. Kurzname in ~/.ssh/config¶
Wer mehrere Schlüssel hat, gibt den richtigen fest vor – sonst probiert ssh alle durch und der
Server bricht mit Too many authentication failures ab. Auf dem eigenen Rechner in
~/.ssh/config:
Host kunde-vps
HostName 203.0.113.10
User root
IdentityFile ~/.ssh/privat_ed25519
IdentitiesOnly yes
Danach reicht ssh kunde-vps. Prüfen, dass es ohne Passwortabfrage reinkommt, bevor es
weitergeht.
4. Passwort-Login abschalten¶
Auf dem Server. Die angemeldete Sitzung offen lassen, bis eine neue Anmeldung mit Schlüssel geklappt hat.
Eine eigene Datei unter sshd_config.d/ anlegen (eine Zeile, zum Kopieren):
printf 'PasswordAuthentication no\nKbdInteractiveAuthentication no\nPermitRootLogin prohibit-password\n' > /etc/ssh/sshd_config.d/00-keyonly.conf
Das 00- im Namen ist wichtig: Bei sshd gilt der erste gefundene Wert, und viele Images
bringen eine 50-cloud-init.conf mit PasswordAuthentication yes mit, die sonst gewinnen würde.
Prüfen, neu starten und kontrollieren:
sshd -t && systemctl restart ssh && sshd -T | grep -Ei 'passwordauth|permitroot'
Erwartet:
permitrootlogin without-password
passwordauthentication no
Zum Schluss aus einem zweiten Terminal ssh kunde-vps – kommt die Anmeldung durch, ist der
Server nur noch mit Schlüssel erreichbar.
Ausgesperrt?
Über die Konsole des Anbieters mit dem Root-Passwort anmelden,
rm /etc/ssh/sshd_config.d/00-keyonly.conf && systemctl restart ssh, Schritt 2 wiederholen.
5. Mail-Sperre aufheben¶
Ein neuer netcup-VPS darf keine Mails verschicken: Die Firewall im Server Control Panel bringt die
Regelgruppe „netcup Mail block“ mit, die ausgehend die Ports 25, 465 und 587 verwirft.
Kontext braucht alle drei: Stalwart stellt über 25 an andere Mailserver zu, und verbundene
externe Postfächer (Gmail, GMX …) versenden über 465 oder 587 (server/external/smtp.ts).
Im Server Control Panel unter Server → Firewall die Gruppe „netcup Mail block“ löschen und speichern. Eingehend lässt die Firewall dort alles durch; abgeschottet wird auf dem Server selbst.
Prüfen auf dem Server (kann ein paar Minuten dauern, bis die Freigabe greift):
timeout 5 bash -c 'echo > /dev/tcp/gmail-smtp-in.l.google.com/25' && echo offen || echo gesperrt
Den Reverse-DNS (PTR) setzt man bei netcup nicht im Server Control Panel, sondern im
Customer Control Panel unter Produkte → (der Server) → rDNS: bei der IPv4-Adresse als
Hostname den Namen des Mailservers (mail.firma.de), Hostname ändern. Ohne passenden PTR landen
Mails schnell im Spam. IPv6 braucht keinen Eintrag: Die Container haben nur IPv4, Stalwart
verschickt über die IPv4-Adresse. Prüfen: dig +short -x 203.0.113.10.
6. Podman installieren¶
Kontext läuft mit Podman und Quadlet: Jeder Dienst ist eine systemd-Unit, startet mit dem Server
und braucht keinen Daemon. Die Dateien liegen in deploy/quadlet/. Auf dem Server:
apt-get update && apt-get install -y git podman
git clone https://github.com/firsttris/kontext /opt/kontext
Debian 13 bringt Podman 5.4 mit, Quadlet ist dabei. Docker nicht zusätzlich installieren: Es legt
eigene Firewall-Regeln an, die sich mit Podmans Netz in die Quere kommen. War es schon drauf, mit
apt-get purge docker-ce docker-ce-cli containerd.io docker-compose-plugin entfernen und die
übrig gebliebenen Tabellen löschen (nft list tables zeigt sie, nft delete table ip nat usw.).
7. DNS bei Cloudflare¶
Liegt die Domain bei Cloudflare, bekommt der Server einen API-Token: Stalwart holt damit sein
Zertifikat für mail.firma.de (ACME DNS-01, siehe Installation),
und die Einträge lassen sich vom Server aus setzen.
Im Cloudflare-Dashboard unter Profil → API Tokens → Create Token die Vorlage „Edit zone DNS“:
- Zone Resources: nur die eine Zone (
firma.de), nicht alle Zonen des Kontos; - Client IP Address Filtering: die IPv4-Adresse des Servers und sein IPv6-Netz (
…::/64) – dann taugt der Token nirgends sonst etwas; - TTL: kein Ablauf, Stalwart erneuert das Zertifikat alle paar Wochen.
Den Token auf dem Server nur für root lesbar ablegen:
umask 077 && nano /root/.cloudflare-token
Prüfen und die vorhandenen Einträge ansehen:
T=$(cat /root/.cloudflare-token)
curl -s -H "Authorization: Bearer $T" "https://api.cloudflare.com/client/v4/zones?name=firma.de" \
| grep -o '"id":"[a-f0-9]\{32\}"' | head -1 # die Zonen-ID
curl -s -H "Authorization: Bearer $T" "https://api.cloudflare.com/client/v4/zones/<zonen-id>/dns_records"
Läuft die Mail der Domain bisher über Cloudflare Email Routing (MX auf
route1.mx.cloudflare.net …), bleibt das bis zum Schluss so: Erst wenn der Server läuft, wird
Email Routing abgeschaltet und der MX umgestellt.
Für den Server zuerst nur die A-Einträge, alle DNS only (graue Wolke), auf die IP des Servers:
kontext, mail, dokumente, mta-sts und openpgpkey. Die Website und die bisherige Mail
bleiben davon unberührt.
Prüfen (nach ein paar Minuten bei allen Resolvern; der des Anbieters merkt sich ein „gibt es nicht“ von vorher etwas länger):
apt-get install -y bind9-dnsutils
for n in kontext mail dokumente mta-sts openpgpkey; do dig +short $n.firma.de @1.1.1.1; done
Die Einträge vor dem ersten Start anlegen und warten, bis auch der Server selbst sie auflöst
(getent hosts kontext.firma.de): Hat er vorher schon gefragt, merkt sich der Resolver des
Anbieters das „gibt es nicht“ bis zu 30 Minuten (bei Cloudflare), und Schreibzeug findet Kontext
beim Start nicht.
8. Firewall¶
Von außen soll den Server selbst nur SSH erreichen. Die Ports der Container (Web, Mail, Anrufe)
leitet Podman weiter (netavark: DNAT, dann forward); sie laufen nicht durch die Eingangskette
des Servers und brauchen dort keine Regel. Kein ufw: Es verwirft weitergeleitete Pakete und
damit die Container-Ports. Stattdessen nftables, in /etc/nftables.conf:
#!/usr/sbin/nft -f
# Kein "flush ruleset": das würfe auch Podmans Regeln weg.
table inet hostfw
delete table inet hostfw
table inet hostfw {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif lo accept
meta l4proto { icmp, ipv6-icmp } accept
tcp dport 22 accept
# Container fragen Podmans DNS (aardvark) am Gateway der Bridge
iifname "podman*" accept
}
}
nft -c -f /etc/nftables.conf && nft -f /etc/nftables.conf && systemctl enable --now nftables
Die angemeldete SSH-Sitzung offen lassen und aus einem zweiten Terminal prüfen, dass ssh
weiter geht.
9. Kontext mit Quadlet starten¶
Jeder Dienst bekommt nur seine eigenen Einstellungen, in einer Datei unter /opt/kontext/deploy/,
nur für root lesbar (sie stehen in .gitignore). Die Geheimnisse für LiveKit und Schreibzeug als
hex, weil das LiveKit-Geheimnis auch in YAML steht:
cd /opt/kontext/deploy && umask 077
KS=$(openssl rand -base64 48 | tr -d '\n'); LS=$(openssl rand -hex 32); SS=$(openssl rand -hex 32)
printf '%s\n' "BETTER_AUTH_URL=https://kontext.firma.de" "BETTER_AUTH_SECRET=$KS" \
"STALWART_ADMIN_KEY=" "LIVEKIT_API_SECRET=$LS" \
"SCHREIBZEUG_URL=https://dokumente.firma.de" "SCHREIBZEUG_CLIENT_SECRET=$SS" > kontext.env
printf '%s\n' "SCHREIBZEUG_URL=https://dokumente.firma.de" \
"SCHREIBZEUG_EMBED_ORIGIN=https://kontext.firma.de" \
"OIDC_ISSUER=https://kontext.firma.de/api/auth" "OIDC_CLIENT_SECRET=$SS" > schreibzeug.env
echo "LIVEKIT_KEYS=kontext: $LS" > livekit.env
echo "SIP_CONFIG_BODY={api_key: kontext, api_secret: \"$LS\", ws_url: \"ws://livekit:7880\", redis: {address: \"redis:6379\"}, sip_port: 5060, rtp_port: \"20000-20100\", use_external_ip: true, symmetric_rtp: true, hide_inbound_port: true, logging: {level: warn}}" > livekit-sip.env
umask 022
SIP_CONFIG_BODY steht in einer Zeile (YAML in Klammerschreibweise): Eine Env-Datei kennt keine
Werte über mehrere Zeilen.
Die Quadlet-Dateien kopieren und Domain und Let's-Encrypt-Adresse eintragen (Quadlet setzt keine
Variablen ein) – in den .container-Dateien und in den eben geschriebenen kontext.env und
schreibzeug.env, die die Beispiel-Domain auch enthalten. Die Telefonbox (kontext-box.container)
läuft im Büro, nicht hier:
cp quadlet/*.network quadlet/*.container /etc/containers/systemd/
rm /etc/containers/systemd/kontext-box.container
sed -i 's/firma\.de/meinefirma.de/g' kontext.env schreibzeug.env
cd /etc/containers/systemd
sed -i 's/firma\.de/meinefirma.de/g' *.container
sed -i 's/^Environment=ACME_EMAIL=.*/Environment=ACME_EMAIL=admin@meinefirma.de/' caddy.container
grep -h 'firma\.de' *.container /opt/kontext/deploy/*.env # jede Zeile nennt jetzt die eigene Domain
systemctl daemon-reload
Jedes sed nur einmal laufen lassen: meinefirma.de enthält selbst firma.de.
Images laden und starten. Mit dem Server starten sie danach von selbst ([Install] in jeder
Datei):
for i in $(grep -h '^Image=' *.container | cut -d= -f2); do podman pull $i; done
systemctl start stalwart redis livekit livekit-sip kontext schreibzeug caddy
podman ps
Caddy holt die Zertifikate gleich beim Start (podman logs caddy). Findet Schreibzeug Kontext
beim Start nicht (DNS noch nicht da, Kontext startet noch), steht in seinem Log
Discovery fetch failed … Provider skipped und in Kontexts Schreibzeug antwortet nicht: harmlos,
Better Auth fragt beim ersten „Anmelden mit Kontext“ erneut (retryDiscovery in
better-auth/plugins/generic-oauth), ein Neustart ist nicht nötig. Im Log von Stalwart steht das
vorläufige Admin-Passwort (podman logs stalwart). Stalwarts Verwaltung ist nur auf dem Server
selbst erreichbar; vom eigenen Rechner über einen Tunnel, dann http://localhost:8080 im Browser:
ssh -L 8080:127.0.0.1:8080 kunde-vps
Danach weiter mit Stalwart einrichten – in der Oberfläche oder wie unten direkt auf dem Server.
10. Stalwart ohne Oberfläche einrichten¶
Alles aus Stalwart einrichten geht auch über Stalwarts
Verwaltungs-API (JMAP, x:<Objekt>/get|set, die Objekte in
crates/registry/src/schema/structs.rs von Stalwart), auf dem Server gegen 127.0.0.1:8080.
Stalwart schreibt sonst selbst ins DNS
Mit Cloudflare-Token bei der Ersteinrichtung legt Stalwart MX, SPF (mx -all) und DMARC
(p=reject) an und stellt damit die Mail der Domain sofort auf den neuen Server um – auch
wenn sie noch woanders läuft. Darum die Ersteinrichtung ohne DNS (Manual) und danach den
Token nur für das Zertifikat und die DKIM-Schlüssel.
Ein kleiner Helfer (die Zugangsdaten liegen in /root/.stalwart-admin als name:passwort):
cat > /root/stalwart-api.sh <<'SH'
#!/bin/sh
exec curl -s -u "$(cat /root/.stalwart-admin)" http://127.0.0.1:8080/jmap/ -H 'content-type: application/json' \
-d "{\"using\":[\"urn:ietf:params:jmap:core\",\"urn:stalwart:jmap\"],\"methodCalls\":$1}"
SH
chmod 700 /root/stalwart-api.sh
Ersteinrichtung. Mit dem vorläufigen Passwort aus podman logs stalwart (Benutzer admin;
nach jedem Neustart vor der Einrichtung ein neues). Ohne Zertifikat und ohne DNS; die
DKIM-Schlüssel erzeugt Stalwart, das Log geht nach podman logs:
curl -s -u 'admin:<vorläufiges-passwort>' http://127.0.0.1:8080/jmap/ -H 'content-type: application/json' -d '{
"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],
"methodCalls":[["x:Bootstrap/set",{"update":{"singleton":{
"serverHostname":"mail.firma.de","defaultDomain":"firma.de",
"requestTlsCertificate":false,"generateDkimKeys":true,"dnsServer":{"@type":"Manual"},
"tracer":{"@type":"Stdout","ansi":false,"multiline":false,"enable":true,"level":"info",
"lossy":false,"buffered":true,"events":{},"eventsPolicy":"exclude"}}}},"0"]]}'
Die Antwort enthält den festen Admin (admin@firma.de) und sein Passwort: als
admin@firma.de:<passwort> nach /root/.stalwart-admin (chmod 600), dann
systemctl restart stalwart. Erst danach laufen die Mail-Ports.
Zertifikat für mail.firma.de per DNS-01 über Cloudflare (Port 443 hat Caddy). Stalwart
holt es nur mit DNS-Verwaltung; publishRecords braucht mindestens einen Typ, und dkim ist
harmlos (eigene Namen v1-…._domainkey, fremde DKIM-Einträge bleiben). Die Ids aus den Antworten
von x:Domain/get, x:DnsServer/get, x:AcmeProvider/get einsetzen:
/root/stalwart-api.sh '[
["x:DnsServer/set",{"create":{"cf":{"@type":"Cloudflare","secret":{"@type":"Value","secret":"<cloudflare-token>"}}}},"0"],
["x:AcmeProvider/set",{"create":{"le":{"challengeType":"Dns01","contact":{"admin@firma.de":true},
"directory":"https://acme-v02.api.letsencrypt.org/directory"}}},"1"]]'
/root/stalwart-api.sh '[["x:Domain/set",{"update":{"<domain-id>":{
"certificateManagement":{"@type":"Automatic","acmeProviderId":"<acme-id>","subjectAlternativeNames":{"mail.firma.de":true}},
"dnsManagement":{"@type":"Automatic","dnsServerId":"<dns-id>","origin":null,"publishRecords":{"dkim":true}}}}},"0"]]'
Nach ein bis zwei Minuten steht im Log ACME order completed; Stalwart erneuert selbst.
Port 587. Stalwarts Standard-Listener haben 465, aber nicht 587 (STARTTLS), den viele Mailprogramme nehmen:
/root/stalwart-api.sh '[["x:NetworkListener/set",{"create":{"sub":{"name":"submission","protocol":"smtp",
"bind":{"[::]:587":true},"useTls":true,"tlsImplicit":false}}},"0"]]'
Einstellungen aus der Installation (X-Forwarded-For, Kontexts Netz als erlaubte IP, Sperrdauer ein Tag, deutsche Suche, 16 gleichzeitige JMAP-Anfragen, Stalwarts RSVP-Seite aus, Anmeldung auf 587 nur nach STARTTLS):
/root/stalwart-api.sh '[
["x:Http/set",{"update":{"singleton":{"useXForwarded":true}}},"0"],
["x:AllowedIp/set",{"create":{"k":{"address":"10.89.250.0/24","reason":"Kontext"}}},"1"],
["x:Security/set",{"update":{"singleton":{"authBanPeriod":86400000,"scanBanPeriod":86400000,
"abuseBanPeriod":86400000,"loiterBanPeriod":86400000}}},"2"],
["x:Search/set",{"update":{"singleton":{"defaultLanguage":"de-DE"}}},"3"],
["x:Jmap/set",{"update":{"singleton":{"maxConcurrentRequests":16}}},"4"],
["x:CalendarScheduling/set",{"update":{"singleton":{"httpRsvpEnable":false}}},"5"],
["x:MtaStageAuth/set",{"update":{"singleton":{"saslMechanisms":{"match":{"0":{
"if":"local_port != 25 && is_tls","then":"[plain, login, oauthbearer, xoauth2]"}},
"else":"false"}}}},"6"],
["x:Action/set",{"create":{"r":{"@type":"ReloadSettings"}}},"7"]]'
httpRsvpEnable: Stalwarts Einladungsmails haben sonst Knöpfe Yes/No/Maybe aufhttps://mail.firma.de/calendar/rsvp– dort antwortet Caddy, nicht Stalwart, der Klick endet in einem TLS-Fehler. Einladungen verschickt Kontext selbst, mit Knöpfen auf seine eigene Seite (siehe Einladungen); Stalwarts Mails gibt es nur noch, wenn ein Handy oder Mailprogramm über CalDAV einlädt.saslMechanisms: Stalwarts Standard hat eine zweite Regel, die auf 587oauthbearerundxoauth2schon vor STARTTLS anbietet (ein externer Scan meldet das). Ohne sie bietet 587 im Klartext gar keinAUTHan, nach STARTTLS alle vier wie bisher. Prüfen:printf 'EHLO test\r\nQUIT\r\n' | nc mail.firma.de 587 | grep -i authgibt nichts aus.ReloadSettingslädt das ohne Neustart.
Mit useXForwarded erwartet Stalwart den Header bei jeder HTTP-Anfrage; Kontext spricht es
direkt an, ohne ihn, und jede Anfrage schreibt X-Forwarded-For header is missing ins Log
(Stalwart nimmt dann die Adresse der Verbindung, crates/http/src/request.rs). Die Meldung aus
dem Log nehmen (Id aus x:Tracer/get) – wirkt erst nach einem Neustart:
/root/stalwart-api.sh '[["x:Tracer/set",{"update":{"<tracer-id>":{"events":{"http.x-forwarded-missing":true}}}},"0"]]'
systemctl restart stalwart
Firmenverwaltung. Rolle Kontext-Server (erbt User, Id b), das Konto
kontext-server@firma.de mit ihr und einem Zufallspasswort; die Id der neuen Rolle aus der
ersten Antwort beim Konto einsetzen:
PW=$(openssl rand -hex 24); echo "kontext-server@firma.de:$PW" > /root/.stalwart-kontext-server
chmod 600 /root/.stalwart-kontext-server
/root/stalwart-api.sh '[["x:Role/set",{"create":{"ks":{"description":"Kontext-Server","roleIds":{"b":true},
"enabledPermissions":{"sysAccountGet":true,"sysAccountQuery":true,"sysAccountCreate":true,
"sysAccountUpdate":true,"sysRoleGet":true,"sysRoleCreate":true,"sysDomainGet":true,
"sysAuthenticationGet":true,"impersonate":true}}}},"0"]]'
/root/stalwart-api.sh '[["x:Account/set",{"create":{"a":{"@type":"User","name":"kontext-server",
"domainId":"<domain-id>","roles":{"@type":"Custom","roleIds":{"<rollen-id>":true}},
"credentials":{"0":{"@type":"Password","secret":"'"$PW"'"}}}}},"0"]]'
Den API-Schlüssel legt das Konto für sich selbst an; er steht einmal in der Antwort (secret,
beginnt mit API_) und kommt als STALWART_ADMIN_KEY in deploy/kontext.env:
curl -s -u "$(cat /root/.stalwart-kontext-server)" http://127.0.0.1:8080/jmap/ -H 'content-type: application/json' \
-d '{"using":["urn:ietf:params:jmap:core","urn:stalwart:jmap"],"methodCalls":[["x:ApiKey/set",
{"create":{"k":{"description":"Kontext","permissions":{"@type":"Inherit"}}}},"0"]]}'
nano /opt/kontext/deploy/kontext.env && systemctl restart kontext
Das Konto des Chefs. Kontext legt die Rolle Firmenverwaltung (Kontext) beim ersten Aufruf
der Firmenverwaltung an, nur mit diesem Namen (server/company/management.ts); sie lässt sich
genauso vorher anlegen. Das Konto bekommt User (b) und diese Rolle, dazu auf Wunsch einen
Alias (vorname.nachname@, landet im selben Postfach). Danach legt der Chef alle weiteren
Mitarbeiter und gemeinsamen Postfächer in Kontext an: Kontext gibt dabei auch die Kalender als
„Nur Verfügbarkeit“ für die Kollegen frei (newcomerPatches in server/company/availability.ts).
Wer Konten an Kontext vorbei per API anlegt, holt das unter Einstellungen → Firma → Mitarbeiter
→ Verfügbarkeit im Kalender → Für alle einschalten nach.
/root/stalwart-api.sh '[["x:Role/set",{"create":{"r":{"description":"Firmenverwaltung (Kontext)"}}},"0"]]'
/root/stalwart-api.sh '[["x:Account/set",{"create":{"c":{"@type":"User","name":"chef","domainId":"<domain-id>",
"roles":{"@type":"Custom","roleIds":{"b":true,"<firmenrollen-id>":true}},
"aliases":{"0":{"enabled":true,"name":"vorname.nachname","domainId":"<domain-id>"}},
"credentials":{"0":{"@type":"Password","secret":"<startpasswort>"}}}}},"0"]]'
11. Backup auf Cloudflare R2¶
Wie in Backups: ein R2-Bucket mit Standort EU, ein R2-API-Token
(Object Read & Write, nur dieser Bucket, IP-Filter mit IPv4 und IPv6-Netz des Servers).
Die Einstellungen für Quadlet in deploy/backup.env, das Restic-Passwort zufällig:
cd /opt/kontext/deploy && umask 077
printf '%s\n' "KONTEXT_DOMAIN=kontext.firma.de" \
"RESTIC_REPOSITORY=s3:https://<konto-id>.eu.r2.cloudflarestorage.com/<bucket>" \
"RESTIC_PASSWORD=$(openssl rand -base64 36)" \
"AWS_ACCESS_KEY_ID=<access-key-id>" "AWS_SECRET_ACCESS_KEY=<secret-access-key>" > backup.env
apt-get install -y restic
./backup.sh # erste Sicherung, legt das Repository an
./backup-control.sh install # Units für die nächtliche Sicherung
systemctl enable --now kontext-backup.timer kontext-backup-control.path
Wer die Sicherung bisher per Cron startet, nimmt den Eintrag heraus, sobald der Timer läuft:
crontab -l | grep -v 'deploy/backup.sh' | crontab -. Danach zeigt die Firmenverwaltung unter
Datensicherung Ziel, letzten Lauf und Speicher; Uhrzeit und „Jetzt sichern“ gehen von dort.
Das Restic-Passwort (grep RESTIC_PASSWORD backup.env) zusätzlich außerhalb des Servers
ablegen, etwa im Passwortmanager: Ohne es ist das Backup nicht lesbar, wenn der Server weg ist.
12. Mail umstellen¶
Zuletzt kommt die Mail der Domain auf den Server. Vorher die Adressen anlegen, die bisher
ankamen: Bei Cloudflare Email Routing stehen sie unter Routing-Regeln (einzelne Adressen
und ein Catch-all). Einzelne Adressen werden Aliase eines Kontos, ein Catch-all wird zur
catchAllAddress der Domain:
/root/stalwart-api.sh '[
["x:Account/set",{"update":{"<konto-id>":{"aliases":{"0":{"enabled":true,"name":"info","domainId":"<domain-id>"}}}}},"0"],
["x:Domain/set",{"update":{"<domain-id>":{"catchAllAddress":"chef@firma.de"}}},"1"]]'
(aliases ersetzt die Liste: vorhandene Aliase mit angeben.)
Dann in Cloudflare unter Email → Email Routing → Einstellungen ganz unten Email Routing deaktivieren; Cloudflare entfernt dabei seine MX-, SPF- und DKIM-Einträge. Website-Einträge bleiben. Sofort danach die eigenen setzen (Bis dahin gibt es keinen MX; absendende Server versuchen es später erneut):
| Typ | Name | Wert |
|---|---|---|
| MX | firma.de |
10 mail.firma.de |
| TXT | firma.de |
v=spf1 mx -all |
| TXT | mail.firma.de |
v=spf1 a -all |
| TXT | _dmarc.firma.de |
v=DMARC1; p=none; rua=mailto:postmaster@firma.de |
| TXT | _mta-sts.firma.de |
v=STSv1; id=… (aus Stalwart, x:Domain/get → dnsZoneFile) |
| TXT | _smtp._tls.firma.de |
v=TLSRPTv1; rua=mailto:postmaster@firma.de |
Die DKIM-Einträge hat Stalwart schon gesetzt (Schritt 10). Stalwart schlägt DMARC gleich mit
p=reject vor; zum Start p=none und nach ein paar Wochen ohne Fehlberichte quarantine. Die
Einträge für Autoconfig und SRV (autoconfig., _imaps._tcp …), die Stalwart ebenfalls vorschlägt,
zeigen auf mail.firma.de:443 – dort antwortet Caddy, nicht Stalwart: weglassen.
Prüfen, dann eine Mail von außen hin und eine zurück (im Original beim Empfänger: SPF, DKIM und DMARC pass):
dig +short MX firma.de @1.1.1.1
echo QUIT | openssl s_client -starttls smtp -connect mail.firma.de:25 -verify_return_error | grep -E "subject=|Verify"
Sperrlisten
VPS-Anbieter vergeben IP-Adressen neu; die neue kann von einem früheren Mieter auf einer Sperrliste stehen (beim ersten Server: Spamhaus CSS). GMX, WEB.DE, T-Online und viele Firmen-Server lehnen Mails von dort ab, Gmail sortiert sie in den Spam. Vor der Umstellung prüfen (über den Resolver des Anbieters; öffentliche wie 1.1.1.1 beantwortet Spamhaus nicht):
R=10.113.0.203 # die IP rückwärts, für 203.0.113.10
for bl in zen.spamhaus.org b.barracudacentral.org bl.spamcop.net bl.mailspike.net; do
echo "$bl: $(dig +short $R.$bl | head -1)"; done
Leer heißt nicht gelistet. Bei Spamhaus auf check.spamhaus.org die IP prüfen und die Austragung beantragen – erst wenn Reverse-DNS und SPF/DKIM/DMARC stimmen:
- Spamhaus nimmt keine Freemail-Adresse (Gmail, GMX …) an, nur eine der eigenen Domain. Die Bestätigung kommt also erst an, wenn die Mail schon auf dem Server läuft – die Austragung kommt deshalb nach der Umstellung.
- Unter Most recently seen HELO values steht, wie sich die IP zuletzt gemeldet hat. Fremde
Namen vor dem eigenen Start sind der Vormieter. Die Empfehlung, den PTR auf diesen Namen zu
setzen, nicht befolgen: PTR, HELO und A-Eintrag bleiben
mail.firma.de. - Im Formular Immediate removal; als Begründung: neuer Mieter seit
, die gesehenen HELOs gehören dem Vormieter, eigener Mailserver mit PTR/HELO mail.firma.de, SPF, DKIM, DMARC, kein offenes Relay. Art der Mails „Personal and business correspondence“, Menge pro Woche, new IP: Yes. Dann den Link in der Bestätigungsmail; ein Bearbeiter prüft den Fall und meldet sich per Mail.
Lässt sie sich nicht austragen: beim Anbieter eine andere IP erfragen oder ausgehend über einen Versanddienst (Smarthost) senden.
Die ersten Mails eines neuen Servers landen bei Gmail oft im Spam, auch ohne Sperrliste:
beim Empfänger „Kein Spam“, die Domain in den Google Postmaster Tools eintragen. Microsoft
(Outlook, Hotmail, Microsoft 365) führt eine eigene Sperre ohne öffentliche Liste; kommt beim
Senden dorthin eine Absage mit S3140, die IP unter sender.office.com freischalten lassen.
Alle öffentlichen Listen auf einmal: mxtoolbox.com/blacklists.aspx.
Ordner für Berichte. An postmaster@ kommen ab jetzt täglich DMARC- und TLS-Berichte (Google,
Microsoft …), an noreply-dmarc@ die Rückläufer auf Stalwarts eigene DMARC-Berichte an fremde
Domains. Beide Adressen landen über die catchAllAddress im Posteingang des Chefs. In Kontext
in seinem Postfach einen Ordner Berichte anlegen und eine Regel: Empfänger enthält
postmaster@firma.de oder noreply-dmarc@firma.de → in Berichte, auch auf vorhandene
Mails. Echte Rückläufer auf eigene Mails gehen an die eigene Adresse und bleiben im Posteingang.
Ob Gmail die eigenen Mails annimmt, steht im Anhang der Google-Berichte (ZIP mit XML): pro
Absender-IP dkim, spf und result – alles pass heißt, an der Einrichtung liegt ein
Spam-Ordner beim Empfänger nicht, sondern am noch fehlenden Ruf der neuen IP.
Zum Prüfen taugt keine Testmail, die man auf dem Server selbst an Port 25 einwirft: ohne DKIM
stuft Stalwarts Spamfilter sie als Spam ein, und Spam geht vor den Regeln in den Spam-Ordner
(server/rules/script.ts). Die nächsten echten Berichte zeigen, ob die Regel greift.
13. Zugangsdaten sichern¶
Was außerhalb des Servers in einen Passwortmanager gehört – nicht in eine Textdatei:
| Eintrag | Wozu | Auf dem Server |
|---|---|---|
| Restic-Passwort | Ohne es ist das Backup nicht lesbar | deploy/backup.env |
Stalwart-Admin (admin@firma.de) |
Notzugang zu Stalwarts Verwaltung | /root/.stalwart-admin |
| Konto des Chefs | Anmeldung in Kontext und Mailprogrammen (nach dem Ändern das neue) | – |
| SSH-Schlüssel, Zugang zum Anbieter | Server und Panel | – |
deploy/kontext.env, deploy/backup.env (als sichere Notiz) |
Schnellere Wiederherstellung; das Backup sichert sie nicht | deploy/ |
Das vorläufige Passwort aus dem Log von Stalwart (Benutzer admin) gilt nach der Ersteinrichtung
nicht mehr. Nicht nötig, weil jederzeit neu zu erzeugen: das Konto kontext-server@ und sein
API-Schlüssel, der Cloudflare-Token, die R2-Schlüssel. Ansehen auf dem Server, etwa
cat /root/.stalwart-admin; Startpasswörter danach dort löschen.
14. Automatische Updates¶
Debian schließt Sicherheitslücken selbst – wichtig bei einem Server mit offenem Port 25. Täglich, und wenn ein Update (etwa der Kernel) es braucht, ein Neustart um 4:30 Uhr, nach dem Backup um 3:00 Uhr; die Dienste starten danach von selbst (Quadlet):
apt-get install -y unattended-upgrades
printf '%s\n' 'APT::Periodic::Update-Package-Lists "1";' 'APT::Periodic::Unattended-Upgrade "1";' \
> /etc/apt/apt.conf.d/20auto-upgrades
printf '%s\n' 'Unattended-Upgrade::Automatic-Reboot "true";' \
'Unattended-Upgrade::Automatic-Reboot-Time "04:30";' \
'Unattended-Upgrade::Remove-Unused-Dependencies "true";' > /etc/apt/apt.conf.d/52kontext-unattended
unattended-upgrade --dry-run -v | grep -i "allowed origins"
Das betrifft nur Debian (Kernel, SSH, Podman …), nicht die Container: Deren Versionen stehen fest in den Quadlet-Dateien und ändern sich nur von Hand, siehe Updates.