Security Signals are statements about something that has occurred that may be shared asynchronously between a publisher and a receiver over what is commonly known as an event “stream”.
In traditional SIEM (Security Information and Event Management) systems, events are inferred through the sharing of information such as access logs where an AI system uses pattern recognition to infer something has occurred. More recently, the IETF defined a universal message format called a “Security Event Token” RFC8417. The format enables various event types to be expressed that can be signed and optionally encrypted as a message between parties. For example, an event may express that a user session was cancelled (logged out), an authentication factor was changed, or an account was provisioned. In contrast to SIEM, Security Events express distinct conclusions about something that has occurred allowing a receiver to take independent action.
When a series of events are shared over a pre-configured Push or Polling connection, this is referred to as a “stream”. In a stream a series of SETs are transferred and acknowledge when successfully received. In some cases, in the event of a failure, a stream can be “reset” to enable recovery of lost of missing SET event messages.
In scenarios where an event publisher has more than one receiver, the OpenID Foundation has developed a specification called the Shared Signal Framework specification that provides an HTTP based API for creating and managing streams. Because i2scim only supports one outbound stream and one inbound stream, i2scim currently supports SSF as a “client”.
The i2scim server supports a pluggable integration interface called the event handler (IEventHandler). For each
installed
plugin, i2scim forwards the current operation to the event handler to perform extended functionality. The Signals
Event Handler is turned on when the environment variable scim.signals.enable is set to true. When enabled, each
processed SCIM transaction is transformed into one or more Security Event Tokens as defined by the
SCIM Events Specification. The mapped SET Event is then
sent to the configured event receiver. An i2scim that receives events reverses the process by mapping received events
into local SCIM operations.
[!NOTE] i2scim currently only supports a single outbound Push Event stream and a single inbound polling event stream.
Yes, the first deployment use-case is to support replication using events. When combined with a i2goSignals server, 2 or more i2scim servers can exchange SCIM provisioning events in order to synchronize replicas. This is very similar to using a message bus such as Apache Kafka. When deployed this way, each “replica” pushes events received to the i2goSignals server which then routes events to every other replica that shares the same “audience” value. Since each replica has its own inbound stream, each replica receives a copy of every event generated by every other node.
i2scim.io supports two standardized mechanisms for point-to-point transfer have been developed:
Use an SSF service capable of routing and retransmitting events to multiple receivers. i2goSignals is intended for this purpose and provides additional functionality such as routing and event recovery.
SSF requires a number of features that are really intended for situations where many streams need to be supported. Because i2scim is often deployed in a clustered configuration, no single server processes all events for a single domain making it impractical for a single receiver to have a single stream from a publishing domain. Because of this, the decision was made to develop a Security Event Router called goSignals (soon to be released) which serves the purpose of:
| Area / Parameter | Description | Default |
|---|---|---|
Common |
||
| scim.signals.enable | Enable the signals extension | false |
| scim.signals.pub.enable | Enable outbound events | true |
| scim.signals.rcv.enable | Enable incoming events | true |
| scim.signals.pub.iss | The issuer value to be used for transmitted SETs (may need to correspond to token signing key) | DEFAULT |
| scim.signals.pub.aud | Value to use for audience (may be a comma separated list) | example.com |
| scim.signals.rcv.iss | The issuer value expected for received SETs | DEFAULT |
| scim.signals.rcv.aud | The audience value expected for received SETs | DEFAULT |
SSF Automatic |
||
| scim.signals.ssf.configFile | File where dynamic configuration is stored to support restarts | /scim/ssfConfig.json |
| scim.signals.ssf.serverUrl | The URL of an Shared Signals Framework Server. Note: for push, the SSF server must be able to configure PUSH Receivers |
NONE |
| scim.signals.ssf.authorization | The authorization header value to pass that enables automatic registration (e.g. an IAT bearer token) | NONE |
Pre Configured Files |
||
| scim.signals.pub.config.file | A file containing transmitter stream information. See Using Pre-config below | NONE |
| scim.signals.rcv.config.file | A file containing the receiver stream configuration information. See using Pre-config below | NONE |
Manual Configuration |
||
| scim.signals.pub.push.endpoint | The URL of the endpoint where events are to be pushed using RFC8935. | NONE |
| scim.signals.pub.push.auth | The authorization header value to be used when pushing events to the endpoint | NONE |
| scim.signals.rcv.poll.endpoint | The URL where i2scim may poll for events using RFC8936 | NONE |
| scim.signals.rcv.poll.auth | The authorization header value to be used when polling | NONE |
Signing and Encryption Configuration |
||
| scim.signals.pub.pem.path | A path to a local file where a PEM encoded PKCS8 private key may be loaded for issuing signed events | NONE |
| scim.signals.pub.pem.value | A PEM PKCS8 encoded private key value minus BEGIN and END text. | NONE |
| scim.signals.pub.issJwksUrl | A URL where the issuer JWKS public key may be loaded from | NONE |
| scim.signals.pub.algNone.override | When set to true, events will not be signed. | false |
| scim.signals.pub.aud.jwksurl | The URL where the audience public key may be loaded. When set, events are encrypted with JWE | NONE |
| scim.signals.pub.aud.jwksjson | The audience public key in JSON form. When set, events are encrypted with JWE Use either jwksurl or jwksjson |
NONE |
| scim.signals.rcv.iss.jwksUrl | The URL where a JWKS public key may be loaded to validate signed events | NONE |
| scim.signals.rcv.iss.jwksJson | The issuer public key in JWKS JSON format used to validate signed events Use either jwksurl or jwksjson |
NONE |
| scim.signals.rcv.pem.path | The location of an PKCS8 PEM format private key which may be used to decrypt JWE encoded events | NONE |
| scim.signals.rcv.pem.value | PKCS8 encoded private key value used to decrypt JWE encoded events | NONE |
Operational Recovery |
||
| scim.signals.pub.unauthorized.retry.max | Maximum 401 retries on a push stream before disabling the stream | 10 |
| scim.signals.pub.unauthorized.retry.delay | Fixed delay (ms) between 401 retries on a push stream | 15000 |
| scim.signals.pub.status.check.interval | Interval (ms) between transmitter /status re-checks while a push stream is paused |
30000 |
| scim.signals.pub.idle.verify.interval | Interval (ms) of push-stream idleness after which a SSF verification event is emitted as a keepalive | 300000 |
| scim.signals.rcv.unauthorized.retry.max | Maximum 401 retries on a poll stream before disabling the stream | 10 |
| scim.signals.rcv.unauthorized.retry.delay | Fixed delay (ms) between 401 retries on a poll stream | 15000 |
| scim.signals.rcv.status.check.interval | Interval (ms) between transmitter /status re-checks while a poll stream is paused |
30000 |
[!NOTE] Because authorization for OpenID SSF is not finalized, this procedure is designed to work with the i2gosignals project only.
With i2goSignals, using the gosignals tool to generate an IAT token to allow i2scim to auto register. For example:
goSignals
goSignals> add server ssf1 https://gosignals.example.com:8888
goSignals> create iat ssf1
eyJhbGciOiJSUzI1NiIsImtpZCI6IkRFRkFVTFQiLCJ0eXAiOiJqd3QifQ.eyJz...
goSignals> exit
When using the goSignals tool, a generated IAT can be used in the i2scim scim.signals.ssf.authorization parameter
combined
with the prefix Bearer, a space, followed by the token returned above.
This is particularly useful when defining a cluster where multiple i2scim instances may be started. Each node will then
register itself
using the same IAT token and receive unique stream endpoints.
When registering each i2scim server instance will:
Because all instances share the same iss and aud values and a common project identifier the i2goSignals server will
route events between nodes
making it possible to implement replication. When an event is published on one node, it will be automatically forward to
each other node in the cluster since each has its own stream.
Since each node has its own stream, each node will receive a copy of every event.
Note that automatic configuration is best used when each i2scim server is deployed standalone (e.g. using the Memory Provider to store data in a file). Each node in the signals cluster needs to get transactions from every other node in order to synchronize.
When i2scim is deployed as a cluster against a common database (e.g. Mongo), then event replication using signals is not needed. In that case, only one node needs inbound events (if needed) and each node can share a common push stream configuration. In this scenario Pre-configuration or Manual configuration is recommended.
In addition to the common and signing parameters, streams can be configured using JSON files which contain the following attributes:
Example publish configuration file (e.g. generated by the goSignals tool):
{
"alias": "sQs",
"id": "6525f54e86b849e69ca1a779",
"description": "Push Receiver",
"token": "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IkRFRkFVTFQiLCJ0eXAiOiJqd3QifQ.eyJzaWQiOlsiNjUyNWY1NGU4NmI4NDllNjljYTFhNzc5Il0sInByb2plY3RfaWQiOiJBa0NVIiwicm9sZXMiOlsiZXZlbnQiXSwiaXNzIjoiREVGQVVMVCIsImF1ZCI6WyJERUZBVUxUIl0sImV4cCI6MTcwNDc2MjQ0NiwiaWF0IjoxNjk2OTg2NDQ2LCJqdGkiOiIyV2IxUUNoNTBjc2RBVUIyTFZtOTlnYzFYazMifQ.Tk5M7yn64kkfxr_ds9CJXMcifvefxxftq4e_gX9-KZzViUyd1SBNofz-_Dfzh5zIMsl0XBiLXLRofQU_yhsh_yGKGz6_9TlOzmwA3tNclJEeaCySOvtyUZ39D773u60Ss3ydXvTUtai8WE5PV5Qmu3wvyTSiABrTIbTv260MOLuk1hisPYQmpNE06BMCv3LIeBaMggZrJKJRTkCmgxHlgdVUh4BAPRlqiKG0jiCED1z6PHsMUaocT_1gVQEuchRdGgZTRBglMCAVSQibBLqOA6d1BrLGVGUKOMtJNj4tb59TrKpM--QCqAksNM02Kj1nOiiac7tR1BcnxBULhAj3gA",
"endpoint": "http://goSignals1:8888/events/6525f54e86b849e69ca1a779",
"iss": "myissuer.io",
"aud": "myissuer.io",
"issJwksUrl": ""
}
This mode is best uses in cases where multiple i2scim nodes are using the Mongo Provider to store data and thus share a common database. In this case, every node should use the same output push stream but only one node should be receiving events on behalf of the entire cluster.
In manual configuration mode, all stream parameters are configured using environment variables as described in the table above.
i2scim mirrors the operational vocabulary defined by the goSignals project so that local stream behavior aligns with
what an operator sees on the transmitter side. The vocabulary covers stream lifecycle, HTTP failure classification,
remote /status interrogation, idle keepalive verification events, and RFC8935 §2.4 protocol-error handling.
Each PushStream and PollStream maintains a status of:
| Status | Meaning | Persisted? |
|---|---|---|
enabled |
Stream is healthy and producing/consuming events. | yes |
paused |
The remote peer reports the stream as paused (typically operator-driven on the goSignals side). i2scim auto-recovers when the remote re-enables. | no |
disabled |
A terminal failure was observed (403, 4xx-other, exhausted retries, RFC8935 protocol error). Operator intervention is required. | yes |
paused is never written to ssfConfig.json — it is a runtime-only state derived from peer status. On restart,
the stream re-enters enabled and a successful first push or poll re-derives the true state.
A short structured errorMsg accompanies any disabled transition (for example
"403 Forbidden: stream revoked" or "RFC8935 invalid_audience: bad aud; jti=...").
Both push and poll responses are classified into a small set of categories. The push table includes RFC8935 §2.4
detection on 400; the poll table is otherwise identical.
| Response | Action |
|---|---|
2xx |
Success. |
Transport error / 5xx |
Exponential backoff (retry.interval → retry.maxInterval), capped at retry.max attempts. T1 fires. |
401 Unauthorized |
Fixed-delay retry (unauthorized.retry.delay), capped at unauthorized.retry.max, then disable. |
403 Forbidden |
Immediate disable (the stream has been revoked or is unauthorized at the policy layer). |
429 Too Many Requests |
Honor Retry-After; no attempt cap. |
4xx (other) |
Immediate disable. |
400 Bad Request with RFC8935 body |
Parse the err field per RFC8935 §2.4. Disable with a structured errorMsg. See below for the JWS carve-out. |
Once a stream is disabled, normal event flow stops until an operator re-enables it.
/status interrogation (T1 and T2)T1 (“reactive”) and T2 (“pre-flight”) use the SSF transmitter /status endpoint to discover whether the remote stream
is enabled, paused, or disabled from the transmitter’s perspective:
enabled state. If
the remote reports paused, the local stream transitions to paused (and a paused-recheck task takes over).The /status URL is discovered lazily:
status_endpoint.status_endpoint, T1 is permanently skipped for that stream.Each probe issues GET <status_endpoint>?stream_id=<id> per
SSF §7.1.2. The
stream_id is the local stream’s id (the same id used during stream registration). If a stream has no id (e.g.
incomplete configuration) the probe is skipped and the stream falls back to ordinary retry/backoff.
Well-known failure alone never causes a stream to be disabled.
When a stream enters paused, a per-stream task polls the remote /status every
scim.signals.{pub|rcv}.status.check.interval ms. When the remote reports enabled, the local stream automatically
recovers and resumes producing/consuming events. No operator action is required to recover from paused.
Push streams emit an SSF
verification event
when no event has been pushed for scim.signals.pub.idle.verify.interval ms. The verification event is signed with the
issuer key (or unsigned if pub.algNone.override=true) and is delivered through the same push channel. It serves as a
liveness check so a long-idle stream does not silently rot.
The keepalive is suppressed while the stream is paused or in active retry backoff. After a restart the idle timer
resets, so the first verification event will fire one full interval after restart even if the stream was idle
beforehand.
When a push receives 400 with an RFC8935-shaped JSON body, the err field is matched against the documented codes
(authentication_failed, invalid_request, invalid_audience, invalid_issuer, invalid_key,
jws_signature_failed, jwe_decryption_failed). The stream is disabled with
errorMsg = "RFC8935 <code>: <description>; jti=<jti>".
A single carve-out applies: jws_signature_failed triggers a one-time PEM reload of the issuer private key from
scim.signals.pub.pem.path / scim.signals.pub.pem.value followed by a single retry of the SET. If the retry also
fails, the stream is disabled. When pub.algNone.override=true, this carve-out does not apply (there is no key to
reload) and the stream is disabled immediately.
ssfConfig.json migrationssfConfig.json files written by pre-PRD-A builds used a boolean errorState field. On load, the file is migrated
in-place: errorState=true becomes status=disabled with errorMsg="migrated from legacy errorState", and
errorState=false becomes status=enabled. New builds only ever write status and errorMsg. Rolling back to a
pre-PRD-A build will not understand the new shape.