DOCS · ACCESS CONTROL

Lock it down.

Four independent controls: a password on a single tunnel, a Google sign-in gate with an allowlist, two-factor on your own account, and a hard spend cap on any API key. Mix as needed — they don't depend on each other.

1 Tunnel password Pro

HTTP Basic Auth on a single tunnel. Enforced at the edge before any request reaches your laptop — unauthenticated traffic never costs you bandwidth. The password is argon2id-hashed; we never store it in the clear.

Set it

curl -X PUT "$API/tunnels/$TID/auth" \
    -H "Authorization: Bearer $JWT" \
    -H "Content-Type: application/json" \
    -d '{ "user": "demo", "password": "a-strong-passphrase" }'
{ "id": "3a1b2c3d-...", "auth_enabled": true }

user is 1–64 chars; password is at least 8. Takes effect on the next request — the edge gate flips synchronously, no reconnect needed.

What a visitor sees

$ curl -i https://myapp.21tunnel.com/
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="myapp"

$ curl -i -u demo:a-strong-passphrase https://myapp.21tunnel.com/
HTTP/1.1 200 OK
...

Remove it

curl -X DELETE "$API/tunnels/$TID/auth" \
    -H "Authorization: Bearer $JWT"
{ "id": "3a1b2c3d-...", "auth_enabled": false }

Plan note: setting a password requires Pro (or an active trial). Free post-trial returns 402 upgrade_required. An existing password keeps working after a downgrade — we don't proactively strip gates.

2 Google sign-in gate Pro

Put a tunnel behind Google OAuth with an email/domain allowlist. Visitors get bounced to Google before they ever reach your service; only allowed identities get a session cookie. Good for internal staging tools you want your team — and only your team — to reach.

Allow a whole domain

curl -X PUT "$API/tunnels/$TID/edge-auth" \
    -H "Authorization: Bearer $JWT" \
    -H "Content-Type: application/json" \
    -d '{
      "provider": "google",
      "allowed_domains": ["acme.com"],
      "allowed_emails": []
    }'
{ "id": "3a1b2c3d-...", "edge_auth_enabled": true }

Anyone with an @acme.com Google identity gets in. Add individual addresses to allowed_emails for contractors on other domains.

Allowlist semantics

  • Both lists empty → any signed-in Google user is allowed (sign-in required, but no allowlist filter).
  • Email exact match → allowed.
  • Email's domain matches a allowed_domains entry → allowed.
  • Otherwise → denied with a clean "access denied" page showing which identity was rejected.

Up to 100 entries each. The allowlist is re-checked on every request, so revoking access is immediate — remove the entry and the next request from that user is blocked even if they still hold a session cookie.

Remove the gate

curl -X DELETE "$API/tunnels/$TID/edge-auth" \
    -H "Authorization: Bearer $JWT"

Wildcard tunnels only. Edge-auth applies to *.21tunnel.com hosts. Custom domains are exempt (the session cookie can't span an arbitrary apex) — gate those at your origin instead. Only google is supported today; other providers return unsupported_provider.

3 Two-factor (TOTP)

Protect your dashboard account with a TOTP authenticator (Google Authenticator, 1Password, Authy, etc.). Enrollment is a two-step round-trip so a secret is never persisted until you prove you loaded it. Ten one-time recovery codes are issued at the end.

Step 1 — enroll (get the secret + QR URI)

curl -X POST "$API/auth/mfa/enroll" \
    -H "Authorization: Bearer $JWT"
{
  "secret_base32": "JBSWY3DPEHPK3PXP",
  "otpauth_uri": "otpauth://totp/21tunnel:you@acme.com?secret=...&issuer=21tunnel"
}

Render otpauth_uri as a QR code (the dashboard does this for you) or type secret_base32 into your authenticator. Nothing is saved server-side yet.

Step 2 — verify (persists + returns recovery codes)

curl -X POST "$API/auth/mfa/enroll/verify" \
    -H "Authorization: Bearer $JWT" \
    -H "Content-Type: application/json" \
    -d '{ "secret_base32": "JBSWY3DPEHPK3PXP", "code": "123456" }'
{
  "status": "enabled",
  "recovery_codes": [
    "k7f2-9a3d", "m1x8-44bc", "...8 more..."
  ]
}

Save the recovery codes now — they're shown once, hashed before storage, and are your only way back in if you lose the authenticator. The TOTP secret is encrypted at rest with the server master key.

Logging in with MFA on

Once enabled, login becomes two calls:

# 1. Password — returns a challenge instead of a JWT
curl -X POST "$API/auth/login" \
    -d '{ "email": "you@acme.com", "password": "..." }'
# → { "mfa_required": true, "challenge_token": "..." }

# 2. Exchange the challenge + a code for the JWT
curl -X POST "$API/auth/login/mfa" \
    -d '{ "challenge_token": "...", "code": "123456" }'
# → { "access_token": "eyJhbG..." } + refresh cookie

code accepts a 6-digit TOTP or one of your recovery codes. A ±1 step (30 s) tolerance covers clock drift. OAuth (Google / GitHub) sign-in does not bypass MFA — if you've enabled it, you complete it regardless of how you started the login.

Disable

curl -X POST "$API/auth/mfa/disable" \
    -H "Authorization: Bearer $JWT"

4 API-key budget cap

Put a hard monthly spend ceiling on any API key. The most important control when you hand a key to an automated agent — the key holder cannot raise its own cap. Only an org owner can, via this endpoint or by rotating the key.

Set / raise / lower

curl -X PUT "$API/tokens/$NONCE/budget" \
    -H "Authorization: Bearer $JWT" \
    -H "Content-Type: application/json" \
    -d '{ "monthlyBudgetUsdCents": 5000 }'

5000 cents = $50/month. Valid range 0 … 1,000,000,000 (up to $10M/mo) or null.

Uncap, or freeze entirely

# Remove the cap (unlimited)
curl -X PUT "$API/tokens/$NONCE/budget" \
    -H "Authorization: Bearer $JWT" \
    -d '{ "monthlyBudgetUsdCents": null }'

# Freeze all spend — incident response
curl -X PUT "$API/tokens/$NONCE/budget" \
    -H "Authorization: Bearer $JWT" \
    -d '{ "monthlyBudgetUsdCents": 0 }'

$NONCE is the first 16 hex chars of the token's nonce, visible in the GET /tokens list. Enforcement is on the agent transport at tunnel-registration time — an over-budget agent's mytunnel register is rejected with a BUDGET_EXCEEDED reason. See the agent guide for how an autonomous agent should handle that.

Next