Pular para o conteúdo principal

Server configuration

Every key the Server binds, its default, whether an Administrator can change it at runtime, and whether a change needs a restart. The Workstation Service and the Tray App are configured separately: see Workstation configuration.

Where a value comes from​

Six sources, lowest precedence first. A later one beats an earlier one, key by key:

SourceWhere it exists
1appsettings.jsonBaked into the image. Not a file a deployment edits.
2appsettings.{ASPNETCORE_ENVIRONMENT}.jsonappsettings.Development.json is in the image and inert because a deployment is not the Development environment. Never run a deployment as Development: that file carries the sample PKI SDK licence and the credentials of a shared test Signer.
3User secretsA developer's machine only.
4Environment variablesThe container: Compose's environment: or App Service's application settings. __ stands for :.
5Command lineNeither posture uses it.
6The overrides an Administrator savedA table in the platform's database, read at start and re-read whenever a setting is saved on the Settings screen.

Source 6 is silent and wins. Setting one of the seven overridable keys in the deployment and watching it have no effect means somebody saved a value on the Settings screen; your value is the default it falls back to when the override is cleared.

Key an Administrator can overrideDefault
Sessions:DefaultDuration1 h
Sessions:MaxDuration24 h
Sessions:PinPromptTimeout60 s
Sessions:MaxPinFailures5
Sessions:PinFailureWindow15 min
Signing:WorkstationReconnectGrace15 s
Audit:RetentionDays365

Those seven take effect without a restart. A row saved under any other key is ignored rather than honoured. Treat every other key on this page as restart-only.

How a key is written​

: in a file, __ in the environment:

{ "Sessions": { "MaxDuration": "12:00:00" } }
environment:
Sessions__MaxDuration: "12:00:00"
az webapp config appsettings set -g <rg> -n <app> --settings Sessions__MaxDuration=12:00:00
  • A single underscore does nothing: Sessions_MaxDuration is a variable the Server never reads.
  • A list is indexed, and the index is part of the key: KeyRingProtector__RetiredCertificates__0__Certificate.
  • A duration is [d.]hh:mm:ss, and hours must stay under 24. 24:00:00 is twenty-four days; one day is 1.00:00:00.
  • A size is a plain count of bytes. A boolean is true or false, lower case.
  • A logging category keeps its dots: Logging__LogLevel__TrustBridge.Server.Common.Vault.

Database and key rings​

ConnectionStrings:TrustBridge​

Default a dev-only local Windows-authentication string · Admin can override no · Restart yes

The platform's only database: schema, Audit Log, both key rings and the Administrator overrides. The image's default cannot work on Linux, so a deployment that forgets this key gets a refused start after about ninety seconds of trying it.

A Command Timeout in the string is the default, not a ceiling for everything. A few commands carry their own limit whatever the string says: the schema apply, the health probe, a Workstation Release package transfer or delete, and each Audit Log write, which gets fifteen seconds. Command Timeout=0, which means no limit, still applies to everything else.

On PostgreSQL it is Npgsql's spelling (Host=, Username=, SSL Mode=), with Database:Engine set to PostgreSql beside it; a string only SQL Server's driver reads is a refused start. It names PostgreSQL itself, not a pooler in transaction mode (PgBouncer's, or Azure Database for PostgreSQL's on port 6432): the schema step's lock is held by one database session, and such a pooler runs one connection's statements on several. Two keys are worth having in it: SSL Mode=Require or stricter for any database the connection leaves the host to reach, because Npgsql's default falls back to plain text when the server offers no TLS; and GSS Encryption Mode=Disable, because the image carries no Kerberos library, and without it the first connection probes for one and the log gains a Cannot load library libgssapi_krb5.so.2 error that changes nothing.

# Docker on Linux, SQL Server beside it
ConnectionStrings__TrustBridge: >-
Server=sqlserver,1433;Database=TrustBridge;User Id=sa;
Password=${MSSQL_SA_PASSWORD};Encrypt=True;TrustServerCertificate=True
# Docker on Linux, PostgreSQL beside it
Database__Engine: PostgreSql
ConnectionStrings__TrustBridge: >-
Host=postgres;Port=5432;Database=trustbridge;Username=trustbridge;
Password=${POSTGRES_PASSWORD};GSS Encryption Mode=Disable
# Azure App Service, managed identity
ConnectionStrings__TrustBridge="Server=tcp:<sqlserver>.database.windows.net,1433;Database=TrustBridge;Authentication=Active Directory Managed Identity;Encrypt=True"

Database:Engine​

Default SqlServer · Admin can override no · Restart yes

The engine this Server's database runs on: SqlServer (SQL Server or Azure SQL) or PostgreSql (PostgreSQL 16 or later, with no extension), in any case. Since Server 1.20.0 both drivers ship in the one image, and this key picks which one the Server uses. A deployment chooses it once, at install: no deployment moves between engines, and pointing an existing one at the other engine is pointing it at a different, empty database, with no administrators, no trail and a vault that never held anything. Nothing moves a running installation's data from one engine to the other.

PostgreSQL is supported on the Docker posture only. The App Service posture stays on Azure SQL with its managed identity. Nothing in the Server refuses PostgreSQL there, so leave this key at its default on App Service (Azure App Service).

A connection string written for the other engine is a refused start, with one Critical line naming this key and ConnectionStrings:TrustBridge, before anything reaches the database. So is a value that names neither engine, a number or two names joined by a comma included. A minimal string such as Server=…;Database=…;User Id=…;Password=… reads under both drivers and is let through; the refusal is for a string only the other engine reads: Host=, Username= or SSL Mode= under SQL Server, Encrypt=, Data Source= or Initial Catalog= under PostgreSQL.

On PostgreSQL the Server creates its database when it is absent, as it does on SQL Server, connecting to the postgres database to do it, so the identity needs CREATEDB for that first start, or a DBA creates the empty database first. Applying migrations needs CREATE on the public schema (or its ownership). To apply them by hand, pass the engine to the tool as well (Server installation):

dotnet ef database update --project src/TrustBridge.Server \
--connection "Host=…;Database=trustbridge;…;Command Timeout=3600" -- --Database:Engine=PostgreSql
environment:
Database__Engine: PostgreSql

Database:MigrateOnStart​

Default true · Admin can override no · Restart yes

Whether the Server applies pending migrations to its database at start, before it serves anything. false does not mean "start anyway": a pending migration then refuses the start, naming the migrations and the command to run. Two states refuse the start whichever way this is set: a database with migrations this build does not contain, and a database that cannot be reached within about ninety seconds. Applying migrations needs db_ddladmin (on PostgreSQL, CREATE on the public schema); an up-to-date database needs no DDL at all.

Three time limits are constants rather than keys: about ninety seconds for a database to start answering, five minutes for another process's migration to finish, and thirty minutes for any single command of a migration. On Azure App Service, WEBSITES_CONTAINER_START_TIME_LIMIT (default 230 s) binds first; raise it to 1800.

KeyRingProtector:Certificate​

Default unset · Admin can override no · Restart yes

Required of every deployment, including one that holds no certificates of its own. A base64-encoded PKCS#12 on one line with no PEM header, carrying an RSA private key. Not a path and not a thumbprint. Four refusals stop the host: not configured, not base64, could not be opened (wrong password or not a PKCS#12), and no usable RSA private key. See Security.

environment:
KeyRingProtector__Certificate: ${KEY_RING_PROTECTOR_CERTIFICATE:?set KEY_RING_PROTECTOR_CERTIFICATE in .env}
# App Service: an app setting, or better, a Key Vault reference
KeyRingProtector__Certificate="@Microsoft.KeyVault(SecretUri=https://<vault>.vault.azure.net/secrets/<name>/)"

KeyRingProtector:CertificatePassword​

Default unset · Admin can override no · Restart yes

The password that opens the protector. Absent means the PKCS#12 needs none, which is the honest default for a protector supplied as configuration.

KeyRingProtector:RetiredCertificates:{n}:Certificate and …:{n}:CertificatePassword​

Default empty · Admin can override no · Restart yes

Protectors this deployment used to seal with, kept so that key material sealed under one of them stays readable. Decrypt-only, and kept permanently: rotation never re-wraps stored keys. A list of objects, because each retired PKCS#12 has its own password, paired by index. Do not template an empty entry: an entry whose Certificate binds to an empty string is a refused start.

environment:
KeyRingProtector__Certificate: <the new one>
KeyRingProtector__RetiredCertificates__0__Certificate: <the one it replaced>
KeyRingProtector__RetiredCertificates__0__CertificatePassword: <if that one had a password>

Logging​

Logging:LogLevel:{category}​

