Security model
What the control plane enforces, and where it stops.
This page describes the security properties as built. Where a protection has a boundary, we say so, because an identity provider you cannot reason about is not one you should trust.
Tenancy and isolation
Each organization's row records the engine deployment that serves it, and every engine call for that organization goes there. A request cannot name an engine URL or token; it may only pick a mounted engine id, and only when the organization is created. The strongest isolation is one engine deployment per customer, and the control plane supports routing many.
- The API returns adsas ids. It never returns engine ids.
- A caller aimed at another organization gets
not_found, the same answer as an object that does not exist. - Denied calls are audited under the caller's own organization, not the target's. One tenant cannot fill another tenant's log, or read another tenant's probing.
- Listing organizations as a customer returns only your own organization.
Who can call the API
Authorization is deny by default. An unknown permission, a missing principal, a principal aimed at another organization, or a scope outside the role is refused. The gate runs before the mutation lock and before any engine call.
| Principal | Established by |
|---|---|
| customer | An OIDC access token whose issuer and subject are already bound to a membership. Email is never the account key, and a token never creates an account. |
| service | A machine credential issued by an owner, stored only as a SHA-256 hash, valid from 60 seconds to 7 days, with an explicit permission scope. |
| provisioner | An inbound SCIM source token. It can call only the SCIM routes of its own source. |
| operator | The platform operator bearer. See break-glass below. |
Roles are owner, admin, app admin, directory admin, auditor, and support, each with a fixed permission set. A credential is checked against its issuer's role at issue time and its membership's role at request time; a later downgrade denies the whole token. Every bad, expired, or revoked bearer receives the identical unauthorized body, so callers cannot tell those cases apart.
OIDC access tokens are verified RS256 against operator-configured keys only. The verifier never fetches keys a token points to (jku, x5u, jwk, x5c), refuses alg=none and HMAC algorithms, and requires the token to be marked as an access token, so an ID token cannot be replayed as an API bearer.
Operator break-glass
An operator credential that can change every tenant at any time is standing privilege. Break-glass mode removes it. With standing_access set to breakglass, the operator bearer alone reaches only health, its own identity, the organization list, organization audit reads, and the break-glass routes. Every other operator call is refused with breakglass_required.
The ceremony
- The operator opens a request with a reason, a scope (named organizations, or
all), and a time-to-live of at most four hours. - Each approver signs the exact request bytes offline with a dedicated Ed25519 key, as an OpenSSH signature in a namespace reserved for break-glass. The server never sees a private key.
- A threshold of named approvers must sign. With two or more approvers configured the threshold cannot be one, so one person can never elevate alone.
- Activation mails the notify list with the scope, expiry, approvers, and reason. Startup refuses break-glass with no notification path unless that risk is explicitly accepted.
- Every call under an elevation is audited in a platform chain and in the chain of the organization it touched.
Standing access is set only in the configuration file. An environment variable cannot quietly restore full access.
Tamper-evident audit
Audit rows are written in the same transaction as the change they describe. If the audit insert fails, the change does not commit and the caller receives audit_unavailable. There is no queue and no later retry.
The chain
Each row's hash is SHA-256 over its sequence number, id, organization, actor, kinds, target, operation, result, request id, time, and the previous row's hash. Database triggers refuse updates and deletes; the only delete permitted is a retention prune covered by an anchor. POST /v1/orgs/{id}/audit/verify recomputes the chain and reports the first broken row.
Holding a copy the database cannot touch
A hash chain alone cannot reveal a cut tail, and someone who rewrites every row from some point on can produce a self-consistent chain. So the system is built to put evidence elsewhere:
- Export. NDJSON pages with checkpoint lines.
adsasctl audit verify-filechecks an export with no bearer and no network. - Checkpoints. Keep the last checkpoint off-host and present it to verify; the answer is
match,truncated,mismatch, orpruned. - Signed delivery. A webhook sink receives batches signed with a per-organization HMAC key held in the secret vault, over TLS 1.3 with redirects refused and private, loopback, and metadata addresses blocked at dial time.
- Retention with anchors. Optional retention of 30 to 3650 days. Before pruning, every row is hashed again; a break refuses the prune so tampering is never sealed over.
SAML signing
Assertions and responses are always signed, with a keypair generated for that one application. A shared certificate would let one tenant obtain assertions signed by the certificate another tenant's service provider trusts; a dedicated keypair closes that.
- Persistent NameIDs are HMAC-SHA256 with a 32-byte per-application salt that the API never returns, so OIDC relying parties cannot compute a user's SAML NameID and two relying parties cannot link users.
- SHA-256, SHA-384, or SHA-512 only. Any SHA-1 algorithm is refused as
insecure_algorithm. - IdP metadata is checked against the application's own certificate fingerprint and re-rendered on the public origin.
- SP metadata import refuses DOCTYPEs, entity declarations, deep nesting, and oversized documents.
Service providers must pin the IdP issuer and certificate from the application's metadata. Catalog inputs, such as a workspace name, are not proved to belong to the caller.
Contextual access
An organization can add restrictions on management actions after the role and scope checks. Rules never grant a permission; when several match, deny beats step-up and step-up beats allow.
- Evidence: required authentication methods from the verified token's
amr, and a maximum age from itsauth_time. - Network: allowed transport-peer CIDRs. Forwarded headers are ignored.
- Time: absolute access windows.
Policies are versioned and immutable. Start in observe mode, where every decision records what would have happened, then enforce. Each decision is recorded with its policy version and a content hash in the same transaction as its audit event.
MFA and recovery
- TOTP and WebAuthn passkeys. WebAuthn requires user verification and prefers a resident key. Neither adsas.id's control plane nor its operator ever sees the TOTP secret or the WebAuthn credential.
- Enrolling a factor needs step-up, and the operator does not bypass step-up. Removing a factor revokes the user's sessions, and removing the last factor is refused.
- Every change made in the console needs a sign-in less than five minutes old.
- Support recovery is two-person: support can request it, and the approver and the executor must each differ from the requester.
- Security questions are never used as authenticators or recovery proof.
- A password reset request for an existing account answers in the same time as one for a missing account.
Secrets and transport
- Client secrets, credential tokens, and invitation tokens are returned once and are not stored in plaintext.
- Configuration refuses inline secrets, unknown or duplicate keys, and secret files readable by other users. Tenant secrets resolve from a vault after authorization.
- Notification mail uses implicit TLS or mandatory STARTTLS at TLS 1.3, with the certificate pinned to the configured host.
- LDAP federation uses verified LDAPS or StartTLS with a read-only bind. There is no insecure override.
- The API rejects browser-origin requests. The browser-facing console is server-rendered, loads no scripts, uses a
__Host-session cookie, and checks CSRF tokens, the exact Origin, and Fetch Metadata on every form.
Limits we state plainly
- The audit hash is not keyed. Completeness is only provable against a checkpoint or delivered batch held outside the database.
- Signing out removes engine sessions; access and refresh tokens already issued stay valid for the residual windows the API reports.
- Contextual access does not collect device posture. Behind a proxy, the transport peer is the proxy.
- The control plane runs as a single writer. Replicated control planes are not supported.
- Wallet linking proves ownership of an Ethereum or Solana key. It is not a sign-in method and does not change MFA assurance.
Reporting a vulnerability
If you believe you have found a security issue in adsas.id, email security@adsas.id. Our contact details are also published at /.well-known/security.txt.
Please include
- The affected host, route, or component, and what an attacker gains.
- Steps to reproduce, with request ids from the
X-Request-Idheader where you have them. - Whether you accessed any data that is not yours.
Please do not
- Access, modify, or delete data belonging to other customers.
- Degrade the service, for example with load testing or denial of service.
- Use social engineering or physical attacks against After Dark Systems staff or facilities.
Test only against organizations you own. We will read every report and reply to the address you write from.