Identity & access
Every gRPC call to FACE passes through one of two interceptors, AuthInterceptor
(unary calls) or StreamAuthInterceptor (streaming calls). Both live in
grpc/cmd/auth_interceptor.go and apply the same rules. Only the short list of
methods below runs without a session token.
Sessions
- The token is opaque and random. A session token carries 256 random bits, base64url-encoded. The client never decodes it. The server keeps only the token’s SHA-256 hash, in FACE’s encrypted application root, together with the role, email and tenant it recorded at sign-in. None of those values come from the client.
- Every request is validated. The interceptor looks the token up in the
session store on every call, reading through a cache that is at most 2 seconds
old. A missing, unknown, revoked or expired token is refused with
invalid or expired session token. A request with no token is refused withauthorization token is not provided. - Lifetime. Sessions last 12 hours.
- Revocation. Logout ends every session of the signed-in subject, as well as
the bearer that made the call. The subject is taken from the verified request
context, never from a token in the request body. Changing or deleting an
account ends that account’s sessions, because a session carries the role it
was issued with. A revocation on any replica is honoured by every replica and
runner within the 2-second cache window, and
TestARevokeOnTheControlPlaneIsRefusedOnARunnertests exactly that. - The signing key is required.
AUTH_JWT_SECRETsigns the short-lived MFA challenge. FACE refuses to start without it and never generates one. A key generated per process would differ between replicas.
Sign-in methods
IdentityService/Login supports three methods.
Password, with optional TOTP
- Passwords are stored as bcrypt hashes.
- If the account has TOTP enrolled, a correct password does not issue a
session. It returns
STATUS_MFA_REQUIREDand a signed MFA challenge that expires after 5 minutes. A session is issued only after a valid TOTP code arrives with that challenge. TOTP verification accepts one 30-second step of clock skew either way. - With
AUTH_GOOGLE_ONLY=true, password sign-in is disabled for everyone except the operator-provisioned break-glass administrator account. An SSO outage can therefore never lock the operator out.
OIDC single sign-on
SSO is enabled when both OIDC_ISSUER and OIDC_CLIENT_ID are set. The issuer
can be any standards-compliant OpenID provider. FACE verifies the ID token itself
against that issuer using the shared security/oidc verifier, so no identity
broker sits in between.
Verifying who someone is does not decide whether they may enter:
- The token must carry an email address with
email_verifiedtrue. Otherwise the result isSSO verification failed. - The allowlist fails closed.
AUTH_ALLOWED_EMAILSlists who may sign in through SSO. If SSO is configured and the allowlist is empty, nobody can sign in through SSO (SSO is not available on this instance), and the operator gets a specific log line explaining why. A verified identity that is not on the list is refused withThis account is not authorized for this instance. - A first-time allowed user is given a local account with the lowest role
(
Reader) and a random password. That password is never derived from the OIDC subject, so password sign-in cannot become a predictable way into an SSO account.
Public bootstrap values
GET /auth/config tells the browser cockpit how to sign in. It returns exactly
four fields and nothing else: the public OIDC client ID, a boolean saying whether
SSO is configured, the operator-declared deployment variant, and
serving_seeded_data, which is always false. No secret ever passes through this
endpoint.
Roles and authorisation
A session’s stored role is mapped through the shared role vocabulary, and the mapping fails closed:
| Stored role (accepted spellings) | FACE role | Policy tier |
|---|---|---|
admin, org_admin, owner | ROLE_ORG_ADMIN | Admin |
writer, editor | ROLE_WRITER | Writer |
reader, viewer | ROLE_READER | Reader |
service, service_account, svc | ROLE_SERVICE_ACCOUNT | Reader |
| anything else, including empty | none: the request is refused | none |
An unrecognised role never falls back to a default. The request is refused with
this account's role is not recognised on this instance, so it grants no access.
Beyond the role, the interceptors enforce the following:
- Service accounts cannot manage the instance. A service account is refused
on configuration, activation, identity and access-management methods with
RBAC: Service Accounts cannot perform management actions. It keeps the reads a runner needs:ListRunners,ListConnectionsandListInstances. In the policy engine it holds the Reader tier, so it cannot drive the agent loop’s write-tier tools. - Licence and subscription checks fail closed. If a signed (Ed25519) licence
is configured, it takes precedence over the tenant registry. An invalid or
expired licence refuses every call with
license expired or invalid. Features restricted by subscription tier are refused with aPermissionDeniederror. - Tenant scoping. The verified tenant is the only tenant a handler may act
on. A handler that accepts a tenant ID in its request has to compare it with
the verified one. The service-account key issuer does exactly that and refuses
with
cannot issue a service account for another tenant. Only an organisation admin can issue service-account keys. - Scoped tokens stay scoped. The token FACE mints for a carrier’s media
socket is refused as an API credential
(
this token is scoped to the media socket and cannot be used for API calls) and refused on the WebRTC signalling endpoint.
The unauthenticated surface
This is the complete list of gRPC methods that skip session authentication.
cmd/auth_exemptions_test.go parses both interceptors and fails if any other
method reaches its handler with no further check. The same test fails if one of
the two gated entries below loses its gate.
| Method (prefix) | Why it has no session token | What protects it instead |
|---|---|---|
/semantics.v1.IdentityService/Login | A caller cannot present a session before it has one. | The credential, MFA and SSO checks described above. |
/semantics.v1.ActivationService/ValidateLicense | The licence key is itself the credential. | It returns a single boolean: whether the key is an Ed25519-signed licence within its validity window. |
/semantics.v1.RaftService/ | Consensus peers carry no user token. | Served only on the mesh mTLS listener. See Zero-trust architecture. |
/semantics.v1.ModelService/ | The backend dials its own model service. | Loopback callers only. Any other caller gets ModelService is loopback-only. |
/grpc.reflection. | Debugging tools carry no user token. | Loopback callers or verified mTLS peers only. Any other caller gets server reflection requires a loopback caller or a verified mTLS peer. |
A small set of plain-HTTP routes sits on the same server:
| Route | Authentication |
|---|---|
GET /auth/config | Public by design. It returns only the four public fields listed above. |
GET /readyz, GET /livez | Public, so the kubelet can probe them. They return check names, pass/fail results and the mesh CA’s expiry date. |
/twilio/voice, /twilio/message | A valid X-Twilio-Signature is required. If the signing token is not configured, the endpoint refuses every request (503) rather than accepting unsigned ones. Inbound messages are accepted only from senders that resolve to a known identity. |
/twilio/stream | A short-lived signed token (10 minutes) in the stream URL FACE generated, checked before the WebSocket upgrade. |
POST /webrtc/signal | A session bearer token, verified exactly as on the gRPC path. |
The voice line is the one prompt path a person can reach without an account, since it sits behind a phone number. The controls on that path are described in AI safety.
What these controls do not establish
- Session authentication is the authentication for user traffic. A verified
mTLS peer identity is recorded as an attribute, never accepted in place of a
session (
TestAVerifiedMTLSPeerStillNeedsABearerToken). Account hygiene (strong passwords, TOTP enrolment, a tight SSO allowlist) therefore bears the full weight of user authentication. - MFA is per account. TOTP is enforced for accounts that have enrolled. FACE does not force every account to enrol.
- One instance serves one customer. Sessions and accounts are deployment-wide, and each customer gets its own FACE instance. The boundary between customers is the instance, not per-request routing inside one instance.
- Revocation takes up to 2 seconds to reach every replica. It is prompt, but not instantaneous.
- Typed context keys prevent collisions, not false values. The identity on a request is exactly as trustworthy as the session check that produced it.