Default Information, with Microsoft.AspNetCore at Warning · Admin can override no · Restart yes

The framework's logging levels, and the one place a level is decided: the console, the rolling file and Application Insights all receive exactly the lines these levels let through. Seven categories carry the lines worth recognising, all at Information or above, so the default already shows them:

CategoryWhat it says
TrustBridge.Server.Common.Persistence.SchemaUpgradeWhich migrations were applied at this start, or why the schema was refused
TrustBridge.Server.Common.Persistence.SchemaGateThat the Administrator overrides could not be re-read after the schema step
TrustBridge.Server.Common.KeyRings.KeyRingPostureWhich protector is sealing now, with its thumbprint and the retired count
TrustBridge.Server.Common.Vault.VaultReadinessWhether the vault is off, open, or latched shut
TrustBridge.Server.Common.Audit.AuditLogAn audit row that could not reach the database, at Critical, carrying the row
TrustBridge.Server.Common.Licensing.PkiLicenseGateThat the PKI SDK refused this Server's licence, so neither the fleet nor the vault can sign until it is renewed, or that there is no licence and the Server will not start
TrustBridge.Pki.Validation.LacunaPkiRuntimeThat the PKI SDK is licensed and ready at start, and where its caches are. At Error, why a configured licence would not load

Raise a category, not the floor: framework debug logging buries these seven lines.

Logging:File:Directory​

Default trustbridge-logs under the process's temporary directory (/tmp/trustbridge-logs) · Admin can override no · Restart yes

Where the rolling compact-JSON file is written, one per day, rolled again past one gigabyte. The admin console's Server log screen reads it. Empty turns the file off, which is what a read-only filesystem sets. A directory the Server cannot create leaves the file off for the life of the process with a Warning; the console is untouched.

Logging:File:RetainedDays​

Default 14 · Admin can override no · Restart yes

How many days of files are kept. Below 1 refuses the start. A bound on disk rather than a retention policy: the console pipeline is the record.

Logging:ApplicationInsights:ConnectionString​

Default unset · Admin can override no · Restart yes

An Application Insights connection string. When set, every line is also sent there as a trace with the correlation id and category as custom dimensions. When unset, APPLICATIONINSIGHTS_CONNECTION_STRING is read instead, so a workspace attached through the App Service portal needs no second setting. A value that is not a connection string refuses the start.

AllowedHosts​

Default * · Admin can override no · Restart yes

The framework's Host header filter. Neither posture needs it.

For a Test Signature to work, a list here must include the loopback's host: localhost (or 127.0.0.1 where the Server binds 0.0.0.0), or the host TestSignature:LoopbackAddress names. A run calls the Server's own contract with that name in its Host header, and the filter answers anything it does not list with a bare 400: every Test Signature fails, and the log says only that POST /v1/sessions answered 400 with no error envelope. This applies to the admin console's Test Signature page as well as the public one.

Behind a reverse proxy​

Who the Server believes a caller is. A deployment with nothing in front of it sets nothing here, and neither does App Service, which uses the framework's own switch instead.

Three things read the caller's address: admission's per-address bound (Admission:RequestsPerAddress), the certificate sign-in challenge bound (Admin:CertificateSignIn:Enabled), and the ClientIp of every audit row. By default that is the connection's address, so behind a reverse proxy it is the proxy's, and every caller is one caller. Naming the proxy makes the Server take X-Forwarded-For and X-Forwarded-Proto from it, and from nobody else.

Two ways to turn this on, and a deployment uses one of them:

SettingForTrusts
ForwardedHeaders:TrustedProxiesA proxy you run and can name: Docker behind nginx, HAProxy or a load balancerThe addresses listed, for as many hops as are listed
ASPNETCORE_FORWARDEDHEADERS_ENABLED=trueApp Service, whose front ends cannot be listedWhoever connects, for one hop. Safe only because the container is reachable through those front ends and nothing else

Setting both refuses the start. A Docker deployment that set ASPNETCORE_FORWARDEDHEADERS_ENABLED under an older guide keeps working, trusting whoever connects, and logs a Warning at start saying so; move it to the list.

environment:
ForwardedHeaders__TrustedProxies__0: "10.0.0.5"
ForwardedHeaders__TrustedProxies__1: "10.0.1.0/24"

ForwardedHeaders:TrustedProxies​

Default empty, meaning the connection's own address · Admin can override no · Restart yes

The proxies whose X-Forwarded-For and X-Forwarded-Proto this Server honours. Each entry is an address (10.0.0.5) or a network in CIDR notation (10.0.1.0/24), IPv4 or IPv6. A connection from anywhere else has both headers ignored, so a client that reaches the Server around the proxy cannot name its own address. Honouring X-Forwarded-Proto is what gives a proxy that terminates TLS Secure cookies and https:// login redirects.

  • List the proxy, not the network its clients are on. A client inside a listed network is trusted as a proxy and can name any address it likes.
  • It is a list, even of one. In the environment every entry takes an index, ForwardedHeaders__TrustedProxies__0. ForwardedHeaders__TrustedProxies=10.0.0.5 would be dropped without a word, so the Server refuses the start instead.
  • A chain is walked back to the first address that is not listed. With a load balancer in front of the proxy, list both. A proxy at the edge should replace X-Forwarded-For with the address it saw (nginx's $remote_addr); a proxy behind a listed load balancer should append to it (nginx's $proxy_add_x_forwarded_for). A proxy behind a TLS-terminating balancer passes the balancer's X-Forwarded-Proto on rather than replacing it with http.
  • Only the address and the scheme. X-Forwarded-Host and X-Forwarded-Prefix are never honoured, and the standard Forwarded header (RFC 7239) is not read. The proxy has to pass the browser's Host header through (nginx: proxy_set_header Host $host), or the login redirect points at the upstream address.

The start is refused for a list that would trust the wrong peers:

EntryWhy it is refused
0.0.0.0/0, ::/0Trusts everybody.
Bits set past the prefix, such as 10.0.0.5/24That is how an interface prints its own address; read as a network it would trust the whole subnet.
Not plain dotted decimal, such as 010.0.0.5, 10 or 0x0a000005The parser reads them as other machines. Write the address the way the Server prints it.
IPv4 in IPv6 form (::ffff:10.0.0.5)Write the IPv4 address; it also matches the dual-stack form.
Link-local IPv6 (fe80::…)The Server sees such a peer with a scope attached, so the entry would never match.
Some IPv6 networksOnes .NET 10 would misread as containing IPv4 clients. An exact IPv6 address is always safe, and so is a network of /96 or shorter whose length is a multiple of 8 (/32, /48, /64).
Not an address at allNamed by its index.
A single unindexed value, or any other key under ForwardedHeaders (such as KnownProxies)Would otherwise be read by nothing.
Set together with ASPNETCORE_FORWARDEDHEADERS_ENABLEDThe framework's switch already trusts everybody, so the list would narrow nothing.

When audit rows still show the proxy, read the start line beginning Forwarded headers:: it names the list, says nothing is honoured, or warns that ASPNETCORE_FORWARDEDHEADERS_ENABLED trusts whoever connects. A connection from a peer the list does not name is logged only at Debug, under Logging:LogLevel:Microsoft.AspNetCore.HttpOverrides; raise that category while checking which address the proxy really connects from.

Deployment configuration and never a screen: which machines may speak for a caller's address is a fact about the network in front of the Server, and a wrong value is a way around every per-address rule.

The signature service​

Where Lacuna Signer is and how TrustBridge authenticates to it. Both or neither: an unconfigured Signer is a supported deployment where /v1/signatures refuses and everything else works; an address with no key refuses the start.

environment:
Signer__BaseAddress: https://signer.example.com
Signer__ApiKey: My Application|43fc0da834e48b4b840fd6e8c371…
Signer__ApplicationName: My Application # only for a key that is the secret half alone
Signer__FolderId: f81d4fae-7dec-11d0-a765-00a0c91e6bf6
Signer__RequestTimeout: "00:00:10"
KeyDefaultMeaning
Signer:BaseAddressunsetSigner's base address. Setting it makes ApiKey required.
Signer:ApiKeyunsetThe single Signer API key TrustBridge holds on behalf of every Client Application, in the whole application|secret form Signer publishes.
Signer:ApplicationNameunsetThe application half, only for a key that carries just the secret. Ignored when the key contains |.
Signer:FolderIdunsetThe Signer folder every document lands in, as a GUID. Unset is the organization's root.
Signer:RequestTimeout10 sThe bound on one Signer call. Non-positive refuses the start.

All restart-only, none overridable.

REST PKI Core​

