Skip to content

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.

Every API response carries a fixed set of hardening headers (SecurityHeadersMiddleware):

HeaderValueWhat it stops
X-Content-Type-OptionsnosniffMIME-sniffing a response into a script
X-Frame-OptionsDENYClickjacking via framing
Referrer-Policystrict-origin-when-cross-originLeaking paths/tokens in Referer
Permissions-Policycamera=(), microphone=(), geolocation=()Silent device access
Strict-Transport-Securitymax-age=31536000; includeSubDomains (HTTPS only)Downgrade to HTTP
Content-Security-Policyframe-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.

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.

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/https and 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. A javascript: 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 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 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.

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.

MethodPath
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-onlybody {enabled}; owner only, otherwise 403 OWNER_REQUIRED

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.

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.

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.

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.

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.