Authentication

May 30, 2026 · View on GitHub

QuackTail uses two independent credential layers. Both matter in production unless you deliberately relax Quack auth on a locked-down tailnet.

LayerQuestionConfigure with
TailnetIs this process on our mesh?TS_AUTHKEY, Headscale preauth key, or browser login → CALL tailscale_up
QuackMay this caller run SQL over HTTP?QUACK_TAILNET_TOKEN, CREATE SECRET, or custom auth macro

Tailnet ACLs control who can open TCP to port 9494. Quack tokens control who may execute SQL once connected. See Quack security.


Tailnet login (Tailscale SaaS)

QuackScale embeds libtailscale (tsnet). Joining matches other embedded Tailscale apps.

ModeHowBest for
Auth keyauthkey in CALL tailscale_up, or TS_AUTHKEY envServers, CI, automation
Persisted statestate_dir on disk after first loginLaptops, repeat use
Browser loginCALL tailscale_login → open login_urlFirst-time dev setup

Production server

export TS_AUTHKEY='tskey-auth-...'
LOAD quackscale;

CALL tailscale_up(
    hostname => 'analytics-hub',
    state_dir => '/var/lib/duckdb/tailscale'
);

Do not commit auth keys in SQL — use env or your secret store.

Developer laptop

CALL tailscale_up() blocks until login completes. For a non-blocking flow:

CALL tailscale_login(
    hostname => 'my-laptop',
    state_dir => '~/.local/share/duckdb/quackscale'
);
CALL tailscale_login_status();  -- poll until status = 'up'

Open login_url in a browser. Reuse state_dir on later runs.

Environment variables (tailnet)

VariableEffect
TS_AUTHKEYAuth key if not passed in CALL tailscale_up
TSNET_FORCE_LOGINForce browser login even when an auth key is set (rare)

Headscale (self-hosted control plane)

Headscale implements the Tailscale control server API. QuackScale uses the same parameters as tailscale up --login-server:

Tailscale CLIQuackScale
--login-server https://hs.example.comcontrol_url => 'https://hs.example.com'
--authkey …authkey => '…' or TS_AUTHKEY
--hostnamehostname => '…'
state directorystate_dir => '…'

Create Headscale preauth keys with headscale preauthkeys create (not the Tailscale admin UI).

headscale users create quackscale
headscale preauthkeys create --user 1 --reusable --expiration 168h
CALL tailscale_up(
    hostname => 'duckdb-node-a',
    control_url => 'https://headscale.example.com',
    authkey => '<headscale preauth key>',
    state_dir => '/var/lib/duckdb/headscale-state'
);

Compose demo: control URL http://headscale:8080, preauth key written to /work/authkey. See examples/README.md.

Notes: Production server_url should be HTTPS. MagicDNS is optional; quack_uri() prefers MagicDNS when available, else tailnet IP.


Quack HTTP tokens

After a node is on the tailnet, Quack still requires application-level auth.

Default Quack behavior (why you override it)

CALL quack_serve(...) generates a random token unless you pass token => '...'. That is fine for local experiments; fleets need a shared token or allowlist.

QuackScale provides quack_token() to read a shared secret from the environment on the server. Clients use the same value via CREATE SECRET or TOKEN.

Environment variables (Quack)

Set on both servers and clients:

VariableRole
QUACK_TAILNET_TOKENPreferred — shared token (≥ 4 characters)
QUACK_TOKENFallback if QUACK_TAILNET_TOKEN is unset

Keep TS_AUTHKEY separate from Quack tokens.


Quack auth modes

Server:

LOAD quack;
LOAD quackscale;

CALL tailscale_up(hostname => 'warehouse-a', state_dir => '…');

CALL quack_serve(
    'quack:127.0.0.1:9494',
    allow_other_hostname => true,
    token => quack_token()
);
CALL tailscale_serve_local(port => 9494);

Client (after tailscale_quack_forward — see GUIDE.md):

LOAD quack;

CREATE SECRET (
    TYPE quack,
    TOKEN 'your-shared-quack-secret',
    SCOPE 'quack:127.0.0.1:19494'
);

ATTACH 'quack:127.0.0.1:19494' AS remote (TYPE quack, DISABLE_SSL true);

SCOPE must match how the client reaches the server. With the forwarder, that is quack:127.0.0.1:<local_port>.

Stateless queries:

FROM quack_query(
    'quack:127.0.0.1:19494',
    'SELECT 42',
    token => 'your-shared-quack-secret',
    disable_ssl => true
);

Mode 2 — Token allowlist (rotation / teams)

Use Quack’s multi-token table:

CREATE TABLE quacktail_tokens (auth_token VARCHAR PRIMARY KEY, label VARCHAR);
INSERT INTO quacktail_tokens VALUES ('primary-2026', 'analytics');

CREATE MACRO quacktail_check_token(sid, client_token, server_token) AS (
    EXISTS (SELECT 1 FROM quacktail_tokens WHERE auth_token = client_token)
);
SET GLOBAL quack_authentication_function = 'quacktail_check_token';

Validate client_token (what the caller sent), not server_token.

Mode 3 — Developer mode (lab only)

CREATE MACRO quacktail_dev_auth(sid, client_token, server_token) AS true;
SET GLOBAL quack_authentication_function = 'quacktail_dev_auth';

Not for production. See Quack developer mode.


End-to-end checklist

Each long-lived server

  1. export TS_AUTHKEY (or Headscale preauth key) and export QUACK_TAILNET_TOKEN
  2. LOAD quack; LOAD quackscale;
  3. CALL tailscale_up(...) with persistent state_dir
  4. Optional: SET GLOBAL quack_authentication_function (Modes 2–3)
  5. CALL quack_serve(..., token => quack_token()); CALL tailscale_serve_local(port => 9494);
  6. Do not call tailscale_down() on steady-state servers

Each one-shot client

  1. Same QUACK_TAILNET_TOKEN available for secrets / quack_query
  2. LOAD quackscale; CALL tailscale_up(...); CALL tailscale_quack_forward(...);
  3. LOAD quack; CREATE SECRET ...; then query / attach
  4. DETACH remote; SELECT 'done'; CALL tailscale_down(); — required or the process hangs

Security

  • Rotate QUACK_TAILNET_TOKEN like an API key; update servers and clients together
  • Restrict tailnet ACLs to who may reach peer TCP 9494
  • allow_other_hostname => true is for tailnet binds — do not expose raw Quack on the public internet without TLS in front (Quack exposure model)

References