Skip to content

Pre-release. v0.1 is not out yet, so there is nothing to install and no public source to clone — the quickstart builds from a checkout.

Configuration

The control plane reads its configuration from environment variables. Every setting has a key and a variable: SubactId:RateLimit:Burst is the key and SubactId__RateLimit__Burst is the variable. Replace each : with __.

The server does not start on invalid configuration. It reports every error at once, names both the key and the variable, and never repeats a configured value.

  • Empty means unset. A blank value takes the default. SubactId__Admin__ApiKey= disables the admin API.
  • Durations are ISO 8601 (PT5M) or .NET time spans (00:05:00). Every duration must be greater than zero. Years and months are refused, because they have no fixed length. Durations in an agent registration are ISO 8601 only.
  • Whole numbers are plain digits, with no sign or separators.
  • Booleans are true or false.
  • URLs must be absolute http or https.
  • Lists are comma-separated. SubactId:RateLimit:TrustedProxies is the only list. Signing keys are numbered instead: SubactId__Signing__Keys__0__Path.
  • Command-line arguments override the environment, for example --SubactId:RateLimit:Burst=5. Use this to try things out, not in a deployment.
Setting Default Notes
SubactId:Issuer — Required. The URL the control plane issues tokens as: the iss of every token. A trailing slash is dropped
SubactId:UpstreamIdp:Issuer — The identity provider’s realm URL. The discovery URL is derived from it
SubactId:UpstreamIdp:MetadataUrl — The full discovery URL instead of Issuer. Set exactly one of the two
SubactId:UpstreamIdp:Audience — Required. The aud a subject token must carry
SubactId:UpstreamIdp:SponsorCheck:Mode poll poll or signals
SubactId:UpstreamIdp:SponsorCheck:UsersUrl — The provider’s admin users collection. Required under poll, refused under signals
SubactId:UpstreamIdp:SponsorCheck:TokenUrl — The provider’s token endpoint. Required under poll, refused under signals
SubactId:UpstreamIdp:SponsorCheck:ClientId — The client the control plane is registered as at the provider. Required under poll, refused under signals
SubactId:UpstreamIdp:SponsorCheck:CacheTtl PT30S How long a person’s status is reused. At most SubactId:Tokens:DefaultTokenTtl. Refused under signals
SubactId:UpstreamIdp:SponsorKeyClaim sub The claim each task records the human under. At most 64 characters, no whitespace
SubactId:UpstreamIdp:BackchannelLogout:Audience — Turns on POST /backchannel-logout. The human-facing client the logout URI is registered on, not UpstreamIdp:Audience

What the two modes do, and when to change SponsorKeyClaim, is in connecting your identity provider.

Each receiver is off until its first setting is set. Unset, its routes do not exist and answer 404. The other settings of each group are refused without it.

Setting Default Notes
SubactId:Scim:BearerToken — Turns on the SCIM 2.0 receiver at /scim/v2. At least 32 characters. A secret
SubactId:Scim:PreviousBearerToken — A second accepted credential, for rotation. At least 32 characters, different from BearerToken. A secret
SubactId:Scim:SponsorKeyAttribute externalId externalId or userName: the attribute that names the person. Must match SponsorKeyClaim
SubactId:Scim:MaxUsers 50000 Most user records held
SubactId:Ssf:Issuer — Turns on the Shared Signals receiver at POST /events. The transmitter’s issuer URL
SubactId:Ssf:Audience — Required with Issuer. The audience every event must carry
SubactId:Ssf:BearerToken — Required with Issuer. At least 32 characters. A secret
SubactId:Ssf:PreviousBearerToken — A second accepted credential, for rotation. At least 32 characters, different from BearerToken. A secret

Section 6 of the spec describes what each receiver accepts.

Setting Default Notes
SubactId:Database:Provider postgres postgres or sqlite
SubactId:Database:ConnectionString — Required for postgres, refused for sqlite. A secret
SubactId:Database:MigrationConnectionString the connection string Refused for sqlite. A role that may change the schema, used only by migrate. A secret
SubactId:Database:Path — The SQLite file. Required for sqlite, refused for postgres