Where REST PKI Core is and how TrustBridge authenticates to it: the engine a Client Application signs its documents with once an Administrator has turned on Sign documents with REST PKI Core on its card on the Client Applications screen (Server 1.18.0 and later). REST PKI Core builds the PAdES around the holder's one signature and keeps the signed document; the Server fetches it from there when the client asks. The integrator sees no difference on the wire.

All three or none. A deployment that sets none of these starts as it always did, and the Client Applications screen refuses to switch an application over, with the one reason that REST PKI Core is not configured. Some of them without the others refuses the start, naming what is missing. An application already switched over on a Server that lost the section is refused each signing call before any key is used, as 500 INTERNAL_ERROR with one Error line.

environment:
RestPkiCore__Endpoint: https://core.pki.rest/
RestPkiCore__ApiKey: TrustBridge|3c6a0f…
RestPkiCore__SecurityContextId: 201856ce-273c-4058-a872-8937bd547d36
KeyDefaultMeaning
RestPkiCore:EndpointunsetThe REST PKI Core instance's address: absolute and https, because the API key travels in every request, and with a path ending in a slash, or the client drops its last segment and every call is a 404. Either is a refused start.
RestPkiCore:ApiKeyunsetThe API key of a REST PKI Core application holding the Worker role, presented as X-Api-Key. A secret: set it from a secret store or an environment variable, never in a committed file.
RestPkiCore:SecurityContextIdunsetThe security context REST PKI Core validates the holder's certificate under, and signs under, by its GUID. A value that is not a GUID is a refused start. The example is REST PKI Core's standard ICP-Brasil context, PkiBrazil.

All restart-only, none overridable.

A section REST PKI Core will not serve is a fault of this deployment, not an outage. A key it refuses (401), a key whose application lacks the Worker role (403), or a security context it does not have answers every signing call 500 INTERNAL_ERROR, with one Error line naming which. REST PKI Core unreachable, failing, rate-limiting or out of quota is 502 SIGNER_ERROR instead, which a client may retry. Any other answer of REST PKI Core's that this Server does not read is 500 INTERNAL_ERROR too, with one Error line carrying REST PKI Core's status. REST PKI Core is asked to answer in English, so its messages reach the log in the log's language.

What is set on REST PKI Core's side​

Three things decide what a document signed there looks like, and none of them is a key here:

  • Trust is the security context's, not the Trusted CAs'. REST PKI Core validates the holder's certificate under the context SecurityContextId names, once before the holder is asked anything and again as it signs, and refuses one it does not trust as 422 CERT_NOT_ACTIVE. This Server's own verdict still gates every signature as well, so the two are asked in turn and can disagree. A deployment whose holders come from CAs beyond ICP-Brasil needs a custom security context, created on REST PKI Core's dashboard by a user with the Manager role and kept in step with the Trusted CAs screen by hand: nothing reflects one into the other. REST PKI Core also checks revocation live, so this Server's grace window for a CA that cannot be reached does not carry over unless the context is set to ignore an unknown revocation status.
  • The signature policy is the REST PKI Core instance's. ICP-Brasil AD-RB, with no timestamp, needs the instance's General:UseBrazilianPdfSigningPolicies on (it is off by default, which signs a plain PAdES Basic) and General:DefaultSignaturePolicy left at Basic (WithTime adds a timestamp). On a self-hosted instance whoever runs it sets them; on Lacuna's hosted instance they are Lacuna's, so ask before relying on the policy.
  • The page marks are the REST PKI Core organization's. REST PKI Core may stamp a link to its own page for the document on the pages before computing the hash, so the marked PDF is the one the holder signs. Whether it does is the organization's PDF marking policy, or else the instance's Pdf:MarkingPolicy, which defaults to WhenPossible. Set it to Never for a deployment whose documents must carry nothing but the signature.

The Server must reach two addresses at REST PKI Core, not one. Endpoint is the address it calls. A signed document is fetched from a URL REST PKI Core answers, which carries a short-lived access ticket and is never stored, and its host is the instance's own site address, not necessarily the endpoint configured here. A deployment that reaches REST PKI Core by an internal name, or behind an egress allowlist, must let the Server out to that site address too, or every download of a REST PKI Core document fails (502 SIGNER_ERROR where it cannot be reached, 500 INTERNAL_ERROR with one Error line where it answers a 4xx) while every signature succeeds. That fetch carries no API key and reads no more than twice Signing:MaxDocumentBytes.

REST PKI Core keeps the signed document; this Server keeps nothing. How long a document stays fetchable is whatever the instance does, and one it no longer has is 422 DOCUMENT_UNAVAILABLE, as a Signer document that is gone is. Switching an application back to the Signer is always allowed, and the documents REST PKI Core signed stay fetchable from it afterwards.

How the signing budget bounds a call signed there​

Signing:RequestBudget covers, as one budget, the certificate's validated upload at admission and every document's run up to the holder's signature: what the upload took is spent from each run's budget, single and batch. A Tray's Signing Authorization under a "workstation" per-transaction session comes between the two and is not counted. Once the holder's key has signed, completion is given a bound of its own, as long as the budget again, which neither the budget nor a client hanging up cuts short, so that a signed document is never abandoned at REST PKI Core for want of time. A call can therefore outlast the budget by up to that much after the key signed, which is why Signing:StrandedAfter must clear twice the budget. Each call to REST PKI Core is also bounded by its client's own 60-second timeout, which is not a key here.

Signing sessions​

Five of the seven Administrator-overridable keys are here. One key in this section is no longer read: Sessions:SignatureAllowance, whose figure is set per Client Application since Server 1.17.0.

environment:
Sessions__DefaultDuration: "01:00:00"
Sessions__MaxDuration: "1.00:00:00"
Sessions__MaxPinFailures: "5"
Sessions__PinFailureWindow: "00:15:00"
Sessions__PinPromptTimeout: "00:01:00"
Sessions__PinRelayTimeout: "00:00:05"
Sessions__ExpirySweepInterval: "00:05:00"
KeyDefaultOverrideRestartMeaning
Sessions:DefaultDuration1 hyesnoThe idle window granted when the client requests none. Non-positive refuses the start.
Sessions:MaxDuration24 h (1.00:00:00)yesnoThe longest window a client may request; a longer request is clamped and the clamped value is echoed. Below DefaultDuration refuses the start.
Sessions:MaxPinFailures5yesnoFailed PIN comparisons inside the window that lock a CPF out of session creation, counted per CPF across all API keys. Also the size of the failed-proof budgets below. Zero refuses the start.
Sessions:PinFailureWindow15 minyesnoThe lockout's rolling window and cooldown, and the window the failed-proof budgets are counted in. Non-positive refuses the start.
Sessions:PinPromptTimeout60 syesnoHow long a Tray prompt stays up before creation gives up. When it runs out the Server takes the prompt down on a protocol 5 machine at once. The workstation's own Tray:PromptTimeout (90 s) is deliberately looser, and is what ends a prompt the Server cannot reach.
Sessions:PinRelayTimeout5 snoyesHow long the cached-PIN pass waits for the machines' answers. Every candidate machine is challenged at once under it, not each in turn, so a machine that never answers costs this much and no more. Checking an A1 Certificate's answer happens after it, on the Server. Non-positive refuses the start. The name is the old relay's and stays because it is a configuration key.
Sessions:ExpirySweepInterval5 minnoyesHow often the sweep closes out lapsed sessions in the record. Changes no verdict. Not validated at start.

The failed-PIN budgets​

Sessions:MaxPinFailures inside Sessions:PinFailureWindow sets three bounds at once. Lowering the figure tightens all three.

BoundCountsWhen it is spent
The CPF lockoutFailed PIN comparisons for one CPF, at most one per request, across all API keysSession creation for that CPF answers TOO_MANY_PIN_ATTEMPTS until the failures age out. A successful creation does not clear them; they age out with the window.
A workstation's failed-proof budgetPIN Proofs one machine gave for one CPF that did not check, whoever else matched, except in a creation where one of that machine's own proofs checkedThat machine is sent no PIN challenge and no prompt for that CPF and counts as unreachable for that holder until its failures age out. Logged at Warning and audited as pin.proof-budget-spent.
The Certificate Vault's budgetPasswords the vault tried for one CPF that opened nothing, once per creation, whoever else matchedThe vault tries none for that CPF until they age out. It stays a candidate that is tried for nothing, so the client is answered PIN_NOT_AVAILABLE rather than WORKSTATION_OFFLINE. Audited as pin.proof-budget-spent naming no machine.

