Skip to content
AI Workforce OS
ES EN
Create account

Security at AI Workforce OS

These are the security controls that exist today, as they can be checked in the code and in the deployment configuration. At the end there is a table of what we do not have yet. We hold no certifications (neither SOC 2 nor ISO 27001) and we do not claim compliance with any framework.

11 min read

On this page
  1. How to read this page
  2. Isolation between organizations
  3. Sign-in and sessions
  4. Enterprise identity: SSO, MFA and SCIM
  5. Roles and permissions
  6. Secrets vault
  7. Human approvals
  8. Spending caps and operating controls
  9. Verifiable audit log
  10. Data to AI models and untrusted content
  11. Code execution
  12. Encryption in transit and headers
  13. Backups and development
  14. What we do not have yet
Back to top

How to read this page#

We only describe what exists. When a control depends on the operator's configuration, we say so. The design assumes that a language model can be fooled by untrusted text, so damage is limited with deterministic controls in the backend (Go) and not with instructions in the prompt: we do not promise that the model is never influenced, only that it can do nothing the backend does not authorize.

The current public deployment runs on a single shared server. According to its configuration in the repository, the agent runtime is in simulation mode (it sends nothing to model providers) and the vault has no encryption key configured, so it stores no real credentials.

Tip

More technical detail in the documentation: security model, and authentication and multi-tenancy.

Isolation between organizations#

Tables holding an organization's data use Postgres row-level security with FORCE ROW LEVEL SECURITY. Each request runs in a transaction that sets the organization with a local setting (app.org_id) and the policies filter on it. If it is not set, no row is visible or writable.

The server works with a role (app_user) that is not a superuser, has no BYPASSRLS and owns no tables. Integration tests check isolation between two organizations. This is logical isolation inside one database, not an instance per customer.

Sign-in and sessions#

  • Passwords hashed with Argon2id (64 MiB, 3 iterations, 2 threads), rehashed at the next sign-in if they were created with weaker parameters.
  • 10 to 128 characters and at least two character classes. The account's own e-mail address and a short list of common passwords are rejected.
  • After 5 failed attempts (password or verification code) the account is locked for 15 minutes. There are also rate limits per e-mail, per IP and per user on sign-in, sign-up, two-step verification, password recovery and SSO.
  • For non-existent accounts an equivalent hash is computed, so response time does not reveal whether an e-mail exists.
  • The access token is a 15-minute JWT. The refresh token is opaque, lasts 30 days, is stored only as a SHA-256 hash and travels in an HttpOnly, Secure, SameSite=Strict cookie. Every use rotates it, and reusing an already-used token revokes the whole family.
  • Renewing and ending a session require the X-WFAI-CSRF header and an allowed origin.
  • E-mail verification (single-use link, 24 hours) is optional: it is turned on with EMAIL_VERIFICATION=true and needs SMTP. Password recovery uses a single-use link valid for 1 hour and closes every open session.

Enterprise identity: SSO, MFA and SCIM#

  • Single sign-on with any OpenID Connect provider (Okta, Entra ID, Google Workspace…): code flow with PKCE, state and nonce, and tokens signed only with RS256 or ES256. The client secret is stored encrypted in the vault.
  • Domains verified with a DNS TXT record, re-checked every 24 hours. SSO and domain-based account creation only work with verified domains.
  • Two-step verification with an authenticator app (TOTP) and 8 recovery codes. An organization can require it of all its members: anyone without it must enroll before getting a session.
  • An organization can enforce SSO and turn off password sign-in. Owners keep their password as emergency access.
  • SCIM 2.0 provisioning (users and groups) with one token per organization stored as a hash. Deactivating a person removes their membership and revokes their sessions in that organization at once.

Note

The required MFA or SSO policy is checked at password sign-in and when switching organizations; sessions that were already open are not closed when it is turned on. SAML is not implemented.

Roles and permissions#

Each membership has one of four roles: owner, admin, member or viewer, each a strict superset of the one below. Permissions have the form resource:action and are checked on the server before each operation. The role is read from the database on every request, so demoting or removing a member takes effect immediately.

