Security model
Threat model, isolation, and what is left to you
Edit this pageOn this page
What Loomscope protects, how, and what it deliberately leaves to you. This is the page to hand to a reviewer.
Threat model
Loomscope holds a map of your network, a list of your weaknesses, and credentials for your infrastructure. It is a high-value target by construction, and the design assumes:
- A daemon host may be compromised. It is on the network being scanned, often in the least trusted segment.
- The database may be exfiltrated. Backups travel to places databases do not.
- A tenant may try to read another tenant. A deployment holding several customers is a normal deployment.
- An operator may make a mistake. A forgotten
WHEREclause should not be a breach.
Tenant isolation
Every business table carries organization_id. Row-level security is
enabled and FORCEd on all of them, so the policy applies even to the
table owner.
The consequence: a query that forgets its tenant filter returns nothing, rather than returning another organisation's estate. Isolation is in the database, not in the application, so it holds regardless of which code path reached the query.
Every endpoint derives the organisation from the authentication context. A client-passed organisation id is never trusted, anywhere. Cross-organisation access requires an explicit scope and writes an audit entry; there is no ambient superadmin.
Credentials at rest
| Secret | Protection |
|---|---|
| Stored credentials | AEAD encryption via pgcrypto, keyed by LOOMSCOPE_KMS_KEY |
| API keys (daemon, SCIM) | SHA-256 hashed; shown once, never recoverable |
| Passwords | Argon2id |
| Invitation links | Hashed; single-use, with an expiry |
| Session cookies | Signed with BETTER_AUTH_SECRET |
LOOMSCOPE_KMS_KEY lives only in the environment. The database alone does
not decrypt anything — which is what makes an exfiltrated dump much less
useful than it looks, and why a backup restored under the wrong key comes
back looking healthy and silently failing.
Daemon privilege
The daemon is the component most likely to be attacked, so it holds the least.
| Property | Setting |
|---|---|
| Linux capabilities | All dropped except NET_RAW |
privileged | Never |
| Filesystem | read_only: true, with a tmpfs for /tmp |
| Privilege escalation | no-new-privileges:true |
| Config mount | Read-only |
| Image tag | Pinned semver, never :latest |
| Inbound connections | None. It polls outbound only |
| Database access | None |
| Cloud credentials | None — cloud discovery runs in the control plane |
Where the host permits unprivileged pings
(net.ipv4.ping_group_range), the daemon needs no capability and no root
at all.
A compromised daemon gets an attacker a scanner and one API key scoped to posting observations for one organisation. It does not get the database, the cloud accounts, or the other daemons.
Network exposure
| Direction | Required |
|---|---|
| Daemon → control plane | Outbound HTTPS. Nothing inbound to the daemon |
| Control plane → database | PostgreSQL |
| Control plane → cloud APIs | Only if a cloud account is configured |
| Control plane → AI provider | Only if the assistant is enabled with a hosted provider |
| Anything → the internet | Otherwise nothing. No telemetry, no licence check, no update ping |
Web application
- CSRF on every cookie-authenticated state-changing endpoint. API keys are exempt, because CSRF is an attack on ambient credentials and a bearer token is not one.
- No
localStorageorsessionStoragefor anything security-relevant in shared client paths. - Secrets never reach the client bundle. Server-only modules are marked and enforced.
- Session revocation is immediate. Removing or deprovisioning a member ends the sessions they already hold, rather than stopping their next sign-in.
Logging
Structured JSON — pino for the control plane, zerolog for the daemon —
with a redacting layer that drops credentials, API keys, SNMP community
strings and session tokens before they reach a log line.
Structured fields are what make that enforceable; interpolated strings are not filterable.
Supply chain
- Images pinned to a semantic version.
- Dependencies audited in CI.
- Secret scanning on every change, with any exception justified inline rather than by a broad ignore rule.
- Migrations linted by
squawkand applied against a seeded database before images publish.
What Loomscope does not do for you
Stated plainly, because a security page that only lists strengths is a marketing page:
- No TLS termination. Port 3000 is cleartext. Put a proxy in front — see TLS and reverse proxy.
- No resource limits in the shipped compose file. A runaway scan can consume the host.
- The
serverandpostgresservices are not hardened the way the daemon is — neither setsread_onlynor drops capabilities. - No rate limiting on sign-in beyond what Better-Auth provides.
- No secret rotation tooling. Rotating
LOOMSCOPE_KMS_KEYis a manual maintenance operation. - No SAML.
Reporting a vulnerability
Please do not open a public issue. SECURITY.md describes the private reporting channel.