One machine's failures never count against another's, but the holder's own machine can be passed over before the lockout trips: a wrong PIN costs it one failed proof for each of the holder's certificates it holds for the session, where the lockout books one. A holder who binds through a token whose PIN is not their vault password spends the vault's budget too, and finds the vault passed over after that many such sessions in the window.

Sessions:SignatureAllowance​

No longer read since Server 1.17.0 · Admin can override — · Restart —

This key used to bound every Signing Session on the deployment by how many signing calls it could make. The Signature Allowance is set per Client Application now: an Administrator sets, changes or clears it on the application's card on the Client Applications screen, and a session is granted the figure of the Client Application that created it. An application with none set creates sessions bounded by their idle window alone.

A Server that finds this key set logs one Warning at start and keeps starting, naming the key and the Client Applications screen. Any value is warned about, 0 and negative ones included, which used to refuse the start. A key with no value (JSON null, or an empty or blank environment variable) sets nothing and is not warned about.

Upgrading a deployment that set it. From 1.17.0 its sessions are unbounded until an Administrator sets the figure on each Client Application that needs one, because no Client Application has a figure after the upgrade and nothing carries the key's value over. Straight after upgrading, set the figure on those applications (1 for an integrator that asked for single-use sessions), then remove the key.

What the figure on a Client Application means: how many signing calls a session may make before it ends as Exhausted, whichever of its two bounds arrives first. One charge per call, not per document: a batch of fifty spends one, and a call whose documents all failed before the certificate signed spends none. Zero or less is refused on the screen. Granted at creation, so changing an application's figure governs only the sessions it creates afterwards. The wire carries no remaining count; the Signing Sessions screen shows the spend.

The signing surface​

environment:
Signing__RequestBudget: "00:01:00"
Signing__AuthorizationTimeout: "00:01:00"
Signing__MaxPendingAuthorizations: "2"
Signing__HashSignTimeout: "00:00:25"
Signing__SignerCancelBudget: "00:00:03"
Signing__StrandedAfter: "00:10:00"
Signing__StrandedSweepInterval: "00:05:00"
Signing__WorkstationReconnectGrace: "00:00:15"
Signing__ReconnectPollInterval: "00:00:00.250"
Signing__MaxDocumentBytes: "10485760"
Signing__TransportHeadroomBytes: "65536"
Signing__MaxBatchItems: "50"
Signing__SignerConcurrency: "32"
KeyDefaultOverrideMeaning
Signing:RequestBudget60 snoThe whole of one synchronous signature, from admission to the signed bytes, or, under a "workstation" per-transaction session, from the moment the holder answers the prompt. A Hash Signing call is bounded by it as a whole. A breach answers SIGN_TIMEOUT. HashSignTimeout plus WorkstationReconnectGrace must fit inside it, or the start is refused. A document signed with REST PKI Core gets a completion bound of its own after the key signed: see REST PKI Core.
Signing:AuthorizationTimeout60 snoHow long a signing call under a "workstation" per-transaction session waits for the holder to answer the Signing Authorization at the Tray App (once per call, a batch included). Unanswered, the call is 422 PIN_NOT_AVAILABLE (timed out) and nothing is signed. It runs outside RequestBudget, so the time a person spends reading and typing never becomes SIGN_TIMEOUT. Non-positive, or over ten minutes, refuses the start: a workstation refuses a longer prompt window. See below.
Signing:MaxPendingAuthorizations2noHow many per-transaction calls may wait for their holder on one machine at once: one prompt on screen and one in line by default. One more is answered at once with 422 PIN_NOT_AVAILABLE (too many calls waiting), and nothing is signed or shown. Calls one after another are never refused by it. Below 1 or above 8 refuses the start.
Signing:HashSignTimeout25 snoHow long a dispatched hash-sign may take before the machine counts as unreachable. Far above the ~340 ms a token spends, because the machine may be busy with someone else's document.
Signing:SignerCancelBudget3 snoHow long the tidy-up may take when a run that signed nothing ends its document at Signer.
Signing:StrandedAfter10 minnoHow long a Signing Operation may sit Pending before the sweep ends it as Failed with RUN_STRANDED. Must exceed twice RequestBudget plus SignerCancelBudget, or the start is refused: twice since Server 1.18.0, because a document signed with REST PKI Core gets a completion bound as long as the budget once the key has signed. A deployment that set it between the old floor (once the budget) and the new one refuses to start, naming the key; the defaults clear it. AuthorizationTimeout is deliberately not part of the rule: a document's record is opened only once its call is authorized. A REST PKI Core run the sweep ends after completion began also gets one Error line, because REST PKI Core may hold a signed document for it that no Signing Operation names.
Signing:StrandedSweepInterval5 minnoHow often that sweep runs. A pass also runs at start. Not validated at start.
Signing:WorkstationReconnectGrace15 syesHow long a sign waits for its bound workstation to come back before WORKSTATION_OFFLINE. Under a "workstation" per-transaction session the same grace is given once more, before the holder is asked. Zero refuses at once and is legal; negative refuses the start.
Signing:ReconnectPollInterval250 msnoHow often that grace re-checks presence. Non-positive refuses the start.
Signing:MaxDocumentBytes10 MB (10485760)noThe largest document the Server signs, measured on the decoded bytes. Over-cap answers 413 in the contract's envelope. Bounds memory, not storage. The transport limits underneath are derived from it.
Signing:TransportHeadroomBytes64 KBnoHeadroom over the encoded document for the rest of the request body.
Signing:MaxBatchItems50noHow many documents one batch call may name, and how many digests one Hash Signing call may carry. A call with more is refused before any is signed. With MaxDocumentBytes, what a batch in flight can occupy: 500 MB on the defaults. On Hash Signing it is how many times one call may use the holder's key, so raising it for batches raises that too.
Signing:SignerConcurrency32noHow many signing runs may be in flight against the signature service at once, across the whole Server. A document signed with REST PKI Core takes a slot as one signed by the Signer does. A larger batch runs in waves. Zero refuses the start. Hash Signing does not pass through it, because it has no conversation with a signature service.

All restart-only except WorkstationReconnectGrace.

Calls waiting for their holder​

Under a "workstation" per-transaction session one call can take WorkstationReconnectGrace, then AuthorizationTimeout, then RequestBudget. The machine is told the last two, and holds the PIN it was given for no longer than the two together (at most an hour of RequestBudget). A deployment whose holders type slowly lengthens AuthorizationTimeout, and nothing else moves with it.

A call waiting on its holder keeps its place under the global throttle (Throttle:ConcurrencyLimit) the whole time, prompt included. Many holders leaving prompts unanswered at once fill that bound faster than signatures do, so a deployment that turns Per-Transaction PIN on for a busy Client Application counts those calls against it, not against SignerConcurrency, which they do not reach until the holder has answered. MaxPendingAuthorizations caps how many one machine can hold, so the most a fleet can hold is that many per machine, however many calls a client fires.

The global throttle​

One bound on how many contract calls the Server runs at once, with a short queue in front of it. A shed call answers 429 SERVER_BUSY with no Retry-After. It applies to the SignSession Contract surface only; the workstation channel, the admin console, the health probe, the Landing Page, the documentation, workstation admission and Workstation Release downloads are outside it (downloads have a cap of their own). The defaults sit nearly seven times above the designed load: a circuit breaker, not a throughput cap.

KeyDefaultMeaning
Throttle:ConcurrencyLimit200How many contract calls may run at once. A call holds its place for as long as its request does, which under a per-transaction session includes the time its holder takes at the prompt. Zero refuses the start; there is deliberately no way to switch it off. Signing:SignerConcurrency is the knob that shapes signing throughput, not this.
Throttle:QueueLimit50How many calls may wait for a place before the next one is shed. Zero is legal and means no queue; negative refuses the start.

Timing diagnostics​

Diagnostics:ServerTiming​

Default false · Admin can override no · Restart yes

Whether every SignSession Contract response carries a Server-Timing header that breaks the call into the legs it reached: gate (the wait for a Signing:SignerConcurrency slot), one signer-… figure per Signer call (or one rest-pki-core-… figure per REST PKI Core call, for a document a Client Application signs with REST PKI Core), workstation (the round trip to the machine), the machine's own ws-… stages, and the call's total. Each leg is in milliseconds, summed over the call, with its count beside it. A leg the call never reached is absent. A call the throttle sheds, and a 401, carry no header.

Server-Timing: gate;dur=0.1;desc="n=1", signer-upload;dur=212.4;desc="n=1", …, workstation;dur=402.7;desc="n=1", total;dur=1203.7

It is for measuring a deployment, for example with a load test, and is left off in production: the header goes to every caller holding an API key, so it tells every Client Application how long the signature service and this deployment's tokens take, and which sessions sign on a workstation rather than in the Certificate Vault.

