Security
Measured on 1 September 2026
Sixi AI is a security product, and it is held to the posture it assesses. This page is that posture: where the data lives, what leaves the service, how it is encrypted, and what is inside the binary. Every claim names the file that enforces it, so a review can ask for the mechanism rather than a badge. The last section is what is not true yet.
Report a vulnerability
Email contact@sixi.ai with “security” in the subject. Include the affected surface, a description, and the steps to reproduce. A proof of concept is handled in confidence. Do not open a public issue. The same contact is published at /.well-known/security.txt (RFC 9116).
Every report is acknowledged, and a disclosure timeline is agreed with the reporter. The default is coordinated disclosure once a fix has shipped. We take no legal action against good-faith research that respects this process, stays in scope, and touches no data beyond what the proof needs.
Sixi AI is a small company. A report is read by the person who wrote the code, which is faster than a queue and is also the honest description of the team behind it.
Where the data lives
| What | Where, and how | Enforced by |
|---|---|---|
| The service | Google Cloud Run, Zurich (europe-west6). One region, no failover outside it. | .github/workflows/go-deploy.yml |
| The database | Firestore, database sixi-eu, Zurich. Client access is denied by rule; every read and write goes through the service. | SIXI_FIREBASE_DATABASE_ID · firestore.rules |
| At rest | Target configurations, credentials, evidence and transcripts are encrypted with Fernet (AES-128-CBC + HMAC-SHA256). The key lives in Secret Manager. A hosted process refuses to start without one. | go/fernet · go/cmd/sixi/serveapi.go |
| In transit | TLS only. HSTS for two years, Content-Security-Policy default-src 'self', frame-ancestors 'none'. | go/api/security_headers.go · firebase.json |
| Retention | Scans 365 days, evidence 90, audit rows 730 (GDPR Art. 5(1)(e)). Each row carries the horizon it was written with, so a policy change never rewrites a promise already made. | go/scanstore/retention.go |
| The trial's attacker model | Gemini, through Google's API. It is not region-pinned, and your agent's replies pass through it to be judged. This is the one place the trial's residency is weaker than the deployed package's. | go/cmd/sixi/hosted.go |
| Your own attacker model | Any OpenAI-compatible, Azure OpenAI, Bedrock or Gemini endpoint, with a declared residency of eu, ch or local. A host that does not match the declaration is refused when the model is constructed, before a scan starts. | go/agent/model.go · PUT /api/user/attacker-key |
| The deployed package | Runs in your network on the model you run. The store is one encrypted file on your host. The licence is verified offline. Nothing leaves. | go/license · sixi doctor |
Residency is a property of the code, not of a contract. The hosted service runs in one Swiss region and stores in one Swiss database. The model it attacks with on the trial is the exception, stated above rather than left for you to find.
What leaves the service, and nothing else
The binary opens outbound connections to six kinds of destination. There is no analytics, no crash reporter, no usage beacon, and the licence never phones home.
| Your target | During a scan. Every dial passes the SSRF guard first. |
| The attacker model | During a scan. Ours on the trial, yours once you set a key. |
| Google Cloud | Firestore, Firebase identity and Secret Manager. The infrastructure the hosted service runs on. |
| Stripe | Only when you open checkout or the billing portal. |
| Microsoft Graph and Azure | Only when you connect a directory. The read is delegated, on your behalf, and stores nothing. |
| A webhook you configured | When a scheduled scan finishes. Counts and a link, never payloads. |
The proof is a grep, not a paragraph. Every file in the tree that can open a connection is one of the six above, and a new one has to be argued for in a commit message:
grep -rlE 'http\.NewRequest|NewRequestWithContext|\.Dial\(|websocket\.Dial|firestore\.NewClient' \ --include=*.go --exclude=*_test.go go/
In the browser, the Content-Security-Policy does the same job: connect-src names this site and Firebase identity, and nothing else can be reached from the page.
Access control
- Identity. Firebase ID tokens on the hosted service. Pipeline tokens for CI (
sixi_followed by 64 hex characters) are stored as a SHA-256 hash, shown once, and revocable per token. - Ownership. Every resource is checked against its owner. A failure answers 404, never 403, so an id cannot be confirmed by probing.
- Rate limits. Per user on every mutating route, ten scan launches a minute among them. Per IP on the public probe. A brake on a stolen token, not a quota.
- Audit trail. Every mutating request writes who, what, when, the outcome and a request id you can quote back to us. Kept 730 days.
- Outbound guard. Private ranges, loopback, link-local and cloud metadata addresses are refused, and the name is resolved before the connection so a DNS rebind cannot slip past. Applied to every target dial and every webhook.
- The image. Distroless, static, non-root. No shell, no package manager. Secrets come from Secret Manager and are masked in every response.
go/api/auth.go · go/api/api_tokens.go · go/api/middleware.go · go/ssrf · go/Dockerfile
What a report carries
By default a report strips the attack templates, the models that ran, and your agent’s transcripts. Full disclosure is a deployment setting for a target you own, and the process says so loudly at startup when it is on.
A shared link is a capability: the scan id and a secret, with only the secret’s hash stored. Thirty days by default, one link per scan, revocable, and it always serves the stripped report whatever the deployment’s own setting. SARIF files and webhook notices carry counts, technique ids and titles. Never a payload, never a reply.
go/api/routes_share.go · go/report/sarif.go · SIXI_REPORT_FULL_DISCLOSURE
Evidence you can check
The bill of materials. /api/sbom is the CycloneDX 1.5 bill of the binary answering the request, read from the linker’s own metadata. It names the build that served it, not the build a release note says was deployed. The engine has ten direct dependencies. The router, JWT verification, SSE, the reverse proxy, SigV4, Fernet and the PDF writer are written in the standard library, because in a security product every dependency is attack surface.
A dead target is never called clean. 149 acceptance cells point the real binary at deliberately broken targets — a 401, a 502 gateway page carrying jailbreak markers, an endpoint that answers under the wrong key, one that never answers — and assert on the rendered report and the exit code that no assessment is claimed that was not made. They exist because the same defect was once found in eight separate places by running the product, and in none of them by reading it.
cd go && go test -v ./acceptance/ | grep -cE '^\s+--- PASS' # 149
Two products, one boundary. The governance console and the attack engine ship as different artefacts, and a test walks the import graph against an allowlist so the attack library cannot be compiled into the binary a governance customer runs. A new edge fails by default.
No secret reaches the browser. A guard runs on every build and fails it if the client bundle reads any server-side variable. sixi doctor checks a deployment’s own posture — encryption, residency, egress, licence — and exits non-zero on any failure.
go/acceptance · go/tower/contract/import_boundary_test.go · scripts/check_client_secrets.sh · go/cmd/sixi/doctor.go
What is not true yet
- No third-party attestation. Sixi AI holds no SOC 2 report and no ISO 27001 certificate, and does not claim one. Ask for the mechanism instead. The sections above are the answer we give a questionnaire.
- No committed disclosure windows. Acknowledgement, assessment and the deadline after which we disclose regardless are agreed per report. We would rather state that than publish a number nobody has committed to.
- The trial’s attacker model is not region-pinned. Set your own key with a declared residency, or take the deployed package, and it is.
- One region, one small team. There is no 24/7 operations centre behind the hosted service. It is the scanner trial. The deployed package is what a production estate runs on, inside your own perimeter.
Questions a questionnaire asks that this page does not answer: contact@sixi.ai.