Securing your app
This section documents the controls Axowl operates on your behalf, and the ones you configure yourself. Each item below maps to a mechanism that exists in the product today — where a control is partly implemented or opt-in, it says so, because a security page that overstates coverage is worse than no page at all.
Transport & browser hardening
Section titled “Transport & browser hardening”Every API response carries a fixed set of hardening headers (SecurityHeadersMiddleware):
| Header | Value | What it stops |
|---|---|---|
X-Content-Type-Options | nosniff | MIME-sniffing a response into a script |
X-Frame-Options | DENY | Clickjacking via framing |
Referrer-Policy | strict-origin-when-cross-origin | Leaking paths/tokens in Referer |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Silent device access |
Strict-Transport-Security | max-age=31536000; includeSubDomains (HTTPS only) | Downgrade to HTTP |
Content-Security-Policy | frame-ancestors 'none' | Framing, in browsers that read CSP before X-Frame-Options |
TLS terminates at the edge; application containers speak HTTP only inside the private network.
The dashboard itself is served from the edge and carries the same family of headers: HSTS,
nosniff, Referrer-Policy, a Permissions-Policy that limits camera, microphone, geolocation
and payment to the dashboard’s own origin, and frame-ancestors 'self' so no other site can embed
it. A full Content Security Policy for the dashboard is currently deployed in report-only mode:
the browser reports what the policy would block without blocking it. It is promoted to an enforced
policy once the report stream is clean, not before — an untested CSP breaks sign-in and payment
pages silently, which is worse than the risk it addresses.
Session security
Section titled “Session security”Cookies. Session cookies are HttpOnly (JavaScript cannot read them), Secure over
HTTPS, and SameSite=Lax. Because HttpOnly keeps the token out of reach of page scripts,
a script injection cannot exfiltrate a session.
CSRF. State-changing requests authenticated by cookie must echo a double-submit token
(X-CSRF-Token) that matches the axowl_csrf cookie; a mismatch is rejected with 403.
Requests that authenticate by Authorization: Bearer, API key, or webhook signature are not
cookie-driven and are verified by their own credential instead.
Session policy (per organization). Under Organization → Settings → Security Policy you set maximum session duration, idle timeout, and the number of concurrent sessions per member. The organization maximum cannot exceed the platform ceiling.
Step-up re-authentication. Consequential actions — deleting an organization, transferring ownership, accepting a transfer, impersonation — require a fresh passkey assertion at the moment of the action, not merely a valid session. Axowl calls this stamping your seal. A stolen or forgotten-open session is not enough to perform them.
Sealed device. A workspace session is sealed only after a passkey ceremony on the device
in use; the server sets that flag itself and never derives it from anything the browser sends.
Sign-in paths that do not involve a passkey (magic link, social, SSO, invitation acceptance)
produce a sealed session only on a browser that already holds a device-trust cookie from an
earlier ceremony. Until now the “no passkey, no workspace” rule was applied by the dashboard
itself plus a handful of sensitive endpoints, so a caller that skipped the dashboard and spoke
to the API directly with an unsealed session could still read workspace data. The API now
checks the flag on every workspace request from a member session: an unsealed session is
limited to the bootstrap surface (sign-in, refresh, passkey registration, your own profile,
organization creation and invitations) and receives 403 SEALED_DEVICE_REQUIRED elsewhere,
which the dashboard turns into the passkey prompt. Machine callers — API keys, server-to-server
tokens, agent connections, your end users — authenticate differently and are not subject to
this check. The check is currently deployed in log-only mode: it records what it would
have refused without refusing it. It is switched to enforcement once that log shows no
legitimate path being caught, not before.
Authentication
Section titled “Authentication”Passkeys (FIDO2/WebAuthn) are the primary factor; magic links, social providers, and enterprise SSO (SAML 2.0 and OIDC) are also supported. Passwords are not the default path.
Key binding. You choose where a member’s passkey private key may live: synced (roams across the member’s own devices via platform keychains) or device-bound (never exportable, strongest reclaim guarantee). Plaintext cloud password managers are refused regardless of the setting.
Hosted sign-in: where a signed-in user is sent
Section titled “Hosted sign-in: where a signed-in user is sent”When your app uses the hosted sign-in page, the user signs in on Axowl’s origin and is then returned to your app. Two rules govern that return:
- The token never travels in a URL. The return address carries a one-time handshake code instead. The code is single-use and expires after 90 seconds; your app exchanges it for the end-user token.
- The return address must be one you registered. Before the user is sent back, the hosted
page checks the address: it must be
http/httpsand on the origin of one of your app’s Callback URLs. Any other address is not followed and receives no code — including an address placed in a crafted sign-in link by a third party. Ajavascript:or other non-web scheme is never followed.
This is why a new page that starts sign-in has to be added to your app’s Callback URLs first: an unregistered address is treated the same as an attacker’s.
The check on return runs in the hosted page itself. Magic-link return addresses are also validated by the server against the same list before the email is sent.
Device trust
Section titled “Device trust”Device policy sets the floor for which machines may reach your workspace:
- Open — any device is trusted on first successful passkey login (BYOD on).
- Hybrid — BYOD stays on, but the organization can pre-register company-owned devices and tell them apart; a device other than the member’s assigned one needs admin approval.
- Fortress — only pre-registered devices are admitted; unrecognized devices are blocked.
The mode also determines how a new device login is handled (allow / require approval / block), so the two settings cannot contradict each other.
Shared-device detection. By default a device is expected to belong to one member. If a different member signs in on the same device, admins are alerted and the user sees a banner. This detection is browser-based and therefore best-effort — it can be evaded by clearing cookies or switching browsers. Identity itself is always proven by the passkey, not by the device signal. Enforced, tamper-resistant device binding is a separate add-on.
Access control
Section titled “Access control”Access is evaluated on two independent axes. Membership answers “is this person in this organization”; entitlement answers “may this person perform this action”. A role is a named bundle of scopes, and scopes are checked per action rather than inferred from a title. Billing is carved out to the Owner and is not granted implicitly by an admin role.
Tenant isolation
Section titled “Tenant isolation”Organization-scoped requests resolve the tenant before the handler runs: the caller must be an active member of the organization named in the URL, otherwise the request is rejected with 401 (not signed in) or 403 (not a member). Repository queries for organization-owned entities apply the resolved tenant as a mandatory filter, and refuse to run when no tenant context is present — isolation fails closed rather than returning a broader result set.
Restricting who can reach your organization
Section titled “Restricting who can reach your organization”Two organization-level controls narrow the surface before authentication succeeds:
- Allowed email domains — members must present an email in your domain list to join or to register a device.
- Country access policy — logins from outside your allowed regions are allowed-and-logged, held for admin approval, or blocked outright, as you choose. The owner’s country is always allowed. Temporary grants cover business travel without weakening the standing policy.
Desktop-app-only access. An owner can turn on desktop-app-only access for the organization’s
workspace (organization settings → Security). Today the setting only records access that does not
come from the Talon desktop app; refusing that access starts once the desktop app is released. A user-agent string or a custom
header proves nothing, because any client can send one, so the API does not look at either.
Instead the desktop app holds a non-exportable device key in the computer’s TPM, certifies a
short-lived in-memory session key with it, and signs every API request (method, path and query,
time, a one-time nonce, and a hash of the session token) with that session key. The API checks
the chain, refuses a request older than two minutes or a nonce it has seen before, and accepts
the request only if that device key belongs to one of the organization’s allowed PCs. Allowed PCs
are company devices pre-registered under Devices with the Talon device ID that the desktop app
shows for that PC (field talonDeviceId on POST /api/org/{slug}/devices/register, 43
characters); blocking or deleting the device there removes it from the list. Requests from an
ordinary browser are answered with 403 DESKTOP_APP_REQUIRED once enforced. Only the owner can
change the setting, and the setting itself stays reachable from a browser with a passkey-sealed
session, so an owner cannot be locked out by turning it on. Machine callers (API keys,
server-to-server tokens, agent connections, end users) are not affected. The check is currently
deployed in log-only mode: it records what it would have refused without refusing it.
| Method | Path | |
|---|---|---|
GET | /api/org/{slug}/settings/desktop-only | {enabled, enforced, canEdit, currentClient, allowedPcCount} — currentClient is how this very request was judged (Valid, Missing, …) |
PUT | /api/org/{slug}/settings/desktop-only | body {enabled}; owner only, otherwise 403 OWNER_REQUIRED |
Bot and abuse resistance
Section titled “Bot and abuse resistance”Public sign-up style endpoints are gated by Cloudflare Turnstile. When the CAPTCHA secret is not configured, those endpoints refuse the request rather than accepting it unverified — a missing control is treated as a closed door, not an open one.
Secrets you hold
Section titled “Secrets you hold”API keys, plugin tokens, and webhook secrets are stored as hashes; the plaintext value is shown exactly once, at creation, and cannot be retrieved afterwards. Each can be rotated or revoked independently, and rotation is recorded. Rotating the organization signing key invalidates outstanding signed permits, so plugins must refresh their cached public key.
Your third-party credentials (SAP, AWS, on-premise systems) are never sent to Axowl. A plugin token proves “this caller is your plugin” and grants no access to your cloud resources.
Tamper-evident audit trail
Section titled “Tamper-evident audit trail”Consequential records are hash-chained and sealed, so modification is detectable rather than merely discouraged — including modification made directly in the database. Verification is independent: a record can be re-hashed and checked against its stored seal without trusting the application that wrote it.
Supply chain and source hygiene
Section titled “Supply chain and source hygiene”Controls that run on the code before it ships, on every pull request:
- Dependency vulnerability gate. Every NuGet package, including transitive ones, is checked against the GitHub Advisory Database; a High or Critical finding fails the build. The same check covers the npm packages of the Cloudflare Workers that handle log ingestion and mail. Moderate findings are reported and picked up by the automated update flow below. The gate also runs on a weekly schedule so that a newly published advisory against an unchanged dependency is still caught.
- Automated dependency updates. Minor and patch updates arrive as one grouped pull request per ecosystem each week; security updates open immediately and are not batched.
- Secret scanning. The commits of every pull request are scanned for credential patterns (API keys, cloud access keys, JWTs, private keys) before merge; a hit fails the check. A scheduled scan covers the whole working tree. The allowlist contains only named test constants and documentation placeholders, never a real secret.
What this does not cover yet, stated so that it is not assumed: the two static documentation and blog sites are built with a generator whose own dependencies are reported but not gated, and static application security testing (SAST) of our source is not part of the pipeline.
Reporting a vulnerability
Section titled “Reporting a vulnerability”If you believe you have found a security issue, email security@axowl.com with enough detail to reproduce it. Please do not disclose publicly until we have responded. We do not pursue legal action against good-faith research that avoids privacy violations, service degradation, and access to data beyond what is needed to demonstrate the issue.