An admin can only assign the member and viewer roles and cannot change their own role, and the last owner cannot be removed or demoted. A person can belong to several organizations with a different role in each. There are no custom roles.

Secrets vault#

OAuth tokens, integration API keys and an organization's own AI keys are encrypted with AES-256-GCM in an envelope scheme: a key-encryption key (KEK) that lives outside the database wraps a per-organization data key, and that key encrypts each secret with a random nonce. The authenticated data binds each ciphertext to its organization and purpose, so if it is copied to another row or organization it does not decrypt.

Without CONNECTIONS_KEK the vault fails closed: it neither stores nor reads real secrets. The KEK is rotated with CONNECTIONS_KEK_PREVIOUS and data keys can be rotated too. Revoking an OAuth connection suspends it, calls the provider's revocation endpoint and destroys the ciphertext; if the provider fails, it is retried.

Integration credentials never travel to the agent runtime or the model. An organization's own AI key is sent to the runtime to call the provider, never inside the prompt. The same vault encrypts the SSO client secret, webhook signing secrets and the Slack and Teams notice URLs.

Human approvals#

An agent can request an action but can never approve it. People with the admin or owner role decide. Each approval is bound by hash to the exact arguments, is consumed once and is re-validated before execution. It expires after 30 minutes by default (APPROVAL_TIMEOUT) and expiring counts as a rejection; project approvals do not expire by default and send hourly reminders.

There are four autonomy levels per agent (suggest, approve_each, rules, autonomous). Integration actions with external, irreversible effects always ask for approval, and an approved e-mail waits at least 60 seconds before going out, during which it can still be cancelled. Each organization can add rules:

  • amount thresholds and forbidden actions;
  • mandatory approval for new recipients;
  • a higher minimum role to decide certain actions;
  • dual approval, with two different people and never the requester;
  • approval for a session (8 hours) or a project (30 days), only for internal, reversible, low or medium risk actions.

Note

Approvals can also be decided from e-mail, Slack or Teams with signed links (HMAC-SHA256), single-use and valid for 24 hours. Opening the link decides nothing: a confirmation is required. These links are never offered for high-risk actions, dual approval, a minimum role or code execution. The option that prevents approving your own requests is off by default, except with dual approval.

Spending caps and operating controls#

Before each runtime call the maximum estimated cost is reserved against the request, the agent and the organization, and afterwards it is reconciled with the larger of the cost reported by the runtime and the cost recomputed by the backend. When a cap is reached, work pauses until a person decides. Each call also carries its own cost cap, and there are limits on delegation depth, retries, time per call and parallel calls per organization.

Operating hours prevent starting new work outside open hours. Anomaly detection applies four explainable rules (spend spike, burst of tool requests, serial rejected approvals and new recipient domains) and records each finding; it does not block on its own and automatic freezing is off by default.

Read-only mode, for the organization or an agent, denies external effects. The kill switch has two levels, freeze and lockdown: any admin or owner can trigger it, only an owner can lift it and with a written reason. The operator can also impose it with KILL_SWITCH, and then no API call can lift it. Held actions go back to the approval queue; a call already sent to the runtime finishes.

Verifiable audit log#

The audit log is a hash chain per organization: each entry includes the SHA-256 of the previous one, so modifying, deleting, reordering or inserting an entry breaks the chain. For the application role it is append-only (UPDATE and DELETE revoked, plus triggers). Before being chained, each entry goes through a scrubber that replaces content and credential-looking keys with [redacted] and truncates long texts.

Audited events include approvals, tool executions and denials, policy decisions, control changes, identity changes (SSO, MFA, password recovery, invitations) and exports and deletions. Password sign-in and sign-out are not audited yet.

  • Verification: GET /api/v1/audit/verify recomputes the chain. If you keep the chain head (number and hash) outside the system, you can pass it as an anchor to detect even a complete rewrite.
  • Export for admins and owners as JSONL, CSV or a ZIP that includes the verification result. The export is not signed.
  • Optional retention: an owner can purge entries older than at least 365 days. The chain stays verifiable from a checkpoint, but the purged content can no longer be proven: keep an export if you need it.