Under postgres, readiness uses its own pool of at most four connections. Count those per instance when you size max_connections. The request pool’s size is Maximum Pool Size in the connection string (100 by default).

Setting Default Notes
SubactId:Signing:Keys:N:Path — Path to a PEM file, typically a mounted secret
SubactId:Signing:Keys:N:Pem — The PEM inline. Set exactly one of Path or Pem. A secret
SubactId:Signing:Keys:N:Kid the RFC 7638 thumbprint An explicit key id
SubactId:Signing:ActiveKid — The key that signs. Required when more than one key is set

N is a number from 0, and gaps are allowed. Every configured key is published in the JWKS; only the active one signs. With no key, a Development host generates an ephemeral key and logs that it did. Any other environment refuses to start. Prefer Path over Pem, so the key is not in the process environment. Operating it covers rotation.

Setting Default Notes
SubactId:Tokens:DefaultTaskTtl PT30M max_task_ttl for a registration that sets none
SubactId:Tokens:DefaultTokenTtl PT5M max_token_ttl for a registration that sets none. At most DefaultTaskTtl
SubactId:Agents:MinTaskTtl PT1M Shortest max_task_ttl a registration may set
SubactId:Agents:MaxTaskTtl P1D Longest max_task_ttl a registration may set
SubactId:Agents:MinTokenTtl PT30S Shortest max_token_ttl a registration may set
SubactId:Agents:MaxTokenTtl PT1H Longest max_token_ttl a registration may set
SubactId:Tasks:SweepInterval PT1M Time between passes that mark expired tasks terminal. PT1S to P1D
SubactId:Tasks:SweepBatchSize 20 Tasks expired per transaction
SubactId:Tasks:Retention P7D How long an ended task is kept before the sweeper deletes it and its grants

A registration’s own max_task_ttl and max_token_ttl apply to its tasks. The SubactId:Tokens:* settings apply only to a registration that sets none. To hold every agent to shorter lifetimes, lower the SubactId:Agents:* bounds.

A token’s lifetime is always cut to what remains of its task, and a task with less than five seconds left is refused a token.

Setting Default Notes
SubactId:Admin:ApiKey — At least 32 characters. Unset, /admin and /audit answer 503. A secret

Records are always written to the ledger. With a sink set, they are also queued and posted to it in the background, at least once.

Setting Default Notes
SubactId:Audit:Sink:Url — Where records are posted. Must not contain credentials
SubactId:Audit:Sink:BearerToken — Sent to the sink. Requires Sink:Url, and that it is https. A secret
SubactId:Audit:DrainInterval PT5S Time between delivery passes. PT1S to PT1H
SubactId:Audit:DrainBatchSize 100 Records posted per request

Audit checkpoints, retention and aggregation

Section titled “Audit checkpoints, retention and aggregation”
Setting Default Notes
SubactId:Audit:Checkpoint:Interval PT1M Time between sealing passes, and the longest a record stays unsealed. PT1S to PT1H
SubactId:Audit:Retention — The default cutoff for audit-archive. At least P31D. Postgres only
SubactId:Audit:Partitions:MonthsAhead 2 Ledger partitions created ahead of the current month. 1 to 12. Postgres only
SubactId:Audit:Aggregation:Enabled true Collapse denials that name nobody, and a task’s renewals, into summary records
SubactId:Audit:Aggregation:Window PT1M PT1S to PT15M

SubactId:Audit:Retention deletes nothing by itself; see retention. Unset, the ledger grows without bound. Turning aggregation off logs a warning at startup. SubactId:Audit:Chains is not a setting, and the server refuses to start if it is set.

A token bucket per source, per instance. A refused request gets 429, Retry-After and slow_down, and is audited with reason rate_limited.

A request spends one of three buckets, chosen by its path:

