Skip to content

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 with authorization 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 TestARevokeOnTheControlPlaneIsRefusedOnARunner tests exactly that.
  • The signing key is required. AUTH_JWT_SECRET signs 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_REQUIRED and 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_verified true. Otherwise the result is SSO verification failed.
  • The allowlist fails closed. AUTH_ALLOWED_EMAILS lists 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 with This 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 rolePolicy tier
admin, org_admin, ownerROLE_ORG_ADMINAdmin
writer, editorROLE_WRITERWriter
reader, viewerROLE_READERReader
service, service_account, svcROLE_SERVICE_ACCOUNTReader
anything else, including emptynone: the request is refusednone

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, ListConnections and ListInstances. 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 a PermissionDenied error.
  • 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 tokenWhat protects it instead
/semantics.v1.IdentityService/LoginA caller cannot present a session before it has one.The credential, MFA and SSO checks described above.
/semantics.v1.ActivationService/ValidateLicenseThe 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:

RouteAuthentication
GET /auth/configPublic by design. It returns only the four public fields listed above.
GET /readyz, GET /livezPublic, so the kubelet can probe them. They return check names, pass/fail results and the mesh CA’s expiry date.
/twilio/voice, /twilio/messageA 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/streamA short-lived signed token (10 minutes) in the stream URL FACE generated, checked before the WebSocket upgrade.
POST /webrtc/signalA 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.