Since Server 1.17.0 it decides only whether the header is sent. Turning it off hides the legs; it does not stop them being measured. The Server times every signing call whatever this says, and writes the legs into the details of the Audit Log rows it writes anyway (signing.completed or signing.failed for a document, hash-signing.completed or hash-signing.failed for a Hash Signing call) as a timing member and, where the machine gave one, a machineReported member. Those rows are read by administrators, never by a Client Application, and the Dashboard's timing figures come from them.

environment:
Diagnostics__ServerTiming: "true"

Workstation admission​

Which machines may ask to join, how much of the queue a stranger can fill, and how long an abandoned request stays. A default deployment sets none of these. All restart-only, none on the Settings screen.

environment:
Admission__InstallerKeys__0: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE…"
Admission__MaxPending: "500"
Admission__RequestsPerAddress: "300"
Admission__RequestWindow: "00:01:00"
Admission__PendingExpiry: "7.00:00:00"
Admission__RequireHardwareBackedKeys: "false"
KeyDefaultMeaning
Admission:InstallerKeysThe Installer Key that ships with TrustBridgeThe Installer Keys this Server accepts admission requests signed with, each a base64 DER SubjectPublicKeyInfo for an ECDSA P-256 public key. Setting this replaces the default rather than adding to it. Name two while changing one: add the new key beside the old, update the fleet, then drop the old entry. A value that will not parse refuses the start.
Admission:MaxPending500How many machines may be waiting for an Administrator at once. Past it, a request from a machine this Server has never seen is refused. Zero refuses the start.
Admission:RequestsPerAddress300How many admission requests one source address may make per window, an IPv6 address counted by its /64. A nuisance bound: behind NAT a whole office is one address, and behind a reverse proxy the whole fleet is counted as the proxy unless ForwardedHeaders:TrustedProxies names it.
Admission:RequestWindow1 minThe fixed window for the above.
Admission:PendingExpiry7 daysHow long a machine's request stays in the queue after the Server last heard from it. Counted from the last request, so a machine still asking never lapses. Expiry is not a refusal.
Admission:RequireHardwareBackedKeysfalseWhether to refuse admitting a machine whose Workstation Key is not in hardware. Such a machine still appears in the queue, with the screen saying why the Admit control refuses it. The posture is the machine's own claim; nothing attests it.

The admin console​

The first Administrator, whether a password alone reaches the console, whether a certificate can, and whether a password still can at all.

environment:
Admin__Bootstrap__Email: admin@example.com
Admin__Bootstrap__Password: ${ADMIN_BOOTSTRAP_PASSWORD}
Admin__Bootstrap__DisplayName: Platform Administrator
Admin__SecondFactor__Required: "true"
Admin__CertificateSignIn__Enabled: "false"
Admin__CertificateSignIn__WebPkiLicense: ${WEB_PKI_LICENSE}
Admin__CertificateSignIn__Required: "false"

Admin:Bootstrap:Email, Admin:Bootstrap:Password, Admin:Bootstrap:DisplayName​

Default unset · Admin can override no · Restart yes

The first Administrator. Read on a Server whose account table is empty, and inert on every start after that: a configured password never re-creates a deleted account or resets a live one. The password must satisfy the identity password rules or the account is not created and the reason is logged. Remove both once you have signed in. With these unset on an empty table, the Server logs a Warning and creates nothing; the console cannot be signed into until something puts an account there.

Admin:SecondFactor:Required​

Default true · Admin can override no, deliberately · Restart yes

Whether an Admin Account must present a second factor as well as its password. Deployment configuration and never a screen: a switch that disables the second factor, reachable from inside the console it protects, is a switch an intruder flips on the way past. A row saved in the database under this key is ignored. Platform-wide, with no per-account exemption.

It is also the break-glass for an Administrator who has lost their authenticator; the procedure is in Security. The weakened posture is announced at every start while it is off.

Admin:CertificateSignIn:Enabled​

Default false · Admin can override no, deliberately · Restart yes

Whether the console offers to sign in with an ICP-Brasil e-CPF. The certificate is chosen on a page of its own, /admin/login/certificate, which a link on the login page leads to, and read from the person's own computer through Lacuna Web PKI, a browser extension with a native part that each person installs once; desktop browsers only. On a phone or a tablet the certificate page says so. Only e-CPF certificates are listed.

Off, the login page shows no trace of it: no link, no stylesheet of its own and no cookie, and a bookmark of the certificate page is sent to the login page. On, it needs Admin:CertificateSignIn:WebPkiLicense, or the Server refuses to start and names that key. On or off, the login page itself carries no script and no licence; only the certificate page does. The login page keeps its password form unless Admin:CertificateSignIn:Required takes it away.

On, the login page opens on the way in the browser last used (Server 1.20.0 and later). An accepted password or certificate, accepted even if a Second Factor step then follows, writes a cookie, TrustBridge.SignInCredential, scoped to /admin/login and kept for a year, that says which. It is not a credential, authorizes nothing and outlives a sign-out. A browser whose last one was a certificate is sent from the login page to the certificate page, unless the address names a refusal or asks for the password form (?usePassword=true); a Password | Digital certificate switch on both pages keeps the other way one click away. Turned off again, the cookie is no longer read or written.

On, an account with a Bound CPF signs in with its certificate alone: no password and no Second Factor code. An Administrator types another account's CPF on the accounts screen, when creating it or with Set Bound CPF on its row, and only while this key is on; nobody sets their own, so nobody signs in with a certificate until another Administrator has set one. An account that holds no Second Factor yet, where Admin:SecondFactor:Required is on, is led through enrolling one first.

The certificate page asks for a challenge at /admin/login/certificate/challenge, rate-limited to ten a minute per source address, and gives up on a request after thirty seconds. Behind a reverse proxy, name it in ForwardedHeaders:TrustedProxies, or every browser shares the proxy's ten, and anybody who can reach the certificate page can use them up for a minute at a time.

A row saved in the database under this key is ignored, for the reason Admin:SecondFactor:Required gives: what decides how a person reaches the console is never configured from inside it.

Admin:CertificateSignIn:WebPkiLicense​

Default unset · Admin can override no · Restart yes

Required when Admin:CertificateSignIn:Enabled is on. The Web PKI licence, in the one-line format Lacuna issues, written as a single string. Not the JSON format: written as an object it binds to nothing, and the Server refuses to start as though no licence were set. It is not the PKI SDK licence (Certificates:Validation:License).

It is not a secret. It is evaluated in the browser, so anyone who loads the certificate page can read it, and it is bound to the domains it names. It has to name the hostname people type: a licence that does not cover it fails the first time the component is used, and the certificate page reports it as a licence problem for the operator.

Admin:CertificateSignIn:Required​

Default false · Admin can override no, deliberately · Restart yes

Whether passwords are switched off for the admin console. On, the login page sends every visitor on to the certificate page, a password posted anyway is refused with nothing checked and nothing counted against the account, and a certificate for an account's Bound CPF is the only way in. Second Factor enrollment on a first certificate sign-in works as it does without it.

It needs Admin:CertificateSignIn:Enabled on. On while that is off describes a console with no way in at all, so the Server refuses to start and names both keys. That is the only start this key refuses.

An account with no Bound CPF cannot sign in while it is on, Administrator or Auditor alike. Another Administrator sets one on the accounts screen, and passwords stay off while they do. Nobody can set their own, so a lone Administrator cannot switch passwords off: a deployment that does needs at least two Administrators, each with a Bound CPF the other set, before turning this on.

It warns at every start while it is on. The warning counts how many enabled accounts hold no Bound CPF, how many of those are Administrators, and how many enabled Administrators there are. When the second number reaches the third, nobody can administer the platform. Where no proxy is named in ForwardedHeaders:TrustedProxies, the warning also says that every browser shares the proxy's ten challenges a minute, so anybody who can reach the certificate page can keep everybody out. Name the proxy before turning this on.

