i2scim

i2scim Security Signals Support

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.

What is a stream?

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”.

How does i2scim support streams?

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.

Can events be used for replication?

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.

How are Events transferred?

i2scim.io supports two standardized mechanisms for point-to-point transfer have been developed:

What if I want to deliver events to more than one receiver?

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.

Why doesn’t i2scim implement SSF Server functionality?

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:

i2scim Signals Configuration Parameters

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

Configuration Options

Automatic Configuration using SSF

[!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:

  1. Perform a client registration using the supplied IAT token. The i2goSignals SSF server issues a client token with a unique identifier but sharing a common project identifier (originating from the IAT)
  2. Each i2scim server then requests a publishing stream and a receiver stream using SSF stream configuration management.

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.

Pre-Configuration Using Stream Files

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.

Manual Configuration

In manual configuration mode, all stream parameters are configured using environment variables as described in the table above.

Operational Behavior

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.

Stream lifecycle (tri-state)

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=...").

HTTP failure classification

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.intervalretry.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:

The /status URL is discovered lazily:

  1. Fetch the SSF well-known configuration and use its declared status_endpoint.
  2. If the well-known is unreachable, fall back to a derived URL (path-segment replacement) once, then continue with exponential backoff.
  3. If the well-known is reachable but does not declare a 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.

Paused-recheck

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.

Idle keepalive verification (push only)

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.

RFC8935 §2.4 protocol errors

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 migration

ssfConfig.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.