Eine Lesezeichen-Verwaltung zum Selbsthosten in reinem PHP, installiert mit einer einzigen Datei. Keine Datenbank, keine Abhängigkeiten, kein Build-Schritt, keine Abrufe bei fremden Servern. Die Lesezeichen jedes Benutzers liegen mit AES-256-GCM verschlüsselt auf dem Server, und der Schlüssel dazu ist mit dem Passwort dieses Benutzers geschützt.
linkvault-install.php hochladen, im Browser öffnen, erstes Konto anlegen. Das ist die ganze Installation.
English version: README.en.md
Beispieldaten, erfunden für den Screenshot.
- Lesezeichen mit verschachtelten Ordnern, Schlagwörtern, Favoriten, Notizen und Volltextsuche
- Import aus Lesezeichen-Dateien der Browser (HTML, auch sehr große, samt Notizen aus Firefox), Export als HTML oder JSON
- Drei Ansichten: Karten, Liste und Kacheln. Die Adresse eines Links ist in jeder davon auf einen Blick zu sehen, auch am Handy.
- Papierkorb mit 30 Tagen Aufbewahrung, Duplikat-Suche, Prüfung auf tote Links
- Automatisch sortieren (Werkzeuge → Automatisch sortieren): schlägt Tags und Ordner vor, ganz ohne KI und ohne fremde Dienste. Grundlage sind Gruppen mit Domains und Stichwörtern (YouTube → Video, Chefkoch → Kochen, „xxx“ → NSFW, …) und deine bisherige Sortierung. Die Gruppen sind eine einzige Liste, die du komplett ändern, ergänzen und löschen kannst, „Standard wiederherstellen“ holt die Originalfassung zurück. Erst kommt eine Vorschau zum Abhaken, vorher wird ein Backup angelegt, danach gibt es „Rückgängig“.
- Tag-Leiste unter den Ordnern: Antippen filtert nach dem Tag, mehrere lassen sich kombinieren (abschaltbar unter Ansicht)
- Tags verwalten und Ordner verwalten: umbenennen, zusammenlegen, löschen. Beim Löschen bleiben die Links immer erhalten, danach gibt es „Rückgängig“.
- Automatische Tagesstände und ein Stand vor jeder größeren Aktion
- Verschlüsseltes Komplett-Backup als einzelne Datei zum Herunterladen (
.lvbak) - Mehrere Benutzer, jeder mit eigenem Schlüssel und eigenem, getrenntem Bereich
- Zwei-Faktor-Anmeldung (TOTP), Wiederherstellungscode, Anmelde-Protokoll
- Als App installierbar (PWA), läuft ohne CDN (Schrift und CSS sind eingebettet)
- Heller und dunkler Modus mit je drei Farbvarianten (Violett, Blau, Grün), auf Wunsch automatisch nach Geräte-Einstellung
- Oberfläche auf Deutsch und Englisch, umschaltbar unter Werkzeuge → Design & Sprache (jeder Benutzer hat seine eigenen Einstellungen, sie werden an seinem Konto gespeichert und gelten auf jedem Gerät)
- Am Desktop komplett mit der Tastatur bedienbar,
Hzeigt alle Tastenkürzel
Mit H blendet LinkVault eine Übersicht ein, sie verschwindet nach 10 Sekunden von selbst.
| Taste | Aktion |
|---|---|
S |
Suchen (auch / und Strg+K) |
N |
Neuen Link anlegen |
V |
Ansicht wechseln |
H |
Hilfe ein- und ausblenden |
Esc |
Schließen, Suche leeren |
↑ ↓ ← → |
Eintrag wechseln |
Leertaste |
Seite des gewählten Eintrags öffnen |
F / C / E / Entf |
Favorit, URL kopieren, Bearbeiten, Löschen (Eintrag unter der Maus oder im Rahmen) |
In der Mehrfachauswahl markiert die Leertaste den Eintrag, Umschalt + Pfeiltaste markiert beim Wandern, A markiert alle angezeigten Einträge und Entf löscht die markierten.
- PHP 7.4 oder neuer (empfohlen 8.x) mit der Erweiterung
openssl - Ein Webserver mit PHP (Apache läuft ohne Zutun, Nginx braucht eine zusätzliche Regel, siehe unten)
- HTTPS. Ohne HTTPS geht dein Passwort im Klartext durchs Netz, und alles Weitere hier ist hinfällig.
- Optional:
sodium(verschlossenes Anmelde-Protokoll),curl(Titel abrufen, Links prüfen),dom(Lesezeichen-Import),mbstring,intl
- Aus dem neuesten Release die Datei
linkvault-install.phpherunterladen und in ein Verzeichnis auf deinem Webserver kopieren. - PHP muss in dieses Verzeichnis schreiben dürfen (es legt dort
index.php, den Ordnerapp/und späterdata/an). linkvault-install.phpsofort im Browser aufrufen und auf „Jetzt installieren“ klicken. Die Datei packt das Programm aus, löscht sich selbst und leitet weiter. Dort das erste Konto anlegen. Dieses Konto ist der Administrator.- Prüfen, dass
data/von außen nicht erreichbar ist (siehe „Den Datenordner schützen“). - Unter Werkzeuge → Konto & Sicherheit einen Wiederherstellungscode anlegen und die Zwei-Faktor-Anmeldung einschalten.
Wer ein frisch hochgeladenes, noch nicht eingerichtetes LinkVault als Erster aufruft, wird Administrator. Schritt 3 also direkt nach dem Hochladen erledigen.
Lieber von Hand? Im Release liegt auch linkvault-<Version>.zip. Entpacken und den Inhalt des Ordners linkvault/ hochladen, also index.php und den Ordner app/. Wer das Repository klont, braucht genau diese beiden.
index.php Einstieg und Einstellungen
app/lib/ Funktionen (Speicher, Konten, Verschlüsselung, Anmeldung, Abrufe, Import, Oberfläche)
app/routes/ Schrift, Icon, Manifest, Favicons und alle JSON-Aktionen
app/views/ Anmeldeseite und Hauptseite (HTML, CSS, JavaScript)
data/ entsteht beim ersten Start, hier liegt alles Geheime
Die Dateien in app/ tun nichts, wenn man sie direkt aufruft, alles läuft über index.php. Unter Apache sperrt eine .htaccess den Ordner zusätzlich.
LinkVault legt .htaccess-Dateien an, die jeden Zugriff auf data/ sperren. Das wirkt unter Apache, wenn AllowOverride eingeschaltet ist. Nginx beachtet .htaccess nicht, dort gehört dieser Block in die server { }-Sektion (derselbe Text liegt auch in data/nginx-deny.conf):
location ^~ /linkvault/data/ {
deny all;
return 404;
}Der Pfad muss zu dem Ordner passen, in dem LinkVault liegt. Im Beispiel ist das /linkvault/. Liegt es direkt im Hauptverzeichnis der Domain, heißt die Zeile location ^~ /data/ {. Stimmt der Pfad nicht, greift die Regel nicht und data/ ist offen. LinkVault schreibt den passenden Pfad selbst in data/nginx-deny.conf.
Bitte testen. Hier darf keine 200 herauskommen:
curl -sS -o /dev/null -w '%{http_code}\n' https://deine-domain.example/linkvault/data/lv_auth.jsonLinkVault prüft das auch selbst: Ist data/ von außen abrufbar, bekommt der Administrator nach dem Anmelden einen deutlichen Hinweis, und zwar so lange, bis der Ordner gesperrt ist.
Alles in data/ ist verschlüsselt oder gehasht, ein Leck wäre also nicht sofort eine Katastrophe. Ein Angreifer hätte damit aber das Material, um Passwörter in Ruhe offline durchzuprobieren, und genau das soll er nicht bekommen.
- Das erste Konto ist der Administrator.
- Alle anderen können auf der Anmeldeseite Zugang anfragen und suchen sich dabei Namen und Passwort selbst aus. Das Konto wird sofort mit eigenem Schlüssel angelegt, bleibt aber zu, bis der Administrator es freigibt.
- Der Administrator kann Anfragen freigeben oder ablehnen, Benutzer sperren, entsperren und löschen und neue Anfragen ganz abschalten.
- Der Administrator kann die Lesezeichen anderer nicht lesen und ihre Passwörter nicht zurücksetzen. Es gibt keinen Generalschlüssel. Wer Passwort und Wiederherstellungscode verliert, hat seine Daten verloren. Das ist Absicht.
- Grenzen gegen Missbrauch: höchstens 3 Anfragen pro Anschluss und Tag, 20 wartende Anfragen, 50 Benutzer insgesamt.
Passwort ──PBKDF2-SHA256, 600.000 Runden, zufälliges Salt (16 Byte)──▶ Schlüssel-Schlüssel (KEK)
│
zufälliger 256-Bit-Datenschlüssel (DEK) ◀──mit AES-256-GCM ausgepackt──────┘
│
└──AES-256-GCM, bei jedem Speichern neue zufällige 96-Bit-Nonce──▶ Lesezeichen, Backups, TOTP-Geheimnis
- Jeder Benutzer bekommt beim Anlegen einen eigenen zufälligen 256-Bit-Datenschlüssel aus PHPs sicherem Zufallsgenerator (
random_bytes). - Der Datenschlüssel liegt nie im Klartext auf der Platte. Gespeichert ist er nur verpackt mit einem Schlüssel, der aus dem Passwort abgeleitet wird.
- Beim Passwortwechsel wird derselbe Datenschlüssel neu verpackt. Die Lesezeichen müssen dafür nicht neu verschlüsselt werden.
- AES-GCM verschlüsselt und prüft zugleich die Echtheit. Manipulierte Dateien lassen sich nicht entschlüsseln, statt Datenmüll zu liefern.
- Fehlt der Schlüssel, speichert LinkVault lieber gar nicht als unverschlüsselt.
| Datei | Inhalt | Ohne Passwort lesbar? |
|---|---|---|
data/bookmarks.json, data/users/<id>/bookmarks.json |
Lesezeichen | Nein (AES-256-GCM) |
…/backups/*.json |
ältere Stände davon | Nein (AES-256-GCM) |
data/lv_auth.json, …/auth.json |
Benutzername, Passwort-Hash (password_hash, bcrypt), Salt, verpackter Datenschlüssel, verschlüsseltes TOTP-Geheimnis, Einstellungen |
Benutzername und Einstellungen ja, alles Geheime nein |
data/autosort.json, …/autosort.json |
deine Fassung der Gruppen fürs automatische Sortieren | Nein (AES-256-GCM) |
data/ui.json, …/ui.json |
Einstellungen der Oberfläche je Benutzer: Design, Sprache, Ansicht, Sortierung | Ja (nichts Geheimes) |
…/state.json |
Stand des Kontos (wartet / aktiv / gesperrt), Zeitpunkte, verschlossene Nachricht der Anfrage | Stand und Zeitpunkte ja |
…/attempts.log |
Anmelde-Protokoll: Zeit, IP-Adresse, versuchter Name | Nein, sofern sodium vorhanden ist (siehe unten) |
…/favicon_cache/ |
Icons der Seiten | Nein (AES-256-GCM). Die Dateinamen sind mit dem Schlüssel gebildete Prüfsummen (HMAC) und verraten die Domain nicht. |
data/throttle/ |
Zähler für Fehlversuche und Merker für bekannte Anschlüsse, Dateinamen sind SHA-256-Prüfsummen der Adresse | Zähler ja, Adressen nein |
data/sessions/ |
PHP-Sitzungen | Der Datenschlüssel darin ist verschlüsselt (siehe unten) |
Mehr gibt es nicht. Keine Datenbank und keine Protokolldatei, in der deine Adressen oder die gespeicherten Domains stehen.
Nach der Anmeldung muss der Datenschlüssel bei jeder Anfrage verfügbar sein. Er liegt dafür nicht im Klartext in der Sitzung:
- Es wird ein zufälliger 256-Bit-Sitzungsschlüssel erzeugt und als Cookie an den Browser geschickt (
HttpOnly,SameSite=Lax, bei HTTPSSecure). - In der Sitzungsdatei auf dem Server steht der Datenschlüssel nur verschlüsselt mit diesem Cookie-Schlüssel.
- Wer die Sitzungsdateien vom Server kopiert, kann damit nichts anfangen. Es braucht beide Hälften.
- Die Sitzungen liegen in einem eigenen Ordner von LinkVault, nicht im gemeinsamen
/tmpdes Hosters. - Bei der Anmeldung gibt es eine neue Sitzungskennung. Die automatische Abmeldung ist einstellbar (15 Minuten bis 30 Tage, Standard 8 Stunden).
- Ein Passwortwechsel meldet alle anderen Geräte ab.
- Passwörter: mindestens 12 Zeichen mit Buchstaben und Zahlen. Gespeichert wird nur ein Hash aus
password_hash(). - Bremse pro Adresse mit wachsenden Sperrzeiten (3 s, 10 s, 30 s, 60 s … bis 15 Minuten). IPv6-Adressen werden als /64-Netz zusammengefasst. Vertraut wird nur
REMOTE_ADDR, nieX-Forwarded-For. - Eine Gesamtbremse verlangsamt kurz alle, wenn sich Fehlversuche von vielen Adressen häufen. Anschlüsse, von denen sich in den letzten 30 Tagen jemand richtig angemeldet hat, sind davon ausgenommen. So kann dich niemand mit einer Flut von Fehlversuchen aussperren.
- Unbekannte Benutzernamen kosten dieselbe Zeit wie falsche Passwörter. Warum eine Anmeldung abgelehnt wird (wartet, gesperrt), erfährt man erst nach dem richtigen Passwort.
- Zwei-Faktor-Anmeldung (TOTP, RFC 6238): läuft mit Aegis, 2FAS, Google Authenticator und ähnlichen. Das TOTP-Geheimnis ist mit dem Datenschlüssel verschlüsselt gespeichert. Jeder Code gilt nur einmal.
- Wiederherstellungscode: 24 zufällige Zeichen (120 Bit). Er verpackt eine zweite Kopie des Datenschlüssels. Wer ihn einsetzt, vergibt ein neues Passwort, der Code ist danach verbraucht und TOTP wird abgeschaltet (meist braucht man ihn, weil das Handy weg ist).
- Heikle Aktionen (Passwort ändern, TOTP ein/aus, neuer Wiederherstellungscode, Benutzer löschen) fragen noch einmal nach dem aktuellen Passwort. Nach fünf falschen Eingaben ist die Sitzung beendet.
- Anmelde-Protokoll: jeder Versuch wird mit Zeit, Adresse und versuchtem Namen notiert und mit einem öffentlichen Schlüssel verschlossen (
sodium_crypto_box_seal). Der Server kann Einträge schreiben, lesen kann sie nur der angemeldete Besitzer. Nach der Anmeldung siehst du, was seit dem letzten Mal passiert ist.
- Strenge Content Security Policy:
default-src 'none', Skripte nur mit dem Einmal-Kennzeichen (Nonce) der jeweiligen Seite, keinerlei fremde Quellen,frame-ancestors 'none',form-action 'self' X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer,Cross-Origin-Opener-PolicyundCross-Origin-Resource-Policy: same-origin, HSTS bei HTTPS,X-Robots-Tag: noindex- CSRF-Token bei jeder ändernden Anfrage, verglichen in konstanter Zeit. Alle Aktionen der Schnittstelle gehen nur per POST.
- Angenommen werden nur Links mit
http://undhttps://.javascript:,data:und Ähnliches wird abgelehnt, auch beim Import.
- Dein Browser ruft nichts bei Dritten ab. Schrift (Inter) und CSS stecken in der Datei. Kein CDN, keine Statistik, keine Google Fonts, kein fremder Favicon-Dienst.
- Favicons und Seitentitel holt dein Server direkt bei der gespeicherten Seite, nicht über Google oder DuckDuckGo.
- Diese Abrufe nach draußen sind gegen Umwege ins interne Netz (SSRF) geschützt: erlaubt sind nur öffentliche IP-Adressen, die Verbindung wird auf die geprüfte Adresse festgenagelt, jede Weiterleitung wird neu geprüft und TLS-Zertifikate werden kontrolliert.
- Der einzige Link nach draußen in der Oberfläche ist ein optionales „im Internet Archive öffnen“ bei toten Lesezeichen. Er tut nichts, solange du ihn nicht anklickst.
Die ehrlichen Grenzen, damit du selbst entscheiden kannst:
- Das ist Verschlüsselung der gespeicherten Daten auf dem Server, keine Ende-zu-Ende-Verschlüsselung. Dein Passwort geht bei der Anmeldung (über HTTPS) an den Server, und während eine Anfrage läuft, liegt der Datenschlüssel im Arbeitsspeicher des Servers. Wer den Server kontrolliert, während du ihn benutzt (ein böswilliger Hoster, ein Angreifer, der
index.phpverändern kann), kann Passwort oder Daten abgreifen. Wogegen der Aufbau schützt: gestohlene oder durchgesickerte Dateien, gestohlene Server-Backups, ein falsch eingestellter Webserver, derdata/ausliefert, neugierige Mitbenutzer oder Admins, die auf der Platte stöbern. - Ein schwaches Passwort bleibt ein schwaches Passwort. Wer an
auth.jsonkommt, kann Passwörter offline durchprobieren. PBKDF2 mit 600.000 Runden und bcrypt machen das langsam, nicht unmöglich. Nimm ein langes, einmaliges Passwort, am besten aus einem Passwort-Manager. - TOTP schützt die Anmeldung, nicht die Dateien. Es hält jemanden auf, der dein Passwort kennt und sich über die Webseite anmelden will. Es hilft nicht gegen jemanden, der dein Passwort und eine Kopie der Dateien hat, denn dann lässt sich der Datenschlüssel direkt auspacken.
- Rahmendaten sind auf dem Server sichtbar: Benutzernamen, Zahl der Benutzer, Stand der Konten, Dateigrößen und Änderungszeiten.
- Die gespeicherten Seiten sehen die IP-Adresse deines Servers, wenn LinkVault Titel oder Icon holt oder einen Link prüft.
- Der Administrator kann Konten samt Daten löschen, und wer Dateizugriff auf den Server hat, kann das auch. Heb dir ein eigenes
.lvbak-Backup auf. - Kein Passwort, keine Daten. Es gibt kein Zurücksetzen per E-Mail und keine Hintertür. Leg den Wiederherstellungscode an und verwahre ihn sicher.
- Das ist ein Hobbyprojekt. Es wurde nicht von einer unabhängigen Sicherheitsfirma geprüft. Der Code ist lesbares PHP ohne Abhängigkeiten, schau also gern selbst nach.
- Automatisch: ein Stand pro Tag (14 bleiben) und einer vor jeder größeren Aktion (10 bleiben), pro Benutzer, verschlüsselt wie die laufenden Daten.
- Von Hand: Werkzeuge → Backups lädt eine
.lvbak-Datei herunter, mit den verschlüsselten Daten und dem verpackten Schlüssel. Ohne das Passwort, das beim Herunterladen galt, ist sie wertlos. Einspielen lässt sie sich in jedes LinkVault. - Für eine vollständige Kopie der Installation den ganzen Ordner
data/sichern.
Ein paar Konstanten ganz oben in index.php:
| Konstante | Standard | Bedeutung |
|---|---|---|
LV_KDF_ITERS |
600000 |
PBKDF2-Runden. Wer den Wert erhöht, hebt vorhandene Schlüssel bei der nächsten Anmeldung auf den neuen Stand. |
LV_IDLE_DEFAULT |
480 |
Minuten ohne Aktivität bis zur automatischen Abmeldung (jeder Benutzer kann das in der Oberfläche ändern) |
LV_SESSION_MAX_DAYS |
30 |
Längste Laufzeit einer Sitzung |
LV_MAX_PENDING / LV_MAX_USERS |
20 / 50 |
Grenzen für Zugangsanfragen und Benutzer |
BACKUP_KEEP_DAILY / BACKUP_KEEP_PRE |
14 / 10 |
So viele Stände bleiben erhalten |
LV_TRASH_DAYS |
30 |
So lange bleiben gelöschte Lesezeichen im Papierkorb |
LV_TAILWIND_CDN |
false |
Nur zum Basteln. true lädt Tailwind von einem CDN und lockert die CSP. Aus lassen. |
LV_AUTH_ENABLED |
true |
Auf einem erreichbaren Server niemals auf false stellen. Das schaltet Anmeldung und Verschlüsselung ab. |
Die neue linkvault-install.php hochladen und aufrufen. Sie erkennt die vorhandene Installation, ersetzt nur index.php und app/ und löscht sich wieder. data/ bleibt, wie es ist. Ältere Datenformate werden bei der nächsten Anmeldung von selbst umgestellt.
Von Hand geht es genauso: index.php und den Ordner app/ durch die neue Fassung ersetzen.
Von 3.5.0 oder älter: Bis dahin bestand LinkVault nur aus der index.php. Seitdem gehört der Ordner app/ dazu. Wer nur die index.php austauscht, bekommt einen Hinweis, dass app/ fehlt. Eigene Werte in den Konstanten oben in der index.php danach wieder eintragen.
Nur für Leute, die LinkVault selbst weitergeben wollen, zum Benutzen braucht das niemand:
php tools/build-release.phpDas legt dist/linkvault-install.php und dist/linkvault-<Version>.zip an. Im Repository erledigt das ein Workflow: unter Actions → Release → Run workflow per Knopf, oder von selbst, sobald ein Tag wie LinkVault-3.6.0 gepusht wird.
Der Code enthält keine Zugangsdaten, Schlüssel oder persönlichen Angaben. Alles Geheime entsteht beim ersten Start und liegt in data/. Die .gitignore in diesem Repository schließt den Ordner aus. Das soll so bleiben.
Bitte ein Issue aufmachen. Lässt sich das Problem ausnutzen, beschreib es zuerst ohne fertigen Angriff.
GNU Affero General Public License v3.0, siehe LICENSE. Du darfst LinkVault frei benutzen, untersuchen, verändern und weitergeben, privat wie geschäftlich. Wer es weitergibt oder verkauft, verändert oder unverändert, muss das unter derselben Lizenz tun und den Quelltext mitliefern. Das gilt auch für alle, die eine veränderte Fassung anderen als Dienst im Netz anbieten: auch sie müssen ihren Quelltext offenlegen.
Schrift: Inter von Rasmus Andersson, SIL Open Font License 1.1, als Auswahl eingebettet. CSS-Bausteine erzeugt mit Tailwind CSS (MIT-Lizenz), eingebettet.

