Shared Signals (SSF, CAEP and RISC)
iya-sts is a Shared Signals transmitter
(OpenID SSF 1.0),
and a small receiver as well. A receiver agrees a stream with it, and it
then delivers a Security Event Token
(RFC 8417) when something happens,
by push (RFC 8935) or poll
(RFC 8936). Subjects use the
RFC 9493 formats. Both vocabularies
that run over SSF are implemented:
CAEP 1.0 (what happened to a
session) and RISC 1.0
(what happened to an account). Every trust realm has its own transmitter,
streams, queues and registers.
Every other protocol here answers a request. This is the one that talks
back: it sends an event that nobody asked for, at the moment the event
happens.
Features
SSF is the pipe, not the vocabulary
SSF covers how a receiver and a transmitter agree a stream, who the events on
it are about, what the events travel in and how they are delivered. It defines
only two events of its own, and both are about the pipe:
- verification: the receiver asked whether the stream is alive, and the
answer comes back along the normal delivery path. This is the only
end-to-end test a stream has. A 200 from the management API means the
configuration was accepted. It says nothing about whether an event can reach
the receiver.
- stream updated: the stream’s status changed, and the receiver is told in
band. A receiver does not have to ask for this event to get it.
The vocabularies run over that pipe:
| Vocabulary |
Events |
About |
Emitted on its own when |
| CAEP |
8 |
a session |
someone signs in, uses single sign-on, signs out, a session expires, a person re-authenticates at a different acr, any credential of a person changes, a directory change moves a claim of somebody holding live tokens, a registered device’s compliance, risk level or credentials change |
| RISC |
14 |
an account |
a person is deleted, disabled or enabled; any mail address or telephone number changes or is removed, or is given to an account after another released it; the recovery address is added, changed, removed or verified; an administrator resets a password (optionally marking it compromised) or issues a reset or activation link, or sets pwdReset over LDAP; recovery codes are cleared, confirmed or used; the account holder opts out or back in on /portal/signals; a person’s registered device is compromised or removed; this service detects a compromised credential (below) |
Since #164 every CAEP and RISC event type has an act here that sends it: a
registered device’s compliance changing (device-compliance-change), its risk
level changing (risk-level-change, principal DEVICE), its keys and Native
SSO secret changing (credential-change), and a person’s device compromised or
removed (RISC credential-compromise and the deprecated sessions-revoked,
with the device beside the person in the subject). Devices lists
each. You can still emit any of them by hand from the console or the
management API.
CAEP events covers what triggers each CAEP event.
A compromised credential this service detects is RISC
credential-compromise
(#231):
| What is detected |
credential_type |
Also sent |
a password that signs in and appears in a data breach (risk.breachCheckAtSignIn) |
password |
account-credential-change-required, once until the password is changed |
| a security key whose signature counter went backwards (a clone) |
fido2-roaming or fido2-platform |
nothing more; the person’s risk standing goes to HIGH. The risk-response policy’s own credential-compromise reaction to that standing is not sent again, so a receiver gets one event for one key |
a certificate revoked with the reason keyCompromise (the portal, an enrollment protocol’s revoke, /admin/pki) |
x509 |
the CAEP credential-change revoke |
| the emailed second factor turned off after too many wrong codes |
urn:iya:sts:credential-type:email-otp |
the CAEP credential-change delete |
A one-time code presented twice is not an event: someone pressing submit
twice looks exactly like a replay. It is a risk-scoring signal instead
(totp-replay, see Risk scoring).
Credentials with no CAEP type use this service’s own URNs
(#236). CAEP 1.0 section 3.3.1
allows any credential type the two parties agree on, and a receiver that does
not know one still learns that a credential of the person changed:
| Credential |
credential_type |
| the emailed second factor |
urn:iya:sts:credential-type:email-otp |
| a SIOPv2 self-issued subject key |
urn:iya:sts:credential-type:self-issued-key |
| an ACME External Account Binding key |
urn:iya:sts:credential-type:acme-eab-key |
| a HOBA key |
urn:iya:sts:credential-type:hoba-key |
| a person’s Kerberos keys |
urn:iya:sts:credential-type:kerberos-key |
| a device key, a Native SSO device secret |
urn:iya:sts:credential-type:device-key, urn:iya:sts:credential-type:device-secret |
A SCEP challenge password is sent as password, with friendly_name naming
it. A CIBA user code is pin.
A credential cannot be written over LDAP
(#237). An add or modify of a
security key, an authenticator app, recovery codes, an app password, a
signing key pair, a HOBA key, a self-issued subject, the emailed factor,
Kerberos keys, a CIBA user code, an enrollment credential or a device secret
is refused with unwillingToPerform (53), for every bind in both modes, and
the refusal names the page or endpoint to use instead. Those doors check
what they store and send the events above. userPassword is still written
over LDAP; deleting it sends credential-change revoke.
RISC opt-out (section 2.8) is the account holder’s choice. On
/portal/signals a person can stop sharing security events about their
account. Their account enters opt-out-initiated; receivers keep being told
everything; and after risc.optOutDelayHours a scheduler job sends
opt-out-effective, after which only opt-out events are sent about them.
They can cancel during the wait, or opt back in afterwards. The wait stops
somebody who has just taken an account over from silencing it at once.
Only the holder moves it: an administrator’s Reset or Clear on the RISC
register keeps the account’s opt state, and a pending opt-out still becomes
effective on schedule.
Every value of mail, telephoneNumber and mobile counts. A second
address changing, or a mobile beside a telephoneNumber, is an
identifier-changed; an address removed is one with no new-value, and it
can then be identifier-recycled to another account. The recovery
address is the first mail value, which /portal/forgot-password mails:
its addition, change, removal or verification is
recovery-information-changed (a change is identifier-changed as well).
A recovery code used is recovery-information-changed, and at sign-in
recovery-activated too.
account-disabled carries a reason only when an administrator gives one
(hijacking or bulk-account, on the console’s disable form or as
riscReason on /admin-api/users/disable).
Five more events belong to this service rather than to any specification.
Each one has no subject, because it is about the issuer rather than about a
person. A receiver that does not know a type ignores it, as SSF says it should.
Type (urn:iya:sts:secevent:event-type:…) |
Sent when |
The document to fetch again |
signing-key-rotated |
a realm’s signing keys rotate (on the schedule, by hand, or in an emergency), or an operator pins a key pair as the realm’s signer for an algorithm or unpins it (reason: requested, sent when the pinned key is PUBLISHED, ahead of its use) |
jwks_uri, crypto_metadata_uri |
federation-key-rotated |
a realm’s OpenID Federation entity key rotates (in an emergency, the current and next keys are revoked), or a retired key is revoked |
entity_configuration_uri |
spiffe-authority-rotated |
a realm’s SPIFFE X.509 or JWT authority is rotated, or the certificate hierarchy under it is rebuilt |
bundle_uri; bundle_changed says whether the bundle itself changed |
tls-certificate-changed |
the certificate the main port presents is re-issued, or the service (or one node of it) starts presenting a certificate other than the one it last announced (reason: restarted). Sent to every realm’s streams |
certificate_uri |
kerberos-tickets-invalidated |
a krbtgt rotation keeps no previous version, so every TGT in the realm is refused: “rotate and invalidate” (reason: invalidated), or an ordinary rotation with krb5.retainedKeyVersions 0 (reason: nothing-retained) |
none |
The first four share one shape: realm, rotated as ` ->
`, `reason` (`scheduled`, `requested` or `emergency`, and for
`tls-certificate-changed` also `restarted`), the address above, and
`event_timestamp`. Each kind of key has its own type so that a receiver
subscribes only to the keys it pins, and every type it receives means one
action: fetch that document again. The refresh-token encryption keys are
never announced, because they are published nowhere.
**The listener's key is made at every start**, so a restart presents a new
certificate. The service keeps the fingerprint it last announced in its store
and compares it with the new certificate once the port is bound. If they
differ, it sends `tls-certificate-changed` with `reason: restarted`, where
`` is the certificate it last announced.
- **Where it works:** only where minted state survives a restart, which is
product mode on postgres or a cluster. In `memory` mode, on an `ldif` store
and in a single-process development service a restart is not announced.
Development builds a new Root at every start anyway.
- **In a cluster:** the record is the SERVICE's, not a node's. Every node's
start is announced, and `` may be a certificate another node
still presents. A receiver behind the balancer should pin the Root, not a
leaf.
- **Never announced:** a self-signed bootstrap certificate or one supplied
through `tls.certificateFile`.
**A stream this service deletes is told first.** SSF 1.0 has no event for a
deleted stream. So when an administrator deletes a stream (on the console or
through `/admin-api`), or `ssf.inactivityAction: delete` removes one, the
receiver is first sent `stream-updated` with status `disabled` and the reason,
and only then is the stream removed. On a push stream the push is attempted
before the stream goes. On a poll stream the SET leaves with the stream unless
the receiver polls in between, as with any disable. A receiver that deletes
its own stream is sent nothing, because it asked.
### Discovery and stream management
SSF fixes no paths. A receiver starts from the transmitter configuration
metadata and discovers every endpoint from it:
| Path | What it is |
|---|---|
| `GET /.well-known/ssf-configuration` | the transmitter configuration metadata. **Never gated**, and still answers while `ssf.enabled` is off |
| `GET /.well-known/ssf-configuration/realm/{id}` | the same document for a trust realm's transmitter, at the path SSF 1.0 section 7.2 builds from an issuer with a path |
| `/ssf/stream` | stream management: `POST` creates, `GET` reads, `PUT` and `PATCH` update, `DELETE` deletes |
| `/ssf/status` | read (`GET`) or change (`POST`) a stream's status |
| `POST /ssf/subjects/add`, `POST /ssf/subjects/remove` | add (an empty 200) or remove (204) a subject, SSF 1.0 sections 8.1.3.2 and 8.1.3.3 |
| `POST /ssf/verify` | ask for a verification event |
| `POST /ssf/poll` | RFC 8936 poll delivery and acknowledgement |
| `POST /ssf/receive`, `GET /ssf/received` | this service as a **receiver** (below) |
| `GET /ssf` | a description of the family; `?format=json` returns the same as data |
In a trust realm every path is under `/realm/{id}`. A realm's metadata is at
both `/realm/{id}/.well-known/ssf-configuration` and the section 7.2 form above,
which inserts the well-known name between the host and the issuer's path. The
subject paths use a slash where SSF's own examples use a colon
(`/subjects:add`). A receiver reads `add_subject_endpoint` from the metadata,
so it never sees the difference. `spec_version` is `1_0`.
A receiver picks its event types in `events_requested`. The transmitter
answers with `events_delivered`, which is the overlap with what it offers
(`ssf.eventsSupported`, `caep.eventsSupported` and `risc.eventsSupported`,
combined).
### Each stream belongs to its receiver
SSF 1.0 section 8 has the transmitter associate each receiver with its streams
and its `aud` values. Here, a stream belongs to the identity that created it:
a client's `client_id`, a person's `sub`, a GNAP client instance, or a Basic
username.
* **Another receiver's stream answers 404**, word for word as for a stream that
does not exist, on every endpoint that takes a `stream_id`. That is the
specification's own wording ("no Event Stream with the given stream_id for
this Event Receiver"), and a different answer would reveal which ids exist.
* **`GET /ssf/stream` with no `stream_id` lists only the caller's own
streams**, and an empty list when it has none.
* **`aud` is set by the transmitter** (section 8.1.1 lists it as
Transmitter-Supplied). A new stream's `aud` is the identifier the receiver
authenticated as. A receiver may instead send `aud` naming one or more of the
names it is associated with: that identifier, plus the application
identifier and every `ssfReceiverId` on its registered application entry. Any
other value is refused. An update may carry `aud`, or any other
Transmitter-Supplied member (`iss`, `events_supported`, `events_delivered`,
`min_verification_interval`, `inactivity_timeout`), only unchanged.
* **A person's stream is about that person.** A stream created with a
person's own credential is theirs, and it carries only events about them.
That credential is an access token a client obtained for them (for example
by the authorization code grant), or their directory name and password over
Basic. Events about anybody else are not delivered to it, whatever its
subject list and `ssf.defaultSubjects` say, and an Add Subject naming
anybody else answers `403 access_denied`. A relying party that needs events
about everybody creates its stream with its own client credentials token.
`ssf.personStreamsSelfOnly` turns this off.
* **`ssf.maxStreams` is per receiver.** Creating a stream past it answers 403.
* **The console's and portal's own streams belong to no remote receiver** and
are managed only on `/admin/ssf` and through `/admin-api`.
### Subjects
A subject can be in any of the eight RFC 9493 formats under their registered
names (`account`, `email`, `iss_sub`, `opaque`, `phone_number`, `did`, `uri`,
`aliases`), in one of SSF 1.0 section 3.5's three (`jwt_id`,
`saml_assertion_id`, `ip-addresses`), or be SSF's **complex subject**, which
carries `"format": "complex"`. The complex subject's members (`user`,
`device`, `session`, `application`, `tenant`, `org_unit` and `group`, plus any
additional name section 3.3 allows) are what make *this session was revoked*
possible to say at all. Each format has a **closed** member set. A subject
carrying a member that its format does not define is refused, and the refusal
names the member, because a conforming receiver has to reject one. Nesting a
complex subject inside another is refused as well. The pre-RFC names
`issuer_subject_id` and `decentralized_identifier` are not accepted.
A stream that names a **person** covers a complex subject naming one of that
person's sessions. `ssf.defaultSubjects` controls what a stream with no
subjects covers. With `ALL`, the default, it covers everybody, and naming a
subject narrows it to the named subjects. Removing the last subject widens it
back to everybody. With `NONE` it covers nobody until a subject is added.
A stream a person owns covers only subjects that name that person, in any of
these forms:
* an `iss_sub` with their subject, including the `sub` a pairwise client is
told;
* an `email`, `phone_number` or `account` subject for their account;
* an `aliases` subject in which any alias names them;
* a complex subject whose `user` member names them.
A complex subject with no `user` member names nobody on such a stream.
### Push and poll delivery
* **Poll (RFC 8936).** The receiver calls `POST /ssf/poll` — the stream's
`delivery.endpoint_url`, which names the stream in its query
(`?stream_id=…`), since RFC 8936's poll endpoint is per stream — and nothing is
dialled. One poll returns at most `ssf.pollMaxEvents` SETs and sets
`moreAvailable` when more are waiting. An acknowledged SET is never handed
out again. Until it is acknowledged, it may be returned again, as RFC 8936
section 2.4 allows.
* **Push (RFC 8935).** The service POSTs each SET to the endpoint the receiver
named on its stream, with the stream's `authorization_header`. A 202 counts
as delivery. A 400 with `{err, description}` means the receiver **refused**
the SET, and it is recorded separately from a network failure. A 200 or 204
is accepted, with a note that it is not quite right.
Push is the one place in SSF where this service makes a request to an address
that a caller chose. Four settings limit it: `ssf.pushDelivery` turns it off,
`ssf.pushAllowedHosts` is a host allowlist (empty by default, meaning any
host), only `https` with the receiver's certificate verified is used — plain
http only with `ssf.pushAllowHttp` and verification off only with
`ssf.pushSkipTlsVerification`, **both in development mode only** (#171);
product reaches a privately certified receiver through `ssf.pushCaFile` — and
every push has a timeout, a cap on the response size and **no redirects**. With push
turned off, SSF still works in full over poll, and `delivery_methods_supported`
lists only poll.
### Stream status
SSF 1.0 section 7.1.2 defines three statuses:
* **enabled**: events are delivered.
* **paused**: nothing is delivered and events **keep queueing**. On a push
stream they are pushed, in the order they happened, when the stream is
enabled again.
* **disabled**: the queue is **dropped**, and the stream's log records how many
events went.
Every status change sends a `stream-updated` event on the stream, whether or
not the receiver asked for that type (section 8.1.5 allows it, and requires it
when the transmitter changes the status). It goes **before** the stream stops
when it is paused or disabled, and after it starts again when it is enabled. On
a poll stream that has been paused or disabled, `POST /ssf/poll` still hands out
the `stream-updated` event and nothing else. Setting the status a stream
already has sends nothing.
A new stream starts as `ssf.streamStatusOnCreate` (`enabled` by default). Set
it to `paused` to test a receiver that has to enable its own stream first.
### Inactivity and transmitter-initiated verification
* **`ssf.inactivityTimeoutS`** (section 8.1.1's `inactivity_timeout`, 0 by
default, meaning none): a stream whose receiver has made no management call
about it, and on a poll stream no poll either, for that long is paused,
disabled or deleted, as `ssf.inactivityAction` says. A pause or disable is
announced with `stream-updated` first. The value is published on every stream
configuration while it is set. It is off by default because a push receiver
never needs to call back after creating its stream.
* **`ssf.verificationEveryS`** (0 by default): every enabled stream is sent a
verification event, with no `state`, when this transmitter has sent it none
for that long. **Send a verification event** on `/admin/ssf` does the same
once.
Both run as the `ssf.stream-maintenance` scheduler job every
`ssf.streamMaintenanceSweepS`. The console's and portal's own streams are left
alone.
### Verification
`POST /ssf/verify` answers **204 as soon as the event is queued**. Section
8.1.4.2 says a receiver must not depend on the event arriving synchronously,
so a failed delivery is not reported in the response: it goes to the stream's
dead-letter queue and log, like any other failed push. A paused stream holds
the event until it is enabled, and a disabled stream refuses the request with
400. The verification event is sent even if the stream did not agree to the
type.
### Signing
Every SET is signed with `ssf.signingAlgorithm`, using the same signer as
every other token here. You can pick any algorithm from the full list: RS256,
the PS and ES families, EdDSA, and the post-quantum ones (ML-DSA, SLH-DSA and
the composite ML-DSA algorithms). A SET has `typ: secevent+jwt`.
`ssf.setCertificateHeader` controls whether it carries the signing key's
certificate chain, as `x5u` (the default), `x5c`, both, or neither.
### Authentication: three schemes, two scopes
The metadata document lists the schemes it accepts in
`authorization_schemes`:
* **OAuth 2.0**: an access token from this service with `ssf:read` or
`ssf:write`. DPoP- and certificate-bound tokens are checked the same way as
at every other protected endpoint.
* **HTTP Basic**: a directory person's name and password, so that a client
that has no token flow yet can still reach every endpoint. Basic carries no
scope, so a Basic caller gets both. `ssf.authBasic` turns the scheme off.
In product mode a person who holds or must hold a second factor is refused
their own password with the `401` a wrong one gets, and uses an
[app password](/iya-sts/authentication.html#the-password-only-doors-and-app-passwords) scoped to `ssf`.
* **GNAP**: a key-bound GNAP access token whose access includes `ssf:read` or
`ssf:write`, so that a GNAP web application can own a stream itself. See
[GNAP](/iya-sts/gnap.html).
`ssf:read` lets a caller read a stream, its status and the poll queue.
`ssf:write` is needed to create, change or delete a stream, change subjects or
status, or ask for verification. A read token that attempts a write gets a 403
that names the scope it needs.
### Limiting what an application's stream may carry
An application entry can carry `ssfAllowedEvents`: `caep`, `risc`, or
particular event type URIs. A stream owned by that application then gets only
those types. The limit applies when the stream is created or updated **and**
again at every delivery, so lifting a limit does not release types that were
held back when the stream was agreed, and a receiver that created its stream
before the limit was tightened gets no way around it. SSF's own two events are
always allowed. An empty value means no limit.
### Dead streams and dead letters
A push that cannot be delivered is not retried unless `ssf.pushRetries` says
so. The default is 0, because a transmitter that retried would hide a
receiver's one-off failure from whoever is testing it. When retries are on,
only failures that might go differently next time are retried: no connection,
a timeout, a 5xx or a 429. A 400 is never retried.
A SET that could not be delivered goes to its stream's **dead-letter queue**
with the reason, the error code and the receiver's HTTP status. That happens
when a push fails for good, when the push backlog is full, and when the stream
is dead. A push stream whose pushes have all failed for
`ssf.deadStreamTimeoutS` is declared **dead**. Nothing more is pushed to it,
and once per timeout the oldest dead letter is pushed as a probe. A dead
stream is also **paused**, with a `stream-updated` event attempted first,
because stopping delivery is a status change the receiver must be told about
(section 8.1.2). When a probe succeeds, or an operator revives the stream, it
is enabled again and announced, and whatever it held is pushed. A stream that
the receiver or an operator paused is never enabled by a revival. Nothing
resends a dead letter except a probe.
Failures are logged once per stream (when it dies and when it revives) and
once per sweep as a summary, never once per SET.
### This service as a receiver
`POST /ssf/receive` accepts a SET pushed **at** this service, for testing a
client that acts as the transmitter. It verifies the signature when it can
find a key. A SET that another party signed is reported as *not verifiable
here*, not as invalid. `GET /ssf/received` lists what arrived.
`ssf.receiveRequireSignature` makes it refuse a SET whose signature fails, the
way a strict receiver would. **Product mode always refuses one**, at this
endpoint and at the console's and portal's own receivers (#117).
Every SET that arrives is recorded, and it is then refused if its header's
`typ` is not `secevent+jwt` (section 4.1.1), if its `iss` is not one
`ssf.receiveIssuers` lists (`invalid_issuer`, section 4.1.6), or if its `aud`
names nothing `ssf.receiveAudiences` lists (`invalid_audience`). Left empty,
the first means this realm's own transmitter issuer and the second means the
endpoint's own URL, for example `https://host/ssf/receive`. The console's and
portal's receivers make the same `typ` and `iss` checks against their own
streams. To receive a federation partner's streams, see *Receiving from a
federation partner*, below.
### Receiving from a federation partner
A realm can receive CAEP and RISC events from a federation partner's
transmitter ([#153](https://github.com/rcbj/iya-sts/issues/153),
[#373](https://github.com/rcbj/iya-sts/issues/373)). The partner is
configured as a **federation relationship** on Protocols → Federation, and
its Shared Signals are a section of that relationship. A partner that signs
nobody in here — a device manager, an endpoint agent, an HR system — is an
`ssf` relationship ([#374](https://github.com/rcbj/iya-sts/issues/374)).
1. **Turn the signals on** on a relationship where this service is the
service provider: `fedSignalsEnabled` (an `ssf` relationship has it on
from the start). The partner's SSF issuer is `fedSignalsIssuer`, or the
relationship's `fedPeer` when that is empty. Set `fedSignalsIssuer` for a
SAML or WS-Federation partner, whose entityID is not its SSF issuer.
2. **Say how this realm authenticates to the partner**: client credentials
(`fedSignalsTokenUrl`, `fedSignalsClientId`, `fedSignalsClientSecret`,
each falling back to the relationship's own `fedTokenUrl`, `fedClientId`
and `fedClientSecret`), or `fedSignalsBearer`. The two signals
credentials are sealed under the key-encryption key wherever keys
persist.
3. **Create the stream**: *Create stream* on the relationship's page, or
`POST /admin-api/federation/signals-create-stream`. This fetches the
partner's `/.well-known/ssf-configuration` and `jwks_uri` first and
checks the document names the issuer. A poll stream
(`fedSignalsDelivery` `poll`) is polled every `ssf.foreignPollS` seconds.
A push stream is given `/federation/signals/{id}` and an authorization
header only the two services know. `fedSignalsEvents` is what the stream
asks for.
4. **Link people.** For a partner people sign in through, the links its
sign-ins wrote are what its `iss_sub` subjects match: `iss` is the
partner (or its SSF issuer) and `sub` the identifier it signs them in
with — for SAML, the persistent NameID. For an `ssf` relationship, link
each person on their page, with the issuer and subject its `iss_sub`
events name them by, or the issuer `opaque` and the id of an opaque
subject. An `email` subject is matched only where the relationship sets
`fedSignalEmailMatch`.
A Security Event Token is acted on only if it verified: its signature against
the keys the partner's SSF configuration names, `typ`, `iss`, the stream's
`aud`, and a `jti` never seen before. Product mode refuses an unverified one.
What it leads to is the `signal-response` XACML policy's decision. By
default:
| Partner | Events | Reaction here |
|---|---|---|
| signs people in | `session-revoked`, `credential-change`, `sessions-revoked`, `credential-compromise`, `account-purged` | end the sessions **this relationship** started for the person — the one a complex subject's `session` names, else every one. A local sign-in and other partners' sessions are untouched |
| signs people in | `account-disabled` | **block this partner's sign-ins** of the person, and end the sessions it started. The account stays enabled; other ways of signing in keep working |
| signs people in | `account-enabled` | lift that block |
| `ssf` only | anything about a person | recorded, nothing more. A global sign-out and an account lock exist for an operator's policy to permit |
| either | `device-compliance-change` | set the device's compliance |
| signs people in | `session-revoked`, `account-disabled`, `account-purged`, `credential-compromise` | **revoke the person's grants and tokens** (#432): every GNAP grant they approved, and every OAuth grant (Grant Management), access and refresh token and unredeemed authorization code held for them. A `session-revoked` reaches only what was issued on the sessions this relationship started. Sessions are left to the rows above. Turned off with `ssf.signalsRevokeGrants` |
`ssf.signalsRevokeGrants` is **on** by default. Turning it off records the
reaction as *skipped* and revokes nothing — so an account the partner reports
compromised keeps every grant and token it gave clients here until somebody
revokes them by hand; turn it off only where the partner's signals are not
trusted that far, and prefer narrowing `partnerRevokeGrantEvents` in the
`signal-response` policy instead.
An administrator can lift a block with *Unblock* on the relationship's page
(`POST /admin-api/federation/signals-unblock`). Every arrival from every
partner, and every block, is on Monitoring → Signals from partners
(`/admin/ssf/transmitters`, `GET /admin-api/ssf/transmitters`).
Development records what it would do and does nothing unless
`ssf.actOnSignalsInDevelopment` is on.
### The console and the portal are registered receivers
Each trust realm has two streams seeded by the service itself, one for the
admin console and one for the user portal. Both ask for every CAEP and RISC
event type, and both take delivery over a real RFC 8935 push to an endpoint of
their own. What arrives is shown at `/admin/signals` and `/portal/signals`.
[Signals received](/iya-sts/signals-received.html) explains how this works, and why an
empty page has five possible causes. **Both act on what they receive**: a
verified event the `signal-response` policy permits ends the receiving
surface's own sessions for the person it names (product mode; development
records it). An unverified event is never acted on.
### Deliberate defects
A transmitter that is always correct is hard to write error handling against,
so each of these switches produces a known mistake.
**`ssf.legacySubClaim` and `ssf.breakSetSignature` make a SET wrong, and are
honoured in development mode only** (#104), **and so is
`risc.googleSubjectType`** (#181), whose `subject_type` RISC 1.0 section 3.1
says new services MUST NOT use. A realm in product mode ignores them where the
SET is built and signed — even one still stored from before the realm was
switched, which is logged once (`STS-CORE-0106`) — and refuses turning them on
(`STS-CORE-0103`). The other two produce SETs that conform to their
specifications, and are honoured in both modes.
| Setting | What it breaks |
|---|---|
| `ssf.legacySubClaim` | adds the deprecated `sub` claim beside `sub_id` |
| `ssf.breakSetSignature` | changes one character of the signature after signing. The first character is changed, not the last, because the last character's padding bits are discarded and a changed last character still verifies |
| `caep.omitEventTimestamp` | leaves out `event_timestamp`, which is optional and which many receivers assume is present |
| `risc.omitEventTimestamp` | the same, on `credential-compromise` |
| `risc.googleSubjectType` | spells the subject discriminator `subject_type` instead of `format`, as the RISC specification's own section 3.1 records one production transmitter doing |
### Not implemented
* **Subjects are not checked.** A stream may name somebody who has never been
here, which is what a receiver's *I do not know this subject* path needs.
* **`verified: true` on Add Subject is believed.** There is no confirmation
step to skip.
* **There is no console or API control that creates a stream.** A stream
holds a delivery endpoint that this service will call, and that address may
only come from a receiver that authenticated at `POST /ssf/stream`.
* **A realm receives only from a transmitter an administrator registered**
([#153](https://github.com/rcbj/iya-sts/issues/153)). It never follows a
URL a SET or a request names; every address comes from that issuer's own
configuration document.
## Development and product mode
| | Development | Product |
|---|---|---|
| Credential at `/ssf/*` | required, but only a check at the door: any name and any password except `invalid` passes Basic. A token carries `ssf:read` or `ssf:write` only when its client declares them in `oauthAllowedScope` (in both modes), and is honoured only while it still does (`STS-SSF-0107`) | required, and a Basic password is checked against the person's hashed `userPassword` |
| Access tokens | verified | verified |
| Subjects of automatic events | a person with no `mail` gets an invented `@example.com` address | a real value from the directory entry, or the issuer/subject pair. RISC's two identifier events are not sent when there is no real value |
| Streams, queues, CAEP and RISC registers | in memory, lost on restart | persisted with other minted state (product mode on postgres) |
See [What is not checked](/iya-sts/what-is-not-checked.html).
**Running several nodes.** Every node needs the same `global.port`, because
the console's and portal's own streams are pushed to
`:`. `global.publicBaseUrl` has to be pinned, because
it becomes every SET's `iss`. Active-active mode refuses to start while it is
empty.
## Configuration
There are 63 settings in three groups, and the SSF table also lists one GNAP
setting that affects streams. Every one can be changed while the service runs
except `ssf.internalReceivers`. The CAEP and RISC groups have no
effect on SSF itself: turning either vocabulary off only removes its event
types from what a stream may ask for.
### Shared Signals (`ssf.*`)
| Setting | Environment variable | Default | Runtime? | What it does |
|---|---|---|---|---|
| `ssf.enabled` | `STS_SSF_ENABLED` | `true` | yes | Turns the whole family on or off. When off, the routes stay and answer 501, and the metadata still answers. |
| `ssf.issuer` | `STS_SSF_ISSUER` | *(empty)* | yes | The `iss` of every SET and of the metadata. Empty means this realm's base URL. |
| `ssf.signingAlgorithm` | `STS_SSF_SIGNING_ALGORITHM` | `RS256` | yes | The JWS algorithm every SET is signed with, post-quantum ones included. |
| `ssf.setCertificateHeader` | `STS_SSF_SET_CERTIFICATE_HEADER` | `x5u` | yes | Whether a SET names its signing key's certificate chain: `none`, `x5c`, `x5u` or `both`. |
| `ssf.deliveryMethods` | `STS_SSF_DELIVERY_METHODS` | `urn:ietf:rfc:8935,urn:ietf:rfc:8936` | yes | Which delivery methods the service agrees to. `push` and `poll` are accepted as shorthand. |
| `ssf.defaultSubjects` | `STS_SSF_DEFAULT_SUBJECTS` | `ALL` | yes | What a stream with no subjects covers: everybody (`ALL`) or nobody (`NONE`). With `ALL`, naming a subject narrows the stream to the named subjects, and removing the last one widens it back to everybody. |
| `ssf.personStreamsSelfOnly` | `STS_SSF_PERSON_STREAMS_SELF_ONLY` | `true` | yes | A stream created with a person's own credential (an access token a client obtained for them, or their name and password over Basic) carries only events about that person, whatever its subject list and `ssf.defaultSubjects` say, and Add Subject naming anybody else is refused with 403. Off, such a stream is treated like a client's. |
| `ssf.streamStatusOnCreate` | `STS_SSF_STREAM_STATUS_ON_CREATE` | `enabled` | yes | The status a new stream starts in. `paused` makes the receiver enable its own stream. |
| `ssf.minVerificationInterval` | `STS_SSF_MIN_VERIFICATION_INTERVAL` | `60` | yes | The `min_verification_interval` the service publishes. A stream that asks for less is refused. |
| `ssf.verificationRateLimit` | `STS_SSF_VERIFICATION_RATE_LIMIT` | `false` | yes | Answers 429 to a verification request that comes sooner than the published interval. |
| `ssf.criticalSubjectMembers` | `STS_SSF_CRITICAL_SUBJECT_MEMBERS` | *(empty)* | yes | The complex-subject members a receiver must understand. Adding a complex subject that leaves one out is refused. |
| `ssf.eventsSupported` | `STS_SSF_EVENTS_SUPPORTED` | verification, stream-updated | yes | Which of SSF's own two event types are offered. |
| `ssf.pushDelivery` | `STS_SSF_PUSH_DELIVERY` | `true` | yes | Whether the service may push SETs at all. When off, only poll is offered. |
| `ssf.pushAllowedHosts` | `STS_SSF_PUSH_ALLOWED_HOSTS` | *(empty)* | yes | Hosts the service may push to. Empty means any host. |
| `ssf.pushAllowHttp` | `STS_SSF_PUSH_ALLOW_HTTP` | `false` | yes | Allows `http://` endpoints, in development mode only. This service's own receivers are exempt in both modes. |
| `ssf.pushSkipTlsVerification` | `STS_SSF_PUSH_SKIP_TLS_VERIFICATION` | `false` | yes | **Development only — a warning.** Pushes to a receiver whose certificate nothing here trusts. Ignored in product, and refused on write there. |
| `ssf.pushCaFile` | `STS_SSF_PUSH_CA_FILE` | *(empty)* | yes | A PEM file of CA certificates a receiver may chain to, beside node's own store. |
| `ssf.pushTimeoutMs` | `STS_SSF_PUSH_TIMEOUT_MS` | `10000` | yes | How long to wait for a receiver to answer a push. |
| `ssf.pushMaxResponseBytes` | `STS_SSF_PUSH_MAX_RESPONSE_BYTES` | `65536` | yes | How much of a receiver's answer is read before the push counts as failed. |
| `ssf.pushRetries` | `STS_SSF_PUSH_RETRIES` | `0` | yes | How many times a failed push is retried. Only failures that could go differently are retried. |
| `ssf.pushRetryDelayMs` | `STS_SSF_PUSH_RETRY_DELAY_MS` | `1000` | yes | The wait before a retry, multiplied by the attempt number. |
| `ssf.pushConcurrency` | `STS_SSF_PUSH_CONCURRENCY` | `8` | yes | How many pushes one process makes at once. `0` removes the cap. |
| `ssf.pushBacklog` | `STS_SSF_PUSH_BACKLOG` | `2000` | yes | How many pushes to one receiver may wait for a slot; each receiver has its own queue and a freed slot goes to them in turn. Past that, the SET is dead-lettered. |
| `ssf.deadStreamTimeoutS` | `STS_SSF_DEAD_STREAM_TIMEOUT_S` | `300` | yes | How long a push stream has to fail completely before it is declared dead. `0` turns this off. |
| `ssf.deadLetterRetentionS` | `STS_SSF_DEAD_LETTER_RETENTION_S` | `3600` | yes | How long an undeliverable SET is kept on the dead-letter queue. |
| `ssf.deadLetterMaxPerStream` | `STS_SSF_DEAD_LETTER_MAX_PER_STREAM` | `1000` | yes | The most dead letters one stream keeps. The oldest is dropped first. |
| `ssf.deadLetterSweepS` | `STS_SSF_DEAD_LETTER_SWEEP_S` | `60` | yes | How often expired dead letters are deleted, due probes are sent and the summary is logged. |
| `ssf.authBasic` | `STS_SSF_AUTH_BASIC` | `true` | yes | Whether HTTP Basic is accepted and advertised. |
| `ssf.internalReceivers` | `STS_SSF_INTERNAL_RECEIVERS` | `true` | **no** | Seeds the console's and the portal's own receiver streams. |
| `ssf.maxStreams` | `STS_SSF_MAX_STREAMS` | `25` | yes | Streams per receiver in a realm. A create past the limit is refused with 403, and the refusal names this setting. The console's and portal's own streams do not count. |
| `ssf.inactivityTimeoutS` | `STS_SSF_INACTIVITY_TIMEOUT_S` | `0` | yes | SSF 1.0 section 8.1.1's `inactivity_timeout`: a stream whose receiver has made no management call about it (and, for a poll stream, no poll) for this long is dealt with as `ssf.inactivityAction` says. `0`, the default, is no timeout. It is off by default because a push receiver never calls back. |
| `ssf.inactivityAction` | `STS_SSF_INACTIVITY_ACTION` | `pause` | yes | `pause`, `disable` or `delete`. A pause or disable is announced with a stream-updated event before the stream stops. |
| `ssf.verificationEveryS` | `STS_SSF_VERIFICATION_EVERY_S` | `0` | yes | Sends every enabled stream a transmitter-initiated verification event (no `state`) when it has had none for this long. `0` sends none on a schedule. |
| `ssf.streamMaintenanceSweepS` | `STS_SSF_STREAM_MAINTENANCE_SWEEP_S` | `60` | yes | How often the `ssf.stream-maintenance` scheduler job applies the two settings above. |
| `ssf.receiveAudiences` | `STS_SSF_RECEIVE_AUDIENCES` | *(empty)* | yes | The `aud` values `POST /ssf/receive` accepts. Empty means the endpoint's own URL. Anything else is recorded and refused with `invalid_audience`. |
| `ssf.receiveIssuers` | `STS_SSF_RECEIVE_ISSUERS` | *(empty)* | yes | The `iss` values `POST /ssf/receive` accepts. Empty means this realm's own transmitter issuer. Anything else is recorded and refused with `invalid_issuer`. |
| `ssf.maxSubjectsPerStream` | `STS_SSF_MAX_SUBJECTS_PER_STREAM` | `100` | yes | How many subjects one stream may name. |
| `ssf.maxQueuedEvents` | `STS_SSF_MAX_QUEUED_EVENTS` | `200` | yes | How many undelivered SETs one stream holds. The oldest is dropped first. |
| `ssf.pollMaxEvents` | `STS_SSF_POLL_MAX_EVENTS` | `20` | yes | The most SETs one poll returns, whatever the receiver asks for. |
| `ssf.maxReceivedEvents` | `STS_SSF_MAX_RECEIVED_EVENTS` | `200` | yes | How many SETs `POST /ssf/receive` keeps for display. |
| `ssf.maxStreamLogEntries` | `STS_SSF_MAX_STREAM_LOG_ENTRIES` | `200` | yes | How many log lines each stream keeps. |
| `ssf.authScopeRead` | `STS_SSF_AUTH_SCOPE_READ` | `ssf:read` | yes | The scope needed to read a stream, its status or the poll queue. |
| `ssf.authScopeWrite` | `STS_SSF_AUTH_SCOPE_WRITE` | `ssf:write` | yes | The scope needed to change anything about a stream. |
| `ssf.receiveEnabled` | `STS_SSF_RECEIVE_ENABLED` | `true` | yes | Whether `POST /ssf/receive` accepts pushed SETs. When off, it answers 501. |
| `ssf.receiveRequireSignature` | `STS_SSF_RECEIVE_REQUIRE_SIGNATURE` | `false` | yes | Refuses a received SET whose signature fails, with 400 `invalid_key`, in development mode. Product mode always refuses one (#117). |
| `ssf.actOnSignalsInDevelopment` | `STS_SSF_ACT_ON_SIGNALS_IN_DEVELOPMENT` | `false` | yes | The console and portal end their own sessions on a received signal in development too; product always does. |
| `ssf.legacySubClaim` | `STS_SSF_LEGACY_SUB_CLAIM` | `false` | yes | Deliberate defect, development only: adds the deprecated `sub` claim beside `sub_id`. |
| `ssf.breakSetSignature` | `STS_SSF_BREAK_SET_SIGNATURE` | `false` | yes | Deliberate defect, development only: changes one character of every SET's signature. |
| `gnap.scopedSignals` | `STS_GNAP_SCOPED_SIGNALS` | `true` | yes | A stream owned by a GNAP application only hears about people who approved a grant to it. A stream owned by a GNAP resource server hears only `session-revoked` for the GNAP tokens and grants audienced to it (#432). |
### CAEP (`caep.*`)
| Setting | Environment variable | Default | Runtime? | What it does |
|---|---|---|---|---|
| `caep.enabled` | `STS_CAEP_ENABLED` | `true` | yes | Offers CAEP's eight event types and keeps the session register. |
| `caep.autoEmit` | `STS_CAEP_AUTO_EMIT` | `true` | yes | Sends CAEP events automatically for the session acts this service can observe. |
| `caep.autoEmitTypes` | `STS_CAEP_AUTO_EMIT_TYPES` | `session-established,session-presented,session-revoked,credential-change,assurance-level-change,token-claims-change,risk-level-change` | yes | Which of those acts produce an event. A type nothing here can cause is dropped with a warning. |
| `caep.eventsSupported` | `STS_CAEP_EVENTS_SUPPORTED` | all eight | yes | Which CAEP types a stream may ask for. Short names are accepted. |
| `caep.assuranceNamespace` | `STS_CAEP_ASSURANCE_NAMESPACE` | `NIST-AAL` | yes | The `namespace` of an `assurance-level-change` emitted by hand. Automatic ones use `urn:sts:acr`. |
| `caep.defaultRiskLevel` | `STS_CAEP_DEFAULT_RISK_LEVEL` | `MEDIUM` | yes | What a `risk-level-change` says when the caller does not choose a level. |
| `caep.reasonLanguage` | `STS_CAEP_REASON_LANGUAGE` | `en` | yes | The language tag the reason members are keyed under. |
| `caep.includeReasons` | `STS_CAEP_INCLUDE_REASONS` | `true` | yes | Whether automatic events carry the optional reason members. |
| `caep.maxSessionsTracked` | `STS_CAEP_MAX_SESSIONS_TRACKED` | `200` | yes | The size of the CAEP session register. The oldest row is dropped first. |
| `caep.eventsPerSession` | `STS_CAEP_EVENTS_PER_SESSION` | `25` | yes | How many recent events a register row lists. The per-type counts are never trimmed. |
| `caep.historyPerSession` | `STS_CAEP_HISTORY_PER_SESSION` | `10` | yes | How many `credential-change` details a row keeps. |
| `caep.omitEventTimestamp` | `STS_CAEP_OMIT_EVENT_TIMESTAMP` | `false` | yes | Deliberate defect: leaves out the optional `event_timestamp`. |
### RISC (`risc.*`)
| Setting | Environment variable | Default | Runtime? | What it does |
|---|---|---|---|---|
| `risc.enabled` | `STS_RISC_ENABLED` | `true` | yes | Offers RISC's fourteen event types and keeps the account register. |
| `risc.autoEmit` | `STS_RISC_AUTO_EMIT` | `true` | yes | Sends RISC events automatically for the directory changes this service can observe. |
| `risc.autoEmitTypes` | `STS_RISC_AUTO_EMIT_TYPES` | every type but `sessions-revoked` | yes | Which of those acts produce an event. |
| `risc.recycleWindowDays` | `STS_RISC_RECYCLE_WINDOW_DAYS` | `365` | yes | How long after an account released an address or number another account taking it is reported as `identifier-recycled`. Also bounded by `risc.maxAccountsTracked`. `0` reports nothing. |
| `risc.optOutDelayHours` | `STS_RISC_OPT_OUT_DELAY_HOURS` | `24` | yes | How long an opt-out waits in `opt-out-initiated` before the `risc.opt-out-effective` job makes it effective. |
| `risc.eventsSupported` | `STS_RISC_EVENTS_SUPPORTED` | all fourteen | yes | Which RISC types a stream may ask for, including the deprecated `sessions-revoked`. |
| `risc.subjectFormat` | `STS_RISC_SUBJECT_FORMAT` | `iss_sub` | yes | The RFC 9493 format of an account subject: `iss_sub`, `email` or `opaque`. The two identifier events always use `email`. |
| `risc.honourOptOut` | `STS_RISC_HONOUR_OPT_OUT` | `true` | yes | Suppresses events for an account in the `opt-out` state, except the four opt-out events. |
| `risc.googleSubjectType` | `STS_RISC_GOOGLE_SUBJECT_TYPE` | `false` | yes | Deliberate defect: spells the subject discriminator `subject_type` on RISC subjects. Development mode only: ignored in product, and turning it on is refused (#181). |
| `risc.reasonLanguage` | `STS_RISC_REASON_LANGUAGE` | `en` | yes | The language tag of the reason members on `credential-compromise`. |
| `risc.includeReasons` | `STS_RISC_INCLUDE_REASONS` | `true` | yes | Whether `credential-compromise` carries its optional reason members. |
| `risc.omitEventTimestamp` | `STS_RISC_OMIT_EVENT_TIMESTAMP` | `false` | yes | Deliberate defect: leaves `event_timestamp` off `credential-compromise`. |
| `risc.maxAccountsTracked` | `STS_RISC_MAX_ACCOUNTS_TRACKED` | `200` | yes | The size of the RISC account register. The oldest opted-in row is dropped first. A row whose holder opted out is never dropped; the register stays over the cap and logs `STS-SSF-0130` instead. |
| `risc.eventsPerAccount` | `STS_RISC_EVENTS_PER_ACCOUNT` | `25` | yes | How many recent events a register row lists. |
| `risc.historyPerAccount` | `STS_RISC_HISTORY_PER_ACCOUNT` | `10` | yes | How many credential-compromise and identifier-change records a row keeps. |
For how a value is resolved and where it can be changed, see
[Configuration](/iya-sts/configuration.html). Each group is on its own console page
(`/admin/ssf`, `/admin/caep`, `/admin/risc`), and every setting can also be
changed with `POST /admin-api/config/set`.
## Design decisions
* **SSF is the pipe; CAEP and RISC are vocabularies over it.** SSF's two
events are about the stream itself. Session and account events are rows in
a table on top of it, so adding RISC's fourteen types changed nothing in the
envelope, the subjects, delivery or stream management.
* **CAEP keeps a register of sessions and RISC keeps a register of accounts,
and they are separate.** A session starts, is used and ends, and one person
can have many. An account *is* the person and outlives every session. RISC
also tracks three things per account that change independently: its
lifecycle, its opt-out state and whether a credential is compromised.
* **Both registers keep a row after the session or account is gone.** A
revoked session disappears from the session store, and a purged account
disappears from the directory. After that, the register row is the only
evidence that anybody was told.
* **RISC watches the directory, not SCIM.** A person can be deleted or
disabled over SCIM, LDAP or the console. A RISC feature that only noticed
SCIM would say nothing about a deprovisioning done with `ldapmodify`.
* **An attribute that is absent is not the same as one that is false.** A
write that does not mention `active` sends no `account-disabled`. Otherwise
every person created without the attribute would be reported as disabled.
* **A transmitter reports, and a receiver decides.** RISC tells a receiver
that an account was disabled or purged here. What the receiver does about
it is the receiver's decision, which is how the profile divides the work.
* **The four opt-out events are never suppressed.** `opt-out-effective`
announces that the account is going quiet, and `opt-in` is the only way a
receiver learns it came back. Holding either back would make an opt-out look
like a transmitter that had stopped working. The middle state,
`opt-out-initiated`, still exchanges everything, so that someone who hijacks
an account cannot silence the events that would report them.
* **The first time a new session is presented, no event is sent.** Every
sign-in ends with the browser returning to the authorization endpoint. If
that counted, `session-established` and `session-presented` would always
arrive milliseconds apart.
* **`initiating_entity` says who really ended a session.** An expiry says
`policy`. An administrator ending someone's session says `admin`. It never
claims the person signed themselves out when they did not.
* **Push is not retried by default.** A transmitter that retried would hide a
receiver's one-off failure. A deployment can set `ssf.pushRetries`.
* **A push that fails goes to a dead-letter queue; logging stays per stream.**
A burst of failures produces one line when the stream dies and one summary
per sweep. Signing a SET that nothing will receive is skipped.
* **The metadata document is always open.** A receiver has to read which
schemes the endpoints take before it can authenticate to them.
* **`aud` is the transmitter's to assign.** It comes from the receiver's own
identity, so a receiver cannot have its events addressed to another receiver.
* **The console's and portal's signals come through a real push.** Passing
events to those pages internally would skip the body, media type,
authorization header and signature, which is everything a receiver does.
Their stream IDs and authorization tokens are derived from the realm and the
surface, so every process and every node arrives at the same values.
* **SETs are worth signing with a post-quantum algorithm.** RFC 8417 forbids
a SET from expiring, so it is read long after it was written.
## In the running service
* **Protocols → Shared Signals** (`/admin/ssf`): the `ssf.*` settings, and
every stream this realm's transmitter holds, with its subjects, queue,
counters, log and dead letters. A dead stream is marked. Each stream has
controls to change its status, delete it, send it a SET, **Send a
verification event**, **Revive** it and **Drop its dead letters**. What `POST /ssf/receive` received is listed too.
There is no control to create a stream (see *Not implemented*).
* **Protocols → CAEP** (`/admin/caep`): the `caep.*` settings, the catalogue
of the eight event types and their members, and the form for sending one by
hand. You pick a session by searching for the person, and only live
sessions are offered.
* **Protocols → RISC** (`/admin/risc`): the `risc.*` settings, the catalogue
of the fourteen event types, and the form for sending one by hand about an
account.
* **Monitoring → Shared Signals**: *CAEP sessions* (`/admin/caep-sessions`),
*RISC accounts* (`/admin/risc-accounts`), *Signals received*
(`/admin/signals`) and *Dead letters* (`/admin/ssf/dead-letters`). The
dead-letters page counts every held SET by cause, error code, receiver HTTP
status, event type, time and stream. It shows the letters themselves, but
not their tokens.
* **The management API** mirrors all of these: `GET /admin-api/ssf` and
`POST /admin-api/ssf/{action}` (`status`, `delete`, `transmit`,
`clear-received`, `revive`, `clear-dead-letters`, `verify`),
`GET /admin-api/ssf/dead-letters`, `GET /admin-api/caep`,
`GET /admin-api/caep/sessions` and `POST /admin-api/caep/{action}` (`emit`,
`reset-session`, `clear`), `GET /admin-api/risc`,
`GET /admin-api/risc/accounts` and `POST /admin-api/risc/{action}` (`emit`,
`reset-account`, `clear`), and `/admin-api/signals`. The request and
response shapes are in `/admin-api/openapi.json`.
* **`GET /ssf`** describes the family, its gate and what it deliberately does
not do. Every endpoint is listed live on `/admin/sts-metadata`.
* **Monitoring → Scheduler** (`/admin/scheduler`) lists the dead-letter sweep
and the stream-maintenance jobs.
Every failure is recorded under an `STS-SSF-NNNN` code. See
[error codes](/iya-sts/error-codes.html).
## Related
* [CAEP events](/iya-sts/caep-events.html): what triggers each of the eight CAEP events
* [Signals received](/iya-sts/signals-received.html): the console and portal as receivers
* [Sessions](/iya-sts/sessions.html), [Signing out](/iya-sts/signing-out.html): the session acts
CAEP reports
* [GNAP](/iya-sts/gnap.html): GNAP streams and the signals a grant produces
* [SCIM](/iya-sts/scim.html), [LDAP](/iya-sts/ldap.html): the directory writes RISC reports
* [OAuth 2.0 & OpenID Connect](/iya-sts/oauth-oidc.html): getting an `ssf:*` token
* [Trust realms](/iya-sts/trust-realms.html), [What is not checked](/iya-sts/what-is-not-checked.html),
[Configuration](/iya-sts/configuration.html)