Data to AI models and untrusted content#

Before text reaches the model, credentials and card numbers are masked, and the backend also masks one-time codes. External content (e-mails, API responses, files, web pages) is normalized, its markdown images and links are neutralized and it is wrapped in a block delimited as data. A heuristic injection score flags the task and adds approval reasons to actions with effects; it is a signal, not the barrier. The barrier is permissions, approvals and limits.

The operator sets a ceiling of AI providers (ALLOWED_PROVIDERS) and each organization can narrow it further; an excluded provider is not called, not even as a fallback. An organization can use its own AI key, and while it is active only that key answers, or point to its own OpenAI-compatible endpoint.

Agent web browsing is off per organization by default. When it is on, requests leave from the backend with SSRF protection that blocks private, loopback and link-local addresses at connection time; outgoing webhooks use the same protection and accept HTTPS only.

Code execution#

Code an agent wants to run is a high-risk action that a person must approve. There is an optional mode, off by default, in which code that only touches the agent's own files runs without approval; turning it on requires org:manage and an explicit risk acknowledgement, and it is audited.

Code runs in a container with no network, a read-only file system, no capabilities, no privilege escalation, an unprivileged user and limits on processes, memory and CPU. gVisor is optional.

Warning

The executor service mounts the host's Docker socket, which is equivalent to administrator access to the host. We recommend running it only on a dedicated, trusted host. It is not part of the public deployment.

Encryption in transit and headers#

Public traffic arrives over HTTPS: the Caddy proxy terminates TLS with automatic certificates and sends a one-year Strict-Transport-Security header on the site and the application. On the application it adds X-Content-Type-Options: nosniff, X-Frame-Options: DENY and Referrer-Policy. Files uploaded by users and artifacts are served with a sandbox CSP so they cannot run code on the application's origin.

Between containers, inside a private network on the same server, the connection to Postgres does not use TLS (sslmode=disable). Secrets are encrypted at the application level, but we have not verified encryption at rest of the hosting provider's disks.

Note

There is no global Content-Security-Policy on the application yet, and the TLS configuration has not been externally assessed.

Backups and development#

A cron job takes a compressed Postgres dump every night (03:30, server time). It is stored on the same server with restrictive permissions and kept for 14 days; the script fails with a message if the file is suspiciously small. There is no off-server copy and no formal recovery time or recovery point objective.

Continuous integration builds and tests the backend and the runtime, builds the frontend and runs the race detector on key packages, end-to-end tests, a Dockerfile linter, govulncheck and npm audit (the last two do not block). Images are published tagged by commit.

What we do not have yet#

An honest status of what procurement teams usually ask for. Where it says it does not exist, it does not exist; where it says partial, read the detail. We give no dates because none is committed.

CapabilityStatus todayDetail
SOC 2 / ISO 27001Does not existNo certifications or external audit.
External penetration testDoes not existOnly our own automated tests and dependency scanners in CI.
SAML SSODoes not existSSO is OpenID Connect only.
Custom rolesDoes not existFour fixed roles.
Breached-password checkDoes not existOnly a short list of common passwords.
Self-service export or deletion of an individual accountDoes not existExport and deletion work per organization; for an account, send a request to info@workforceai.es.
Choice of data residencyDoes not existA single server; a region cannot be chosen.
Encryption keys in a KMSDoes not existThe KEK is supplied through an environment variable (CONNECTIONS_KEK).
Signed or externally anchored audit exportPartialVerification accepts an anchor you keep yourself; the platform does not sign or anchor the chain head.
Off-server backups and periodic restore testsDoes not existNightly dumps live on the same server.
High availabilityDoes not existA single instance.
Secret scanning and static analysis in CIDoes not existThere are govulncheck, npm audit and a Dockerfile linter, but no secret scanning or CodeQL.
Public status pageDoes not existNo public incident or availability history.
Published legal documents (privacy, terms, DPA)Under reviewDrafts pending review by a lawyer.
Strong isolation of the code executorPartialA restricted container, but the executor has access to the host's Docker socket.

Tip

To report a vulnerability, see the vulnerability disclosure page or write to info@workforceai.es.