Zum Inhalt springen

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 explizites allow_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_serve aufrufen, erzeugt der Server ein zufälliges Token und gibt es in der Spalte auth_token zurück (oder Sie übergeben eines explizit über quack_serve(uri, token := '...')). Clients müssen dieses Token bei jeder Verbindung vorweisen, entweder über ein auf die Server-URI beschränktes quack-Secret oder über die explizite Option TOKEN in der ATTACH-Anweisung / dem Funktionsaufruf quack_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 true zurü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 zur quack_connection_id für diesen Client.
  • client_token: Das Token, das der Client gesendet hat.
  • server_token: Das auf dem Server konfigurierte Token (über quack_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: Die quack_connection_id des aufrufenden Clients (also dieselbe ID, die der Authentifizierungs-Hook als Argument session_id gesehen 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_function registriert 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 x beginnt mit WITH und 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 siduser 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';