It is the break-glass. When nobody can produce a certificate the Server accepts (a Trusted CA removed, a lapsed PKI licence, a Web PKI licence that no longer names the hostname, a browser update that broke the extension, or a last Administrator's Bound CPF cleared), set it to false and restart: password sign-in comes back with no other change. Turn it on again afterwards; nothing reminds you, because the warning speaks only while passwords are off.

A session opened with a password before the switch is not ended by it. A password sign-in that was waiting at the code step when the Server restarted is sent back to the login form.

The Audit Log​

Audit:RetentionDays​

Default 365 · Admin can override yes · Restart no

How long audit events are kept; a daily purge deletes older rows. Below 1 refuses the start: a retention of zero would delete the whole trail on the next pass.

The purge does not touch Signing Activity, the Dashboard's counts: they name no holder and are kept for as long as the installation is, so its figures "since the installation" outlive this retention and will disagree with a count of the surviving audit rows. Both are right.

The purge never deletes a row Signing Activity has not counted yet. On a Server's first start with 1.17.0 or later, Signing Activity is back-filled from the whole Audit Log, a chunk at a time, and the purge deletes nothing until that is done; after that it never deletes past the point the count has settled to. On a running Server that point is five minutes behind now, so this changes nothing. It matters only when the count has stalled, or the Server was stopped for longer than the retention, and then the purge logs a warning saying where it is held and catches up once the count does.

There is no setting for where a failed audit write goes. An entry the database will not take is written to the Server log at Critical, carrying the row itself, and the admin console raises an alert. It reaches you through whatever log pipeline the deployment has, so audit fallback belongs in your log retention, not your backups.

The Dashboard​

The calendar the admin console's Dashboard reads Signing Activity in (Server 1.17.0 and later). Signing Activity is counted off the Audit Log a minute at a time, five minutes behind, into hourly buckets in UTC; this key says where a day and a month start when those hours are read back. The lag and the cadence are not settings.

environment:
Dashboard__TimeZone: America/Sao_Paulo

Dashboard:TimeZone​

Default UTC · Admin can override no · Restart yes

The IANA time zone whose midnight starts a Dashboard day and whose first of the month starts a month: America/Sao_Paulo for a deployment whose operators read the day in Brasília time, where a day starts at 03:00 UTC. It is the deployment's calendar, never the reader's. A zone the Server cannot find refuses the start, naming this key, and so does a Windows zone id such as E. South America Standard Time, whose refusal names the IANA zone to write instead.

Read once, at start. Changing it re-counts nothing: the same UTC hours are read back into different days, so it can be corrected at any time. In a zone whose offset is not a whole number of hours, the hour that holds midnight counts with the day it starts in.

Languages​

environment:
Language__DefaultCulture: pt-BR
Language__SupportedCultures__0: en
Language__SupportedCultures__1: pt-BR
KeyDefaultMeaning
Language:DefaultCultureenWhat a request with no usable Accept-Language is answered in. Not a deployment-wide language: the reader's browser preference and the switcher win. A value outside the supported set, or a typo, refuses the start.
Language:SupportedCulturesempty, meaning en and pt-BREvery language the page realms answer in. Naming a set replaces it.

The contract surface, the logs and the Audit Log stay English permanently.

The published documentation​

Documentation:ApiReferenceEnabled​

Default true · Admin can override no · Restart yes

Whether the browsable reference is served at /scalar. The machine-readable document at /openapi/v1.json is the integration contract, is always served, and is not affected. Both answer without a credential.

The Test Signature​

Whether the Landing Page lets a visitor make a Test Signature: a real signature, through the Server's own contract, the certificate's holder and Lacuna Signer (or REST PKI Core, for a key whose Client Application signs its documents with it), on a one-page document the Server writes about the certificate. It is how an operator proves a fresh deployment works end to end from a browser, and how a demonstration shows the product signing. Off by default.

An Administrator can make one either way, from the admin console's own Test Signature page (/admin/test-signature, under System), behind the admin login and its second factor. An Auditor sees there whether the public page is on, and no form. Off, the public page does not exist: /test-signature answers 404 and the Landing Page is what it was.

On, the Landing Page links to /test-signature, where whoever asks types a CPF, its PIN, optionally a CNPJ, a Client Application's API key, optionally a fallback e-mail address, a timeout, and how many Documents to sign. The run is one request: a Signing Session created with that key, the certificate's details, the document, its signature and the signed PDF, and the session ended afterwards whether the run succeeded or failed. The asker gets the certificate's public details and the signed PDF, or what was refused, or which step failed, with the contract's error code beside a failure at the signature step (Server 1.24.0). No session or signature id reaches the browser.

More than one document is one batch call (Server 1.23.0): up to 50, or Signing:MaxBatchItems where that is lower, signed as numbered copies of the page in one POST /v1/signatures/batch call, so one charge of the Signature Allowance and, under a Per-Transaction PIN, one confirmation at the Tray App for them all. The documents that signed come back as one ZIP, test-signatures.zip, and each that did not is named beside it by the step it failed at; the run counts as completed if at least one signed. A timeout that runs out while the signed copies are fetched keeps the ones already fetched. A run of one document is the single call it always was.

  • The key is the asker's, and the Server holds none. Every call of the run goes through the API-key check, the global throttle and the Audit Log as that key's Client Application, exactly as an integrator's call does. So a demonstration begins by handing somebody a key, and whoever holds it could use it against the contract directly. The page writes every field back into the page that answers the form, the API key and the PIN included, so that Sign again is one press (Server 1.23.0). That page is sent no-store, and a demonstration on a shared machine ends by closing the tab. The Server logs neither the key nor the PIN, and forgets both once that page is written.
  • Any CPF may be tried, and a run replaces a Signing Session already active for the same CPF and CNPJ, as creating a session with that key from any client would; the page says so beside the form. A wrong PIN counts towards the holder's lockout. One run per CPF at a time.
  • A key whose Client Application has Per-Transaction PIN on works: under a "workstation" session the run waits for the holder at the Tray App within its timeout.
  • Each run writes test_signature.completed, test_signature.failed (with the step) or test_signature.refused (with the reason) to the Audit Log, with the asker's address, the CPF and CNPJ and the key's Client Application, and for a batch how many documents were asked for and, once completed, how many signed. Never the PIN or the key.
  • The page needs no route to the internet.
environment:
TestSignature__Enabled: "true"
TestSignature__MaxTimeout: "00:02:00"
KeyDefaultOverrideMeaning
TestSignature:Enabledfalseno, deliberatelyWhether the Landing Page offers a Test Signature. It governs the public page alone. Deployment configuration and never a screen: it puts a signing form at the root of the host, and a row saved in the database under it is ignored.
TestSignature:MaxTimeout2 minnoThe longest timeout an asker may type, and the value the form starts at. The typed timeout bounds the whole run and is the duration of the session it creates. A typed value above this is refused, not cut down. Under one second or over one hour refuses the start.
TestSignature:LoopbackAddressunset (where the Server listens)noWhere a run reaches the Server's own /v1. Unset, an address the Server listens on, plain HTTP before HTTPS, with a wildcard binding such as http://+:8080 read as http://localhost:8080. Set, an absolute http or https address; anything else refuses the start.

All three are restart-only, and MaxTimeout and LoopbackAddress are checked at start even with Enabled off, because the admin page uses them.

Keep MaxTimeout below your reverse proxy's read timeout. A run is one HTTP request held open until the signed document is back, and a proxy that stops waiting cuts it. App Service's front end ends a request after 230 seconds; nginx's proxy_read_timeout is 60 seconds unless raised; Cloudflare's proxy answers 524 at 100 seconds. Leave room for the holder to answer a Tray App prompt, for the signature service, and 15 seconds beyond the value for the run to end its session. Behind nginx's default or Cloudflare, the default of two minutes is too long. The page itself stops waiting a minute after the typed timeout, or on Escape, and brings the form back as it was sent.

Where the loopback goes:

  • App Service listens on plain HTTP behind the platform's TLS, so the default is right.
  • The Docker posture listens on HTTPS alone (https://+:8443), with a certificate issued for the name visitors type, which localhost is not, so the default would fail on every run, the admin page's included. Add a plain-HTTP listener bound to the container's own loopback, ASPNETCORE_URLS: https://+:8443;http://localhost:8080, and the default picks it. Nothing outside the container can reach it, and the compose file publishes only 8443. A deployment behind your own proxy already listens on plain HTTP and needs nothing.
  • Whichever host the loopback calls, AllowedHosts has to admit it.

Upgrading from Server 1.14.x: TestSignature:Cpfs, ApiKey, FallbackEmail, Turnstile:SiteKey, Turnstile:SecretKey and Timeout are no longer read (the asker types what they supplied, there is no Cloudflare Turnstile check, and Timeout is now MaxTimeout). A Server that finds any of them set logs one Warning naming them and starts. Remove them, and revoke the key ApiKey held on the Client Applications screen: it is still a live key of its Client Application.

The workstation channel​

How an admitted machine proves it is itself on every connection, a short-lived assertion signed by its Workstation Key, and how long the Server waits to hear from a connected machine. The two assertion keys are not knobs a healthy deployment touches; ClientTimeout is one to change once, after the fleet is updated.

environment:
Workstations__ClockSkewTolerance: "00:01:00"
Workstations__MaxAssertionLifetime: "00:05:00"
Workstations__ClientTimeout: "00:00:30"
KeyDefaultMeaning
Workstations:ClockSkewTolerance60 sHow far apart the Server's clock and a workstation's may be before that machine cannot connect. Applied at both ends. The Server's log names the drifting machine and the offset, positive when the machine is behind the Server (A workstation assertion was refused: Expired or NotYetValid). Negative or over one hour refuses the start.
Workstations:MaxAssertionLifetime5 minThe longest validity window honoured on an assertion; a longer claim is refused. Bounds the nonce cache, which holds at most 100 000 nonces and refuses rather than forgets when full. Non-positive or over one hour refuses the start.
Workstations:ClientTimeout30 sHow long a connected machine may go silent before the Server hangs up on it. See below.

All restart-only, none overridable.

Workstations:ClientTimeout​

Default 30 s (00:00:30) · Admin can override no · Restart yes

How long a workstation may go without the Server hearing anything from it, a call or a keep-alive ping, before the Server closes its connection. The machine then reads offline on the fleet page at its next refresh (within 10 s), its last-seen is written, and the disconnect lands in the Audit Log. A machine that is still there reconnects on its own. This only matters for machines that vanish: a pulled cable, lost Wi-Fi, a closed laptop lid, a firewall that drops the connection silently. A machine that shuts down or stops its Service cleanly reads offline at the next refresh whatever this says.

Set it to 00:00:15 once every machine runs a Workstation release newer than 1.1.0, and not before. The Server and a current Workstation Service ping every 5 s; Workstation 1.0.x and 1.1.0 ping every 15 s, and nothing the Server says changes that. Held to 15 s, those machines would be hung up on after nearly every ping, and each hang-up is a reconnect, a handshake and a connect/disconnect pair in the Audit Log. The fleet page's Version column is where to check. Each machine the setting is too short for is named in the Server's log at Warning, once per connection: look for pings every 00:00:15, but Workstations:ClientTimeout is.

At 15 s a machine that vanishes reads offline within about 25 s; at the default, about 40 s. A machine on long polling (behind a proxy that refuses WebSockets, for example) is not timed out by this at all: it never pings, and its connection ends on the connection layer's own idle timeout once its polls stop.

A disconnect row in the Audit Log says why, as "reason" in its details: timed-out for this timeout, closed, revoked, removed, returned-to-queue, server-stopping or error. The machine's Events tab on the fleet page shows the same word, except for removed: a removed machine has no panel, so its last disconnect is read on the Audit Log, under the machine's id. A machine on long polling that went quiet reads closed, so the row also names its transport.

Write it as hh:mm:ss. A bare 15 binds as fifteen days and 0:15 as fifteen minutes. Shorter than 10 s (twice the 5 s a current machine pings at) or longer than 5 minutes refuses the start.

Rolling a Server and its fleet across protocol 6​

The channel is at protocol 6 (Server 1.15.0, Workstation 1.6.0), which adds the Signing Authorization: under a session whose creator has Per-Transaction PIN on, the Server asks the bound machine's Tray for the PIN for every signing call. This Server's window is 5–6. Workstation 1.7.0 to 1.10.0 stay at protocol 6.

MachineProtocolAgainst Server 1.15.0 or later
Workstation 1.6.0 to 1.10.06Fully supported. Deliver Workstation 1.8.0 and later with Server 1.20.0 or later, the first whose workstation channel takes their Workstation Report: an earlier one closes the connection of a 1.8.0 report past 32 KB, while 1.8.1 and later keep their reports under 32 KB until the Server has taken one, and stay connected. The console that Workstation 1.10.0 reports is shown from Server 1.23.0.
Workstation 1.5.05Connects and serves every session as before. It is never sent a Signing Authorization, so for a session whose creator has Per-Transaction PIN on it counts as offline: such a holder whose machines are all on protocol 5 is answered 422 WORKSTATION_OFFLINE, and the Server logs a Warning naming the machine and its protocol each time it passes one over.
Workstation 1.4.x and older4 or lessRefused at the handshake, with update required on its fleet row and in the Audit Log. It cannot be offered a release either: update it by hand or by Group Policy.

Before upgrading the Server, check the Fleet screen for any admitted machine still on 1.4.x, and update it to Workstation 1.5.0 first. Then upgrade the Server, upload a protocol 6 release and let the fleet take it. Turn Per-Transaction PIN on for a Client Application only once its holders' machines run 1.6.0 or later. A protocol 6 machine cannot connect to a Server before 1.15.0 (window 4–5): it is answered update the Server first, gets no licence, and every certificate reads UNTRUSTED in its Tray App until the Server is upgraded.

What changes for integrators: nothing, unless their Client Application has Per-Transaction PIN on. The protocol is the channel's, not the contract's.

Rolling a Server and its fleet across protocol 5​

Superseded in one respect by protocol 6, above: a Server from 1.15.0 has the window 5–6, so the protocol 4 machine this section describes as connecting is refused. The rest is how protocol 5 arrived.

Protocol 5 (Server 1.13.0, Workstation 1.5.0) is the version on which the Server stops sending the client application's PIN to any workstation. Session creation challenges each candidate machine with a nonce, the machine answers with a PIN Proof over the PIN it has cached (or, for an A1 Certificate, over its A1 PIN's verifier), and the Server decides whether it matched. Servers 1.13.0 to 1.14.1 have the window 4–5.

MachineProtocolAgainst Server 1.13.0 to 1.14.1
Workstation 1.5.05Fully supported.
Workstation 1.4.x4Connects, is licensed, signs for sessions already bound to it and can be offered a release. It is never sent a PIN challenge or a prompt, so for session creation it counts as offline: a holder whose machines are all on protocol 4 is answered 422 WORKSTATION_OFFLINE, and the Server logs a Warning naming the machine each time it passes one over.
Workstation 1.0.x to 1.3.02 or 3Refused at the handshake, with update required on its fleet row and in the Audit Log. It cannot be offered a release either: update it by hand or by Group Policy.

That upgrade was not "Server first, the fleet within one release". A machine on protocol 4 serves no new sessions until it updates, so the Server and the fleet moved together: Workstation 1.5.0 was uploaded on the Workstation Releases screen as part of the Server upgrade, and offered at once. A protocol 5 machine cannot connect to a Server before 1.13.0 (window 3–4).

What changes for integrators: nothing. The PIN still arrives in POST /v1/sessions and goes no further than the Server. INVALID_PIN now means that every candidate gave a proof and none checked against the PIN sent.

Rolling a Server and its fleet across protocol 4​

Protocol 4 (Server 1.9.0, Workstation 1.4.0) is the version on which the Server tells a machine which Workstation Release it is offered, so Server-delivered updates need it at both ends. In every handshake a protocol 4 or later machine tells the Server whether Group Policy has turned Server-delivered updates off, the Publisher Pin its Service was built with, and how its last update ended. The Server shows all three on the machine's row and writes workstation.update-installed, workstation.update-failed or workstation.update-refused to the Audit Log when the reported outcome changes.

An offer reaches only an Admitted machine below the release whose Publisher Pin lists the release's signer and whose policy leaves updates on. Offering a machine the release it already holds again is how to retry after a failed install, since a machine never retries one by itself. A release offered to any machine cannot be deleted until the offers are withdrawn. The screens are described in Admin console.

Workstation Release downloads​

How the Server hands an offered machine its Workstation Release. A release offered to every machine is up to 500 machines × about 100 MB from one Server, so the one key here is about not stampeding. A default deployment does not set it.

A machine downloads from GET /workstations/releases/{version}. Every request, a resumed range as much as the first, carries a fresh assertion signed by the machine's Workstation Key and is served only while the Server offers that release to that machine, so withdrawing an offer stops a download at its next request. Every refusal answers the same 401; the Server's log says which check failed (A Workstation Release download was refused). Downloads are outside the global throttle and write no Audit Log row of their own; the trail records what each machine then did with the file.

A download is served from the copy in /tmp/trustbridge-releases, rebuilt from the database when it is missing, or read straight from the database where that directory cannot be written, which the Server logs once at Warning.

environment:
WorkstationReleases__MaxConcurrentDownloads: "20"

WorkstationReleases:MaxConcurrentDownloads​

Default 20 · Admin can override no · Restart yes

How many release downloads the Server sends at once. A download holds its place for as long as its request does. One over it is answered 503 with Retry-After, never the contract's 429 SERVER_BUSY, with a wait drawn afresh between 30 and 90 seconds so that machines turned away together do not return together. Each machine also waits a random delay of up to ten minutes before it starts, which spreads most of a rollout before this bound is reached.

Raise it for a Server on a fast link whose fleet should update sooner; lower it where the link is shared with signing traffic that must not wait. Zero or negative refuses the start.

Workstation Reports​

What the Server keeps of the Workstation Report each machine sends it unasked (Server 1.20.0 and later; Workstation 1.8.0 is the first release that sends one). Every claim in a report replaces the one before it, so only the errors need a retention: one group per machine, source, logger category and template, with a running count, kept until the machine has not reported it for a while. They are never written to the Audit Log. A default deployment does not set it.

The Server also keeps each machine's console (Server 1.23.0, from machines on Workstation 1.10.0 or later): the Console Account, whether the console session is locked, whether a Tray App runs there and its version, and how many other sessions have one, as the last report said it. It needs no retention: it is kept for an offline or revoked machine, goes with a removed one, is cleared by a report that says reporting is off, and is never written to the Audit Log.

environment:
WorkstationReports__ErrorRetentionDays: "30"

WorkstationReports:ErrorRetentionDays​

Default 30 · Admin can override no · Restart yes

How many days a machine's error group is kept once the machine has stopped reporting it. A daily sweep deletes the groups no report has added to for longer than this, by this Server's clock, so a machine whose clock is wrong neither loses its groups early nor keeps them forever; a group the machine reports again is kept, its count running on. The count of lines a machine logged beyond the 100 kinds one report lists goes the same way. A machine that is removed takes its groups with it, whatever their age. The fleet page's N kinds of error (24h) chip counts only the past day whatever this says, so lowering it trims the detail panel's history and changes no chip.

Below 1 refuses the start: the next pass would delete every error group the fleet reported.

The Certificate Vault​

environment:
Vault__Enabled: "true"
Vault__RevalidationInterval: "01:00:00"

Vault:Enabled​

Default false · Admin can override no, deliberately · Restart yes

Whether the Certificate Vault exists on this Server, that is, whether it may hold a private key of its own. Off by default because the tier makes the platform's founding claim conditional; an operator opts in. A row saved in the database under this key is ignored, for the same reason as Admin:SecondFactor:Required. Turning it on gives the PKI SDK licence, which every deployment already carries, a second job (reading the Server's own Vault Certificates) and brings the vault latch with it. Turning it off deletes nothing: the rows stay, revalidation stops, and the paths that would read them answer that this Server holds no key ring.

