Sicherheit
Diese Seite beschreibt Quacks Sicherheitslage durchgängig: was der Server offenlegt, was lokal bleibt, die Rolle eines TLS-terminierenden Reverse-Proxys und die Authentifizierungs- / Autorisierungs-Callbacks, die der Server bei jeder Verbindung und jeder Abfrage ausführt.
Expositionsmodell
Ein Quack-Server legt die gesamte SQL-Oberfläche der zugrunde liegenden DuckDB-Instanz offen, einschließlich Lese- und Schreibzugriff auf jede Tabelle, die die Sitzung des Servers sehen kann. Deshalb liefert die Erweiterung konservative Standardwerte, die versehentliche Exposition verhindern:
- Der Server erzeugt beim Start ein zufälliges Authentifizierungstoken, das der Client bei jeder Verbindung mitliefern muss.
- Der Server bindet nur an
localhost; nicht-lokale Hostnamen erfordern ein explizitesallow_other_hostname => true. - Der Server nutzt kein TLS selbst. TLS nur für Localhost-Kommunikation würde Abhängigkeiten ohne echten Nutzen einführen.
Bestpractice Setzen Sie Quack bei jedem Deployment jenseits von rein lokal nicht direkt ins Internet. Wir empfehlen, einen bewährten HTTP-Reverse-Proxy davorzustellen und den Proxy TLS terminieren zu lassen.
Der Quack-Client ist mit diesen Annahmen gebaut: Für nicht-lokale URIs geht er standardmäßig von HTTPS aus, sodass ein korrekt vorgeschalteter Server auch von der Clientseite „einfach funktioniert“. Siehe Quack mit einem Reverse-Proxy absichern für nginx- und Caddy-Rezepte (Produktion und lokaler Test).
Authentifizierung und Autorisierung
Bei jedem Datenbankaufruf sind zwei getrennte Entscheidungen zu treffen:
- Authentifizierung: Wer ist der Aufrufer? Stellt die Identität fest, üblicherweise indem der Aufrufer ein Credential liefert (z. B. ein Token, ein Passwort, ein Clientzertifikat).
- Autorisierung: Darf er das tun? Stellt fest, ob ein bereits authentifizierter Aufrufer eine bestimmte Abfrage gegen eine bestimmte Menge von Objekten ausführen darf.
Quack führt das als zwei getrennte Hooks aus: die Authentifizierung, wenn ein Client sich erstmals verbindet, und die Autorisierung vor jeder Abfrage, die der Client ausführen möchte.
Standardkonfiguration
Beide Hooks werden mit eingebauten Standardwerten ausgeliefert, die für die lokale Entwicklung und Einzelbenutzer-Deployments geeignet sind. Jeder ist als überschreibbarer Callback für Deployments mit strengeren Anforderungen verfügbar.
Von Haus aus gilt folgende Konfiguration:
- Authentifizierung ist tokenbasiert. Wenn Sie
quack_serveaufrufen, erzeugt der Server ein zufälliges Token und gibt es in der Spalteauth_tokenzurück (oder Sie übergeben eines explizit überquack_serve(uri, token := '...')). Clients müssen dieses Token bei jeder Verbindung vorweisen, entweder über ein auf die Server-URI beschränktesquack-Secret oder über die explizite OptionTOKENin derATTACH-Anweisung / dem Funktionsaufrufquack_query. Der Standard-Authentifizierungs-Callback vergleicht das vom Client gelieferte Token mit dem gespeicherten Token des Servers. - Autorisierung ist permissiv. Der Standard-Autorisierungs-Callback gibt für jede Abfrage
truezurück. Es findet keine weitere Filterung statt.
Beide Callbacks können durch benutzerdefinierten Code ersetzt werden, einschließlich einfacher SQL-Makros. Siehe die Beispiele unten.
Der Callback-Vertrag
Zwei Einstellungen halten den Namen der Funktion, die als Hook für Authentifizierung / Autorisierung aufgerufen wird:
| Einstellung | Standard | Aufgerufen wenn |
|---|---|---|
quack_authentication_function |
quack_check_token |
Ein neuer Client verbindet sich (CONNECTION_REQUEST). |
quack_authorization_function |
quack_nop_authorization |
Ein Client stellt eine Abfrage (PREPARE_REQUEST). |
Beide Aufrufe müssen ein BOOLEAN zurückgeben: true lässt die Anfrage zu, alles andere (einschließlich eines Query-Fehlers) weist sie mit Authentication failed / Authorization failed zurück.
Alles, was sich als Funktion mit passender Stelligkeit und Rückgabetyp BOOLEAN auflösen lässt, funktioniert: eingebaute Skalarfunktionen, von einer anderen Erweiterung registrierte skalare UDFs oder SQL-Makros.
Authentifizierung erwartet (VARCHAR, VARCHAR, VARCHAR), Autorisierung (VARCHAR, VARCHAR).
Die Callbacks laufen in einer frischen, transienten serverseitigen Verbindung. Sie können also Tabellen lesen, andere UDFs aufrufen und auf Erweiterungen zugreifen, aber jeder Aufruf startet eine neue Sitzung und kann sich nicht auf sitzungslokalen Zustand stützen.
Authentifizierungs-Hook
Der Server ruft die Authentifizierungsfunktion auf, indem er bei jedem CONNECTION_REQUEST die folgende SQL-Anweisung ausführt:
SELECT ⟨quack_authentication_function⟩(⟨session_id⟩, ⟨client_token⟩, ⟨server_token⟩);Die Argumente sind wie folgt definiert:
session_id: Vom Server erzeugte Sitzungs-ID (zufällige 32-Zeichen-Zeichenkette). Wird zurquack_connection_idfür diesen Client.client_token: Das Token, das der Client gesendet hat.server_token: Das auf dem Server konfigurierte Token (überquack_serve(token := ...)oder automatisch erzeugt).
Authentifizierung überschreiben
Der sauberste Weg, eigene Authentifizierung einzubinden, ist ein MACRO.
Beispiel: Multi-Token-Tabelle
Gegen eine kleine Tabelle erlaubter Tokens authentifizieren (z. B. eines pro Benutzer):
CREATE TABLE quack_tokens (auth_token VARCHAR, user_name VARCHAR);INSERT INTO quack_tokens VALUES ('alice-key-123', 'alice'), ('bob-key-456', 'bob');
CREATE MACRO check_token(sid, client_token, server_token) AS ( EXISTS (SELECT 1 FROM quack_tokens WHERE auth_token = client_token));
SET GLOBAL quack_authentication_function = 'check_token';Nun wird jeder Client zugelassen, dessen Token in quack_tokens steht, alle anderen werden abgewiesen. Benutzer hinzuzufügen / zu entfernen ist eine reguläre Operation INSERT / DELETE.
Beispiel: Entwicklermodus (immer erlauben)
Wenn Sie lokal in einer sandboxierten Umgebung entwickeln, können Sie die Authentifizierung im „Entwicklermodus“ nutzen, die jede eingehende Verbindung erlaubt:
CREATE MACRO developer_mode_auth(sid, client_token, server_token) AS true;SET GLOBAL quack_authentication_function = 'developer_mode_auth';Autorisierungs-Hook
Der Server ruft die Autorisierungsfunktion auf, indem er bei jedem PREPARE_RESPONSE die folgende SQL-Anweisung ausführt:
SELECT ⟨quack_authorization_function⟩(⟨connection_id⟩, ⟨query⟩);Die Argumente sind wie folgt definiert:
connection_id: Diequack_connection_iddes aufrufenden Clients (also dieselbe ID, die der Authentifizierungs-Hook als Argumentsession_idgesehen hat).query: Der vollständige SQL-Text, den der Client ausführen möchte.
Autorisierung überschreiben
Autorisierung läuft einmal pro PREPARE_REQUEST, mit der Verbindungs-ID und dem vollständigen SQL-Text. Übliche Formen:
Beispiel: Nur-Lesen
CREATE MACRO read_only(sid, query) AS regexp_matches(upper(trim(query)), '^(SELECT|FROM|WITH|EXPLAIN|DESCRIBE|SHOW)\b');
SET GLOBAL quack_authorization_function = 'read_only';Für aufwändigere Autorisierungsfunktionen siehe den Abschnitt Jenseits von SQL-Makros.
Jenseits von SQL-Makros
SQL-Makros decken die meisten Authentifizierungs- und Autorisierungsfälle ab, der Rumpf eines Makros ist aber auf einen einzelnen Ausdruck beschränkt und kann DML nicht direkt ausführen: In einem Makro gibt es kein INSERT, UPDATE oder DELETE.
Für Richtlinien, die jeden Aufruf in einer Tabelle protokollieren, Zustand in-process über Aufrufe hinweg halten oder anderweitig imperative Logik steuern müssen, registrieren Sie stattdessen eine Skalarfunktion über eine DuckDB-Erweiterung.
DuckDB-Erweiterungen können in C++ (der Hauptsprache) oder in jeder Sprache mit Bindings zur C-Erweiterungs-API von DuckDB geschrieben werden, einschließlich Rust, C und Go. Die registrierte Authentifizierungs- oder Autorisierungsfunktion muss dieselbe Signatur (VARCHAR, ...) → BOOLEAN wie die SQL-Makros oben bereitstellen. Sobald die Erweiterung geladen ist, richten Sie quack_authentication_function oder quack_authorization_function auf den Funktionsnamen.
Python-UDFs, die über
con.create_functionregistriert werden, gelten nur für die Verbindung, die sie erzeugt hat. Quack ruft jeden Callback auf einer frischen serverseitigen Verbindung auf, daher sind Python-UDFs zum Dispatch-Zeitpunkt nicht sichtbar und können nicht als Authentifizierungs- oder Autorisierungs-Callbacks verwendet werden. Registrieren Sie die Funktion über eine DuckDB-Erweiterung, damit sie global sichtbar ist.
Beispiel: Nur-Lese-Abfragende
Ein in sich geschlossenes Beispiel: ein Server, der Tokens pro Benutzer verlangt und jeden Benutzer auf Nur-Lese-Abfragen beschränkt.
CREATE TABLE quack_tokens (auth_token VARCHAR, user_name VARCHAR);INSERT INTO quack_tokens VALUES ('analytics-team-token', 'analytics');
CREATE MACRO check_token(sid, client_token, server_token) AS ( EXISTS (SELECT 1 FROM quack_tokens WHERE auth_token = client_token));
CREATE MACRO read_only(sid, query) AS ( regexp_matches(upper(trim(query)), '^(SELECT|FROM|WITH|EXPLAIN)\b'));
CALL quack_serve('quack:localhost', token => 'analytics-team-token');
SET GLOBAL quack_authentication_function = 'check_token';SET GLOBAL quack_authorization_function = 'read_only';Ein Client mit dem richtigen Token verbindet sich nun und kann SELECTs ausführen, aber INSERT INTO quack.t ... über den normalen SQL-Pfad scheitert zur Autorisierungszeit.
Warnung Der Präfix-Regex oben ist illustrativ, kein robustes Nur-Lese-Filter: Eine Abfrage wie
WITH x AS (SELECT 1) INSERT INTO t SELECT * FROM xbeginnt mitWITHund verändert trotzdem Daten. Für produktive Nur-Lese-Durchsetzung hängen Sie die Datenbank im Nur-Lese-Modus an oder nutzen Sie eine Autorisierungsfunktion, die den geparsten Anweisungstyp prüft statt des rohen Abfragetexts.
Beispiel: Zugriffskontrollliste pro Benutzer
Um eine Zugriffskontrollliste (ACL) pro Benutzer umzusetzen, legen Sie einen eigenen Authentifizierungs-Hook an, der Paare sid → user speichert, damit die Autorisierung nachschlagen kann, wer fragt.
Weil Makros nicht schreiben können, muss die Aufzeichnungsseite eine skalare UDF sein, die z. B. von einer eigenen DuckDB-Erweiterung definiert wird. Die Autorisierungsseite kann ein Makro sein:
-- (populated by the auth UDF when a client connects)CREATE TABLE quack_sessions (sid VARCHAR PRIMARY KEY, user_name VARCHAR);
-- per-user query allowlist (your own data model)CREATE TABLE quack_user_acls (user_name VARCHAR, query_kind VARCHAR);
CREATE MACRO acl_check(sid, query) AS ( EXISTS ( SELECT 1 FROM quack_sessions s JOIN quack_user_acls a ON a.user_name = s.user_name WHERE s.sid = sid AND regexp_matches(upper(trim(query)), '^' || a.query_kind || '\b') ));
SET GLOBAL quack_authorization_function = 'acl_check';