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.
| Layer | Question | Configure with |
|---|---|---|
| Tailnet | Is this process on our mesh? | TS_AUTHKEY, Headscale preauth key, or browser login → CALL tailscale_up |
| Quack | May 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.
| Mode | How | Best for |
|---|---|---|
| Auth key | authkey in CALL tailscale_up, or TS_AUTHKEY env | Servers, CI, automation |
| Persisted state | state_dir on disk after first login | Laptops, repeat use |
| Browser login | CALL tailscale_login → open login_url | First-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)
| Variable | Effect |
|---|---|
TS_AUTHKEY | Auth key if not passed in CALL tailscale_up |
TSNET_FORCE_LOGIN | Force 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 CLI | QuackScale |
|---|---|
--login-server https://hs.example.com | control_url => 'https://hs.example.com' |
--authkey … | authkey => '…' or TS_AUTHKEY |
--hostname | hostname => '…' |
| state directory | state_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:
| Variable | Role |
|---|---|
QUACK_TAILNET_TOKEN | Preferred — shared token (≥ 4 characters) |
QUACK_TOKEN | Fallback if QUACK_TAILNET_TOKEN is unset |
Keep TS_AUTHKEY separate from Quack tokens.
Quack auth modes
Mode 1 — Single shared token (recommended)
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
export TS_AUTHKEY(or Headscale preauth key) andexport QUACK_TAILNET_TOKENLOAD quack; LOAD quackscale;CALL tailscale_up(...)with persistentstate_dir- Optional:
SET GLOBAL quack_authentication_function(Modes 2–3) CALL quack_serve(..., token => quack_token()); CALL tailscale_serve_local(port => 9494);- Do not call
tailscale_down()on steady-state servers
Each one-shot client
- Same
QUACK_TAILNET_TOKENavailable for secrets /quack_query LOAD quackscale; CALL tailscale_up(...); CALL tailscale_quack_forward(...);LOAD quack; CREATE SECRET ...;then query / attachDETACH remote; SELECT 'done'; CALL tailscale_down();— required or the process hangs
Security
- Rotate
QUACK_TAILNET_TOKENlike an API key; update servers and clients together - Restrict tailnet ACLs to who may reach peer TCP 9494
allow_other_hostname => trueis for tailnet binds — do not expose raw Quack on the public internet without TLS in front (Quack exposure model)