Security notes

What this service protects, what it does not, and the decisions that are easy to undo by

accident.

The thing being protected

A one-time code, and the records that say where it goes. EndpointTrust is not in the password

path: Windows validates the AD password. EndpointTrust decides whether a second factor is

required, delivers it, verifies it, and signs the answer.

So the attacks that matter are the ones that redirect a code, skip a code, or

forge an answer.

Redirecting a code

POST /v1/auth/sms has no destination field. The destination is read from

phone_numbers, where a partial unique index guarantees exactly one live row per person.

A compromised endpoint has no request it can make that names a number.

The one relaxation is agent pairing, which is off by default and is documented in full in

pairing-security.md.

Administrators can change a number, which is the legitimate path, and the old number is

retired rather than deleted so the change is visible afterwards.

Skipping a code

Three ways a sign-in can legitimately proceed without one, and all three are counted and

shown prominently on the overview page:

bounded by a signed grant. Never an offline code check.

configuration validator refuses pass_through with no deadline and refuses a deadline

under enforce, so a stale bypass cannot survive a mode change.

A fourth is not a bypass but looks like one in a report: an unprotected path. The

coverage field on every audit event says which it was.

Forging an answer

Every document the endpoint acts on is a compact JWS signed ES256 by the tenant's own key,

and the endpoint holds only that tenant's public key. Decisions are bound to the attempt,

the device, the session and the policy version — a signature alone is not enough, because

the same key legitimately signs other documents.

Secrets, and where they are

SecretStored asRecoverable?
Administrator passwordscrypt, N=2^17No
Admin session tokenSHA-256 of the cookieNo — a database dump does not hand over live sessions
Device bearer credentialSHA-256No — returned once, at activation
Enrollment credentialSHA-256 + a 6-character hintNo — shown once
One-time codeSHA-256No — which is why a test has to read it off the wire
Tenant signing key (private)AES-256-GCM under MASTER_KEYYes, with the key
Tenant SMS credentialsAES-256-GCM under MASTER_KEYYes, with the key — they must be replayed to a third party

MASTER_KEY is in the environment, never in the database. **A database dump alone cannot

sign a licence or read a tenant's SMS credentials.**

Things that are never written down

Per the endpoint's own rules, and enforced on this side too:

request and in transient memory.

applied at write time rather than at read time.

The console's own surface

takes a string. Company names, hostnames and usernames are attacker-influenced text

rendered next to live controls, and two tests assert it holds.

has no client-side JavaScript, so the strictest possible policy costs nothing.

style="…" attributes outright. Every dynamic dimension in the UI — bar widths, column

heights — is SVG geometry instead, and a test walks every page asserting that not one

inline style attribute exists. The day somebody adds one it will silently fail to

render, so the test is what catches it.

be added without it. The session cookie is SameSite=Lax as well.

403.

is a 404, and there is a test that uses a real id from a real other tenant.

do the same work — the password is verified against a dummy hash when the address is

unknown.

SSRF

The http SMS provider posts to a URL the tenant chooses. sms/guards.ts resolves the

host, checks the resolved address, and connects to that address, so a DNS record that

changes between the check and the connection does not help. Loopback, link-local,

169.254.169.254 and private ranges are refused, at save time and at send time.

Entitlement

Only a signature-verified Stripe webhook writes subscriptions.seats. Events are applied

at most once (Stripe's event id is a primary key). A failure answers 500 so Stripe retries.

A seat reduction never releases anybody's seat — choosing which person loses their

second factor is not a decision a webhook handler makes silently, and doing it there would

let a billing event unprotect a machine.

A failed payment does not immediately revoke a licence. past_due still entitles,

because a card that bounced must not lock an organisation out of its own computers while

Stripe spends days on dunning.

Known limits

Nothing in this product changes that.

logoff data and no "signed in right now" figure, anywhere, on purpose.

the verification in this design. EndpointTrust has no HR record to check against.

second factor on the console itself yet. That is the most obvious next piece of work in

this repository.

a colleague's account and hands over a generated password shown once. That is a deliberate

trade — an invitation flow needs a mail sender, a signed token, an expiry and a reset

path, and each is another way into an organisation's logon policy — but it does mean a

forgotten administrator password is an operator's problem, not a self-service one.