Path Bucket
POST /oauth2/introspect Introspection, per source
/backchannel-logout, /scim, /events Signals, per receiver per source
/healthz, /readyz none
Everything else, /oauth2/token included the main bucket, per source
Setting Default Notes
SubactId:RateLimit:Enabled true true or false
SubactId:RateLimit:PermitsPerMinute 600 Sustained requests per source
SubactId:RateLimit:Burst 120 Bucket size
SubactId:RateLimit:Signals:PermitsPerMinute 6000 Sustained signals per source per receiver
SubactId:RateLimit:Signals:Burst 5000 Signal bucket size: the most sessions a provider can end in one burst
SubactId:RateLimit:Introspection:PermitsPerMinute 6000 Sustained introspection calls per source, about one per high-risk tool call
SubactId:RateLimit:Introspection:Burst 1200 Introspection bucket size
SubactId:RateLimit:TrustedProxies — CIDR networks whose X-Forwarded-For is trusted. 0.0.0.0/0 is refused
  • Each bucket refills every second by PermitsPerMinute / 60, rounded up. Each Burst must be at least one second’s worth of its rate.
  • n replicas admit up to n times the configured rate.
  • Agents behind one egress address are one source. Behind a proxy, set TrustedProxies, or every caller shares the proxy’s bucket. List every hop.

Caps the work one instance takes on at once, across all callers. Each kind of work has its own limit and queue: the token endpoint, introspection, the paths that revoke (/oauth2/revoke, /backchannel-logout, /scim, /events, /admin), and everything else. A request that finds no turn gets 503, Retry-After: 1 and temporarily_unavailable, and is audited with reason overloaded. /healthz and /readyz are never limited.

Setting Default Notes
SubactId:Overload:Enabled true true or false
SubactId:Overload:ConcurrencyLimit 16 per core 1 to 100000. Requests run at once, per kind. A pod with no CPU limit sees every core of its node
SubactId:Overload:QueueLimit 4 × ConcurrencyLimit 0 to 1000000. Requests that may wait, per kind
SubactId:Overload:QueueTimeout PT1S How long a request may wait. At most PT30S

When the database cannot answer, requests get 503, temporarily_unavailable and Retry-After: 5. No token is issued without its audit record.

Each is a startup error.

  • SubactId:Tokens:DefaultTokenTtl is at most SubactId:Tokens:DefaultTaskTtl.
  • Each SubactId:Agents:Min* is at most its Max*, and MinTokenTtl is at most MinTaskTtl.
  • Each SubactId:Tokens:Default* is within its SubactId:Agents:* bounds.
  • Under poll, SponsorCheck:CacheTtl is at most SubactId:Tokens:DefaultTokenTtl, so a disabled user’s tasks end within one token lifetime.
  • Each rate-limit Burst is at least one second of its PermitsPerMinute.
  • SubactId:Audit:Sink:BearerToken requires an https Sink:Url.
  • SubactId:UpstreamIdp:Issuer and MetadataUrl are not both set.
  • No SponsorCheck URL, client id or CacheTtl under signals.
  • No SubactId:Scim:* without SubactId:Scim:BearerToken, and no SubactId:Ssf:* without SubactId:Ssf:Issuer.
  • Each PreviousBearerToken differs from its BearerToken.
  • Each database provider refuses the other provider’s settings.
  • Exactly one of Pem or Path per signing key.

These settings hold secrets. None is logged, repeated in an error or returned by any endpoint. Pass them as mounted files or secret references, never in a committed manifest.

  • SubactId:Database:ConnectionString and SubactId:Database:MigrationConnectionString
  • SubactId:Signing:Keys:N:Pem
  • SubactId:Admin:ApiKey
  • SubactId:Audit:Sink:BearerToken
  • SubactId:Scim:BearerToken and SubactId:Scim:PreviousBearerToken
  • SubactId:Ssf:BearerToken and SubactId:Ssf:PreviousBearerToken

SubactId.Server doctor loads this configuration as the server would and reports what is wrong; see the command line.

Variable What it does
ASPNETCORE_HTTP_PORTS The port the server listens on. The container image sets 5100
ASPNETCORE_ENVIRONMENT Development makes the server generate a signing key when none is set. Not for deployment
SUBACTID_ADMIN_KEY Read by agent apply, not by the server
Subact ID Pre-release. v0.1 is not out yet.

© 2026 Nikola Živković PR Agencija za programerske usluge Novi Sad. Subact ID is its product.

LegalTermsPrivacy