Vault:RevalidationInterval​

Default 1 h · Admin can override no · Restart yes

How often the Server rechecks the verdict of what the vault holds. Not validated at start: a non-positive value is reported at Critical and the worker declines to run.

Certificate validation and the PKI SDK licence​

The same section name the Workstation Service reads, with two keys that mean something only there. The licence is required and loaded at every start, vault or not, because it is the licence the whole fleet runs on. Only a deployment with the vault on resolves the anchor set; the timeouts and grace window are validated at every start regardless.

environment:
Certificates__Validation__License: ${PKI_LICENSE}
Certificates__Validation__UseIcpBrasil: "true"
Certificates__Validation__UseMachineRootStore: "false"
Certificates__Validation__UseLacunaTestPki: "false"
Certificates__Validation__RevocationGraceWindow: "1.00:00:00"
Certificates__Validation__RevocationTimeout: "00:00:05"
Certificates__Validation__CheckTimeout: "00:00:30"

Certificates:Validation:License​

Default unset · Admin can override no · Restart yes

Required on every deployment, with or without a Certificate Vault. The whole LacunaPkiLicense.config Lacuna issues, base64-encoded on one line, not a path. The image carries no licence, so a renewal is a settings change and a restart rather than a rebuild.

It is the fleet's licence as well as the Server's. The Server hands the value, exactly as configured, to every Admitted workstation in the answer to its handshake; that is how an installed Workstation Service is licensed, and there is no licence to set on a machine. A machine holds it in memory only and is handed it again on every connection, so restarting the Server reaches the whole fleet: every connected machine reconnects and applies the value, and a machine that was offline picks it up when it next connects. A Pending machine is handed nothing.

base64 -w0 LacunaPkiLicense.config
[Convert]::ToBase64String([IO.File]::ReadAllBytes("LacunaPkiLicense.config"))
StateWhat happens
Missing (unset, empty or blank)The Server refuses to start, naming this key. Checked before the schema step, so a Server that is going to refuse does not migrate the database first.
Refused (expired, garbled, not base64, not a licence)The Server starts and logs one Critical line naming this setting, with one Error line saying why where that is known. Every admin page carries a banner saying that neither the fleet nor the vault can sign until the licence is renewed. The value is still handed to the fleet, and each machine that refuses it says so at Critical. With the vault on, every Vault Certificate reports UNTRUSTED and imports are refused outright.
LoadedOne line at start saying the SDK is licensed and ready.

It expires, and nothing warns before that date. The first notice is the Critical line and the banner, and by then nothing signs, in the vault or on any machine. Renewing it is replacing this value and restarting the Server; there is no runtime path, and an Administrator cannot override it from the console. Put its expiry on whatever calendar tracks certificate renewals. The SDK only reads a licence from a file, so the value is written to /tmp/trustbridge-pki and deleted in the same call; a read-only filesystem with no writable /tmp cannot load one.

The anchor sources​

KeyDefaultMeaning
Certificates:Validation:UseIcpBrasiltrueTrust the ICP-Brasil roots the PKI SDK carries: the production anchor set, embedded, no download. Turning it off is a trust decision.
Certificates:Validation:UseMachineRootStorefalseAlso trust the machine's own root store. On the Server, a Linux container, this does nothing; it is the workstation's knob.
Certificates:Validation:UseLacunaTestPkifalseAlso trust the Lacuna Test PKI. Development only: Lacuna publishes the private keys of those sample certificates. A Server with this on logs Critical at start and shows a warning banner on every admin page.
Certificates:Validation:UseServerTrustedCastrueA workstation's switch. Inert on the Server.
Certificates:Validation:TrustedRootPathsemptyNot honoured on the Server. A Server that finds it set starts anyway and logs one Critical line naming the key and the admin page that replaced it. Add a Trusted CA at /admin/trusted-cas instead.

Two Critical lines describe an anchor set that anchors nothing: no anchors at all, and anchors that are only intermediates with no self-signed root, so no chain can terminate. Both make every certificate UNTRUSTED.

The revocation bounds​

KeyDefaultMeaning
Certificates:Validation:RevocationGraceWindow24 h (1.00:00:00)How long the last successful revocation check keeps a certificate ACTIVE while its CA cannot be reached; past it the certificate goes STALE. This is the trust decision: exactly how long a certificate keeps signing after its CA stopped answering, revoked or not. Zero is legal and strictest; negative refuses the start.
Certificates:Validation:RevocationTimeout5 sHow long one CRL download or issuer fetch may take before the CA counts as unreachable. Also bounds each fetch of the Trusted CA discovery's AIA walk. Non-positive refuses the start.
Certificates:Validation:CheckTimeout30 sThe outer bound on one certificate's whole check, including the SDK's OCSP fallback, which has no bound of its own. Zero waits indefinitely and is not refused.

Settings that are not appsettings keys​

SettingBelongs toWhere
ASPNETCORE_ENVIRONMENTThe frameworkUnset in both postures, and unset means Production, which keeps appsettings.Development.json inert.
ASPNETCORE_URLSKestrelhttps://+:8443 on Docker, HTTPS only, or https://+:8443;http://localhost:8080 to make Test Signatures (see The Test Signature). http://+:8443 on Docker behind a reverse proxy that terminates TLS, with the Kestrel__Certificates__* settings removed. Not set on App Service.
Kestrel__Certificates__Default__Path / __PasswordKestrelThe Server's own TLS certificate on Docker. Nothing to set on App Service.
WEBSITES_PORTApp Service8080.
WEBSITES_CONTAINER_START_TIME_LIMITApp Service1800, so a long migration is not killed and restarted in a loop.
ASPNETCORE_FORWARDEDHEADERS_ENABLEDThe frameworktrue on App Service, or every audit row records the platform's front end instead of the caller. It trusts whoever connects, so a proxy you run is named in ForwardedHeaders:TrustedProxies instead; setting both refuses the start.
APPLICATIONINSIGHTS_CONNECTION_STRINGApp ServiceRead when Logging:ApplicationInsights:ConnectionString is unset.
TMPDIRThe processMoves all three temporary directories (trustbridge-pki, trustbridge-logs, trustbridge-releases). There is deliberately no TrustBridge setting for it.
View as Markdown