Configuração
Todas as chaves do appsettings.json do Lacuna Bulk Signer — tipo, padrão, override por variável de
ambiente e se são obrigatórias.
Origens de configuração, em ordem de precedência
Origens posteriores sobrescrevem as anteriores:
appsettings.json(padrões embutidos)appsettings.{Environment}.json(por exemplo,appsettings.Production.json)appsettings.json+appsettings.{Environment}.jsonencontrados emBULK_SIGNER_CONFIG_DIR- Variáveis de ambiente (
Section__Sub__Key)
É a etapa do BULK_SIGNER_CONFIG_DIR que permite manter o binário em um local de instalação somente
leitura (/opt/bulksigner, %ProgramFiles%\Lacuna\BulkSigner) e a configuração de produção, editada
pelo operador, em outro lugar (/etc/bulksigner, %ProgramData%\Lacuna\BulkSigner\config). Os
scripts de instalação definem essa variável; se você mudar os caminhos de instalação, atualize a
variável também.
O mapeamento para variáveis de ambiente segue a regra do ASP.NET Core: uma chave JSON como
Signing:Certificate:Pfx:Password corresponde a Signing__Certificate__Pfx__Password (o sublinhado
duplo é o separador).
appsettings.json distribuído é neutro para produçãoO appsettings.json que acompanha os binários e a imagem não declara perfis de assinatura, nem bloco
Signer, nem segredo de nenhum tipo — nem mesmo um Auth:ApiKey de exemplo. Isso importa porque ele
é lido com qualquer nome de ambiente, e a configuração pode sobrescrever uma chave, mas nunca removê-la:
antes da 2.3.1, os perfis de exemplo desse arquivo se mesclavam por baixo dos perfis declarados em
variáveis de ambiente. A única consequência na atualização: uma implantação que nunca definiu o próprio
Auth:ApiKey estava rodando com o antigo valor de exemplo e agora se recusa a iniciar, indicando a
chave. Defina Auth__ApiKey como descrevem todos os roteiros de instalação.
Marcadores usados nas tabelas
| Marcador | Significado |
|---|---|
| REQUIRED | O serviço se recusa a iniciar (ou a assinatura se recusa a rodar) sem um valor não vazio. |
| SECRET | Sensível — prefira o override por variável de ambiente a um valor gravado em um arquivo versionado. |
Logging / Logging:File
As configurações padrão do Microsoft.Extensions.Logging (Logging:LogLevel:*) funcionam
normalmente; o bloco Logging:File configura o destino de arquivo.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Logging:LogLevel:Default | string | Information | Logging__LogLevel__Default | Nível de log padrão. |
Logging:LogLevel:Microsoft.AspNetCore | string | Warning | Logging__LogLevel__Microsoft.AspNetCore | Reduz o volume de mensagens do framework. O mesmo nível mínimo Warning é aplicado no código a Microsoft.AspNetCore e à categoria de comandos de banco de dados do Entity Framework Core, de modo que nem o pipeline de requisições nem cada instrução SQL executada chegam ao log em Information. Comandos que falham continuam sendo registrados. |
Logging:File:Path | string | data/logs/bulksigner-.log | Logging__File__Path | REQUIRED. Template de caminho do destino de arquivo. O - final antes de .log, somado à rotação diária, produz bulksigner-yyyyMMdd.log. |
Logging:File:RollingInterval | string | Day | Logging__File__RollingInterval | Um de Day, Hour, Minute, Infinite. |
Logging:File:FileSizeLimitBytes | long | 50000000 | Logging__File__FileSizeLimitBytes | Limite por arquivo; acima dele, o destino passa a escrever em …_001.log. Faixa válida: 64 KB a 10 GB. |
Logging:File:RetainedFileCountLimit | int | 14 | Logging__File__RetainedFileCountLimit | Arquivos mais antigos são apagados conforme a rotação avança. Faixa válida: 1–365. |
Logging:File:MinimumLevel | string | Information | Logging__File__MinimumLevel | Um de Verbose, Debug, Information, Warning, Error, Fatal. |
Logging:File:WriteToConsole | bool | true | Logging__File__WriteToConsole | Quando verdadeiro, também escreve na saída padrão. O mesmo formatador com mascaramento roda nos dois destinos. |
Logging:AzureTable — um segundo destino de log
Uma implantação em um sistema de arquivos efêmero — um container que é substituído em vez de
reiniciado — perde data/logs/ toda vez. Este bloco envia os eventos de log também para uma tabela do
Azure Storage, de modo que o fluxo de diagnóstico sobreviva ao host. É a última de três partes do estado
que podem ir para o Azure: os arquivos já podem ficar em um compartilhamento do Azure Files
(Storage:Provider), e o
registro operacional, no Azure SQL (Database).
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Logging:AzureTable:Enabled | bool | false | Logging__AzureTable__Enabled | Desligado, a menos que seja definido. true com um bloco incompleto causa uma recusa de boot que indica a chave ausente, e não um destino silenciosamente inativo. |
Logging:AzureTable:TableName | string | bulksignerlogs | Logging__AzureTable__TableName | Precisa existir previamente — o serviço não a cria. Somente caracteres alfanuméricos, de 3 a 63, sem começar com dígito; um nome inválido é recusado no boot, e não na primeira escrita. |
Logging:AzureTable:MinimumLevel | string | (herda Logging:File:MinimumLevel) | Logging__AzureTable__MinimumLevel | Restringe o que vai para a tabela sem restringir o arquivo. Defini-lo abaixo do mínimo global não tem efeito — esse filtro é aplicado antes de qualquer destino. |
Logging:AzureTable:ServiceUri | string | — | Logging__AzureTable__ServiceUri | REQUIRED quando habilitado. O endpoint da tabela, por exemplo https://contosologs.table.core.windows.net. Uma URL com query string é recusada — é assim que chega uma shared access signature (SAS), e a SAS deliberadamente não está entre as credenciais de armazenamento deste produto. A recusa também garante que este valor não seja secreto, o que permite exibi-lo com segurança no banner de inicialização. |
Logging:AzureTable:Credential | string | — | Logging__AzureTable__Credential | REQUIRED quando habilitado. ManagedIdentity, ServicePrincipal ou AccountKey. Não tem valor padrão — veja abaixo. |
Logging:AzureTable:AccountName | string | — | Logging__AzureTable__AccountName | Obrigatório para AccountKey e somente nesse modo: a credencial de chave compartilhada precisa do nome da conta, que não pode ser extraído do ServiceUri sem adivinhar se a URL é um endpoint de produção ou de emulador. |
Logging:AzureTable:AccountKey | string | — | Logging__AzureTable__AccountKey | SECRET. Somente no modo AccountKey. |
Logging:AzureTable:TenantId | string | — | Logging__AzureTable__TenantId | Somente no modo ServicePrincipal. |
Logging:AzureTable:AppId | string | — | Logging__AzureTable__AppId | Somente no modo ServicePrincipal. |
Logging:AzureTable:AppSecret | string | — | Logging__AzureTable__AppSecret | SECRET. Somente no modo ServicePrincipal. Override por env recomendado. |
Logging:AzureTable:QueueLimit | int | 10000 | Logging__AzureTable__QueueLimit | Eventos mantidos em memória enquanto a tabela está inacessível. Acima desse limite, os eventos são descartados e contabilizados — e informados em /api/ready e em bulksigner_log_sink_dropped_total. O limite existe porque, com um buffer ilimitado, uma indisponibilidade longa derrubaria por falta de memória um serviço de assinatura só porque o log falhou. |
Logging:AzureTable:BatchSizeLimit | int | 100 | Logging__AzureTable__BatchSizeLimit | Eventos por escrita. Limitado a 100, o teto documentado de uma transação de grupo de entidades; acima disso, um lote deixa de ser uma única transação, em vez de apenas ficar mais lento. |
Logging:AzureTable:BatchPeriodSeconds | int | 5 | Logging__AzureTable__BatchPeriodSeconds | Quanto tempo um lote parcial espera antes de ser escrito. |
As chaves de credencial são as mesmas usadas por Storage:AzureFiles e pelo blob de material de
assinatura de um perfil, de modo que um operador que já configurou uma credencial de armazenamento
neste produto já sabe configurar todas. A identidade precisa da role Storage Table Data Contributor
na tabela ou na conta de armazenamento.
Um bloco que não indica a credencial é recusado no boot. Recorrer a uma identidade de desenvolvimento faria uma identidade gerenciada ausente ou não atribuída funcionar em um laptop e falhar só em produção.
Prefira ManagedIdentity a AccountKey. A Microsoft recomenda proibir a autorização por Shared
Key, e muitas organizações definem allowSharedKeyAccess = false na conta — nesse caso, o AccountKey
nem pode ser configurado. Uma chave de conta também concede acesso total ao plano de dados da conta
inteira, então uma chave usada para uma tabela de logs também dá acesso a qualquer compartilhamento
do Azure Files que exista na mesma conta.
Duas regras que são impostas, não recomendadas
A tabela nunca pode ser o único destino. O Logging:File:Enabled é a chave para desligar o arquivo
(em um sistema de arquivos raiz somente leitura), mas uma configuração em que os dois destinos locais
estejam desligados é recusada no boot: se a tabela parar de aceitar escritas, a própria falha de
registro não poderia ser registrada. Mantenha o destino de arquivo ou o WriteToConsole — em um
container, ele corresponde à saída padrão, que o docker logs e o streaming de logs do App Service já
capturam.
Sem TTL, sem regra de ciclo de vida, sem exclusão em lote — a tabela cresce até você apagar registros dela. Leia Retenção antes de habilitar este recurso, e agende o script de limpeza. Se duas implantações compartilham uma conta de armazenamento, use uma tabela para cada uma.
Database e ConnectionStrings
O banco de dados operacional — jobs e seu histórico, eventos operacionais, a flag de pausa do pipeline, as regras de aprovação congeladas e as aprovações registradas — fica em SQLite por padrão, mas pode ficar em um SQL Server 2022+ próprio ou em um Azure SQL Database.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Database:Provider | enum | Sqlite | Database__Provider | Sqlite ou SqlServer. Não diferencia maiúsculas de minúsculas; se ausente, vale Sqlite, então uma implantação existente não precisa configurar nada. Um valor não reconhecido é recusado no boot, com a indicação da chave, do valor informado e dos nomes válidos. |
ConnectionStrings:Default | string | Data Source=data/db/bulksigner.db | ConnectionStrings__Default | REQUIRED com SqlServer — e SECRET nesse caso, porque contém a credencial inteira (login SQL, Entra ID, identidade gerenciada, autenticação integrada do Windows; não há chave de credencial separada). Com Sqlite, pode ser omitida — o padrão acima é de fato usado. Use um caminho SQLite dentro de Storage:Root para que uma única montagem cubra a árvore de dados e o banco. |
Deliberadamente, não há um discriminador Database:Credential: o SQL Server expressa a
autenticação na connection string há trinta anos, e acrescentar um segundo mecanismo ao lado de um que
já funciona só criaria uma forma de os dois divergirem.
O provider do banco operacional é independente de Storage:Provider — arquivos em um compartilhamento
do Azure Files com o banco em SQLite, ou o contrário, são combinações comuns. Nenhuma das combinações,
por si só, torna suportada a execução de mais de uma instância: para isso existe o
Cluster:Enabled, que exige SqlServer e
AzureFiles, mas não é ativado por eles. Sem essa chave, veja
Operação.
Azure SQL Managed Instance e SQL Server em uma VM se enquadram em configure como SqlServer, não
testado — a mesma implementação os atende e não há nenhuma diferença conhecida, mas nenhum dos dois é
testado.
Três recusas de boot se referem a ConnectionStrings:Default, e quais se aplicam depende do
provider:
- Com
Sqlite, uma connection string que aponta para um local do Azure Files — acessar um arquivo de banco de dados por SMB é a forma documentada de corrompê-lo. - Com
SqlServer, o inverso: um data source que aponta para um arquivo em vez de um servidor, o que acontece quando uma implantação troca o provider e esquece o caminho SQLite na configuração. - Com
SqlServer, uma string ausente. Nenhum servidor é presumido, ao passo que, comSqlite, existe um caminho de arquivo padrão real.
Nenhuma recusa exibe a connection string, porque ela pode conter uma senha; apenas o data source é citado.
Antes de apontá-lo para o SQL Server
- O banco de dados precisa existir previamente. O Bulk Signer cria as tabelas, não o banco de dados. O probe de boot abre uma conexão com o banco indicado na connection string; por isso, um banco ausente é interpretado como banco operacional inacessível, e a migração não é executada.
- Um login com as permissões abaixo, mapeado para um usuário nesse banco de dados.
- Criptografia que o cliente aceite. O
Encrypttem padrãoTrueno cliente SQL, então um servidor on-premises cujo certificado TLS não é confiável para o host faz o login falhar com um erro certificate chain … not trusted. Instale um certificado confiável no servidor (a solução correta) ou, conscientemente e somente onde um ataque man-in-the-middle não for uma preocupação, acrescenteTrustServerCertificate=True. O Azure SQL não precisa de nenhum dos dois.
Menor privilégio. O serviço lê e escreve suas próprias tabelas e aplica migrações no boot. Isso é
db_datareader + db_datawriter + db_ddladmin — não db_owner:
-- Uma vez, por um DBA, no banco de dados que o Bulk Signer vai usar.
CREATE USER [bulksigner] FOR LOGIN [bulksigner]; -- um login SQL
-- No Azure SQL com uma managed identity ou service principal, em vez disso:
-- CREATE USER [<nome-da-identidade-ou-do-app>] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [bulksigner];
ALTER ROLE db_datawriter ADD MEMBER [bulksigner];
ALTER ROLE db_ddladmin ADD MEMBER [bulksigner]; -- para o boot que aplica uma migração
Esse bloco pressupõe que o banco de dados e o login já existem e que você está conectado a esse banco de dados. A forma de criá-los varia conforme o engine:
-- SQL Server: a partir do master, depois troque.
CREATE DATABASE [BulkSigner];
GO
ALTER DATABASE [BulkSigner] SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;
GO
CREATE LOGIN [bulksigner] WITH PASSWORD = '<uma senha forte>';
-- ou, para autenticação integrada do Windows: CREATE LOGIN [DOMINIO\HOSTNAME$] FROM WINDOWS;
GO
USE [BulkSigner];
GO
-- …então o bloco CREATE USER + ALTER ROLE acima.
-- Azure SQL: DUAS conexões, porque USE não troca de banco de dados lá e
-- CREATE DATABASE precisa rodar a partir do master, sozinho.
-- Conexão 1, para o master:
CREATE DATABASE [BulkSigner];
GO
-- Conexão 2, para o próprio BulkSigner: o bloco CREATE USER + ALTER ROLE acima.
O READ_COMMITTED_SNAPSHOT já vem ligado no Azure SQL. Sem ele, as leituras do dashboard obtêm
locks compartilhados e ficam bloqueadas pelas escritas do pipeline — o que aparece como "o dashboard
trava enquanto um lote é assinado", e não como uma configuração de banco de dados. O Bulk Signer
informa essa configuração e nunca executa o comando que a altera: isso exige acesso exclusivo a um
banco de dados que pertence a você.
É o db_ddladmin que cria as tabelas e os índices, então ele é necessário no primeiro boot e em
qualquer boot após uma atualização que traga uma migração. Manter as três roles é o padrão mais simples
e mais seguro; a migração roda a cada boot e não faz nada quando não há o que aplicar.
As duas formas de credencial
Nos dois casos, a credencial vai na connection string. Prefira a forma sem senha sempre que o host puder se autenticar com a própria identidade — assim não há segredo para rotacionar, vazar em um log ou ser encontrado em um backup.
# Sem senha — Azure SQL, de um host com uma managed identity atribuída pelo sistema
Server=tcp:sqlsrv01.database.windows.net,1433;Initial Catalog=BulkSigner;Authentication=Active Directory Managed Identity;Encrypt=True;
# Sem senha — on premises, de uma conta de serviço do Windows (autenticação integrada)
Server=sqlsrv01;Initial Catalog=BulkSigner;Integrated Security=True;Encrypt=True;
# Com um segredo — um login SQL
Server=sqlsrv01;Initial Catalog=BulkSigner;User ID=bulksigner;Password=<segredo>;Encrypt=True;
# Com um segredo — um service principal do Entra
Server=tcp:sqlsrv01.database.windows.net,1433;Initial Catalog=BulkSigner;Authentication=Active Directory Service Principal;User ID=<app-id>;Password=<client-secret>;Encrypt=True;
Para usar uma identidade atribuída pelo usuário, acrescente o client id dela como
User Id=<client-id> — diferentemente de Storage:AzureFiles, cujo modo ManagedIdentity aceita
somente a identidade atribuída pelo sistema, porque aqui é o próprio cliente SQL que obtém o token.
Quando instalado como serviço do Windows, o serviço roda com a conta virtual NT SERVICE\LacunaBulkSigner,
que acessa a rede como a conta de computador — então o login a criar no SQL Server é
DOMINIO\HOSTNAME$, e não o nome da própria conta virtual.
ConnectionStrings:Default é uma única chave de configuração, então não há como manter o servidor no
appsettings.Production.json e fornecer apenas a senha pelo ambiente. A string completa vem do JSON
(o que é aceitável quando ela não tem senha) ou do ambiente. Um valor no JSON mantido junto com a
variável de ambiente é silenciosamente ignorado.
Trocar de provider começa com um banco vazio
Não há importador nem verificação no boot para um arquivo SQLite que ficou para trás. Uma implantação
que define Database:Provider = SqlServer encontra um schema vazio: sem jobs, sem histórico, sem
eventos operacionais — e sem snapshots de aprovação nem aprovações registradas, justamente as duas
coisas que o produto, fora isso, retém para sempre, porque são a evidência de quem autorizou um arquivo
de pagamento.
Arquive o antigo db/bulksigner.db deliberadamente, antes da troca, e guarde-o por todo o tempo em
que a sua política de retenção exigir a evidência que ele contém. Copie-o com o serviço parado e
mantenha um cliente SQLite à mão. A troca inversa tem a mesma característica. Veja
Instalação.
O que o boot informa sobre o banco operacional
Toda implantação recebe uma linha operational store no banner de resumo de prontidão, com o provider
e, com SqlServer, também o servidor e o banco de dados — nunca a connection string. Uma implantação
SqlServer recebe mais duas linhas:
store status, resultado de um probe no boot. Um banco operacional inacessível é informado e não impede o host de iniciar (um banco de dados fora do ar durante uma janela de manutenção não pode transformar uma reinicialização em indisponibilidade); a migração não é executada, o/api/readyfica vermelho até o banco responder, e o próximo boot que o encontrar aplica o schema.store isolation, além de um aviso no console de operação, quando oREAD_COMMITTED_SNAPSHOTestá desligado. Quando está ligado, nada é informado.
Comportamento específico do engine que você não configura
- Com
Sqlite, toda conexão recebejournal_mode=WAL,synchronous=NORMALebusy_timeout=30000. O WAL impede que as escritas de status por job do pipeline sejam serializadas no fsync de escritor único do SQLite (o teto de vazão com valores mais altos dePipeline:MaxConcurrency). - Com
SqlServer, a repetição em falhas transitórias está ligada e não tem ajuste — a tentativa inicial mais até seis novas tentativas para os números de erro que o cliente SQL classifica como transitórios, com cada intervalo crescendo exponencialmente, limitado a 30 segundos. Ela fica ligada porque, na prática, rodar com o Azure SQL a exige. Deliberadamente, não há chave de configuração: um limite de repetições que o operador consegue ajustar é um limite que acaba ajustado para zero durante um incidente.
Signing
A validação falha imediatamente na inicialização se alguma chave obrigatória estiver ausente ou for inválida.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Signing:PkiSdkLicense | string | "" | Signing__PkiSdkLicense | REQUIRED, SECRET. String de licença do Lacuna PKI SDK (base64). Prefira a forma por variável de ambiente. |
Signing:ProfileSecretsKey | string | "" | Signing__ProfileSecretsKey | SECRET. A chave com a qual é criptografado todo segredo mantido por um perfil de assinatura armazenado. Deixe-a sem definir, a menos que os perfis da implantação tenham algum segredo — veja abaixo. |
Signing:TrustLacunaTestRoot | bool | false | Signing__TrustLacunaTestRoot | Confia também na raiz da PKI de teste da Lacuna — a emissora dos certificados de teste Turing / Fermat — em um ambiente de homologação. Recusada no boot quando ASPNETCORE_ENVIRONMENT é Production. Não é segredo. Veja abaixo. |
Signing:Certificate:Source | enum | Pfx | Signing__Certificate__Source | REQUIRED. Um de Pfx, Pkcs11, WindowsStore, AzureKeyVault, AwsKms (novo na 2.20.0). Somente a subárvore correspondente abaixo é consultada. |
Signing:ProfileSecretsKey — a chave que criptografa os segredos dos perfis armazenados
Os perfis de assinatura ficam no banco operacional (veja
Signing:Profiles[]), então esta chave é
obrigatória para toda implantação cujos perfis tenham algum segredo — uma senha de PKCS#12, um
segredo de aplicativo do Azure Key Vault, uma secret access key da AWS (a de um bloco AwsKms ou a de um
objeto S3 de material de assinatura), uma credencial de blob de material de assinatura ou um arquivo
PKCS#12 enviado pelo dashboard. Cada um deles é criptografado com ela, e o banco nunca armazena nenhum
deles em texto claro.
Defina-a antes do primeiro boot que importa perfis e antes que alguém crie pelo dashboard um perfil com segredo. A importação é recusada em vez de gravar um segredo que não consegue proteger, com a indicação desta chave, da variável de ambiente dela e dos perfis que têm segredo; os formulários de criação e edição do dashboard recusam da mesma forma. Uma implantação cujos perfis não têm nenhum segredo — um PFX sem senha, um token PKCS#11 cujo PIN vem de uma variável de ambiente, um certificado do repositório do Windows, um perfil em que os aprovadores assinam — não precisa de chave, e ela nunca é solicitada.
Deliberadamente, ela não fica no banco de dados: uma chave armazenada ao lado do texto cifrado não
protege contra nada que importe. É também por isso que o key ring de sessão não é reaproveitado para
ela — com Cluster:Enabled, ou onde
Auth:SessionKeyRing:Placement o colocar,
esse ring é, ele próprio, um conjunto de linhas no mesmo banco.
Três casos, tratados de formas diferentes de propósito:
- Dados protegidos de perfil no banco e nenhuma chave para lê-los interrompem o boot. A solução é uma variável de ambiente, e não uma linha em um banco de dados que só seria acessível com o serviço rodando.
- Uma importação ou um salvamento que criaria dados protegidos sem chave é recusado no mesmo momento, de modo que o banco nunca acaba armazenando um valor que ninguém consegue abrir.
- Uma chave definida, mas errada — rotacionada, restaurada de outro lugar, digitada errado —
não causa recusa. O serviço inicia, os perfis que ele não consegue ler sobem degradados
(identificados no banner de inicialização e no
/api/ready), e todo perfil sem segredo continua assinando. Definir uma variável de ambiente resolve uma chave ausente, mas não resolve uma chave rotacionada; por isso, a recusa seria permanente.
Não há custódia de chaves nem caminho de recuperação, e rotacioná-la tem o mesmo efeito que perdê-la.
Faça backup dela onde você faz backup da senha de criptografia, do ApproverPortal:LinkSecret e do
ApproverSecondFactor:SeedSecret. A recuperação, se for necessária, consiste em digitar novamente a
senha ou a credencial de cada perfil indicado, na página dele no dashboard, e reiniciar.
O PIN do PKCS#11 não é um dos valores que ela protege, e nunca passa a ser: ele continua sendo lido
da variável de ambiente indicada por Pkcs11:PinEnvVar, e um PIN gravado em qualquer lugar de onde
possa ser lido de volta é recusado no boot.
Signing:TrustLacunaTestRoot — certificados de teste para homologação
O serviço valida toda assinatura somente com as raízes da ICP-Brasil — a chave do perfil quando
assina, o verificador depois, e o certificado de um aprovador antes de o token pedir o PIN —, sem usar
nada do repositório do sistema operacional. A PKI de teste pública da Lacuna (os certificados de
teste Turing / Fermat) não está abaixo dessas raízes, então, por padrão, um job assinado com um desses
certificados falha, e um aprovador que apresente um deles é recusado com approval.certificate-invalid.
Signing:TrustLacunaTestRoot = true amplia o conjunto de confiança para a ICP-Brasil, o conjunto de
confiança do Windows do PKI SDK (o repositório da máquina, no Windows) e a raiz de teste da Lacuna, para
um ambiente de homologação que queira rodar o produto distribuído com os certificados de teste em vez de
comprar um e-CPF real para cada aprovador. Três pontos importantes:
- Ela é recusada com o nome de ambiente
Production. O boot falha indicando a chave, o ambiente e a solução: um host de homologação usaASPNETCORE_ENVIRONMENT=Staging(ou qualquer nome diferente deProduction). O Azure App Service usaProductionpor padrão quando nada define o nome, então, nele, as duas configurações andam juntas. - Ela é um único conjunto de confiança para o host inteiro. A chave de todo perfil, toda verificação
e o certificado de todo aprovador são validados com as mesmas raízes; não há variante por perfil. A
linha
trust setdo banner de inicialização indica a raiz de teste da Lacuna quando ela está ligada, e o console e o log durável exibem um aviso a cada boot. - Ela não é segredo. Um certificado raiz é material público, e a chave é um booleano.
Deixe-a sem definir em toda implantação que assine qualquer coisa real.
Signing:Certificate:Pfx — quando Source = Pfx
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Signing:Certificate:Pfx:Path | string | "" | Signing__Certificate__Pfx__Path | REQUIRED a menos que Blob ou S3 esteja definido — exatamente um local. Caminho absoluto para o arquivo .pfx/.p12. |
Signing:Certificate:Pfx:Password | string | "" | Signing__Certificate__Pfx__Password | SECRET. String vazia é permitida para arquivos de teste sem senha. Prefira a forma por variável de ambiente. |
Signing:Certificate:Pkcs11 — quando Source = Pkcs11
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Signing:Certificate:Pkcs11:ModulePath | string | "" | Signing__Certificate__Pkcs11__ModulePath | REQUIRED. Caminho absoluto para o driver PKCS#11 do fabricante (.so/.dll/.dylib). |
Signing:Certificate:Pkcs11:Thumbprint | string | "" | Signing__Certificate__Pkcs11__Thumbprint | REQUIRED. Thumbprint SHA-1 (hexadecimal, sem espaços) do certificado de assinatura no token. Obrigatório mesmo quando o token contém uma única identidade. |
Signing:Certificate:Pkcs11:PinEnvVar | string | BULK_SIGNER_PKCS11_PIN | Signing__Certificate__Pkcs11__PinEnvVar | Nome da variável de ambiente que fornece o PIN. O validador impede a inicialização se uma chave Pin literal aparecer em Pkcs11. |
Signing:Certificate:WindowsStore — quando Source = WindowsStore
Somente Windows. Na inicialização, o validador recusa esta origem em hosts que não são Windows.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Signing:Certificate:WindowsStore:StoreLocation | string | CurrentUser | Signing__Certificate__WindowsStore__StoreLocation | CurrentUser ou LocalMachine. Use LocalMachine quando o certificado foi importado para toda a máquina; a conta de serviço não enxerga o repositório CurrentUser do operador. |
Signing:Certificate:WindowsStore:StoreName | string | My | Signing__Certificate__WindowsStore__StoreName | Nome lógico do repositório. My é o repositório pessoal. |
Signing:Certificate:WindowsStore:Thumbprint | string | "" | Signing__Certificate__WindowsStore__Thumbprint | REQUIRED. Thumbprint SHA-1 (hexadecimal, sem espaços). |
Signing:Certificate:AzureKeyVault — quando Source = AzureKeyVault
A chave privada permanece no cofre, e cada assinatura é uma chamada remota de assinatura; o certificado
público correspondente é fornecido separadamente como um .cer. Endpoint, AppId, AppSecret e
KeyName são sempre obrigatórios, além de exatamente um entre CerPath (um arquivo neste host),
Blob (um objeto no Azure Blob Storage) e
S3 (um objeto no Amazon S3). A inicialização falha indicando
cada um que estiver ausente.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Signing:Certificate:AzureKeyVault:Endpoint | string | "" | Signing__Certificate__AzureKeyVault__Endpoint | REQUIRED. URL do cofre. Precisa ser uma URL https:// absoluta — um nome DNS puro é recusado na inicialização. |
Signing:Certificate:AzureKeyVault:AppId | string | "" | Signing__Certificate__AzureKeyVault__AppId | REQUIRED. ID de aplicativo (client) do registro de aplicativo do Microsoft Entra ID. |
Signing:Certificate:AzureKeyVault:AppSecret | string | "" | Signing__Certificate__AzureKeyVault__AppSecret | REQUIRED, SECRET. Client secret do Entra ID. Diferentemente do PIN do PKCS#11, este é permitido em arquivo de configuração, mas a forma por variável de ambiente é recomendada. |
Signing:Certificate:AzureKeyVault:KeyName | string | "" | Signing__Certificate__AzureKeyVault__KeyName | REQUIRED. Nome do objeto key no cofre que executa a assinatura. Um objeto certificate do cofre não é aceito. |
Signing:Certificate:AzureKeyVault:CerPath | string | "" | Signing__Certificate__AzureKeyVault__CerPath | REQUIRED a menos que Blob ou S3 esteja definido — exatamente um local. Caminho para o .cer que contém o certificado público de KeyName. O perfil fica degradado se a chave pública dele não corresponder à da chave do cofre. |
Signing:Certificate:AwsKms — quando Source = AwsKms
Novo na 2.20.0. A chave privada é uma chave RSA de assinatura do AWS KMS que nunca sai da AWS, e cada
assinatura é uma requisição Sign ao KMS; o certificado público correspondente é fornecido
separadamente, como um .cer. Region, KeyId e Credential são sempre obrigatórios, além de
exatamente um entre CerPath (um arquivo neste host),
Blob e S3.
Cada recusa indica o campo. Um KMS inacessível, uma credencial recusada, uma chave inutilizável ou um
certificado que não corresponde a ela deixam o perfil degradado no boot, em vez de impedir o host de
iniciar. Veja Certificados para a configuração na AWS, as permissões do IAM e a cota
de requisições.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Signing:Certificate:AwsKms:Region | string | "" | Signing__Certificate__AwsKms__Region | REQUIRED. Por exemplo, sa-east-1. Nunca é obtida da cadeia de regiões do SDK da AWS: um perfil não pode assinar em uma região que ninguém escolheu. |
Signing:Certificate:AwsKms:KeyId | string | "" | Signing__Certificate__AwsKms__KeyId | REQUIRED. Um key ID, um ARN de chave ou um nome alias/…. A chave precisa ser SIGN_VERIFY, RSA_2048 / RSA_3072 / RSA_4096 e Enabled. |
Signing:Certificate:AwsKms:Credential | enum | "" | Signing__Certificate__AwsKms__Credential | REQUIRED. AccessKey ou InstanceRole. Não tem valor padrão — um modo omitido autenticaria como quer que a cadeia padrão do SDK da AWS encontre no host. Dentro da AWS, prefira InstanceRole. |
Signing:Certificate:AwsKms:AccessKeyId | string | "" | Signing__Certificate__AwsKms__AccessKeyId | REQUIRED para AccessKey, recusada para InstanceRole. A metade identificadora do par; não é mascarada nos logs. |
Signing:Certificate:AwsKms:SecretAccessKey | string | "" | Signing__Certificate__AwsKms__SecretAccessKey | REQUIRED para AccessKey, recusada para InstanceRole, SECRET. Permitida na configuração; forma por variável de ambiente recomendada. Mascarada nos logs e criptografada com Signing:ProfileSecretsKey quando o seed a armazena. |
Signing:Certificate:AwsKms:CerPath | string | "" | Signing__Certificate__AwsKms__CerPath | REQUIRED a menos que Blob ou S3 esteja definido — exatamente um local. O .cer emitido para a chave do KMS. O perfil fica degradado se a chave pública dele não corresponder à da chave. |
Um P7S simples é recusado com esta origem, como com AzureKeyVault: nenhuma das duas assina SHA-1. Os
formulários Novo perfil e Editar certificado do dashboard também oferecem a origem, e criar um
perfil por ali executa as mesmas verificações do KMS antes de gravar qualquer coisa — veja
Dashboard.
…:Blob — lendo o arquivo do Azure Blob Storage
Um host sem disco local durável — um container, um App Service, um pod do AKS — não tem onde
armazenar um .pfx ou um .cer. As três origens que apontam para um arquivo podem, em vez disso,
apontar para um blob.
Disponível em Pfx (com o .pfx, em vez de Path) e em AzureKeyVault e AwsKms (com o
.cer, em vez de CerPath), no bloco legado e em cada entrada de Signing:Profiles[]. Exatamente um
entre o caminho local, este bloco, o bloco S3 e (para Pfx) um
arquivo enviado pelo dashboard; dois quaisquer definidos, ou nenhum, é recusado no boot. Nada aqui é
herdado da credencial AzureKeyVault ao lado nem de Storage:AzureFiles.
Substitua <SRC> abaixo por Pfx, AzureKeyVault ou AwsKms.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Signing:Certificate:<SRC>:Blob:Url | string | (não definido) | Signing__Certificate__<SRC>__Blob__Url | REQUIRED quando o bloco está presente. URL completa do blob, por exemplo https://contoso.blob.core.windows.net/certificates/signer.cer. Precisa ser uma URL https:// absoluta, precisa indicar um container e um blob, e não pode ter query string — isso seria uma shared access signature, que não é uma credencial aceita. Por causa dessa regra, a URL nunca é secreta e é exibida no banner de inicialização. Qualquer host é aceito, então nuvens soberanas não precisam de chave extra. |
Signing:Certificate:<SRC>:Blob:Credential | enum | (não definido) | Signing__Certificate__<SRC>__Blob__Credential | REQUIRED quando o bloco está presente. Um de ManagedIdentity, ServicePrincipal, AccountKey. Não tem valor padrão — usar silenciosamente a identidade Azure do próprio host não é uma decisão que o produto toma por você. |
Signing:Certificate:<SRC>:Blob:TenantId | string | (não definido) | Signing__Certificate__<SRC>__Blob__TenantId | REQUIRED para ServicePrincipal. Obrigatório mesmo quando o AppId coincide com o do bloco AzureKeyVault — esse bloco não tem chave de tenant, e nada é herdado. |
Signing:Certificate:<SRC>:Blob:AppId | string | (não definido) | Signing__Certificate__<SRC>__Blob__AppId | REQUIRED para ServicePrincipal. Precisa de Storage Blob Data Reader no container. |
Signing:Certificate:<SRC>:Blob:AppSecret | string | (não definido) | Signing__Certificate__<SRC>__Blob__AppSecret | REQUIRED para ServicePrincipal, SECRET. Forma por variável de ambiente recomendada. |
Signing:Certificate:<SRC>:Blob:AccountKey | string | (não definido) | Signing__Certificate__<SRC>__Blob__AccountKey | REQUIRED para AccountKey, SECRET. Concede acesso total ao plano de dados da conta inteira e não pode ter o escopo restringido; gera um aviso na inicialização. |
Pfx, o blob é a chave de assinaturaUma chave de conta concede acesso total ao plano de dados da conta de armazenamento inteira, não pode
ter o escopo restringido e não expira. Com AzureKeyVault e AwsKms, o blob contém um .cer —
material público. Com Pfx, ele contém um arquivo PKCS#12, então uma chave de conta vazada equivale à
sua chave de assinatura. Prefira ManagedIdentity, ou ServicePrincipal quando o host conseguir
acessar um tenant.
O arquivo é lido uma vez, no boot — um blob renovado exige reiniciar o serviço, exatamente como um
arquivo local renovado. As violações das regras de formato acima (os dois ou nenhum, Url ou
Credential ausente, uma query string) são recusadas no boot, mas um blob inacessível ou ilegível
deixa o perfil degradado e o host rodando: o perfil é identificado no banner de inicialização, no log
durável e no /api/ready, os jobs roteados para ele falham com profile.degraded, e todos os outros
perfis continuam assinando. Veja Certificados.
Até a 2.0.x, um único perfil cujo certificado não carregasse — um caminho digitado errado, uma senha errada, um blob ou cofre inacessível — impedia o boot inteiro. Os perfis agora são editados pelo dashboard, e uma página servida por um host que se recusa a iniciar não serve como caminho de recuperação; por isso, somente esse perfil fica degradado. Corrija o certificado e reinicie: uma chave é aberta uma vez na inicialização e nunca é recarregada.
…:S3 — lendo o arquivo do Amazon S3
Novo na 2.20.0. O mesmo arquivo pode, em vez disso, ser um objeto em um bucket S3 — na AWS ou em um
armazenamento compatível com S3, como MinIO ou Ceph, indicado por ServiceUrl. Disponível nas mesmas
três origens que o Blob, no bloco legado e em cada entrada de Signing:Profiles[]. Exatamente um
entre o caminho local, Blob, S3 e (para Pfx) um arquivo enviado; dois quaisquer definidos, ou
nenhum, é recusado no boot, com a indicação dos campos. A credencial é a do próprio bloco: ela nunca é
obtida do bloco AwsKms ao lado nem de Backup:S3 — ler um objeto e usar uma chave são permissões
separadas.
Substitua <SRC> abaixo por Pfx, AzureKeyVault ou AwsKms.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Signing:Certificate:<SRC>:S3:Bucket | string | (não definido) | Signing__Certificate__<SRC>__S3__Bucket | REQUIRED quando o bloco está presente. Exibido no banner de inicialização como s3://<Bucket>/<Key>. |
Signing:Certificate:<SRC>:S3:Key | string | (não definido) | Signing__Certificate__<SRC>__S3__Key | REQUIRED quando o bloco está presente. A chave do objeto; diferencia maiúsculas de minúsculas. |
Signing:Certificate:<SRC>:S3:Region | string | (não definido) | Signing__Certificate__<SRC>__S3__Region | REQUIRED a menos que ServiceUrl esteja definido. Por exemplo, sa-east-1. Nunca é obtida do ambiente do host. |
Signing:Certificate:<SRC>:S3:Credential | enum | (não definido) | Signing__Certificate__<SRC>__S3__Credential | REQUIRED quando o bloco está presente. AccessKey ou InstanceRole. Não tem valor padrão. A identidade precisa de s3:GetObject sobre esse único objeto. |
Signing:Certificate:<SRC>:S3:AccessKeyId | string | (não definido) | Signing__Certificate__<SRC>__S3__AccessKeyId | REQUIRED para AccessKey, recusada para InstanceRole. Não é mascarada nos logs. |
Signing:Certificate:<SRC>:S3:SecretAccessKey | string | (não definido) | Signing__Certificate__<SRC>__S3__SecretAccessKey | REQUIRED para AccessKey, recusada para InstanceRole, SECRET. Forma por variável de ambiente recomendada. Mascarada nos logs e criptografada com Signing:ProfileSecretsKey quando armazenada. |
Signing:Certificate:<SRC>:S3:ServiceUrl | string | (não definido) | Signing__Certificate__<SRC>__S3__ServiceUrl | Um endpoint compatível com S3 (MinIO, Ceph). http:// ou https:// absoluto. |
Signing:Certificate:<SRC>:S3:ForcePathStyle | bool | false | Signing__Certificate__<SRC>__S3__ForcePathStyle | Endereçamento no estilo de caminho; true para quase todo endpoint compatível com S3. |
O objeto é lido uma vez, no boot, com limite de 256 KiB para um PKCS#12 e de 64 KiB para um .cer;
um objeto que não pode ser lido deixa degradado somente esse perfil, identificado no banner, no log
durável e no /api/ready. Com AccessKey, o banner emite um aviso — mais forte com Pfx, em que o
objeto é um arquivo PKCS#12 e a secret access key protege a chave de assinatura; prefira
InstanceRole quando o host roda dentro da AWS. Os formulários Novo perfil e Editar certificado
do dashboard oferecem o mesmo local para um perfil armazenado.
Veja Certificados para os comandos de descoberta de thumbprint, o passo a passo de configuração no Azure e um detalhamento de cada origem.
Signing:Profiles[] — perfis de assinatura por pasta
Um perfil de assinatura agrupa sob um nome todas as decisões por pasta (formato, certificado,
verificação, criptografia, validação de certificado, aprovação). Cada perfil escolhe a pasta monitorada
que o alimenta, uma pasta por perfil; o Storage:Inputs[].Profile é apenas o dado do seed (carga
inicial) para essa escolha (veja abaixo).
Os perfis de assinatura ficam no banco operacional. No primeiro boot com uma tabela de perfis vazia, as chaves abaixo são importadas como linhas; daí em diante, o banco é a referência e esta seção é ignorada. Nada é mesclado, e editá-la depois desse primeiro boot não tem efeito — o log de inicialização avisa isso, indicando a seção, a cada boot que a encontra ainda preenchida. Mantenha-a se quiser que uma implantação nova já suba configurada; remova-a quando os perfis estiverem no banco.
Daí em diante, um perfil é consultado, criado e editado na página Perfis de assinatura do dashboard — o comportamento (o formato e as opções de ligar/desligar abaixo), a pasta de entrada, a regra de aprovação e o certificado —, e é desabilitado ali, em vez de apagado. É também nessa página que se confere, depois do primeiro boot, o que o seed importou. Veja Dashboard.
Uma mudança em um perfil armazenado entra em vigor sem reinicialização — exceto a do certificado.
Toda escrita de perfil é percebida pelo pipeline no polling que ele já faz, então um perfil editado vale
para o próximo job capturado — formato, Verify, Encrypt, CheckCNAB240, CheckCnab240PaymentDates,
SimpleP7s, a pasta de entrada e toda a regra de aprovação — em até um Pipeline:PollIntervalSeconds,
em todas as instâncias. Uma mudança de certificado é gravada na hora, mas exige reinicialização:
um perfil mantém aberto um handle de chave privada, e trocar a chave de um job que está no meio de uma
assinatura não é um risco que valha a pena correr por um campo que muda mais ou menos uma vez por ano.
Até a reinicialização, o perfil fica marcado como aguardando reinício, e a lista de perfis, o banner e a
origem registrada em cada job continuam indicando o certificado de fato em vigor — o que mantém o
registro de auditoria de um job fiel ao que de fato o assinou.
Há dois modos de configuração suportados, e ambos descrevem o que vai para o seed:
- Modo legado (padrão —
Signing:Profiles[]omitido ou vazio). Um perfil chamadodefaulté derivado do blocoSigning:Certificateexistente mais oEncryption:Enablede gravado como uma linha real. Nenhuma mudança de configuração é necessária para uma instalação simples, com um único certificado. - Modo de perfis (declare
Signing:Profiles[]). Cada entrada é um perfil nomeado, com certificado e postura próprios. OSigning:Certificateé ignorado. Cada entrada é validada como se fosse o bloco global de certificado — as mesmas regras dePfx/Pkcs11/WindowsStore/AzureKeyVault/AwsKmsse aplicam a cada perfil.
Um perfil com segredo precisa do
Signing:ProfileSecretsKey
definido antes desse primeiro boot. Uma senha de PKCS#12, um segredo de aplicativo do Azure Key Vault,
uma secret access key da AWS (a de um bloco AwsKms ou a de um objeto S3 de material de assinatura) ou
uma credencial de blob de material de assinatura é criptografado em repouso, e a importação é recusada
em vez de gravá-lo em texto claro.
As regras da tabela abaixo são impostas sempre que um perfil muda: no seed, para uma entrada declarada aqui, e nos formulários do dashboard, para um perfil criado ou editado ali. Um perfil que já está no banco nunca é revalidado no boot, então uma regra que ficou mais rígida em uma atualização aparece como um aviso ou um perfil degradado, e não como um host que não inicia.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Signing:Profiles[].Name | string | n/a | Signing__Profiles__0__Name | REQUIRED. Mesma regex dos nomes de pasta: ^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$. Único na lista. É por esse nome que o Storage:Inputs[].Profile do seed e o ?profile= de um upload REST se referem ao perfil, e ele aparece em labels de métricas, chips do dashboard e mensagens de auditoria. |
Signing:Profiles[].Enabled | bool | true | Signing__Profiles__0__Enabled | Se novos arquivos podem ser roteados para este perfil. Um perfil é desabilitado, nunca apagado, para que os jobs históricos continuem apontando para uma regra real e os jobs já enfileirados para um perfil desabilitado rodem até o fim. Imposto no ponto de entrada dos arquivos: uma pasta monitorada, um upload, uma nova varredura e uma nova tentativa são todos recusados com profile.disabled. Definido na página do perfil depois do primeiro boot; desabilitar ali é recusado enquanto o perfil é alimentado por uma pasta monitorada, que é indicada — remova a pasta no mesmo salvamento ou passe-a antes para outro perfil. O default não pode ser desabilitado de forma alguma — Enabled: false nele causa uma recusa de boot, já que é para ele que vai um upload que não indica perfil. |
Signing:Profiles[].Format | enum | n/a | Signing__Profiles__0__Format | REQUIRED para todo perfil, exceto o default: Pades, Cades ou Xades. Somente o default — o perfil para onde vai um upload que não indica perfil — pode deixá-lo sem definir; nesse caso, o formato é detectado arquivo a arquivo, pela extensão. |
Signing:Profiles[].Method | enum | Local | Signing__Profiles__0__Method | Local (assinar com o certificado local configurado), LacunaSigner (encaminhar ao Lacuna Signer para um participante humano) ou SignerFolder (novo na 2.16.0 — assinar, com o certificado do próprio perfil, documentos que uma pessoa criou em uma pasta vinculada do Lacuna Signer; a direção oposta). Um perfil LacunaSigner ou SignerFolder é recusado em um host sem a seção Signer. Veja Integração com o Lacuna Signer para os dois métodos do Signer. |
Signing:Profiles[].Verify | bool | true | Signing__Profiles__0__Verify | Quando falso, o worker não faz a verificação pós-assinatura. O banner de inicialização emite um aviso, para que a postura de baixa confiança fique visível ao operador, e desligá-la pelo dashboard pede confirmação. |
Signing:Profiles[].Encrypt | bool | false | Signing__Profiles__0__Encrypt | Quando verdadeiro, o worker criptografa a saída assinada com AES-256-GCM. Exige Encryption:Enabled = true (o validador recusa a combinação inválida na inicialização). |
Signing:Profiles[].ValidateCertificate | bool | true | Signing__Profiles__0__ValidateCertificate | Quando falso, o worker não faz a verificação de cadeia / revogação do certificado antes de assinar. O banner de inicialização emite um aviso. O perfil default derivado no modo legado usa false, preservando o comportamento das instalações anteriores aos perfis. Precisa ser false quando Method = LacunaSigner — não há certificado local a validar. |
Signing:Profiles[].PreserveFileExtension | bool | false | Signing__Profiles__0__PreserveFileExtension | Quando verdadeiro, a saída assinada mantém a extensão do arquivo original usando o infixo .signed no estilo PAdES: o CAdES grava remessa.signed.rem em vez de remessa.rem.p7m; o XAdES grava nota.signed.nfe em vez de nota.signed.xml. Válido somente quando Format = Cades ou Xades — a saída PAdES já preserva o .pdf, então o validador recusa a flag nesse caso. Use quando um sistema posterior no fluxo (um banco que recebe remessas assinadas, por exemplo) exigir a extensão original. |
Signing:Profiles[].SaveAsPem | bool | false | Signing__Profiles__0__SaveAsPem | Quando verdadeiro, a assinatura CAdES é gravada codificada em PEM (com o delimitador -----BEGIN PKCS7-----) em vez de DER puro, e o nome de saída passa a ser <nome>.pem em vez de <nome>.p7m. Válido somente quando Format = Cades. A verificação sempre é feita sobre os bytes DER, antes da codificação PEM; com Encrypt = true, o envelope BSENC envolve o texto PEM. Pode ser combinado com PreserveFileExtension; nesse caso, o nome segue essa flag, e apenas o conteúdo é PEM. |
Signing:Profiles[].SimpleP7s | bool | false | Signing__Profiles__0__SimpleP7s | Novo na 2.17.0. Quando verdadeiro, o perfil assina um P7S simples em vez da ADR-Básica da ICP-Brasil: um CMS anexado comum, com SHA-1, sem LCRs no envelope, sem o atributo signing-certificate e sem política de assinatura, mas com o atributo signing-time, o horário da assinatura (2.18.1) — para um destinatário cujo sistema não lê nada mais novo. A cadeia continua incluída, o nome de saída continua o do CAdES (<nome>.p7m, ou como PreserveFileExtension / SaveAsPem o definirem), e com Verify = true a saída continua sendo verificada, com uma política de validação compatível com a assinatura, e não com a da ICP-Brasil, que recusa uma assinatura sem política. O certificado é validado contra as mesmas ACs raiz que a ADR-Básica exigiria e precisa ser RSA (outro tipo de chave é recusado na assinatura, com uma mensagem que diz isso). Válido somente quando Format = Cades e Method = Local, e recusado quando Certificate:Source é AzureKeyVault ou AwsKms, origens que nunca assinam SHA-1. Ao lado de um conjunto Approval.Signers que inclui aprovadores, os aprovadores também assinam um P7S simples (2.18.0) — toda assinatura de um job é feita sob a política que o job congelou ao ficar retido —, então os certificados deles também precisam ser RSA. Uma linha armazenada que as regras recusariam assina ADR-Básica, e todas as superfícies informam a política em vigor. O SHA-1 é um resumo obsoleto: escolha-o só para um destinatário que precise dele. Ligá-lo pelo dashboard pede confirmação antes, e o banner de inicialização o indica como policy=simple-p7s (SHA-1). Veja Certificados. |
Signing:Profiles[].CheckCNAB240 | bool | false | Signing__Profiles__0__CheckCNAB240 | Quando verdadeiro, todo arquivo roteado por este perfil é interpretado e validado como uma remessa CNAB240 do Banco do Brasil antes de ser assinado. Um arquivo fora do padrão nunca chega ao assinador: o job vai para Failed com ErrorMessage = cnab240.invalid, a cópia preparada é movida para a pasta de erro, e as violações são registradas no histórico do job e como um evento operacional Cnab240ValidationFailed. Aplica-se tanto a Local quanto a LacunaSigner. A validação é apenas estrutural — veja Arquivos de pagamento CNAB240. A correspondência do nome da chave não diferencia maiúsculas de minúsculas, então CheckCnab240 também funciona. |
Signing:Profiles[].CheckCnab240PaymentDates | bool | true | Signing__Profiles__0__CheckCnab240PaymentDates | Novo na 2.15.0. Lido somente quando CheckCNAB240 é verdadeiro. Quando verdadeiro (o padrão), uma remessa cuja Data do Pagamento mais antiga já passou é recusada na chamada de assinatura com ErrorMessage = cnab240.payment-date-passed. Quando falso, esse arquivo é assinado, e o histórico do job e um evento operacional Cnab240PaymentDateCheckSkipped registram que ele estava vencido — para um banco que processa pagamentos com data passada no próximo dia útil. A validação estrutural não é afetada. Lido no momento da assinatura e nunca congelado no job, então uma mudança vale já para a próxima assinatura, sem reinicialização, inclusive a de um job retido. Veja Arquivos de pagamento CNAB240. |
Signing:Profiles[].Approval | aninhado | ausente | Signing__Profiles__0__Approval__… | Opcional. Se presente, os jobs deste perfil ficam retidos em AwaitingApproval antes de existir qualquer assinatura. Válido somente junto com CheckCNAB240 = true — recusado no seed e, depois, recusado nos dois sentidos na página do perfil (não é possível adicionar uma regra de aprovação com a verificação desligada, nem desligar a verificação enquanto houver uma regra). Um job que, mesmo assim, chegue à etapa de aprovação sem a interpretação falha com approval.content-unmeasured em vez de ficar retido. Veja abaixo. |
Signing:Profiles[].Certificate.* | aninhado | n/a | Signing__Profiles__0__Certificate__… | REQUIRED quando Method = Local, a menos que o perfil seja sem chave (Approval.Signers = Approvers, abaixo). Mesmo formato do bloco global Signing:Certificate — Source mais a subárvore Pfx / Pkcs11 / WindowsStore / AzureKeyVault / AwsKms. Um erro de formato em qualquer entrada — uma chave ausente, dois locais para o mesmo arquivo — impede a inicialização com um erro agregado; um certificado bem formado que não abre deixa esse perfil degradado e o host rodando. Recusado quando Method = LacunaSigner. |
Signing:Profiles[].Signer.Name | string | n/a | Signing__Profiles__0__Signer__Name | REQUIRED quando Method = LacunaSigner. Nome de exibição do participante para quem o Lacuna Signer enviará o documento. |
Signing:Profiles[].Signer.Email | string | n/a | Signing__Profiles__0__Signer__Email | REQUIRED quando Method = LacunaSigner. E-mail do participante — precisa conter @. |
Signing:Profiles[].Signer.Identifier | string | n/a | Signing__Profiles__0__Signer__Identifier | REQUIRED quando Method = LacunaSigner. Identificador nacional do participante (CPF no Brasil). |
Signing:Profiles[].SignerFolder.Id | string (GUID) | n/a | Signing__Profiles__0__SignerFolder__Id | Novo na 2.16.0. REQUIRED quando Method = SignerFolder, ignorado em qualquer outro método. O id da pasta do Lacuna Signer cujos documentos este perfil assina — a própria pasta, nunca as subpastas —, copiado da pasta no Signer. Único entre os perfis: duas entradas com a mesma pasta impedem o boot, citando o primeiro dono. O SignerFolder.Name, opcional, serve só para exibição — é o que a página do perfil mostra para a pasta. Veja Assinando em uma pasta do Lacuna Signer abaixo. |
Signing:Profiles[].Approval — a etapa de aprovação
Se presente, os jobs deste perfil param antes de existir qualquer assinatura e aguardam uma pessoa.
Válido somente junto com CheckCNAB240 = true — um aprovador que não pode ver o valor não está
aprovando nada de fato, e o validador recusa a combinação na inicialização. Aplica-se tanto a Local
quanto a LacunaSigner. Passo a passo completo: Aprovações.
Como o resto do perfil, o bloco abaixo é dado de seed: depois do primeiro boot, o pool, o quórum, o prazo de espera e o conjunto de assinantes são editados na página do perfil, e a mudança vale para o próximo job que ficar retido. Um job já retido mantém a regra congelada nele no momento da retenção, então editar um pool nunca autoriza nada retroativamente. Remover alguém de um pool revoga imediatamente o link de aprovador dessa pessoa.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
…Approval.MinimumApprovers | int | 1 | Signing__Profiles__0__Approval__MinimumApprovers | O quórum: quantos membros distintos do pool precisam aprovar. No mínimo 1 e não maior que o pool — um quórum maior que o pool nunca pode ser atingido, então todo job ficaria retido para sempre; por isso, o validador o recusa. |
…Approval.ExpiresAfter | TimeSpan | ausente | Signing__Profiles__0__Approval__ExpiresAfter | Prazo de espera opcional no formato d.hh:mm:ss — "2.00:00:00" são quarenta e oito horas. Precisa ser positivo. Um job retido por mais tempo é cancelado com o motivo Approval window expired., e a cópia preparada dele é movida para error/. Ausente (o padrão), um job retido espera indefinidamente. A janela é medida com base no prazo congelado no job no momento da retenção. É um temporizador de manutenção, não um controle sobre pagamentos vencidos — quem recusa uma remessa cujas datas já passaram é a verificação da data de pagamento. |
…Approval.Signers | string | ProfileKey | Signing__Profiles__0__Approval__Signers | O conjunto de assinantes: de quem são as assinaturas presentes na saída de um job deste perfil — ProfileKey (o próprio certificado do perfil, como antes), Approvers (cada pessoa que aprova coassina com um certificado próprio) ou ProfileKeyAndApprovers (ambos). Um erro de digitação é recusado com a indicação dos três valores. Congelado no job junto com o resto da regra quando ele fica retido. Os dois conjuntos que incluem aprovadores precisam de um meio de assinatura neste host — um WebPki:License para um certificado no navegador do aprovador, ou o CloudHub para um certificado mantido por um provedor em nuvem, e qualquer um dos dois basta — e de uma forma de identificar o aprovador (ApproverPortal:Enabled ou uma seção Auth:EntraId); são recusados quando falta qualquer um dos dois, e também com qualquer Format diferente de Cades, já que a assinatura de um aprovador é uma coassinatura CAdES. Além disso, ProfileKeyAndApprovers é recusado junto com Method = LacunaSigner. Com Approvers, o perfil é sem chave: o bloco Certificate dele não é exigido nem validado, nada é aberto para ele na inicialização, e ele não fica degradado por não ter certificado. Veja Aprovações. |
…Approval.Approvers[] | array | [] | Signing__Profiles__0__Approval__Approvers__0__… | REQUIRED e não vazio quando Approval está presente. O pool de pessoas autorizadas a aprovar — não uma lista de pessoas que precisam, todas, aprovar. Com três entradas e MinimumApprovers: 1, nenhuma pessoa específica é obrigatória. |
…Approval.Approvers[].Name | string | n/a | …__Approvers__0__Name | REQUIRED. Nome de exibição. É o que o registro de auditoria mostra para este aprovador. |
…Approval.Approvers[].Email | string | n/a | …__Approvers__0__Email | REQUIRED, precisa conter @ e ser único dentro do pool (sem diferenciar maiúsculas de minúsculas). Um duplicado permitiria que uma mesma pessoa ocupasse duas vagas do pool e atingisse sozinha um quórum de dois. Mascarado na narração do console e nos logs duráveis; armazenado por inteiro no snapshot de aprovação do job. |
…Approval.Approvers[].Cpf | string | n/a | …__Approvers__0__Cpf | REQUIRED. Onze dígitos, com ou sem pontuação (123.456.789-09 e 12345678909 são ambos aceitos). Os dígitos verificadores são validados na inicialização — um erro de digitação identifica outra pessoa, e a linha de auditoria resultante parece tão legítima quanto uma correta. Serve somente para exibição e auditoria: nenhuma lógica depende dele. Mascarado nos logs duráveis. |
ExpiresAfter com o componente de diasUm valor de três componentes é hh:mm:ss somente enquanto o primeiro número for 23 ou menos; a partir
de 24, o .NET o lê como dias, então "48:00:00" é interpretado como quarenta e oito dias e passa
na verificação de duração positiva. O valor não é recusado — uma janela longa pode ser intencional —,
mas o banner de inicialização avisa a partir de 24 dias, citando tanto o valor resolvido
(expires=1152h) quanto a grafia que o corrige. O boot é o único momento em que isso pode ser
detectado.
Exemplo: um perfil de pagamentos que retém para aprovação
{
"Name": "pagamentos-bb",
"Format": "Cades",
"Method": "Local",
"CheckCNAB240": true,
"Certificate": {
"Source": "Pfx",
"Pfx": { "Path": "/etc/bulksigner/pagamentos.pfx", "Password": "" }
},
"Approval": {
"MinimumApprovers": 2,
"ExpiresAfter": "2.00:00:00",
"Approvers": [
{ "Name": "Maria Silva", "Email": "maria@empresa.com.br", "Cpf": "12345678909" },
{ "Name": "João Souza", "Email": "joao@empresa.com.br", "Cpf": "111.444.777-35" },
{ "Name": "Ana Ferreira", "Email": "ana@empresa.com.br", "Cpf": "52998224725" }
]
}
}
O seed recusa, antes que o primeiro job rode — e, depois, a página do perfil recusa ao salvar: um bloco
Approval sem CheckCNAB240; um pool vazio; um MinimumApprovers abaixo de 1 ou maior que o pool; um
e-mail malformado, ou o mesmo e-mail duas vezes; um CPF cujos dígitos verificadores não conferem; um
ExpiresAfter não positivo; um conjunto de assinantes com aprovadores em um host sem meio de assinatura
ou sem forma de identificar um aprovador.
A etapa de aprovação é real — um job de fato não é assinado até que pessoas suficientes aprovem —, mas
o /approve/{jobId} não exige credencial: qualquer pessoa que consiga acessar o link pode aprovar ou
rejeitar em nome de qualquer membro do pool do job. O banner de inicialização emite um aviso para todo
perfil com aprovação configurada, a cada boot. Habilite o ApproverPortal ou o
login pelo Entra ID para restringir isso, e
leia Aprovações antes de expor o host a uma rede acessível pelos navegadores
dos aprovadores.
Assinando em uma pasta do Lacuna Signer — Method = SignerFolder
Um terceiro método de assinatura. O passo a passo para o operador está em Integração com o Lacuna Signer.
Um perfil neste método assina documentos que uma pessoa cria no Lacuna Signer, na pasta indicada
pelo SignerFolder.Id dele, com o titular do certificado do perfil como participante. Este host os
encontra por uma varredura periódica (Signer:FolderSweepIntervalSeconds)
e, quando o Signer:WebhookSecret está definido, segundos depois
de uma notificação do Signer; ele assina o hash entregue pelo Signer com a chave do próprio perfil, e o
Signer incorpora a assinatura ao documento e o guarda — nada entra em input/ e nada chega a output/.
O titular é associado pelo CPF que o certificado traz (certificados e-CPF e e-CNPJ trazem um), nunca
por um campo digitado. O que o seed recusa — o mesmo que um salvamento pelo dashboard recusa
(Integração com o Lacuna Signer) — é:
- Nenhum
SignerFolder.Id, ou um que não seja um GUID — o Signer identifica uma pasta pelo id; copie-o da pasta no Signer. - Uma pasta a que outra entrada já está vinculada — duas entradas com o mesmo
SignerFolder.Idfazem a segunda ser recusada, citando a primeira; os ids são comparados como GUIDs, não como texto. - O perfil
default— é para ele que vão os uploads sem perfil e as pastas monitoradas sem vínculo, e este método não recebe arquivos. - Nenhuma configuração
Signer:*neste host — os documentos são encontrados e assinados com oSigner:Endpointe aSigner:ApiKeydeste host. Verify,Encrypt,ValidateCertificateouCheckCnab240iguais atrue— o Signer guarda o documento, escolhe o container e valida o certificado no início da assinatura, e nunca entrega um arquivo a este host. Note queVerifyeValidateCertificatetêm padrãotrue; por isso, uma entradaSignerFolderdeclara as duas comofalse.- Um bloco
Approval— o fluxo do documento é decidido no Signer. - Uma pasta monitorada vinculada a ele — um
Storage:Inputs[].Profileque cita o perfil. Os documentos dele nascem no Signer, então nenhum arquivo é lido para ele. - Uma origem de certificado por host com
Cluster:Enabled, como para qualquer perfil. Todas as outras origens servem, inclusive um PKCS#12 enviado e o Azure Key Vault.
Um certificado sem CPF não está entre elas: o seed não abre nenhum certificado, então um perfil assim
sobe e é informado como degradado — assim como um perfil armazenado em um host que depois perdeu as
configurações Signer:*, e um cuja pasta o Signer depois diz não existir
(Integração com o Lacuna Signer).
O Format é ignorado: o Signer escolhe PAdES para um PDF, XAdES para XML e CAdES para os demais, e o
job registra qual. Em um PDF, a aplicação da política ICP-Brasil a essa assinatura PAdES depende da
configuração UseBrazilianPdfSigningPolicies da instância do Signer, e não do Bulk Signer — o padrão
ADR-Básica do produto não a alcança.
{
"Signer": {
"Endpoint": "https://signer.lacunasoftware.com",
"FolderSweepIntervalSeconds": 300
},
"Signing": {
"Profiles": [
{
"Name": "contratos",
"Method": "SignerFolder",
"SignerFolder": { "Id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" },
"Verify": false,
"ValidateCertificate": false,
"Certificate": {
"Source": "Pfx",
"Pfx": { "Path": "/etc/bulksigner/certificates/empresa-ecnpj.pfx" }
}
}
]
}
}
com Signer__ApiKey e Signing__Profiles__0__Certificate__Pfx__Password fornecidas pelo ambiente.
Storage:Inputs[].Profile — roteamento por pasta
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Storage:Inputs[].Profile | string? | null (→ "default") | Storage__Inputs__0__Profile | Opcional, e dado de seed: lida uma única vez, no primeiro boot com uma tabela de perfis vazia, e aplicada ao perfil que ela indica — a linha do perfil passa então a guardar a pasta, uma pasta por perfil, e a chave é ignorada nos boots seguintes. Nulo ou vazio vincula ao perfil default. Uma pasta que o seed não consegue vincular — o perfil dela não está em Signing:Profiles[], ou uma pasta anterior já foi vinculada a esse perfil — fica sem atribuição, é informada na inicialização e aparece assim na página Pastas de entrada e no /api/ready; os arquivos dela esperam até que um perfil a escolha. O host inicia de qualquer forma. |
Até a 2.1.x, era esta chave que roteava uma pasta, e ela era lida a cada boot. Agora o vínculo fica na linha do perfil no banco operacional. Depois do primeiro boot, uma pasta é roteada, movida ou desvinculada na página do perfil (Pasta de entrada, em Editar comportamento) e somente ali: editar esta chave não muda nada, e o log de inicialização avisa isso com uma linha de aviso que conta as chaves de pasta ignoradas. Uma mudança entra em vigor no próximo arquivo, em todas as instâncias, sem reinicialização. Mantenha a chave se quiser que uma implantação nova já suba roteada; apague-a quando os perfis estiverem no banco, e essa linha desaparece.
Nada mais em Storage:Inputs[] mudou de lugar. Name, Path, Provider, AzureFiles,
PollIntervalSeconds e as duas listas de itens ignorados são configuração do host: lidas a cada boot,
validadas pelas regras de
Storage:Inputs[] exatamente como
antes, e não editáveis em nenhum lugar da interface web. A única chave que saiu nunca foi uma
característica da pasta — ela dizia qual regra de assinatura os arquivos da pasta recebem, e quem deve
dizer isso é o perfil.
Uma pasta que nenhum perfil escolheu fica sem atribuição: o observador dela não enfileira nada, uma
nova varredura a ignora e avisa isso, e os arquivos dela ficam esperando. Um perfil vinculado a uma pasta
que este host não configura mais é informado no banner de inicialização, como uma linha
profile-input-folder: no /api/ready que não reprova o probe, e como um alerta na página de perfis do
dashboard.
Exemplo: três perfis roteados por pasta, um por origem de certificado
Os perfis são independentes, então uma única implantação pode misturar modelos de custódia de chave — um
HSM para NF-e, um PFX em disco para contratos e uma chave mantida em cofre para faturas —, com cada pasta
monitorada alimentando o perfil de que precisa. A escolha da pasta passa a ser feita em cada perfil, na
página dele, depois do primeiro boot; a chave Profile de cada entrada de Storage:Inputs[] define essa
escolha no seed, uma única vez, no mesmo boot que importa estes três perfis.
"Signing": {
"PkiSdkLicense": "<env-var>",
"Profiles": [
{
"Name": "nfe",
"Format": "Xades",
"Verify": true,
"Encrypt": false,
"ValidateCertificate": true,
"Certificate": {
"Source": "Pkcs11",
"Pkcs11": { "ModulePath": "/usr/lib/x86_64-linux-gnu/pkcs11/libsofthsm2.so", "Thumbprint": "...", "PinEnvVar": "BULK_SIGNER_PKCS11_PIN" }
}
},
{
"Name": "contracts",
"Format": "Pades",
"Verify": true,
"Encrypt": true,
"ValidateCertificate": true,
"Certificate": {
"Source": "Pfx",
"Pfx": { "Path": "/etc/bulksigner/contracts.pfx", "Password": "" }
}
},
{
"Name": "invoices",
"Format": "Pades",
"Verify": true,
"Encrypt": false,
"ValidateCertificate": true,
"Certificate": {
"Source": "AzureKeyVault",
"AzureKeyVault": {
"Endpoint": "https://my-vault.vault.azure.net/",
"AppId": "8f2c1b3e-1111-2222-3333-444455556666",
"AppSecret": "",
"KeyName": "bulk-signer-invoices-key",
"CerPath": "/etc/bulksigner/certificates/invoices.cer"
}
}
}
]
},
"Storage": {
"Root": "/var/lib/bulksigner",
"Inputs": [
{ "Name": "nfe-incoming", "Path": "/var/lib/bulksigner/input-nfe", "Profile": "nfe" },
{ "Name": "contracts-incoming", "Path": "/var/lib/bulksigner/input-contracts", "Profile": "contracts" },
{ "Name": "invoices-incoming", "Path": "/var/lib/bulksigner/input-invoices", "Profile": "invoices" }
]
}
O perfil invoices deixa o AppSecret vazio no arquivo e o obtém do ambiente. Elementos de array são
vinculados por índice posicional, então o segredo do terceiro perfil é:
export Signing__Profiles__2__Certificate__AzureKeyVault__AppSecret='…'
Como esse perfil tem um segredo, o primeiro boot também precisa de Signing__ProfileSecretsKey
definida — a importação criptografa o segredo do aplicativo com ela e é recusada sem ela. Os outros dois
não têm nenhum segredo.
Esse índice é posicional, não baseado em nome. Inserir um novo perfil acima de invoices o desloca
para o índice 3, a variável de índice 2 deixa de chegar a ele, e o seed falha com
Signing:Profiles[3].Certificate.AzureKeyVault.AppSecret is required. Confira novamente todas as
variáveis de ambiente indexadas depois de reordenar a lista. Isso só importa até o seed rodar: depois do
primeiro boot, o segredo fica, criptografado, no perfil armazenado, e alterá-lo — por exemplo, um client
secret rotacionado — é feito na página do perfil (Editar certificado), seguido de uma
reinicialização, e não alterando a variável.
O banner de inicialização lista cada perfil resolvido com o formato, a origem do certificado e as flags
de verificação/criptografia/validação de certificado. Perfis com Verify=false ou
ValidateCertificate=false emitem avisos adicionais, para que a postura de baixa confiança fique
registrada nos logs duráveis. Um perfil cujo certificado não abriu é listado com o prefixo
DEGRADED · e o motivo ao lado.
Signer — conexão com o Lacuna Signer
Um tenant do Lacuna Signer por host — o endpoint e a chave de API são globais, não por perfil. O validador só se ativa se esta seção existir: se ela for totalmente omitida, nada aqui é exigido, que é o caso de uma implantação somente local. Se qualquer parte dela for definida, o bloco inteiro é validado, porque um bloco configurado pela metade é uma implantação que só descobriria a falha no primeiro envio ao Lacuna Signer. Veja Integração com o Lacuna Signer.
Até a 2.0.x, estas chaves só eram exigidas quando alguma entrada de Signing:Profiles[] tinha
Method = LacunaSigner — uma pergunta que a configuração não consegue mais responder, já que os perfis
ficam no banco operacional. Agora a exigência fica no perfil: um perfil que seleciona
Method = LacunaSigner é recusado — no seed ou na página do perfil — quando esta seção está ausente.
O mesmo fato decide se o worker de polling do assinador remoto roda, então um host com estas
configurações está pronto para receber, depois do boot, um perfil apontado para o Lacuna Signer, sem
reinicialização. Duas consequências na atualização: um bloco Signer: preenchido pela metade agora
impede o boot mesmo que nada o use (a mensagem indica as duas chaves e sugere remover a seção como
solução), e um host com o bloco inteiro definido, mas sem nenhum perfil que o use, agora roda o worker de
polling ocioso. Remova a seção se este host assina tudo localmente.
A mesma regra vale para Method = SignerFolder (2.16.0): ele é recusado quando esta seção está ausente,
e são estas configurações que iniciam a varredura de pastas do Signer. Um perfil SignerFolder
armazenado que se encontra em um host sem elas fica degradado, em vez de impedir o boot
(Integração com o Lacuna Signer).
Endpoint e ApiKey servem aos dois métodos. PollIntervalSeconds, TimeoutHours e
MaxConsecutiveApiFailures só são lidas para LacunaSigner; FolderSweepIntervalSeconds e
WebhookSecret, só para SignerFolder.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Signer:Endpoint | string | "" | Signer__Endpoint | REQUIRED quando algum perfil usa LacunaSigner ou SignerFolder. URL base da instância do Lacuna Signer. Padrão na nuvem: https://signer.lacunasoftware.com. Implantações on-premises apontam para a instância do cliente. |
Signer:ApiKey | string | "" | Signer__ApiKey | REQUIRED, SECRET quando algum perfil usa LacunaSigner ou SignerFolder. Formato esperado: application-id|secret. O valor literal é removido dos logs. |
Signer:WebhookSecret | string | "" (notificações desligadas) | Signer__WebhookSecret | Novo na 2.16.0. SECRET. O token que o Lacuna Signer apresenta em uma notificação — o webhook dele, registrado nas integrações da organização no Signer com autenticação Bearer e este valor como token (passo a passo em Integração com o Lacuna Signer). Enviado como Authorization: Bearer <secret> e comparado em tempo constante — nunca como X-API-Key, que é a credencial REST do próprio produto. Vazio, o POST /api/signer/notifications responde 404 e só a varredura de pastas encontra documentos, o que custa latência e nunca um documento. Defini-lo conta como definir o bloco Signer, então Signer:Endpoint e Signer:ApiKey passam a ser obrigatórias. Pelo menos 32 caracteres; removido dos logs. |
Signer:PollIntervalSeconds | int | 30 | Signer__PollIntervalSeconds | Com que frequência o worker de polling percorre cada linha AwaitingSigner. Faixa válida: 1–3600. |
Signer:TimeoutHours | int | 168 (7 dias) | Signer__TimeoutHours | Quanto tempo um job pode ficar em AwaitingSigner antes de falhar com code = signer.timeout. Faixa válida: 1–8760. |
Signer:MaxConsecutiveApiFailures | int | 5 | Signer__MaxConsecutiveApiFailures | Limite de erros transitórios consecutivos por documento antes de o worker de polling desistir desse documento. Contador em memória — reiniciar o serviço o zera. A partir da 2.16.0, um 5xx, 408, 429 ou a página de erro de um proxy também conta para ele — veja Falhas de API. |
Signer:FolderSweepIntervalSeconds | int | 300 (5 minutos) | Signer__FolderSweepIntervalSeconds | Novo na 2.16.0. Com que frequência cada pasta do Lacuna Signer vinculada a um perfil SignerFolder habilitado é listada em busca de documentos pendentes. A varredura é o que garante que um documento seja assinado; por isso, este é o máximo que um documento espera até este host percebê-lo. Ela roda com o pipeline pausado ou não — a descoberta só enfileira jobs; a pausa impede que eles sejam reivindicados — e, com Cluster:Enabled, toda instância faz a varredura: um documento encontrado por duas instâncias é assumido uma vez só, porque o banco operacional registra cada par (documento, ação do fluxo) já assumido e recusa o segundo. Faixa válida: 60–86400. |
WebPki — Lacuna Web PKI no navegador do aprovador
Um aprovador que coassina um arquivo de pagamento (em um perfil cujo
conjunto de assinantes é Approvers ou
ProfileKeyAndApprovers) pode fazer isso com um certificado no próprio token ou no próprio repositório
de certificados, por meio da extensão de navegador Lacuna Web PKI. A biblioteca que se comunica com a
extensão acompanha o produto e nunca é carregada de uma CDN, então um aprovador em uma LAN sem acesso à
internet ainda recebe a página. O que o Web PKI precisa do host é uma licença vinculada aos domínios
da implantação, e é nesta seção que ela fica.
A licença não é segredo. Ela é enviada em texto claro ao navegador de todo aprovador, que é onde o
Web PKI a confere com o domínio da página; por isso, ela deliberadamente não é mascarada nos logs. Todos
os lugares que a informam dizem apenas se está configurada ou não, sem exibi-la: a linha
web pki license do banner de inicialização e a página Sistema.
Ela também não causa recusa de boot. A regra fica onde ficam todas as regras de perfil — no seed e
na página do perfil: um perfil cujo conjunto de assinantes inclui aprovadores é recusado, com a
indicação desta chave e do CloudHub:ApiKey, enquanto nenhum dos dois estiver configurado. Uma
licença (um certificado no navegador) ou o
CloudHub (um certificado na nuvem) é um
meio de assinatura, e basta um dos dois. Uma implantação cujos conjuntos de assinantes são todos
ProfileKey não precisa de nenhuma seção WebPki. Em localhost, o Web PKI funciona sem licença.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
WebPki:License | string | "" | WebPki__License | Obrigatória para um perfil em que os aprovadores assinam, a menos que o CloudHub esteja configurado no lugar dela. Qualquer uma das duas formas que a Lacuna emite — a string binária (base64) ou o documento JSON —, repassada ao navegador sem alteração. Vinculada aos domínios da implantação: solicite-a à Lacuna para os nomes de host em que os aprovadores vão abrir a página. Deliberadamente não marcada como SECRET. |
{
"WebPki": {
"License": "<a licença que a Lacuna emitiu para os domínios desta implantação>"
}
}
CloudHub — Lacuna CloudHub para certificados em nuvem
Um aprovador cujo certificado ICP-Brasil foi emitido no HSM de um provedor — um certificado em nuvem — não tem nada que um navegador consiga acessar, então o Web PKI não consegue assinar por ele. O Lacuna CloudHub reúne esses provedores atrás de uma única API: o produto abre uma sessão para o CPF do aprovador, envia o navegador dele ao provedor que ele escolher, recebe-o de volta no endereço desta própria implantação e assina o arquivo servidor a servidor com o certificado que o provedor autenticou. É esta seção que faz esse meio de assinatura existir em um host; sem chave, nenhuma página o oferece, e a rota de retorno dele fica inacessível.
Ele atende a um lote tão bem quanto a um arquivo. Desde a 2.14.0, o Aprovar N selecionados do portal do aprovador oferece a nuvem ao lado do navegador — e sozinha, em um host sem licença do Web PKI —, com um único login no provedor para o lote inteiro. Nada aqui precisa ser configurado à parte para isso.
A chave é o que liga o recurso, e ela é segredo. Diferentemente da licença do Web PKI, o
CloudHub:ApiKey é uma credencial ao portador para toda sessão que este host abre, então ela é mascarada
em todos os logs. Defina-a pela variável de ambiente. O validador só se ativa se a chave existir: se
ela for omitida, nada aqui é exigido; se for definida, a seção inteira é validada.
O PublicBaseUrl é obrigatório junto com a chave, e é configurado em vez de derivado. O CloudHub
precisa de um endereço absoluto para onde devolver o navegador, e este produto gera todos os outros
links relativos à requisição. Derivar o endereço de headers encaminhados foi descartado: um endereço
derivado errado só é descoberto quando um aprovador fica preso no provedor, enquanto um endereço
configurado errado é recusado no boot. Informe o esquema, o host e o prefixo de caminho, se houver, em
que os aprovadores abrem o portal, sem query e sem fragmento.
Ele não causa recusa de boot para um perfil. Um perfil em que os aprovadores assinam precisa desta
seção ou de uma licença do Web PKI, e qualquer uma das duas basta; essa regra é verificada no seed e na
página do perfil. Um host com CloudHub e sem licença do Web PKI é uma implantação legítima em que todo
aprovador assina na nuvem. Nada testa o CloudHub no boot nem no /api/ready: a linha cloudhub do
banner de inicialização, a página Sistema e o /api/ready/details dizem apenas se está configurado
ou não, indicando o endpoint, e uma indisponibilidade do lado da Lacuna nunca altera a prontidão deste
host.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
CloudHub:ApiKey | string | "" | CloudHub__ApiKey | SECRET. A chave de API do CloudHub que a Lacuna emitiu para esta implantação. É ao defini-la que o meio de assinatura em nuvem passa a ser oferecido. |
CloudHub:Endpoint | string | https://cloudhub.lacunasoftware.com/ | CloudHub__Endpoint | Onde o CloudHub está. Omita para usar a instância pública da Lacuna. Precisa ser uma URL http(s) absoluta quando a chave está definida. |
CloudHub:PublicBaseUrl | string | "" | CloudHub__PublicBaseUrl | REQUIRED quando ApiKey está definida. O endereço absoluto pelo qual os aprovadores acessam esta implantação — esquema, host e prefixo de caminho, sem query, sem fragmento —, para onde o CloudHub devolve o navegador. Recusado no boot quando relativo ou malformado. |
{
"CloudHub": {
"ApiKey": "<defina via CloudHub__ApiKey; nunca neste arquivo>",
"PublicBaseUrl": "https://bulksigner.example.com"
}
}
Os modelos de arquivo de ambiente do pacote de implantação (deploy/linux/bulksigner.env.sample,
deploy/docker/.env.sample) trazem uma linha CloudHub__ApiKey= comentada exatamente para isso.
Encryption
Desligada por padrão. O validador só roda quando Enabled = true.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Encryption:Enabled | bool | false | Encryption__Enabled | Chave geral. Quando verdadeira, o worker criptografa o artefato assinado com AES-256-GCM entre a verificação e a promoção. |
Encryption:Password | string | "" | Encryption__Password | SECRET. Senha do PBKDF2. Permitida na configuração (use appsettings.Production.json, que está no gitignore), mas a forma por variável de ambiente é preferível. |
Encryption:PasswordEnvVar | string | BULK_SIGNER_ENCRYPTION_PASSWORD | Encryption__PasswordEnvVar | Nome da variável de ambiente que fornece a senha. Se não estiver vazia no boot, ela prevalece sobre Encryption:Password. |
Encryption:Salt | string | "" | Encryption__Salt | REQUIRED quando Enabled = true. Salt do PBKDF2 codificado em base64; decodificado, precisa ter pelo menos 16 bytes. Salts não são secretos. Mudar o salt invalida todos os envelopes anteriores. |
Encryption:Iterations | int | 600000 | Encryption__Iterations | Contagem de iterações do PBKDF2-HMAC-SHA256. Rejeitada abaixo de 10000. |
A perda da senha é irrecuperável — não há endpoint de descriptografia no servidor nem custódia de chaves. Veja Criptografia.
Auth
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Auth:ApiKey | string | "" | Auth__ApiKey | REQUIRED, SECRET. Chave de API estática, com no mínimo 16 caracteres. Enviada no header X-API-Key por clientes programáticos; colada em /login por operadores para receber um cookie. Desde a 2.3.1, o appsettings.json distribuído não traz valor para ela, então toda implantação precisa definir a própria chave. |
Auth:CookieName | string | lbs-auth | Auth__CookieName | Nome do cookie emitido por /api/auth/login. SameSite=Strict, HttpOnly, marcado como seguro quando a requisição foi HTTPS. |
Auth:ApiKeyHeader | string | X-API-Key | Auth__ApiKeyHeader | Header HTTP lido pelo esquema de chave de API. Renomeie apenas se uma convenção de proxy reverso exigir. |
Veja Segurança sobre a rotação da chave de API e o tempo de vida da sessão em cookie.
Auth:SessionKeyRing — onde o key ring de sessão fica
Novo na 2.20.0. Deixe esta seção sem definir, a menos que o host não tenha nenhum disco que dure
mais que o processo. Os dois cookies de sessão — o do operador e o do aprovador — são protegidos por um
único key ring do ASP.NET Data Protection, e um ring que não sobrevive a uma reinicialização desconecta
todo mundo a cada deploy, sem que nada informe isso. Sem a chave, o ring fica onde sempre ficou: em
keys/, dentro de Storage:Root, em uma instância única, e no banco operacional com
Cluster:Enabled. A chave existe para a implantação
que fica sem nenhum lugar durável para mantê-lo — um único container sem volume, como um cujo
compartilhamento de trabalho está em armazenamento de objetos.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Auth:SessionKeyRing:Placement | string | ausente | Auth__SessionKeyRing__Placement | DataRoot ou OperationalStore. Ausente (o padrão), não muda nada: keys/ dentro de Storage:Root fora do modo cluster, o banco operacional nele. OperationalStore mantém o ring como linhas na tabela SessionProtectionKeys — em texto claro, protegido pelo controle de acesso do próprio banco de dados, sem a criptografia DPAPI do Windows —, e o boot então não cria o diretório keys/. DataRoot declara explicitamente o padrão de instância única. Um valor não reconhecido impede o boot do host, indicando a chave, o valor e os dois válidos. |
Duas combinações são recusas de boot, cada uma indicando as duas chaves:
DataRootcomCluster:Enabled = true. Um ring dentro deStorage:Rootpertence a um único host, então, atrás de um balanceador de carga, um cookie criado por uma instância é rejeitado pela seguinte — uma desconexão intermitente. Remova a chave; o modo cluster já mantém o ring no banco.OperationalStorecomDatabase:Provider = Sqlite(inclusive com o provider ausente, que é SQLite). O arquivo do banco fica no mesmoStorage:Rootquekeys/, então a mudança não sobrevive a nada a quekeys/não sobreviveria, e, no Windows, abre mão da criptografia DPAPI em troca de nada. UseSqlServerou deixe o ring emkeys/.
Três pontos importantes antes de defini-la:
- Ela desconecta todo mundo uma vez. As chaves que já estão em
keys/não são copiadas para o banco, então o primeiro boot comOperationalStorecomeça um ring novo. Remover a chave depois é mais uma desconexão. Os links de aprovador não são afetados — eles são derivados deApproverPortal:LinkSecret. - Ela não é inferida de
Database:Provider = SqlServer, deliberadamente. Uma implantação de instância única com SQL Server e um disco durável mantém o ring emkeys/, criptografado com DPAPI no Windows, exatamente como antes; movê-lo é uma decisão, não um efeito colateral de uma atualização. - Um banco inacessível significa que ninguém consegue entrar, sem recorrer a um ring por host. O acesso de leitura ao banco de dados passa a ser a capacidade de criar uma sessão como qualquer pessoa, e o que quer que faça o backup desse banco contém material de sessão válido — veja Segurança.
A linha session key ring do resumo de prontidão indica onde o ring está e o que o colocou ali, a cada
boot. Se ela mostrar default — Auth:SessionKeyRing:Placement not set em um host em que você a definiu,
o nome da chave está digitado errado ou o valor está em branco: um nome errado não se associa a nada
e não é recusado por nada, e um valor em branco é lido como ausente. O log durável registra a mesma frase
uma vez por boot, no nível Information, como Session key ring: ….
{
"Database": { "Provider": "SqlServer" },
"Auth": {
"SessionKeyRing": { "Placement": "OperationalStore" }
}
}
Auth:EntraId — login opcional pelo Microsoft Entra ID
Ativado pela presença — não há flag Enabled. Se a seção for omitida (o padrão), tudo se comporta
exatamente como sem ela; uma implantação isolada da rede nunca precisa de um tenant da Microsoft. Se a
seção for escrita, as três chaves passam a ser obrigatórias: uma seção parcialmente preenchida impede
o boot do host, indicando a chave ausente. Tratar "presente, mas vazio" silenciosamente como
desligado é exatamente o que leva um operador a acreditar que um controle está ativo quando não está.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Auth:EntraId:TenantId | string | (ausente) | Auth__EntraId__TenantId | REQUIRED quando a seção está presente. O GUID do diretório (tenant), ou um domínio verificado (por exemplo, contoso.onmicrosoft.com). Os pseudo-tenants multi-tenant common / organizations / consumers são recusados — o modo é de tenant único por design. |
Auth:EntraId:ClientId | string | (ausente) | Auth__EntraId__ClientId | REQUIRED quando a seção está presente. O ID de aplicativo (client) do registro de aplicativo. Precisa ser um GUID válido — caso contrário, um erro de digitação aqui só apareceria no momento do login, como um erro AADSTS pouco claro. |
Auth:EntraId:ClientSecret | string | (ausente) | Auth__EntraId__ClientSecret | REQUIRED quando a seção está presente, SECRET. O client secret do cliente confidencial para o fluxo de authorization code. Defina-o pela variável de ambiente; nunca o versione. |
Não há uma quarta chave. A authority, o caminho de callback, os escopos, o cookie e o tempo de vida dele são todos derivados.
O que muda ao ativar o login pelo Entra ID
| Onde | Seção ausente (padrão) | Seção presente |
|---|---|---|
/login | Formulário de chave de API | Botão Entrar com a Microsoft. O formulário de chave de API deixa de aparecer. |
POST /api/auth/login | Troca a chave de API por um cookie | Não emite cookie nem para uma chave correta. Desligado, não apenas escondido. |
| Cookies de operador existentes | Válidos durante a janela deslizante de 8 horas | Deixam de atender às políticas imediatamente. Planeje a transição como um "desconectar todo mundo". |
REST X-API-Key | Funciona | Inalterado. Clientes automatizados nunca percebem o modo. |
| Páginas de operador | Qualquer cookie autenticado | Exige a app role Administrator. |
/approvals e a página de aprovação por job | Somente o link do portal do aprovador | Link ou uma sessão com a role Approver; o pool congelado continua determinando quais jobs ficam visíveis. |
| Identificação registrada na aprovação | SelfDeclaredEmail / LinkDerivedEmail | Acrescenta EntraIdEmail para decisões tomadas em uma sessão do Entra. |
| Sair | Limpa o cookie | Limpa somente a sessão do Bulk Signer. A sessão da Microsoft continua ativa, então clicar em "entrar" de novo funciona sem pedir credenciais — comportamento normal de SSO, não um defeito. |
As duas app roles
As roles vêm das claims de role do token — atribuições de app role e nada mais. Deliberadamente, não há mapeamento por grupo de segurança: com ele, editar um grupo no tenant seria uma mudança de autorização invisível. Os valores no manifesto do registro de aplicativo precisam coincidir exatamente com estas strings:
| Valor da role | Dá acesso a | Página de destino após o login |
|---|---|---|
Administrator | Todas as páginas e ações de operador que o cookie de chave de API concede hoje. Sem níveis. | / |
Approver | Somente as telas de aprovação. A role dá acesso, mas o pool congelado ainda decide quais jobs a pessoa vê, com base na claim de e-mail. | /approvals |
| (ambas) | Ambas. É permitido acumular as duas funções; a segregação de funções é garantida pelas verificações de role. | / |
| (nenhuma) | Nada. Uma conta que se autentica, mas não tem nenhuma role, é recusada em /access-denied. | — |
Um returnUrl validado sempre prevalece sobre o destino baseado em role, então os deep links continuam
funcionando.
Como uma pessoa conectada é identificada
O nome registrado do operador é o UPN. Todo evento de auditoria gravado por um Administrator
conectado — um perfil criado ou editado, o pipeline pausado, jobs limpos, um backup executado — traz a
claim preferred_username do token, e o menu do usuário mostra o mesmo valor. Usa-se o UPN em vez do
nome de exibição porque ele é único no tenant em qualquer momento, enquanto o nome de exibição é texto
livre que duas pessoas podem compartilhar. É o nome no momento da ação, não uma chave durável. Um token
sem preferred_username ainda consegue entrar, e os eventos dele aparecem como (anonymous); o login
registra um aviso com os tipos de claim recebidos. Isso vale só para o nome do operador — os aprovadores
continuam sendo associados aos pools pelo e-mail, como descrito acima.
Um aprovador do Entra é cumprimentado pelo nome de exibição. A saudação do portal mostra a claim
name do token e, quando o tenant não a envia, o endereço de e-mail — nunca o UPN, que, para um
convidado, tem a forma #EXT#. Em qualquer caso, a decisão é registrada com o e-mail.
A janela do segundo fator acompanha a sessão do tenant. Quando
ApproverSecondFactor:Enabled está ligado, a janela de verificação de um
aprovador do Entra é indexada pela claim sid do token, uma claim padrão do ID token que não exige
mudança no registro de aplicativo; quando um token não a traz, o login gera um identificador e registra
o motivo. Sair apaga a janela da sessão, porque a sessão do tenant sobrevive ao logout local do
produto, e o novo login silencioso de um colega na mesma estação de trabalho a herdaria.
Uma sessão do dashboard aberta durante a atualização para a 2.2.1 ou posterior mantém o ticket antigo, que não leva a identidade do operador para dentro da página. Saia e entre de novo uma vez depois de atualizar.
Exemplo — configuração mínima
{
"Auth": {
"ApiKey": "…",
"EntraId": {
"TenantId": "11112222-3333-4444-5555-666677778888",
"ClientId": "99990000-aaaa-bbbb-cccc-ddddeeeeffff",
"ClientSecret": ""
}
}
}
Auth__EntraId__ClientSecret='<o client secret do registro de aplicativo>'
Auth:ApiKey continua obrigatória — este modo não afeta o X-API-Key da API REST. As três chaves do
Entra também podem ser definidas somente pelo ambiente, que é a forma natural para um container ou uma
unit do systemd. O passo a passo do registro de aplicativo está em
Instalação.
Storage
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Storage:Root | string | data | Storage__Root | REQUIRED. Raiz na qual são criados logs/, keys/ (o key ring de sessão, a menos que ele fique no banco operacional — veja Auth:SessionKeyRing) e — enquanto Database:Provider for Sqlite — db/, além de processing/, output/ e error/ quando Storage:Provider é LocalFileSystem. Esta raiz é sempre local, quaisquer que sejam os providers: um caminho com um esquema remoto — azurefiles://, azureblob:// ou s3:// — é recusado no boot. Sobrescreva conforme o alvo — /var/lib/bulksigner no Linux, C:\ProgramData\Lacuna\BulkSigner\data no Windows, /var/lib/bulksigner no Docker. |
Storage:Provider | enum | LocalFileSystem | Storage__Provider | LocalFileSystem, AzureFiles, AzureBlob ou S3 (os dois últimos novos na 2.20.0). Define onde fica o compartilhamento de trabalho — processing/, output/, error/. logs/, keys/ e db/ sempre ficam locais. Omiti-lo não muda nada; um valor não reconhecido impede o boot do host, indicando os valores válidos. Veja Providers de armazenamento e topologias. |
Storage:AzureFiles | objeto | ausente | Storage__AzureFiles__* | A conta, o compartilhamento, o diretório opcional e a credencial do compartilhamento de trabalho no Azure Files — e o padrão herdado por toda pasta de entrada que indica AzureFiles. Veja abaixo. |
Storage:AzureBlob | objeto | ausente | Storage__AzureBlob__* | Novo na 2.20.0. O mesmo para o Azure Blob Storage: o container, o prefixo opcional e a credencial do compartilhamento de trabalho, e o padrão herdado por toda pasta de entrada AzureBlob. Veja o compartilhamento de trabalho em um container Blob. |
Storage:S3 | objeto | ausente | Storage__S3__* | Novo na 2.20.0. O mesmo para o S3: o bucket, o prefixo opcional, a região ou o endpoint e a credencial do compartilhamento de trabalho, e o padrão herdado por toda pasta de entrada S3. Veja o compartilhamento de trabalho em um bucket S3. |
Storage:Inputs[] | array de {Name, Path, Provider?, AzureFiles?, AzureBlob?, S3?, PollIntervalSeconds?, IgnoredExtensions?, IgnoredPrefixes?, Profile?} | [{Name="default", Path="{Root}/input"}] | Storage__Inputs__0__Name, Storage__Inputs__0__Path, … | Uma ou mais pastas de entrada monitoradas. Os jobs são marcados com o Name da pasta e com o perfil de assinatura que escolheu a pasta; uma pasta que nenhum perfil escolheu fica sem atribuição e não enfileira nada. Veja abaixo as regras de validação, e Storage:Inputs[].Profile para o que o seed faz com a única chave de roteamento. |
Providers de armazenamento e topologias
Novo na 2.20.0: o Azure Blob Storage e o Amazon S3 se juntam ao sistema de arquivos local e ao Azure Files. Duas perguntas decidem onde os arquivos ficam, e elas são respondidas separadamente:
- Cada pasta de entrada monitorada indica o próprio provider (
Storage:Inputs[N].Provider, que herdaStorage:Providerquando não diz nada). Copiar um arquivo de uma pasta para processamento é uma cópia, e uma cópia pode atravessar qualquer coisa — então uma pasta pode ser o container Blob de um cliente, enquanto outra é um bucket e uma terceira é um diretório local. - O compartilhamento de trabalho —
processing/,output/eerror/— é um único lugar, em um único provider (Storage:Provider). A promoção, a movimentação para a pasta de erro e a devolução.rejectmovem arquivos entre essas três pastas, e uma movimentação entre dois serviços e duas credenciais não tem uma condição que valha para os dois.output/em um container Blob comerror/em um bucket não é uma configuração que este produto aceite.
O Storage:Root fica neste host, quaisquer que sejam as respostas: logs/, keys/ e — com SQLite —
db/ ficam ali.
| Provider | Pasta de entrada | Compartilhamento de trabalho | Lease de um arquivo | Uma movimentação é | Com Cluster:Enabled | Uma pasta existe quando |
|---|---|---|---|---|---|---|
LocalFileSystem (padrão) | Observada pelo FileSystemWatcher; polling só se a pasta indicar um intervalo | Diretórios dentro de Storage:Root | Controle interno deste processo | Uma renomeação | Recusado | O produto a cria |
AzureFiles | Polling | Um compartilhamento, com prefixo Directory opcional | Mantido pelo serviço | Uma renomeação dentro do compartilhamento | Admitido | O produto a cria |
AzureBlob | Polling | Um container, com Prefix opcional | Mantido pelo serviço (um lease de blob) | Uma cópia no servidor seguida de uma exclusão — recuperável, não atômica | Admitido | O blob marcador dela existe — o de uma pasta de entrada nunca é criado pelo produto |
S3 | Polling | Um bucket, com Prefix opcional | Nenhum — toda exclusão é condicionada a um ETag, e uma cópia preparada é comparada com a impressão digital dela antes da assinatura | Uma cópia no servidor seguida de uma exclusão — recuperável, não atômica | Recusado | O objeto marcador <prefixo>/ dela existe — o de uma pasta de entrada nunca é criado pelo produto |
Os cinco exemplos abaixo são as topologias para as quais o produto foi feito. As seções por provider, mais adiante, trazem todas as chaves e todas as recusas; estes exemplos mostram como as peças se combinam.
Exemplo: tudo neste host
O padrão, por extenso. Nenhum Provider em lugar nenhum significa LocalFileSystem em todo lugar, e
uma atualização que não acrescenta nada não muda nada:
"Storage": {
"Root": "/var/lib/bulksigner",
"Inputs": [
{ "Name": "remessas", "Path": "/var/lib/bulksigner/input" },
{ "Name": "contratos", "Path": "/srv/contratos/entrada" }
]
}
processing/, output/ e error/ são diretórios dentro de /var/lib/bulksigner, ao lado de logs/,
db/ e keys/. As duas pastas são observadas pelo FileSystemWatcher; dê um PollIntervalSeconds a
uma delas só se for um compartilhamento de rede montado cujos eventos não sejam confiáveis.
Exemplo: tudo em um único container do Azure Blob
Um host praticamente sem disco durável, cujos arquivos ficam todos em um container — o compartilhamento
de trabalho sob um prefixo prod, e a pasta de entrada ao lado dele, no mesmo container, herdando o
bloco:
"Storage": {
"Root": "/var/lib/bulksigner", // continua local: logs/, db/ e keys/
"Provider": "AzureBlob",
"AzureBlob": {
"ContainerUrl": "https://contosoblobs.blob.core.windows.net/bulksigner",
"Prefix": "prod", // só do compartilhamento de trabalho; uma pasta nunca o herda
"Credential": "ManagedIdentity"
},
"Inputs": [
{
"Name": "remessas",
"Path": "entrada/remessas", // um prefixo no mesmo container, fora de prod/
"PollIntervalSeconds": 30 // na prática obrigatório: um container é lido por polling
}
]
}
A pasta herda AzureBlob, o container e a identidade, e o lugar dela é o Path. Crie o marcador dela
uma vez, antes do primeiro boot — os três marcadores do compartilhamento de trabalho o próprio produto
cria:
az storage blob upload --account-name contosoblobs --container-name bulksigner \
--name entrada/remessas --data "" --metadata hdi_isfolder=true --auth-mode login
A identidade precisa de Storage Blob Data Contributor no container. Com Cluster:Enabled — e o banco
SQL Server que esse modo exige —, esta topologia é admitida como está. Em um host cujo disco não dura
mais que o processo, defina também
Auth:SessionKeyRing:Placement = OperationalStore
(somente com SQL Server), ou cada deploy desconecta todo mundo.
Exemplo: tudo em um único bucket S3
O mesmo formato na AWS, com o role da própria instância:
"Storage": {
"Root": "/var/lib/bulksigner",
"Provider": "S3",
"S3": {
"BucketName": "bulksigner",
"Prefix": "prod",
"Region": "sa-east-1", // obrigatório, a menos que ServiceUrl indique um serviço compatível
"Credential": "InstanceRole"
},
"Inputs": [
{ "Name": "remessas", "Path": "entrada/remessas", "PollIntervalSeconds": 30 }
]
}
aws s3api put-object --bucket bulksigner --key entrada/remessas/
Duas coisas diferem do container, e as duas são declaradas, não escondidas. O S3 não tem lease, então
todo job é comparado com a impressão digital registrada ao preparar a cópia imediatamente antes de ser
assinado, e falha com job.staged-copy-changed se ela não bater; e o marcador de instância é gravado, não
mantido sob lease, então um segundo host neste bucket não pode ser detectado — rode exatamente um. Esta
topologia é recusada com Cluster:Enabled.
Exemplo: pastas de entrada neste host, o compartilhamento de trabalho em um container Blob
Os arquivos chegam ao host — um ERP os coloca em um diretório local — e a saída assinada é coletada de
um container. A pasta declara LocalFileSystem explicitamente, porque uma pasta que não indica provider
herdaria AzureBlob:
"Storage": {
"Root": "/var/lib/bulksigner",
"Provider": "AzureBlob",
"AzureBlob": {
"ContainerUrl": "https://contosoblobs.blob.core.windows.net/bulksigner",
"Credential": "ServicePrincipal",
"TenantId": "8f2c1a5e-0d4b-4c9a-9b7e-3f6a2d1c0b9e",
"AppId": "5a7d9c3e-1b2f-4e6a-8c0d-9e1f2a3b4c5d"
// O AppSecret NÃO fica aqui — ele está no ambiente, abaixo.
},
"Inputs": [
{ "Name": "remessas", "Provider": "LocalFileSystem", "Path": "/var/lib/bulksigner/input" }
]
}
Storage__AzureBlob__AppSecret=<client secret> # variável de ambiente recomendada; nunca versione
A pasta local mantém o FileSystemWatcher, e nada precisa ser criado no container: sem Prefix,
processing/, output/, error/ e bulksigner-instance.json ficam na raiz dele e são criados no boot.
Um arquivo é preparado copiando-o do host para processing/ — a única operação que atravessa providers
—, e a entrada só é apagada depois que o artefato assinado é verificado em output/.
Exemplo: uma pasta de entrada no Blob e uma no S3, o compartilhamento de trabalho no Azure Files
Dois parceiros entregam arquivos de remessa, um no próprio container Blob e outro em um armazenamento
on-premises compatível com S3 (MinIO, Ceph e similares, indicado por ServiceUrl), e o compartilhamento
de trabalho é um compartilhamento do Azure Files. Cada pasta tem o próprio bloco e a própria credencial,
sem herdar nada:
"Storage": {
"Root": "/var/lib/bulksigner",
"Provider": "AzureFiles",
"AzureFiles": {
"AccountName": "contosofiles",
"ShareName": "bulksigner",
"Credential": "ManagedIdentity"
},
"Inputs": [
{
"Name": "parceiro-azure",
"Provider": "AzureBlob",
"Path": "erp/outbox",
"PollIntervalSeconds": 30,
"AzureBlob": {
"ContainerUrl": "https://parceiroblobs.blob.core.windows.net/remessas",
"Credential": "AccountKey" // a conta do parceiro: nenhuma identidade nossa lá
}
},
{
"Name": "parceiro-onprem",
"Provider": "S3",
"Path": "erp/outbox",
"PollIntervalSeconds": 60,
"S3": {
"BucketName": "remessas",
"ServiceUrl": "https://minio.parceiro.internal:9000",
"ForcePathStyle": true,
"Credential": "AccessKey",
"AccessKeyId": "bulksigner"
}
}
]
}
Storage__Inputs__0__AzureBlob__AccountKey=<chave da conta de armazenamento do parceiro> # nunca versione
Storage__Inputs__1__S3__SecretAccessKey=<secret access key> # nunca versione
O compartilhamento do Azure Files é testado como compartilhamento; o container e o bucket são testados
uma vez cada pela verificação de inicialização do armazenamento de objetos, que recusa o boot somente se
o serviço ignorar uma das requisições condicionais de que o produto depende (ou, no Blob, se uma
política de imutabilidade recusar as exclusões dele) — um serviço inacessível vira uma linha vermelha
storage-container: ou storage-bucket: no /api/ready, e o host inicia. Uma chave de conta do Blob dá
acesso total ao plano de dados da conta inteira do parceiro e nunca expira, então prefira uma identidade
que o parceiro possa restringir ao container, quando ele aceitar conceder uma. Os dois marcadores de
entrada precisam existir antes (az storage blob upload … --metadata hdi_isfolder=true,
aws s3api put-object --key erp/outbox/ --endpoint-url https://minio.parceiro.internal:9000). E esta
topologia é recusada com Cluster:Enabled, só por causa da pasta S3: o S3 não tem lease, então uma
pasta nele dá a várias instâncias apenas a proteção que uma pasta local daria.
Storage:Inputs[] — regras de validação (impostas na inicialização)
Nameprecisa corresponder a^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$e ser único na lista — somente letras minúsculas, dígitos e hifens internos, de 1 a 40 caracteres, começando e terminando com caractere alfanumérico. Os nomes aparecem em query strings de URL, labels de métricas e na interface do dashboard.Pathnão pode ser vazio e precisa resolver para um diretório que não seja igual ao caminho de nenhuma outra entrada, nem subdiretório ou diretório pai dele. Pastas sobrepostas causariam enfileiramento duplicado e atribuição ambígua.- Limite recomendado: 16 entradas. Mais do que isso infla a cardinalidade das métricas e deixa a página Pastas de entrada densa demais para ser útil.
- Quando
Storage:Inputsé omitido por completo, o serviço cria uma pasta chamadadefaultem{Storage:Root}/input.
Storage:Provider / Storage:AzureFiles — o compartilhamento de trabalho
Opcional, e ausente de toda implantação que mantém o armazenamento local. Definir
Storage:Provider = AzureFiles move o compartilhamento de trabalho — processing/, output/ e
error/ — para um compartilhamento do Azure Files acessado pelo SDK do próprio serviço, sem montagem
SMB e sem dependência no nível do host. O Storage:Root continua local e segue guardando logs/ e
db/.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Storage:AzureFiles:AccountName | string | n/a | Storage__AzureFiles__AccountName | REQUIRED quando um provider resolve para AzureFiles. Nome da conta sem sufixo; o endpoint é https://<AccountName>.file.core.windows.net. |
Storage:AzureFiles:ShareName | string | n/a | Storage__AzureFiles__ShareName | REQUIRED. O compartilhamento de trabalho. Somente protocolo SMB — um compartilhamento NFS é recusado no boot, com a indicação do nome. |
Storage:AzureFiles:Directory | string? | null (raiz do compartilhamento) | Storage__AzureFiles__Directory | Prefixo opcional dentro do compartilhamento, no qual processing/, output/ e error/ são criados. Permite que várias implantações usem o mesmo compartilhamento. Somente para o compartilhamento de trabalho — defini-lo em uma entrada de Storage:Inputs[] é recusado no boot, porque o diretório de uma pasta é o Path dela. |
Storage:AzureFiles:Credential | enum | n/a | Storage__AzureFiles__Credential | REQUIRED e sem valor padrão. ManagedIdentity, ServicePrincipal ou AccountKey. Um bloco parcial para o modo escolhido impede o boot do host, indicando a chave ausente. |
Storage:AzureFiles:TenantId / :AppId | string | n/a | Storage__AzureFiles__TenantId, …__AppId | Somente no modo ServicePrincipal. |
Storage:AzureFiles:AppSecret | string | n/a | Storage__AzureFiles__AppSecret | SECRET. Somente no modo ServicePrincipal. Permitido na configuração; override por ambiente recomendado. |
Storage:AzureFiles:AccountKey | string | n/a | Storage__AzureFiles__AccountKey | SECRET. Somente no modo AccountKey. Gera aviso na inicialização: uma chave compartilhada dá acesso total ao plano de dados da conta inteira, não pode ser restrita a um compartilhamento e nunca expira. |
O ManagedIdentity aceita somente a identidade atribuída pelo sistema — uma identidade atribuída pelo
usuário não é lida, e indicar uma causa uma falha de autenticação na primeira chamada, e não um erro de
configuração. Deliberadamente, a credencial não é DefaultAzureCredential, então nunca recorre à
identidade do az login de um desenvolvedor.
Os dois modos com token precisam de uma das roles de dados de arquivo privilegiados: conceda
Storage File Data Privileged Contributor, com escopo no compartilhamento. Uma role somente leitura
não basta nem para uma pasta de entrada, já que o pipeline obtém um lease do arquivo de entrada
enquanto o copia para processamento e o apaga após a verificação. Veja
Segurança.
Um compartilhamento de trabalho, não vários. processing/, output/ e error/ precisam ficar
juntos, porque promover um artefato verificado e mover a cópia preparada de um job que falhou são
operações de renomeação, e a renomeação do Azure não funciona entre compartilhamentos nem entre
contas de armazenamento. As pastas de entrada continuam sendo várias e independentes — cada uma pode
indicar a própria conta e o próprio compartilhamento —, porque copiar para processamento a partir de uma delas é
uma cópia.
O que a inicialização recusa e o que ela apenas informa. Um provider ou modo de credencial não
reconhecido, um bloco de credencial parcial, um compartilhamento NFS, um caminho remoto
(azurefiles://, azureblob:// ou s3://) em Storage:Root, em Logging:File:Path ou — enquanto
Database:Provider for Sqlite — em
ConnectionStrings:Default, e uma pasta de entrada que colida com uma das raízes de trabalho no
compartilhamento de trabalho: tudo isso impede o host de iniciar. Um compartilhamento
inacessível, não: ele é informado no console de operação, na página Sistema e pelo
/api/ready, e o host sobe — um compartilhamento fora do ar às 3h não pode transformar uma
reinicialização em um serviço que não inicia.
O bulksigner-instance.json fica ao lado de processing/, output/ e error/. Ele registra o nome
do host e o ID do processo da instância que assumiu o compartilhamento, e é mantido sob um lease sem
expiração durante toda a vida dessa instância. Não mexa nele: é ele que avisa, no próximo boot, que uma
segunda instância está assinando a partir deste compartilhamento. Veja
Operação.
Os downloads sempre passam em streaming pela aplicação — nenhuma URL de shared access signature é
criada para um artefato assinado, então o GET /api/jobs/{id}/output se comporta da mesma forma,
qualquer que seja o provider que guarda output/. Deliberadamente, não há ajuste de repetição: a
política do SDK está definida no código (três tentativas, backoff exponencial de 500 ms a 5 s, timeout
de rede de 30 segundos), e este produto já faz novas tentativas acima dela.
Overrides por pasta
Cada pasta de entrada monitorada escolhe o próprio provider e herda o resto, de modo que ler o compartilhamento de um cliente na conta dele, enquanto o compartilhamento de trabalho fica na sua, é um override por pasta, e não uma segunda implantação.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Storage:Inputs[N].Provider | enum? | herda Storage:Provider | Storage__Inputs__N__Provider | LocalFileSystem, AzureFiles, AzureBlob ou S3 (os dois últimos novos na 2.20.0 — veja as pastas de entrada no Blob e no S3), por pasta. Uma pasta pode ler um compartilhamento enquanto outra continua em disco local durante uma migração. |
Storage:Inputs[N].Path | string | n/a | Storage__Inputs__N__Path | REQUIRED. Um caminho de sistema de arquivos em uma pasta local; em uma pasta AzureFiles, o diretório dentro do compartilhamento, separado por /. Uma barra invertida é recusada no boot — é um separador local sem significado em um compartilhamento. |
Storage:Inputs[N].AzureFiles:* | objeto | herda Storage:AzureFiles campo a campo | Storage__Inputs__N__AzureFiles__AccountName, … | Os mesmos membros do bloco acima, exceto Directory. A herança verifica nulo, não vazio: uma chave omitida é herdada, e uma string vazia significa que esta pasta não tem esse valor. O Directory é recusado aqui. |
Storage:Inputs[N].PollIntervalSeconds | int? | herda WatchedFolder:PollIntervalSeconds | Storage__Inputs__N__PollIntervalSeconds | Na prática, REQUIRED em uma pasta AzureFiles — uma pasta que não resolva para nenhum intervalo é recusada no boot. Em uma pasta local, a presença da chave é a ativação: definir um intervalo acrescenta enumeração periódica ao comportamento do observador dessa pasta, que é a solução para uma pasta montada a partir de um compartilhamento de rede. Faixa válida: 5–3600. |
É a presença de Storage:Inputs[N].PollIntervalSeconds que ativa o polling em uma pasta local; o
WatchedFolder:PollIntervalSeconds global (padrão 30) é consultado apenas para a frequência.
Uma implantação local existente que não define intervalo por pasta mantém inalterado o comportamento
orientado a eventos.
Exemplo: o compartilhamento de trabalho no Azure Files, entradas ainda locais
"Storage": {
"Root": "/var/lib/bulksigner",
"Provider": "AzureFiles",
"AzureFiles": {
"AccountName": "contosofiles",
"ShareName": "bulksigner",
"Directory": "prod",
"Credential": "ManagedIdentity"
},
"Inputs": [
{ "Name": "default", "Path": "/var/lib/bulksigner/input", "Provider": "LocalFileSystem" }
]
}
Os artefatos assinados vão para bulksigner/prod/output na conta contosofiles; logs/ e db/
ficam em /var/lib/bulksigner. Se você remover a linha Provider da pasta de entrada, ela herda
AzureFiles; nesse caso, o Path dela passa a ser um diretório dentro do compartilhamento, e ela é
enumerada a cada WatchedFolder:PollIntervalSeconds — o Azure Files não publica notificações de
mudança, então uma pasta que não resolva para nenhum intervalo é recusada no boot.
Exemplo: pastas de entrada também em um compartilhamento, autenticando com chave de conta
Para um host que não tem nenhum acesso ao tenant — um servidor on-premises sem identidade gerenciada
e sem registro de aplicativo —, o AccountKey é o modo que resta:
"Storage": {
"Root": "/var/lib/bulksigner",
"Provider": "AzureFiles",
"AzureFiles": {
"AccountName": "contosofiles",
"ShareName": "bulksigner",
"Credential": "AccountKey"
},
"Inputs": [
{ "Name": "remessas", "Provider": "AzureFiles", "Path": "entrada/remessas", "PollIntervalSeconds": 30 },
{ "Name": "contabil", "Path": "entrada/contabil", "AzureFiles": { "ShareName": "financeiro" }, "PollIntervalSeconds": 300 },
{ "Name": "legacy", "Provider": "LocalFileSystem", "Path": "/mnt/legacy/incoming" }
]
}
Storage__AzureFiles__AccountKey=<chave da conta de armazenamento> # variável de ambiente recomendada; nunca versione
Cinco pontos que este exemplo mostra:
- A chave é um segredo para todos os compartilhamentos que ela abre.
contabilsobrescreveShareNamee nada mais, entãoAccountName,Credentiale a chave são herdados campo a campo. Escrever"AccountKey": ""em uma pasta significa que essa pasta não tem chave, e não um pedido para herdar uma. - O
AccountKeygera um aviso a cada boot, no console de operação e no log durável, indicando cada compartilhamento que ele abre. - Sem prefixo
Directory, as raízes de trabalho ficam na raiz do compartilhamento. As pastas de entrada podem ficar no compartilhamento de trabalho, mas uma entrada cujoPathseja uma das três raízes, fique dentro dela ou a contenha é recusada no boot — caso contrário, essa colisão apagaria um artefato assinado a cada iteração, enquanto informaria todos os jobs comoCompleted. - As duas pastas remotas definem o próprio intervalo, e a local deliberadamente não define nenhum.
É por não definir intervalo que o
legacycontinua orientado a eventos. - O banner confirma, exibindo
azure credential = AccountKey,work share = contosofiles/bulksigner, o provider e o local de cada pasta (input providers) eazure shares = 2 reachable— os compartilhamentos são testados separadamente, então uma chave que abre um e não o outro fica visível na inicialização.
Pastas de entrada no Azure Blob Storage — Storage:Inputs[].AzureBlob
Novo na 2.20.0. Uma pasta de entrada monitorada pode ser um prefixo em um container do Azure Blob
Storage. Defina Provider = AzureBlob na pasta; o bloco usa os nomes de chave do
destino de backup, e o que a pasta omitir é herdado campo a campo de um bloco
Storage:AzureBlob de nível superior (nulo, não vazio, como no Azure Files) — exceto Prefix, que é
exclusivo do compartilhamento de trabalho. O compartilhamento de trabalho é independente da pasta: ele
pode continuar local, ficar no Azure Files ou ser
um container Blob próprio.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Storage:Inputs[N].Path | string | n/a | Storage__Inputs__N__Path | REQUIRED. O prefixo da pasta dentro do container, separado por / — erp/outbox. A raiz do container, uma barra invertida e um segmento vazio são recusados no boot. |
Storage:Inputs[N].AzureBlob:ContainerUrl | string | herda Storage:AzureBlob:ContainerUrl | Storage__Inputs__N__AzureBlob__ContainerUrl | REQUIRED. https://<conta>.blob.core.windows.net/<container>. http://, uma URL que indica um blob e qualquer query string (uma shared access signature) são recusados no boot; uma connection string também — nesta chave ou em uma chave ConnectionString —, sem que a recusa a repita. |
Storage:Inputs[N].AzureBlob:Credential | enum | herda | Storage__Inputs__N__AzureBlob__Credential | REQUIRED e sem valor padrão. ManagedIdentity, ServicePrincipal ou AccountKey. A credencial do próprio armazenamento — nunca emprestada de Backup:AzureBlob nem de um blob de material de assinatura. |
Storage:Inputs[N].AzureBlob:TenantId / :AppId | string | herda | …__AzureBlob__TenantId, …__AppId | Somente no modo ServicePrincipal. |
Storage:Inputs[N].AzureBlob:AppSecret | string | herda | Storage__Inputs__N__AzureBlob__AppSecret | SECRET. Somente no modo ServicePrincipal; override por ambiente recomendado. Mascarado em todos os logs. |
Storage:Inputs[N].AzureBlob:AccountKey | string | herda | Storage__Inputs__N__AzureBlob__AccountKey | SECRET. Somente no modo AccountKey: acesso total ao plano de dados da conta inteira. Mascarada em todos os logs. |
Storage:Inputs[N].AzureBlob:Prefix | — | — | — | Recusado em uma pasta: o lugar de uma pasta no container é o Path dela. Storage:AzureBlob:Prefix é reservado ao compartilhamento de trabalho no Blob. |
Storage:Inputs[N].PollIntervalSeconds | int? | herda WatchedFolder:PollIntervalSeconds | Storage__Inputs__N__PollIntervalSeconds | Na prática, REQUIRED, como no Azure Files: um container só é lido por enumeração periódica, e uma pasta que não resolva para nenhum intervalo é recusada no boot. |
A pasta precisa existir antes de o host iniciar, e o produto nunca a cria. Uma pasta em um
container é um blob marcador de pasta de tamanho zero, com o nome do caminho da pasta e o metadado
hdi_isfolder=true — a convenção do próprio Azure, a que as ferramentas do Data Lake e o blobfuse
gravam. Assim, uma pasta apagada ou digitada errado aparece como ausente, e não como silenciosa;
recriá-la por você transformaria uma pasta apagada enquanto o host estava parado em uma pasta
silenciosa. Crie-a uma vez, com uma identidade que tenha acesso de escrita:
az storage blob upload --account-name contosoblobs --container-name remessas \
--name erp/outbox --data "" --metadata hdi_isfolder=true --auth-mode login
O New Folder do Storage Explorer, em uma conta sem namespace hierárquico, não cria nenhum blob, então não gera um marcador. Um arquivo é capturado no próximo polling depois que o blob dele é confirmado (committed), copiado para processamento sob um lease de blob, assinado no compartilhamento de trabalho e apagado do container sob um segundo lease somente se ainda for, byte a byte, o que foi copiado — a mesma regra de todas as outras pastas.
Quando a pasta não consegue funcionar, ela informa isso em todo lugar, e o host continua rodando:
- O probe de inicialização. No boot, cada container em que uma pasta fica é testado sob o prefixo
dessa pasta, em um caminho aninhado
.bulksigner-probe/que nenhuma listagem oferece: dois blobs pequenos são gravados, cada condição que o produto envia é comprovadamente recusada — uma gravação e uma cópia comIf-None-Match: *sobre um blob existente, uma gravação e uma exclusão comIf-Matchem um ETag desatualizado —, e os dois blobs são apagados. Um container que executa uma condição em vez de recusá-la, ou cujas exclusões são recusadas por uma política de imutabilidade ou por uma retenção legal (legal hold), causa uma recusa de boot que indica a condição eStorage:Inputs[N].AzureBlob:ContainerUrl: o pipeline apaga toda entrada que assina, então não consegue rodar ali. Um container que não responde — apagado, com credencial recusada, com a conta fora do ar — não causa recusa: o host inicia, o banner diz por quê, e o/api/readymostra uma linha vermelhastorage-container:<conta>/<container>(um retrato do boot) ao lado da linha ao vivo da própria pasta. O probe precisa de escrita e exclusão no container —Storage Blob Data Contributor, de que as exclusões do pipeline já precisam. - Um marcador de pasta ausente aparece como ausente, nunca como uma pasta vazia, no status da
pasta, na linha dela no
/api/readye na página Sistema, cada um indicando o comandoaz storage blob upload … --metadata hdi_isfolder=trueacima para aquele container e caminho. Nada o recria — nem uma reinicialização, nem o observador reabrindo a pasta. - As falhas são classificadas pelo código de erro do próprio serviço. Uma credencial recusada
(
AuthorizationFailure,AuthorizationPermissionMismatch,AuthenticationFailed, …) para a pasta na primeira ocorrência; throttling e falhas do serviço (ServerBusy,InternalError,OperationTimedOut, um 5xx ou a falta de resposta) são registrados e superados; um blob que sumiu entre a listagem e a leitura não é falha de ninguém; um container ausente ou desabilitado, e qualquer coisa não reconhecida, conta para parar a pasta. A política de repetição do próprio cliente é curta (três novas tentativas exponenciais, no máximo cinco segundos de intervalo, timeout de rede de 30 segundos), porque o observador já faz novas tentativas acima dela. Toda falha de armazenamento é registrada no log com o provider, o nome da pasta, o local, a operação do Blob e a mensagem do próprio serviço. - Um append blob ou um page blob nunca é um arquivo. Um append blob fica visível enquanto o produtor
ainda está acrescentando dados a ele, então é recusado na leitura — pelo observador, por uma nova
varredura e por uma nova tentativa — e o motivo é registrado na pasta (a página Pastas de entrada o
mostra), enquanto os block blobs ao lado continuam sendo processados. Faça upload dos arquivos de
remessa como block blobs, que é o padrão de todo SDK e do
az storage blob upload. - O modo cluster admite pastas de entrada no Blob: um lease de blob é mantido pelo serviço, como um lease do Azure Files, então a retenção da entrada que impede duas instâncias de copiar o mesmo arquivo para processamento continua valendo entre instâncias.
Veja o Exemplo: uma pasta de entrada no Blob e uma no S3, o compartilhamento de trabalho no Azure Files.
O compartilhamento de trabalho em um container Blob — Storage:Provider = AzureBlob
Novo na 2.20.0. Storage:Provider = AzureBlob coloca o compartilhamento de trabalho —
processing/, output/, error/ e o marcador de instância bulksigner-instance.json — em um único
container do Azure Blob Storage, sob o Storage:AzureBlob:Prefix opcional. O bloco é o mesmo que uma
pasta de entrada herda, lido por inteiro; o Storage:Root continua local para logs/, db/ e keys/.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Storage:AzureBlob:ContainerUrl | string | n/a | Storage__AzureBlob__ContainerUrl | REQUIRED. https://<conta>.blob.core.windows.net/<container>, com as mesmas recusas da URL de uma pasta (sem http://, sem URL de blob, sem shared access signature, sem connection string). |
Storage:AzureBlob:Prefix | string? | null (raiz do container) | Storage__AzureBlob__Prefix | Opcional, separado por /. Permite que várias implantações usem o mesmo container. Uma barra invertida ou um segmento vazio são recusados no boot. Somente para o compartilhamento de trabalho — recusado em uma pasta. |
Storage:AzureBlob:Credential | enum | n/a | Storage__AzureBlob__Credential | REQUIRED e sem valor padrão. ManagedIdentity, ServicePrincipal (+ TenantId, AppId, AppSecret) ou AccountKey (+ AccountKey). Os dois segredos são mascarados em todos os logs. |
O que muda neste compartilhamento de trabalho, e o que não muda:
- Os marcadores das três raízes são criados no boot, como os diretórios de uma árvore de trabalho
local — ao contrário do marcador de uma pasta de entrada, que é seu e nunca é criado. Uma pasta de
entrada no Blob, no mesmo container, precisa ficar longe dos prefixos do compartilhamento de trabalho;
uma que seja
processing/,output/ouerror/, que contenha uma delas ou que fique dentro de uma delas é recusada no boot. - A cópia preparada é mantida sob um lease de blob infinito durante toda a vida do job, quebrado e assumido quando um job retido é retomado depois de uma reinicialização — a mesma retenção que um compartilhamento oferece.
- Não há renomeação, então uma movimentação é uma cópia no servidor seguida de uma exclusão. A
promoção, a devolução
.rejecte a movimentação para a pasta de erro copiam com o Copy Blob From URL síncrono eIf-None-Match: *— de modo que um destino aparece inteiro ou não aparece, e nunca é substituído — e só então apagam a origem, condicionando a exclusão à versão que foi copiada. Um blob maior que o limite de 256 MiB dessa cópia passa em streaming pelo host, com a mesma condição. ComManagedIdentityouServicePrincipal, a cópia autoriza a origem com um token de portador da mesma identidade; nenhuma shared access signature é criada. - Uma movimentação interrompida é concluída no início seguinte. Uma falha entre a cópia e a exclusão
deixa uma duplicata em
processing/— nunca um arquivo perdido, nunca um arquivo parcial emoutput/. A recuperação na inicialização conclui um job cujo artefato assinado já está emoutput/, e completa em uma única pasta oerror/<id>/copiado pela metade de um job que falhou, em vez de dividi-lo. Só a devolução.rejectnão é reconciliada (ela acontece depois que o job terminou); a cópia que sobrar dela emprocessing/é removida por Limpar Jobs ou pela exclusão do job. - O marcador de instância é mantido sob um lease de blob: um segundo host apontado para o mesmo
container é informado pelo nome, e o lease é quebrado e assumido, exatamente como em um
compartilhamento. Com
Cluster:Enabled, que admite um compartilhamento de trabalho no Blob, o vínculo do marcador com o banco continua recusando um segundo cluster. - O probe de inicialização também roda no container do compartilhamento de trabalho, sob o prefixo dele, e recusa o boot por uma condição ignorada ou por uma política de imutabilidade ou retenção legal, como para uma pasta.
Uma pasta que não indica Provider herda AzureBlob de Storage:Provider e, assim, passa a ser uma
pasta Blob no container do compartilhamento de trabalho, com o próprio Path como prefixo — escreva
Provider = LocalFileSystem em uma pasta que deve ficar no host. Veja o
Exemplo: tudo em um único container do Azure Blob e
o Exemplo: pastas de entrada neste host, o compartilhamento de trabalho em um container Blob.
Pastas de entrada no S3 — Storage:Inputs[].S3
Novo na 2.20.0. Uma pasta de entrada monitorada pode ser um prefixo em um bucket S3 — na AWS ou em um
serviço compatível com S3, como MinIO ou Ceph, indicado pelo endpoint. Defina Provider = S3 na pasta;
o bloco usa os nomes de chave do destino de backup, e o que a pasta omitir é herdado campo a
campo de um bloco Storage:S3 de nível superior (nulo, não vazio). O compartilhamento de trabalho fica
onde está, a menos que Storage:Provider = S3 também o coloque em um bucket —
abaixo.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Storage:Inputs[N].Path | string | n/a | Storage__Inputs__N__Path | REQUIRED. O prefixo da pasta dentro do bucket, separado por / — erp/outbox. A raiz do bucket, uma barra invertida e um segmento vazio são recusados no boot. |
Storage:Inputs[N].S3:BucketName | string | herda Storage:S3:BucketName | Storage__Inputs__N__S3__BucketName | REQUIRED. Um único nome de bucket, sem /. |
Storage:Inputs[N].S3:Region | string | herda | Storage__Inputs__N__S3__Region | REQUIRED a menos que ServiceUrl esteja definido, e sem valor padrão — caso contrário, o SDK usaria o que o ambiente do host disser. O nome de sistema, por exemplo sa-east-1. |
Storage:Inputs[N].S3:ServiceUrl | string | herda | Storage__Inputs__N__S3__ServiceUrl | Um endpoint compatível com S3: uma URL http:// ou https:// absoluta que indica o host e nada mais (sem caminho, sem query string). |
Storage:Inputs[N].S3:ForcePathStyle | bool? | herda; senão, false | Storage__Inputs__N__S3__ForcePathStyle | Endereçamento no estilo de caminho, de que a maioria dos endpoints compatíveis com S3 precisa. Só faz sentido ao lado de ServiceUrl. |
Storage:Inputs[N].S3:Credential | enum | herda | Storage__Inputs__N__S3__Credential | REQUIRED e sem valor padrão. InstanceRole (a cadeia do próprio SDK: instance profile, task role do ECS, IRSA) ou AccessKey. A credencial do próprio armazenamento — nunca emprestada de Backup:S3, de um objeto S3 de material de assinatura nem do bloco do AWS KMS. URLs pré-assinadas não são oferecidas. |
Storage:Inputs[N].S3:AccessKeyId | string | herda | Storage__Inputs__N__S3__AccessKeyId | Somente no modo AccessKey. Não é um segredo por si só. |
Storage:Inputs[N].S3:SecretAccessKey | string | herda | Storage__Inputs__N__S3__SecretAccessKey | SECRET. Somente no modo AccessKey; override por ambiente recomendado. Mascarada em todos os logs. |
Storage:Inputs[N].S3:Prefix | — | — | — | Recusado em uma pasta: o lugar de uma pasta no bucket é o Path dela. Storage:S3:Prefix é reservado ao compartilhamento de trabalho no S3. |
Storage:Inputs[N].PollIntervalSeconds | int? | herda WatchedFolder:PollIntervalSeconds | Storage__Inputs__N__PollIntervalSeconds | Na prática, REQUIRED: um bucket só é lido por enumeração periódica — os eventos do EventBridge e do SQS dele deliberadamente não são consumidos —, e uma pasta que não resolva para nenhum intervalo é recusada no boot. |
O endpoint faz parte do lugar em que um arquivo está. Um arquivo em uma pasta S3 é registrado como
s3://<host-do-endpoint>/<bucket>/<caminho> — s3.<região>.amazonaws.com na AWS, o host e a porta do
ServiceUrl em um serviço compatível —, então o mesmo bucket e a mesma chave em dois serviços são dois
arquivos.
A pasta precisa existir antes de o host iniciar, e o produto nunca a cria, pelo mesmo motivo que vale
para uma pasta no Blob. No S3, uma pasta é o objeto marcador <prefixo>/ de tamanho zero — o que o
Create folder do console do S3 grava. Crie-o uma vez:
aws s3api put-object --bucket remessas --key erp/outbox/
Para um serviço compatível com S3, acrescente --endpoint-url https://minio.internal:9000; o cliente
mc do próprio MinIO não cria objeto marcador, então use o comando acima.
O S3 não tem lease, então nada retém o objeto enquanto um job trabalha nele. Um arquivo é capturado
no próximo polling, copiado para processamento, e o ETag e a impressão digital dele (tamanho e SHA-256)
são registrados. Depois que a assinatura é verificada, ele é apagado por um DeleteObject condicionado
com If-Match — a comparação e a exclusão são uma única requisição. Quando o ETag do objeto ainda é o
registrado na cópia para processamento, essa é a condição. Quando não é — um produtor gravou o objeto de
novo —, o tamanho e o SHA-256 dele são comparados primeiro com a impressão digital: bytes idênticos (um
novo upload do mesmo arquivo) são apagados, com a exclusão condicionada ao novo ETag; bytes diferentes
ficam onde estão e são registrados como InputDiverged; e um objeto regravado de novo durante a
tentativa adia a exclusão e é comparado outra vez. A quarentena de arquivos já processados e a exclusão
de jobs apagam com a mesma condição. Limpar Jobs é a única exceção: ele remove a entrada de cada job
limpo com um DeleteObject simples, tenha ou não o produtor regravado o objeto desde então — limpe os
jobs só quando os produtores tiverem terminado de usar esses arquivos. Essas garantias valem tanto quanto
o tratamento que o serviço dá a If-Match e If-None-Match: em um serviço compatível com S3, use uma
versão que respeite requisições condicionais — o probe de inicialização abaixo recusa uma que não
respeite.
Quando a pasta não consegue funcionar, ela informa isso em todo lugar, e o host continua rodando:
- O probe de inicialização. No boot, cada bucket em que uma pasta fica é testado uma vez, sob o
prefixo da primeira dessas pastas, em um caminho aninhado
.bulksigner-probe/que nenhuma listagem oferece: dois objetos pequenos são gravados, e cada condição que o produto envia precisa voltar como 412 —PutObjecteCopyObjectcomIf-None-Match: *sobre um objeto existente,PutObjecteDeleteObjectcomIf-Matchem um ETag desatualizado. Depois, os dois objetos são apagados. Um serviço que executa qualquer uma das quatro em vez de recusá-la, ou que a rejeita como não suportada (501 Not Implemented, ou um400que recusa a requisição como inválida) em vez de avaliá-la, causa uma recusa de boot que indica essa condição eStorage:Inputs[N].S3:BucketName: no S3, as condições são tudo o que impede uma movimentação de sobrescrever um arquivo assinado e uma exclusão de entrada de remover bytes que um produtor regravou. Um bucket que não responde — ausente, com credencial recusada, com o endpoint fora do ar — não causa recusa: o host inicia, o banner diz por quê, e o/api/readymostra uma linha vermelhastorage-bucket:<endpoint>/<bucket>(um retrato do boot) ao lado da linha ao vivo da própria pasta. O probe precisa des3:PutObjectes3:DeleteObjectsob o prefixo da pasta, de que as exclusões do próprio pipeline já precisam. - Um bucket ausente, uma credencial recusada ou um marcador de pasta ausente aparecem como falha,
nunca como uma pasta silenciosa, no status da pasta, na linha dela no
/api/readye na página Sistema; um marcador ausente indica o comandoaws s3api put-objectacima para aquele bucket e prefixo, e nada o recria. - As falhas são classificadas pelo
Codede erro do próprio S3. Uma credencial recusada (AccessDenied,InvalidAccessKeyId,SignatureDoesNotMatch,ExpiredToken, qualquer outro 401 ou 403) para a pasta na primeira ocorrência; throttling e falhas do serviço (SlowDown,ServiceUnavailable,InternalError,RequestTimeout, um 5xx ou a falta de resposta) são registrados e superados; um objeto que sumiu entre a listagem e a leitura (NoSuchKey) não é falha de ninguém; um bucket ausente (NoSuchBucket),AllAccessDisabled, uma região errada e qualquer coisa não reconhecida contam para parar a pasta. O cliente faz uma nova tentativa, no modo padrão, com timeout de 30 segundos, antes que o observador veja qualquer coisa. Toda falha de armazenamento é registrada no log com o provider, o nome da pasta, o local, a operação do S3 e a mensagem do próprio serviço. - Uma pasta S3 é recusada com
Cluster:Enabled, com a indicação dessa chave e doStorage:Inputs[N].Providerda pasta: o S3 não tem lease nenhum, então uma pasta nele dá a várias instâncias apenas a proteção que uma pasta local daria. - Um serviço que mantém diretórios reais por trás da API S3 (SeaweedFS e, em geral, armazenamentos baseados em sistema de arquivos) pode informar a pasta como presente assim que existir qualquer objeto dentro dela, mesmo sem o marcador; o marcador continua sendo o que o produto pede, e, na AWS, é a única coisa que faz a pasta existir.
Veja o Exemplo: uma pasta de entrada no Blob e uma no S3, o compartilhamento de trabalho no Azure Files para uma pasta em um serviço compatível com S3.
O compartilhamento de trabalho em um bucket S3 — Storage:Provider = S3
Novo na 2.20.0. Storage:Provider = S3 coloca o compartilhamento de trabalho — processing/,
output/, error/ e o marcador de instância bulksigner-instance.json — em um único bucket S3, na
AWS ou em um serviço compatível com S3, sob o Storage:S3:Prefix opcional. O bloco é o mesmo que uma
pasta de entrada herda, lido por inteiro, com as chaves e as regras das
pastas de entrada; o Storage:Root continua local para
logs/, db/ e keys/.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Storage:S3:BucketName | string | n/a | Storage__S3__BucketName | REQUIRED. |
Storage:S3:Prefix | string? | null (raiz do bucket) | Storage__S3__Prefix | Opcional, separado por /. Uma barra invertida ou um segmento vazio são recusados no boot. Somente para o compartilhamento de trabalho — recusado em uma pasta. |
Storage:S3:Region / ServiceUrl / ForcePathStyle | Storage__S3__… | Como em uma pasta: Region é REQUIRED a menos que ServiceUrl esteja definido, sem valor padrão. | ||
Storage:S3:Credential (+ AccessKeyId, SecretAccessKey) | enum | n/a | Storage__S3__Credential, Storage__S3__SecretAccessKey | REQUIRED e sem valor padrão. InstanceRole ou AccessKey. O segredo é mascarado em todos os logs. |
A identidade precisa de s3:GetObject, s3:PutObject, s3:DeleteObject e s3:ListBucket no bucket,
além de s3:AbortMultipartUpload para a cópia de objetos grandes descrita abaixo; acrescente uma regra
de ciclo de vida que aborte uploads multipart incompletos depois de um dia, para que uma cópia cujo
cancelamento também tenha falhado não seja cobrada para sempre.
O que muda neste compartilhamento de trabalho, e o que não muda:
- Os marcadores das três raízes são criados no boot, como os diretórios de uma árvore de trabalho
local. Uma pasta de entrada S3 no mesmo bucket e no mesmo endpoint precisa ficar longe dos prefixos do
compartilhamento de trabalho; uma que seja
processing/,output/ouerror/, que contenha uma delas ou que fique dentro de uma delas é recusada no boot. - Não há renomeação, então uma movimentação é uma cópia no servidor seguida de uma exclusão, como no
Blob: a promoção, a devolução
.rejecte a movimentação para a pasta de erro fazem umCopyObjectcomIf-None-Match: *(e o ETag da origem) e depois apagam a origem, condicionando a exclusão ao ETag copiado. Um objeto acima do limite de 5 GiB doCopyObjecté copiado em partes e concluído com o mesmoIf-None-Match: *. Uma movimentação interrompida é concluída no início seguinte, exatamente como no Blob. - Não há lease, então uma cópia preparada não fica protegida contra escrita entre a preparação e a
assinatura. Qualquer pessoa com acesso de escrita ao bucket poderia regravá-la. Por isso, neste
compartilhamento de trabalho, todo job — não só um job CNAB240, cujo hash de conteúdo é verificado
em qualquer provider — é comparado, imediatamente antes de a assinatura existir, com o tamanho e o
SHA-256 registrados quando a cópia foi preparada. Uma divergência faz o job falhar com
job.staged-copy-changed, move a pasta dele paraerror/e não assina nada; a entrada fica na pasta dela. Isto é detecção com falha definitiva, onde um compartilhamento ou um container oferecem prevenção. Um job CNAB240 cujos bytes mudaram — inclusive duranteAwaitingApproval— continua falhando comapproval.content-changed. - O envelope assinado de um aprovador (
<arquivo>.approvals.p7m) é substituído por umPutObjectcondicionado comIf-Matchao ETag com que foi lido — e criado comIf-None-Match: *—, então o envelope de um colega que tenha chegado nesse meio-tempo é recusado e nada é registrado; o aprovador assina de novo. - O marcador de instância é gravado, não mantido sob lease. Sem um lease, nada distingue uma segunda
instância ativa de uma que travou, então um segundo host no mesmo bucket e prefixo não pode ser
detectado: a linha
work-share-ownerdo/api/readycontinua verde e diz isso, assim como a página Sistema e o banner de inicialização. Rode exatamente uma instância por compartilhamento de trabalho no S3. - Um compartilhamento de trabalho no S3 é recusado com
Cluster:Enabled, com a indicação dessa chave e deStorage:Provider: o S3 não tem lease nenhum, então um compartilhamento de trabalho nele dá a várias instâncias apenas a proteção que uma árvore de trabalho local daria. - O download, o pacote de saídas assinadas, a exclusão de jobs e Limpar Jobs passam em streaming pelo produto, como em qualquer outro provider; nenhuma URL pré-assinada é criada.
- O probe de inicialização também roda no bucket do compartilhamento de trabalho, sob o prefixo
dele, com a chave
Storage:S3:BucketName, e recusa o boot por uma condição ignorada, como para uma pasta. Ele não testa a condição da conclusão multipart, que só um objeto acima de 5 GiB atinge.
Uma pasta que não indica Provider herda S3 de Storage:Provider e, assim, passa a ser uma pasta S3
no bucket do compartilhamento de trabalho, com o próprio Path como prefixo e um PollIntervalSeconds
que ela precisa ter — escreva Provider = LocalFileSystem em uma pasta que deve ficar no host. Veja o
Exemplo: tudo em um único bucket S3.
Storage:Inputs[].IgnoredExtensions / Storage:Inputs[].IgnoredPrefixes (por pasta)
Arrays opcionais. A lista efetiva de itens ignorados é a união da lista base global
WatchedFolder:IgnoredExtensions / WatchedFolder:IgnoredPrefixes com os acréscimos por pasta. As
listas por pasta se somam à lista base; elas não conseguem desfazer o filtro de algo que a lista
global já filtra. Exemplo: com a lista base padrão (.tmp, .part, .crdownload, .swp), uma pasta
que declara IgnoredExtensions: [".bak"] filtra .bak e .tmp etc.
Exemplo: duas pastas, uma com uma regra extra de ignorados
"Storage": {
"Root": "/var/lib/bulksigner",
"Inputs": [
{ "Name": "default", "Path": "/var/lib/bulksigner/input" },
{
"Name": "legal",
"Path": "/mnt/legal/incoming",
"IgnoredExtensions": [".bak"]
}
]
}
Pipeline
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Pipeline:PollIntervalSeconds | int | 2 | Pipeline__PollIntervalSeconds | Com que frequência o worker consulta a fila quando está ocioso. Menor = captura mais rápida, mais leituras no SQLite. Faixa válida: 1–3600. |
Pipeline:MaxConcurrency | int | 1 | Pipeline__MaxConcurrency | Limite máximo de jobs simultâneos em andamento. O padrão 1 é sequencial. Aumente para ganhar vazão em implantações com PFX. Faixa válida: 1–32. PKCS#11 / WindowsStore: mantenha em 1, a menos que o token / CSP permita sessões simultâneas — veja Certificados. |
Pipeline:RejectAlreadyProcessedFileNames | bool | true | Pipeline__RejectAlreadyProcessedFileNames | Novo na 2.13.0, ligado por padrão. Recusa um arquivo cujo nome já pertence a um job Completed ou ainda ativo — a comparação vale para o host inteiro (todas as pastas monitoradas, todos os perfis, todos os uploads) e não diferencia maiúsculas de minúsculas, porque o output/ é uma única pasta. Vindo de uma pasta monitorada ou de uma nova varredura, o arquivo vira um job que já nasce Failed com file.already-processed e é movido, sem assinatura, para a pasta error/ desse job; um upload é recusado com 409 e o mesmo código. Jobs Failed e Canceled não reservam nome, e uma nova tentativa não está sujeita à regra. Apagar o job que detém o nome (em /jobs) faz com que ele volte a ser aceito. Desligue para um produtor que reutiliza o mesmo nome de arquivo fixo todos os dias. |
WatchedFolder
O detector de estabilidade impede a captura de um arquivo que ainda está sendo escrito — o observador
espera até que o tamanho e a data da última escrita permaneçam idênticos ao longo de
StabilityRequiredSamples verificações consecutivas.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
WatchedFolder:StabilityPollIntervalMs | int | 500 | WatchedFolder__StabilityPollIntervalMs | Intervalo entre verificações de estabilidade. Faixa válida: 50–10000. |
WatchedFolder:StabilityRequiredSamples | int | 3 | WatchedFolder__StabilityRequiredSamples | Amostras idênticas consecutivas necessárias antes do enfileiramento. Faixa válida: 1–100. |
WatchedFolder:StabilityConcurrency | int | 8 | WatchedFolder__StabilityConcurrency | Quantos arquivos candidatos cada pasta estabiliza e enfileira simultaneamente. A verificação de estabilidade bloqueia por aproximadamente StabilityRequiredSamples × StabilityPollIntervalMs por arquivo, então processá-los um de cada vez limita a entrada a cerca de um arquivo por esse intervalo; sobrepor as esperas mantém o pipeline alimentado quando um lote grande de arquivos chega de uma vez. Faixa válida: 1–64. |
WatchedFolder:StabilityTimeoutSeconds | int | 60 | WatchedFolder__StabilityTimeoutSeconds | Espera máxima antes de desistir de um arquivo que nunca se estabiliza. Faixa válida: 1–3600. |
WatchedFolder:PollIntervalSeconds | int? | 30 | WatchedFolder__PollIntervalSeconds | Com que frequência uma pasta com polling é enumerada — e não se ela faz polling. Sobrescrito, por pasta, por Storage:Inputs[N].PollIntervalSeconds, e é a presença dessa chave que liga o polling em uma pasta local; definir apenas esta não muda nada em lugar nenhum. Uma pasta AzureFiles sempre faz polling e usa este valor, a menos que defina o próprio. Faixa válida: 5–3600. |
WatchedFolder:IgnoredExtensions | array | [".tmp", ".part", ".crdownload", ".swp"] | n/a (use a configuração) | Extensões de arquivo que o observador ignora por completo. |
WatchedFolder:IgnoredPrefixes | array | [".", "~$"] | n/a (use a configuração) | Prefixos de nome de arquivo que o observador ignora (dotfiles, arquivos de lock do Office). |
Upload
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Upload:Enabled | bool | true | Upload__Enabled | Novo na 2.10.0. Se este host aceita arquivos por upload. false desliga o recurso dos dois lados ao mesmo tempo: POST /api/files responde 409 com upload.disabled — antes de o perfil ser resolvido, então uma requisição recusada não revela nada sobre quais perfis existem — e a página Jobs não mostra o botão Enviar arquivos. Pastas monitoradas, nova varredura e nova tentativa não são afetadas, então uma implantação alimentada apenas pelas pastas não perde nada. Lido uma vez no boot; religá-lo exige reinicialização. |
Upload:MaxBytes | long | 104857600 (100 MiB) | Upload__MaxBytes | Limite rígido do corpo da requisição de upload — tanto em POST /api/files quanto na caixa de diálogo Enviar arquivos do dashboard. Aumente para PDFs digitalizados pesados. Mínimo: 1024. |
Dashboard
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Dashboard:PollIntervalSeconds | int | 5 | Dashboard__PollIntervalSeconds | Intervalo de atualização, no servidor, das páginas ao vivo do dashboard. Faixa válida: 1–60. |
Branding — o logotipo do cliente nas páginas de login e de aprovação
Novo na 2.10.0. A página de login e as páginas do aprovador (/approve/{id}, /approvals e a página
que exige o link) mostram a marca do produto. Se você indicar aqui um logotipo do cliente, essas
páginas passam a mostrá-lo também: acima de uma versão reduzida da marca do produto no cartão de login e
ao lado dela nos cabeçalhos do aprovador. Um logotipo por implantação, sobre fundo branco — todos esses
cartões são brancos, então uma marca que só funciona sobre fundo escuro precisa ser exportada de novo.
Nada mais muda: a barra do aplicativo, os títulos das páginas e o favicon continuam sendo os do produto.
O logotipo é lido uma vez, na inicialização, de um arquivo no host ou de um blob do Azure — os mesmos dois lugares em que o material de um certificado pode estar, e pelo mesmo motivo: em um host do Azure App Service, o operador não tem onde colocar um arquivo. Indique um dos dois. Não indicar nenhum é válido e significa sem logotipo; indicar os dois é recusado no boot.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Branding:CustomerLogo:Path | string | — | Branding__CustomerLogo__Path | Caminho para o arquivo de imagem no host — informe um caminho absoluto. A extensão define o content type e precisa ser .png, .jpg/.jpeg, .webp ou .svg; qualquer outra é recusada no boot. No Docker, use um bind mount somente leitura (o arquivo compose do pacote de implantação traz um exemplo comentado). Permitido com Cluster:Enabled, mas, nesse caso, o arquivo precisa ser idêntico em todas as instâncias; nada verifica isso. |
Branding:CustomerLogo:Blob:Url | string | "" | Branding__CustomerLogo__Blob__Url | URL https:// completa do blob. Uma query string é recusada (sem SAS). A extensão do nome do blob segue a mesma regra da extensão do caminho. Mesmo formato do bloco …:Blob de um certificado; nada é herdado desse bloco nem de Storage:AzureFiles. |
Branding:CustomerLogo:Blob:Credential | string | — | Branding__CustomerLogo__Blob__Credential | ManagedIdentity, ServicePrincipal ou AccountKey. Sem valor padrão. Uma credencial por token precisa da role Storage Blob Data Reader no container. |
Branding:CustomerLogo:Blob:TenantId / AppId / AppSecret | string | — | Branding__CustomerLogo__Blob__… | Somente no modo ServicePrincipal; os três são obrigatórios. AppSecret é SECRET — defina-o pelo ambiente. |
Branding:CustomerLogo:Blob:AccountKey | string | — | Branding__CustomerLogo__Blob__AccountKey | Somente no modo AccountKey. SECRET, e dá acesso à conta de armazenamento inteira — prefira uma credencial por token. |
Recusado no boot, com a indicação da chave: Path e Blob definidos ao mesmo tempo; uma extensão
fora das cinco; um bloco de blob sem Url ou sem Credential, um modo de credencial sem os valores
correspondentes ou uma URL com query string. Cada um desses casos é uma configuração que não poderia
funcionar, qualquer que fosse o conteúdo do arquivo.
Informado e tolerado: o arquivo ou blob está ausente, ilegível ou inacessível; o arquivo está vazio
ou passa de 256 KiB; os bytes não correspondem à extensão. O serviço inicia, assina e atende; as
páginas mostram só a marca do produto; e o motivo aparece na linha customer logo do banner de
inicialização, como aviso no log de inicialização e como alerta na página Sistema até a próxima
reinicialização. O /api/ready não traz linha para ele — uma imagem não deve decidir se um serviço de
assinatura está pronto. Corrija o arquivo ou a configuração e reinicie.
Como ele chega ao navegador. GET /branding/customer-logo serve os bytes anonimamente, porque as
páginas que o exibem são mostradas antes do login. A URL contém um hash dos bytes e é armazenada em
cache como imutável, então uma reinicialização com um arquivo novo a altera em todos os lugares ao
mesmo tempo, e ninguém precisa limpar o cache. Um SVG é servido com uma política que impede a execução
de qualquer script que ele contenha. Sem logotipo carregado, a rota responde 404 com
branding.customer-logo-not-available.
{
"Branding": {
"CustomerLogo": {
// Um arquivo neste host. Absoluto; .png, .jpg, .jpeg, .webp ou .svg; no máximo 256 KiB.
"Path": "/etc/bulksigner/customer-logo.png"
// OU, para um host sem disco onde colocá-lo (App Service): um blob do Azure, lido uma vez no boot.
// Remova o Path acima se usar isto — os dois definidos é recusado.
// ,"Blob": {
// "Url": "https://contoso.blob.core.windows.net/branding/customer-logo.svg",
// "Credential": "ManagedIdentity"
// }
}
}
}
ApproverPortal
Dá suporte à fila por aprovador em /approvals. Desligado por padrão, de modo que uma implantação
que já usa a etapa de aprovação é atualizada sem mexer na configuração. Lido uma vez na
inicialização — ao mudar qualquer coisa, reinicie.
De propósito, é uma seção de nível superior em vez de um bloco por perfil: um link identifica uma pessoa, e a mesma pessoa costuma participar dos pools de vários perfis.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
ApproverPortal:Enabled | bool | false | ApproverPortal__Enabled | Chave geral. Quando falsa, nenhum link é resolvido, nenhuma sessão é emitida, e a única forma de aprovação é o link por job. |
ApproverPortal:LinkSecret | string | — | ApproverPortal__LinkSecret | REQUIRED quando habilitado, SECRET — nunca o versione. O link de cada aprovador é HMAC-SHA256(este, o e-mail dele), então qualquer pessoa que o leia pode aprovar arquivos de pagamento em nome de qualquer aprovador configurado. Mínimo de 32 caracteres, verificado na inicialização. Precisa ser durável: gerar um a cada boot invalidaria o favorito de todos os aprovadores a cada reinicialização. Mudá-lo revoga o link de todos os aprovadores de uma vez — a medida drástica prevista para o caso de "o segredo vazou". |
ApproverPortal:DecidedLookback | TimeSpan | 90.00:00:00 | ApproverPortal__DecidedLookback | Até quando, no passado, a aba Aprovados do portal mostra itens. Limita o quanto vale um link roubado. A aba também é limitada a 200 linhas por carregamento e avisa quando o limite é atingido. |
ApproverPortal:SessionLifetime | TimeSpan | 30.00:00:00 | ApproverPortal__SessionLifetime | Tempo de vida do cookie emitido na troca do link. Com expiração deslizante, para que um aprovador que está percorrendo uma fila não seja desconectado no meio de uma decisão. |
ApproverPortal:PollInterval | TimeSpan | 00:00:10 | ApproverPortal__PollInterval | Com que frequência um portal aberto relê a fila, para que a decisão de um colega, uma liberação ou um prazo de espera vencido apareçam sem que o aprovador precise clicar em nada. É uma chave própria, e não uma fração do Dashboard:PollIntervalSeconds, porque esta se multiplica por todos os aprovadores com uma aba aberta, enquanto aquela é ajustada por poucos operadores. Faixa válida: 00:00:01 a 00:05:00, verificada somente quando Enabled é verdadeiro. Não há valor que desligue o polling — um intervalo longo é a forma de reduzir a carga, e a página tem, de qualquer forma, um botão de atualização manual. Cuidado: um 10 sem formato é lido como dez dias e é recusado, com a indicação do valor recebido. |
Enabled = true sem nenhum perfil de assinatura armazenado com regra de aprovação gera um aviso
de inicialização, não uma recusa — um portal sem nenhum pool mostra uma fila vazia a todos os
aprovadores e parece quebrado, então o banner e o log durável avisam isso.
Até a 2.0.x, isso impedia o boot. Com os perfis no banco operacional e o Signing:Profiles[] servindo
só como seed, a recusa dispararia exatamente no estado a que esta página recomenda chegar: pools no
banco e a seção apagada.
{
"ApproverPortal": {
"Enabled": true,
"LinkSecret": "substitua-por-32+-caracteres-aleatorios-mantidos-secretos",
"DecidedLookback": "90.00:00:00",
"SessionLifetime": "30.00:00:00"
}
}
O link de cada aprovador é mostrado na página Sistema do dashboard, um por pessoa configurada. Envie a cada aprovador apenas o link dele; trate-o como a senha dessa pessoa. Veja Aprovações.
Console:Dashboard
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Console:Dashboard:Enabled | bool | true | Console__Dashboard__Enabled | Se o dashboard ao vivo no terminal pode substituir a narração de log por job na saída padrão. Ele só é ativado quando esta chave é verdadeira e o processo é um console em primeiro plano (não serviço do Windows / systemd / Docker) e a saída padrão é um terminal interativo. Por isso, instalações como serviço e em container não são afetadas por esta chave. Defina-a como false quando rodar o binário em primeiro plano e quiser logs simples em fluxo contínuo. |
Idioma de exibição — deliberadamente não configurável
Não há seção de configuração para o idioma da interface, e isso é uma decisão, não uma lacuna. As
páginas web são exibidas em en-US ou pt-BR conforme uma preferência de apresentação por
navegador: o seletor de idioma faz um POST no endpoint anônimo POST /api/culture, que grava por um
ano o cookie de cultura padrão do ASP.NET Core. A ordem de resolução é cookie → o Accept-Language
do navegador → en-US, então um operador brasileiro recebe português já no primeiro carregamento,
sem que ninguém configure nada, e não há configuração no servidor que se sobreponha ao que cada leitor
escolheu.
O que permanece sempre em inglês, independentemente da escolha do leitor: as mensagens de auditoria
persistidas (elas são evidência), a saída de log, o dashboard de console, o texto dos problemas
retornados pela API REST, os valores de JobStatus no protocolo e todo o vocabulário e formatação do
CNAB240. Veja Dashboard.
LogViewer
Dá suporte ao armazenamento em memória das exceções recentes e à página /logs do dashboard. Todos os
valores são lidos uma vez na inicialização — ao mudá-los, reinicie.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
LogViewer:Enabled | bool | true | LogViewer__Enabled | Chave geral. Quando falsa, o destino em memória não é conectado, a página /logs mostra um aviso de recurso desabilitado, e o link de navegação fica oculto. |
LogViewer:MaxEntries | int | 20 | LogViewer__MaxEntries | Tamanho do buffer limitado em memória e número máximo de entradas exibidas. As entradas mais antigas são descartadas quando esse limite é ultrapassado. Faixa válida: 1–1000. |
LogViewer:RefreshIntervalSeconds | int | 5 | LogViewer__RefreshIntervalSeconds | Intervalo de atualização automática da página /logs. Faixa válida: 1–60. A página também tem um botão de atualização manual. |
LogViewer:Levels | string[] | ["Error","Fatal"] | LogViewer__Levels__0, … | Níveis de log que o armazenamento captura (sem diferenciar maiúsculas de minúsculas). Nomes válidos: Verbose, Debug, Information, Warning, Error, Fatal. Nomes vazios ou desconhecidos impedem a inicialização. O Logging:File:MinimumLevel continua sendo aplicado antes — ampliar esta lista para níveis abaixo desse mínimo não captura nada, porque esses eventos nunca chegam ao destino. |
Veja Dashboard.
Readiness
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Readiness:RequireApiKey | bool | false | Readiness__RequireApiKey | Novo na 2.6.0. Quando verdadeiro, GET /api/ready exige autenticação por chave de API ou cookie — a mesma proteção que o Metrics:RequireApiKey aplica a /api/metrics. Ao contrário daquela chave, fica desligado por padrão, porque o principal consumidor do probe é um health check de plataforma que não consegue enviar a chave: o do Azure App Service precisa ser respondido anonimamente, e um 401 em todas as instâncias torna todas elas não saudáveis de uma vez. Ligue-o onde quem executa o probe consiga enviar X-API-Key (os headers de um probe do Kubernetes, um agente de monitoramento) ou onde não haja probe. Lido uma vez na inicialização; a linha ready do banner mostra (anonymous) ou (API key). O /api/ready/details exige autenticação de qualquer forma, e o /api/ready nunca traz detalhes. |
O /api/ready agora retorna ready e o name e o ok de cada verificação — o campo detail por
verificação não é mais retornado. Esse detalhe indicava o host do SQL Server, todos os compartilhamentos
de entrada e a localização de um certificado degradado: nada disso é credencial, mas, em conjunto,
formava um mapa da implantação legível por qualquer pessoa que chegasse à porta. O mesmo relatório, com
todos os detalhes, está em /api/ready/details, protegido pela chave de API ou por uma sessão de
operador, com a mesma regra 200 / 503. Um orquestrador que lê o código de status não é afetado; um
monitoramento que interpretava o detail passa a usar a rota de detalhes e acrescenta o header. Cada
mudança de veredito de uma verificação é gravada uma vez no log durável, então o histórico continua
registrado.
Metrics
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Metrics:RequireApiKey | bool | true | Metrics__RequireApiKey | Quando verdadeiro, /api/metrics exige autenticação por chave de API ou cookie. Defina como falso apenas se o coletor do Prometheus estiver dentro do perímetro de confiança e a rede estiver fechada. |
Veja API REST para o inventário completo das métricas.
Statistics
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Statistics:Enabled | bool | true | Statistics__Enabled | Chave geral. Quando falsa, o coletor não faz nada (sem registro, sem locking), nenhuma linha é gravada, e o painel do dashboard fica oculto. As estatísticas são uma linha por job concluído no banco operacional: elas sobrevivem a reinicializações, e todas as instâncias de um cluster leem os mesmos números. Desligar a chave não apaga as linhas já registradas — religá-la volta a mostrá-las; para limpar o painel, use o Limpar Jobs. |
Veja Estatísticas de jobs para o significado de cada número.
Backup
Dá suporte ao recurso de backup do banco de dados: a página /backup do dashboard, o
GET|POST /api/backup e o agendador. Desligado por padrão, e somente para SQLite — com
Database:Provider = SqlServer, Backup:Enabled = true causa uma recusa de boot, em vez de
simplesmente não fazer nada, porque o backup do banco operacional é responsabilidade da rotina desse
SGBD. Como o modo cluster exige SqlServer, essa combinação é, por definição, impossível nele.
Lido uma vez na inicialização. Somente o bloco do destino selecionado é lido.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Backup:Enabled | bool | false | Backup__Enabled | Chave geral. Quando falsa, nada é agendado, o botão de iniciar e o POST /api/backup recusam com backup.disabled, e a página explica o que ligar. true com Database:Provider = SqlServer impede o boot, indicando as duas chaves e a solução. |
Backup:Destination | string | Disk | Backup__Destination | Disk, S3 ou AzureBlob. Se ausente, vale Disk — o destino que não precisa de credencial. Não diferencia maiúsculas de minúsculas; um valor não reconhecido é recusado no boot, com a indicação da chave, do valor informado e dos nomes válidos. |
Backup:IntervalHours | int? | (ausente) | Backup__IntervalHours | Com que frequência um backup roda automaticamente. Ausente significa somente manual — o recurso está ligado, o botão funciona, mas nada roda por temporizador. Faixa válida: 1–8760. Deliberadamente, é um intervalo e não um horário do dia: um horário precisa de fuso horário, e este produto não tem um. A contagem parte da última execução bem-sucedida, de modo que uma reinicialização não a zera e uma execução com falha não a consome. Se nunca houve backup, um backup é devido imediatamente. |
Backup:RetainCount | int | 14 | Backup__RetainCount | Quantos artefatos manter no destino. 0 mantém todos — e nem chega a listar o destino —, para um bucket ou container cujas próprias regras de ciclo de vida fazem a limpeza. Faixa válida: 0–1000. É uma contagem e não uma idade, porque uma contagem continua válida quando alguém muda o IntervalHours. A limpeza roda apenas após um armazenamento bem-sucedido e não pode fazer a execução falhar. |
Backup:Disk:Path | string | — | Backup__Disk__Path | REQUIRED quando Destination = Disk. Sem padrão, de propósito: qualquer padrão plausível colocaria o backup no mesmo disco do banco de dados copiado. Um caminho UNC ou volume de rede montado serve. Quatro recusas de boot: ausente; o esquema azurefiles:// do próprio produto (deliberadamente, um destino de backup não é um provider de armazenamento); um caminho dentro de uma pasta de entrada monitorada (o pipeline capturaria, assinaria e depois apagaria o backup); e um caminho dentro de processing/, output/, error/ ou db/. |
Backup:S3:BucketName | string | — | Backup__S3__BucketName | REQUIRED quando Destination = S3. |
Backup:S3:Region | string | — | Backup__S3__Region | REQUIRED quando Destination = S3 e nenhum ServiceUrl está definido. O nome de sistema da região, por exemplo sa-east-1. Sem valor padrão: o SDK recorreria ao ambiente do host ou ao perfil compartilhado, então uma região omitida decidiria, por acidente, onde fica armazenada uma cópia do registro de aprovação de pagamentos. |
Backup:S3:Prefix | string | "" | Backup__S3__Prefix | Prefixo de chave dentro do bucket. Normalizado — bulksigner, bulksigner/ e /bulksigner/ significam a mesma coisa. |
Backup:S3:Credential | string | — | Backup__S3__Credential | REQUIRED quando Destination = S3. AccessKey ou InstanceRole. Sem valor padrão — a própria cadeia do SDK da AWS autenticaria com qualquer identidade que o host tivesse. Uma URL pré-assinada não é uma credencial aceita. |
Backup:S3:AccessKeyId | string | — | Backup__S3__AccessKeyId | REQUIRED quando Credential = AccessKey. Deliberadamente não mascarado nos logs: é a metade identificadora do par, e mascará-lo removeria o único valor que diz qual chave uma requisição recusada usou. Defini-lo com InstanceRole causa uma recusa de boot. |
Backup:S3:SecretAccessKey | string | — | Backup__S3__SecretAccessKey | REQUIRED quando Credential = AccessKey. SECRET. Defini-lo com InstanceRole causa uma recusa de boot. |
Backup:S3:ServiceUrl | string | — | Backup__S3__ServiceUrl | Um endpoint de API compatível com S3 em vez da AWS — MinIO, Ceph, Wasabi, Backblaze B2. URL http:// ou https:// absoluta. Quando definido, Region passa a ser opcional. |
Backup:S3:ForcePathStyle | bool | false | Backup__S3__ForcePathStyle | Endereça os buckets como um segmento de caminho (host/bucket/key) em vez de subdomínio. false é o que a própria AWS espera; quase todo endpoint compatível com S3 precisa de true — deixá-lo falso com MinIO ou Ceph causa uma falha de DNS que cita um hostname que você nunca configurou. |
Backup:AzureBlob:ContainerUrl | string | — | Backup__AzureBlob__ContainerUrl | REQUIRED quando Destination = AzureBlob. A URL https completa do container, por exemplo https://contoso.blob.core.windows.net/bulksigner-backups. Recusada no boot quando não é uma URL https absoluta, não indica uma conta, tem mais de um segmento de caminho (coloque um caminho em Prefix) ou tem query string — que é como uma SAS chega; recusá-la garante que este valor nunca seja secreto. O container não é criado automaticamente. |
Backup:AzureBlob:Prefix | string | "" | Backup__AzureBlob__Prefix | Prefixo de nome de blob dentro do container. Normalizado como o do S3. |
Backup:AzureBlob:Credential | string | — | Backup__AzureBlob__Credential | REQUIRED quando Destination = AzureBlob. ManagedIdentity, ServicePrincipal ou AccountKey — grafados exatamente como os de Storage:AzureFiles. Sem valor padrão. O ManagedIdentity aceita somente a identidade atribuída pelo sistema. A identidade precisa da role Storage Blob Data Contributor — escrita, e não o Storage Blob Data Reader de que um blob de certificado precisa. |
Backup:AzureBlob:TenantId / AppId | string | — | Backup__AzureBlob__TenantId, … | REQUIRED quando Credential = ServicePrincipal. |
Backup:AzureBlob:AppSecret | string | — | Backup__AzureBlob__AppSecret | REQUIRED quando Credential = ServicePrincipal. SECRET. Defini-lo com outro modo causa uma recusa de boot — um segredo que esta implantação não usa é um segredo que ninguém vai rotacionar. |
Backup:AzureBlob:AccountKey | string | — | Backup__AzureBlob__AccountKey | REQUIRED quando Credential = AccountKey. SECRET. Concede acesso total ao plano de dados da conta de armazenamento inteira e não pode ser restrita a um container. Defini-la com outro modo causa uma recusa de boot. |
Nada é herdado de Storage:AzureFiles nem do bloco de um blob de material de assinatura, mesmo quando
o mesmo aplicativo do Entra é indicado: essas credenciais concedem acesso de leitura a recursos
diferentes, e esta precisa de acesso de escrita.
Exemplo: um intervalo para um volume local
{
"Backup": {
"Enabled": true,
"Destination": "Disk",
"IntervalHours": 24,
"RetainCount": 14,
"Disk": { "Path": "/backup/bulksigner" }
}
}
Exemplo: um endpoint compatível com S3 (MinIO)
{
"Backup": {
"Enabled": true,
"Destination": "S3",
"IntervalHours": 12,
"RetainCount": 0, // as regras de ciclo de vida do bucket fazem a poda
"S3": {
"BucketName": "bulksigner-backups",
"ServiceUrl": "https://minio.internal:9000",
"ForcePathStyle": true,
"Credential": "AccessKey",
"AccessKeyId": "…",
// Na prática, defina via Backup__S3__SecretAccessKey, não aqui.
"SecretAccessKey": ""
}
}
}
Veja Retenção para entender como isso se encaixa no quadro mais amplo de retenção.
Cluster — implantação com múltiplas instâncias
Modo cluster — mais de uma instância ativa cooperando sobre um único banco operacional e um único compartilhamento de trabalho. Desligado por padrão, e, desligado, é byte a byte o produto de instância única: uma implantação que nunca escreve esta seção não precisa de nada e não muda nada na atualização.
Estas três chaves são a menor parte da ativação do modo. O custo dele está descrito em
Alta disponibilidade e seus limites — leia essa página antes de definir
Enabled = true —, e a implantação é descrita em Azure App Service (modo cluster), que
também traz as configurações de plataforma (afinidade de sessão, o caminho do health check, Always On)
que não têm chave aqui porque não cabe a este produto defini-las.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Cluster:Enabled | bool | false | Cluster__Enabled | Chave geral. Quando verdadeira, as recusas de boot abaixo se aplicam; quando falsa ou ausente, nenhum mecanismo de cluster é registrado e nada muda. Há exatamente uma topologia com múltiplas instâncias suportada: um Azure Web App (container Linux) com escala horizontal em um único App Service Plan. |
Cluster:HeartbeatSeconds | int | 15 | Cluster__HeartbeatSeconds | Com que frequência esta instância grava uma linha informando que está ativa. Faixa [5, 300]; valores fora dela são recusados no boot. Lida somente quando o modo está ligado. |
Cluster:StaleAfterSeconds | int | 60 | Cluster__StaleAfterSeconds | Quanto tempo uma instância pode ficar sem enviar heartbeats antes que as demais a presumam morta. Faixa [15, 3600], e recusada no boot quando vale menos de três intervalos de heartbeat — um limiar tão curto presume a morte após um ou dois heartbeats perdidos, e um heartbeat pode se perder por motivos que não são morte (uma pausa de coleta de lixo, um banco operacional que demorou um instante, um container que a plataforma deixou sem recursos por um momento). O padrão são quatro intervalos. |
O que Enabled = true recusa no boot
Toda configuração de cluster que não poderia funcionar causa uma recusa que indica as chaves e a solução:
Database:Providerprecisa serSqlServer. O banco operacional é o ponto de coordenação do cluster, e um arquivo SQLite não pode ser compartilhado entre hosts.- O compartilhamento de trabalho e todas as pastas de entrada monitoradas precisam estar em um
provider cujos leases fiquem no serviço —
AzureFilesouAzureBlob(AzureBlobdesde a 2.20.0). O lease do armazenamento local de arquivos não exclui nada fora do próprio processo, e uma pasta local de uma instância é invisível para as demais. A pastadefault, criada automaticamente sem configuração, é local, então uma primeira execução com a chave ligada também é recusada. OS3é recusado para os dois, com a indicação deStorage:Providerou doStorage:Inputs[N].Providerda pasta: o S3 não tem lease nenhum, então um bucket dá a várias instâncias apenas a proteção que uma pasta local daria. Auth:SessionKeyRing:Placement = DataRooté recusado (desde a 2.20.0): um ring dentro deStorage:Rootpertence a um único host.- As origens de certificado por host
Pkcs11eWindowsStoresão recusadas. Um token ou um repositório de máquina fica em uma única máquina, e as instâncias de cluster são intercambiáveis. UsePfx(de preferência lido de um blob ou de um objeto S3),AzureKeyVaultouAwsKms. Um blocoCertificateobsoleto em um perfil comMethod = LacunaSignercontinua tolerado, como em qualquer outro caso. É uma regra sobre um perfil, então é aplicada sempre que uma origem de certificado é declarada: no boot, para uma entrada deSigning:Profiles[]ou para o bloco legadoSigning:Certificate, e no formulário do dashboard quando um perfil é criado ou o certificado dele é alterado. Um perfil salvo antes de a chave ser ligada gera só um aviso de inicialização com o nome do perfil e a origem — um perfil armazenado nunca é revalidado, e a recusa tiraria do ar justamente a página que resolve o problema. Esse perfil assina na instância em que encontrou o hardware e falha como perfil degradado em todas as outras.
Uma recusa não tem nada a ver com configuração, e está listada aqui porque parece ter: com o modo
ligado, o próprio marcador do compartilhamento de trabalho registra a qual banco operacional o
compartilhamento pertence, e uma instância cujo banco não corresponde se recusa a iniciar, indicando os
dois. É uma característica do compartilhamento, e não do appsettings.json, e é a única coisa que
detecta dois clusters apontados para o mesmo compartilhamento de trabalho.
Deliberadamente, uma condição de cluster gera um aviso, não uma recusa: modo cluster com
Logging:AzureTable:Enabled = false registra um Critical na inicialização, porque, na topologia
suportada, o disco da instância é efêmero e os arquivos de log rotacionados são descartados a cada
reciclagem. De qualquer forma, a regra de que a tabela nunca pode ser o único destino mantém o destino
de arquivo local ligado.
Identidade e vivacidade
A identidade é derivada, nunca configurada, e deliberadamente não há chave para ela. Uma instância
se identifica pelo ID de instância da própria plataforma (WEBSITE_INSTANCE_ID, que o App Service
define em toda instância) ou, onde essa variável não existe, pelo nome da máquina, mais um
identificador de encarnação novo a cada boot. O App Service cria e destrói instâncias por regra de
escala, então um nome digitado por um operador seria uma chave que ninguém conseguiria manter correta;
é a encarnação que permite a uma instância distinguir a própria vida anterior de uma instância
estranha.
Cada instância mantém uma linha no banco operacional — identidade, encarnação, versão da aplicação,
quando iniciou, quando informou pela última vez que estava ativa —, atualizada a cada
Cluster:HeartbeatSeconds e presumida morta depois de Cluster:StaleAfterSeconds. A
página Sistema exibe essa tabela na visão Instâncias. Duas coisas
dependem dela no boot, deliberadamente de naturezas diferentes — uma assume a identidade, a outra só
avisa:
- Uma instância que, ao subir, encontra a própria identidade já enviando heartbeats a desloca,
indicando a encarnação deslocada no console e no log, e continua. É a sobreposição típica de uma
reimplantação: o App Service roda o container antigo e o novo com o mesmo ID de instância e mantém o
antigo ativo até o novo estar aquecido. O processo deslocado se retira no próximo heartbeat — não
captura nenhum job novo, termina os que já tem e mostra uma linha
cluster-instancevermelha no próprio/api/ready, que não reprova o probe. O dono de um job é a encarnação, além da identidade, então em qualquer momento exatamente um processo captura trabalho com um determinado nome, e o que o processo deslocado deixar inacabado é assumidoCluster:StaleAfterSecondsdepois do deslocamento. Um desligamento limpo desativa a linha de heartbeat, então um sucessor não tem nada a deslocar. Dois hosts que de fato apresentam o mesmo nome se deslocam mutuamente, com avisos dos dois lados, em vez de serem recusados; a única recusa que resta é a de um registro que perdeu todas as disputas de escrita pela própria linha. - Instâncias ativas em uma versão diferente da aplicação registram um Critical, e o boot continua. Na topologia suportada, as atualizações exigem parada total, então isso indica um deployment slot trocado para dentro de um cluster em execução, ou uma implantação que não parou todas as instâncias. De propósito, é um aviso, e não uma recusa: a recusa impediria as instâncias de subir por todo o tempo que um heartbeat morto da versão antiga levasse para ficar obsoleto, que é exatamente quando um operador precisa que elas subam.
Se o banco operacional estiver inacessível no boot, o registro não é feito, assim como a migração, e o host inicia mesmo assim. O heartbeat faz o registro no primeiro envio que conseguir chegar ao banco — deslocando um detentor ativo da identidade exatamente como o boot teria feito —, então a instância só fica ausente da visão Instâncias até esse momento.
Até a 2.4.x, uma instância que, ao subir, encontrasse a própria identidade enviando heartbeats se recusava a iniciar (a 2.4.3 passou a fazê-la esperar), então uma troca de imagem in-place, no App Service, custava pelo menos um início recusado. As atualizações continuam exigindo parada total, e parar, implantar, iniciar continua sendo a receita mais limpa; veja Azure App Service.
O key ring de sessão acompanha a chave, sem nada a configurar
Com a chave desligada, o ring de Data Protection fica em keys/, dentro de Storage:Root,
criptografado com DPAPI no Windows (a menos que
Auth:SessionKeyRing:Placement o mova). Com
ela ligada, ele passa a ser um conjunto de linhas no banco operacional, em texto claro, protegido pelo
controle de acesso do próprio banco de dados — porque um ring derivado de uma raiz local pertence
estruturalmente a um único host; assim, atrás de um balanceador de carga, um cookie criado por uma
instância é rejeitado pela seguinte, o que as pessoas percebem como uma desconexão intermitente, sem que
nada, em lugar nenhum, a informe. Os dois cookies dependem dele, então o problema afeta operadores e
aprovadores da mesma forma.
Ele acompanha a chave sem que seja preciso pedir, e a única configuração que indica o local do ring não
consegue desfazer isso: Auth:SessionKeyRing:Placement = DataRoot com o modo cluster é uma recusa de
boot, porque uma implantação que pode escolher o local do ring independentemente da topologia não pode
ter a opção de escolher a combinação que não funciona. Desde a 2.20.0, um host em cluster não cria mais
um diretório keys/ vazio no boot. Veja
Segurança e
Alta disponibilidade.
Telemetry
Exportação opcional para o Azure Application Insights. Desligada por padrão; quando desligada, o serviço não depende do Application Insights e não faz chamadas de saída para ele.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Telemetry:Enabled | bool | false | Telemetry__Enabled | Chave geral. Quando true, uma connection string é obrigatória — a inicialização falha sem ela. |
Telemetry:ConnectionString | string? | null | Telemetry__ConnectionString | SECRET. Connection string do Application Insights. Deixe-a sem definir para fornecê-la pela variável de ambiente padrão APPLICATIONINSIGHTS_CONNECTION_STRING. |
Telemetry:RoleName | string | Lacuna.BulkSigner | Telemetry__RoleName | Informado como a dimensão cloud_RoleName, para que vários serviços que compartilham um recurso continuem distinguíveis. |
Veja Telemetria para saber o que é coletado e as consultas KQL para analisar esses dados.
RateLimiting
Políticas de limite de requisições (rate limiting) com janela fixa, por IP.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
RateLimiting:Enabled | bool | true | RateLimiting__Enabled | Chave geral. Desabilite em instalações em rede fechada. |
RateLimiting:Upload:PermitsPerWindow | int | 30 | RateLimiting__Upload__PermitsPerWindow | Requisições permitidas por janela em POST /api/files. |
RateLimiting:Upload:WindowSeconds | int | 60 | RateLimiting__Upload__WindowSeconds | Duração da janela da política de upload. |
RateLimiting:Upload:QueueLimit | int | 0 | RateLimiting__Upload__QueueLimit | Quantas requisições acima do limite esperam, em vez de serem rejeitadas imediatamente. 0 = rejeitar imediatamente. |
RateLimiting:Actions:PermitsPerWindow | int | 60 | RateLimiting__Actions__PermitsPerWindow | Requisições permitidas por janela nos endpoints de ação (nova tentativa, cancelar, nova varredura, limpeza, pausar, retomar). |
RateLimiting:Actions:WindowSeconds | int | 60 | RateLimiting__Actions__WindowSeconds | Duração da janela da política de ações. |
RateLimiting:Actions:QueueLimit | int | 0 | RateLimiting__Actions__QueueLimit | Tamanho da fila da política de ações. |
RateLimiting:Approval:* | mesmo formato | 10 por 60 s | RateLimiting__Approval__PermitsPerWindow, … | Cota da rota anônima POST /api/approvals/{id}, separada da cota das ações de operador. Os IDs de job são GUIDs v4, e é esta política que os torna impossíveis de adivinhar por uma máquina, e não apenas por uma pessoa. |
RateLimiting:Export:* | mesmo formato | 10 por 60 s | RateLimiting__Export__PermitsPerWindow, … | Cota das exportações: a exportação da fila do portal do aprovador; desde a 2.11.0, o GET /api/jobs/export da página Jobs; e, desde a 2.20.0, a planilha de pagamentos da página Jobs (GET /api/jobs/payments/export) e o GET /api/logs/export da página Exceções recentes. Limita a velocidade com que cópias de uma fila podem ser feitas, e fica separada da cota Approval para que uma rajada de exportações nunca consuma as permissões de que um colega precisa para registrar uma decisão. |
RateLimiting:SignerNotification:* | mesmo formato | 600 por 60 s | RateLimiting__SignerNotification__PermitsPerWindow, … | Novo na 2.16.0. Cota do POST /api/signer/notifications, o webhook do Lacuna Signer. Generosa porque o único chamador legítimo entrega todos os eventos da organização a partir de poucos endereços, então a partição por IP é, na prática, a do Signer; quem mantém estranhos de fora é o segredo, não esta cota. Uma entrega limitada recebe 429, que o Signer tenta de novo como qualquer resposta não 2xx, e a varredura de pastas encontra o que uma nova tentativa deixar para trás. |
As respostas bloqueadas pelo limite de requisições trazem code = "rate-limited" no envelope de erro.
Hosting
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
Hosting:RequireHttps | bool | false | Hosting__RequireHttps | Controla o redirecionamento HTTPS no próprio processo. false (padrão) para instalações como serviço e Docker que fazem a terminação TLS em um proxy reverso. Aparece no banner de resumo de prontidão como https redirect = on/off. |
Hosting:ForwardedHeaders:Enabled | bool | false | Hosting__ForwardedHeaders__Enabled | Lê o endereço e o esquema do cliente de X-Forwarded-For e X-Forwarded-Proto. Desligado por padrão, e, desligado, é byte a byte o produto como foi entregue — nenhum middleware é adicionado. Ligá-lo exige um conjunto de confiança: TrustAnyProxy, KnownProxies ou KnownNetworks; caso contrário, o boot é recusado. Aparece no banner de resumo de prontidão como forwarded headers = …, indicando o conjunto de confiança em vez de apenas on. |
Hosting:ForwardedHeaders:TrustAnyProxy | bool | false | Hosting__ForwardedHeaders__TrustAnyProxy | Aceita um header encaminhado vindo de qualquer endereço anterior na cadeia. É a configuração prevista para o Azure App Service, cujo front-end não tem endereço estável para listar. ⚠️ Em uma implantação com proxy reverso, significa que qualquer pessoa que consiga acessar o Kestrel diretamente pode se apresentar com qualquer endereço de cliente. Não pode ser combinada com as duas chaves abaixo — uma lista ao lado dela seria ignorada, então a combinação é recusada no boot em vez de ser resolvida por precedência. |
Hosting:ForwardedHeaders:KnownProxies | string[] | [] | Hosting__ForwardedHeaders__KnownProxies__0 | Endereços IP simples cujos headers encaminhados são aceitos (10.4.0.7, ::1). Um valor que não seja endereço IP é recusado no boot, com a indicação do valor. Os padrões de loopback do framework não são mantidos — quando esta seção define um conjunto de confiança, ele é o conjunto completo, então um proxy neste host precisa estar listado. |
Hosting:ForwardedHeaders:KnownNetworks | string[] | [] | Hosting__ForwardedHeaders__KnownNetworks__0 | Faixas CIDR cujos headers encaminhados são aceitos (10.4.0.0/16). Recusada no boot se não for uma faixa CIDR ou se o endereço tiver bits além do comprimento do prefixo — 10.4.0.7/16 descreve 10.4.0.0/16, e um arquivo que diz uma faixa enquanto o host confia em outra merece fazer o boot falhar. |
Hosting:ForwardedHeaders:ForwardLimit | int | 1 | Hosting__ForwardedHeaders__ForwardLimit | Quantas entradas são lidas a partir do fim (à direita) de cada header encaminhado — uma por proxy pelo qual a requisição realmente passa, o que dá 1 tanto para um balanceador de carga de plataforma quanto para um único proxy reverso. Faixa [1, 16]; valores fora dela são recusados no boot. O "ilimitado" do framework é deliberadamente inacessível, porque, em uma cadeia ilimitada, a ponta mais distante foi escrita pelo cliente. |
Por que os headers encaminhados são uma chave do produto, e não apenas a chave do framework
O ASP.NET Core tem a própria chave, ASPNETCORE_FORWARDEDHEADERS_ENABLED=true, que confia em
qualquer origem anterior, sem forma de restringir isso. Definir tanto ela quanto
Hosting:ForwardedHeaders:Enabled é recusado no boot, com a indicação das duas: cada uma acrescenta
o processamento de headers encaminhados, então todo header seria processado duas vezes, e um
ForwardLimit de um aceitaria silenciosamente dois saltos.
Duas coisas no produto dependem do endereço do cliente, e é por isso que decidir em quais headers confiar é uma questão de configuração, e não um detalhe:
- A partição do limite de requisições. Atrás de um balanceador de carga, todos os chamadores chegam com um único endereço, então a cota por cliente da rota de aprovação anônima vira uma única cota compartilhada pelo mundo inteiro.
- O endereço registrado em uma aprovação, que é um dos controles compensatórios da rota de aprovação anônima. Um endereço que, na verdade, é o do balanceador de carga não diz nada sobre quem decidiu.
No Azure App Service, a configuração prevista é Enabled = true com TrustAnyProxy = true, e o risco
que isso traz é eliminado restringindo o acesso à origem — veja
Azure App Service.
ApproverSecondFactor
Um segundo fator TOTP para aprovadores — um código de um aplicativo autenticador, conferido com uma inscrição por pessoa. Desligado por padrão, então a atualização não muda nada. Lido uma vez na inicialização; ao mudar qualquer coisa, reinicie.
Vale para o host inteiro, e não por perfil de assinatura, e essa escolha é estrutural, não uma conveniência: uma regra por perfil é congelada no job quando ele fica retido, e a autenticação não pode estar nesse snapshot — o snapshot registra qual regra se aplicou, de modo que editar este arquivo nunca pode servir para contornar a autorização. Uma chave que vale para o host inteiro não tem nada por perfil a congelar, então um job que ficou retido antes de o fator ser habilitado não precisa de migração nem de tratamento especial.
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
ApproverSecondFactor:Enabled | bool | false | ApproverSecondFactor__Enabled | Chave geral. Quando falsa, nada muda em nenhuma tela de aprovação, e nenhum valor de ApproverSecondFactor:* é lido. Quando verdadeira: o painel de inscrição aparece em /approvals, toda decisão no portal pede um código uma vez por janela de verificação, e as duas rotas que não têm sessão de navegador — POST /api/approvals/{id} e uma decisão a partir de /approve/{jobId} — passam a recusar de imediato. Essa última parte quebra qualquer ERP que faça aprovações por REST; veja Aprovações. |
ApproverSecondFactor:SeedSecret | string | — | ApproverSecondFactor__SeedSecret | REQUIRED quando habilitado. SECRET — nunca o versione. A chave com a qual o seed do autenticador de cada aprovador é criptografado em repouso (PBKDF2-HMAC-SHA256 → AES-256-GCM). Mínimo de 32 caracteres, o mesmo mínimo exigido do ApproverPortal:LinkSecret. Ela criptografa os seeds; não os deriva — deliberadamente, os seeds são aleatórios por aprovador, para que quem tem o primeiro fator não consiga gerar o segundo. Perdê-la ou rotacioná-la significa que todos os aprovadores precisam se inscrever de novo. |
ApproverSecondFactor:VerificationWindow | TimeSpan | 00:20:00 | ApproverSecondFactor__VerificationWindow | Por quanto tempo um fator comprovado vale antes de precisar ser comprovado de novo. Zero é válido e significa "pedir a cada decisão" — a configuração mais rígida, e não uma desativação. Absoluta, nunca deslizante, e restrita a uma sessão de navegador. Faixa válida: 00:00:00 a 08:00:00. Congelada em cada janela no momento em que o código é digitado, de modo que editá-la vale para as janelas abertas depois e não muda nada nas que já estão abertas. |
ApproverSecondFactor:Issuer | string | Lacuna Bulk Signer | ApproverSecondFactor__Issuer | O rótulo que um aplicativo autenticador mostra acima do código. Vai no parâmetro issuer da URI de provisionamento e é repetido no label dela, porque os aplicativos divergem sobre qual dos dois leem. Defina-o quando um diretório abriga vários ambientes e, sem isso, um aprovador veria duas contas com nomes idênticos. |
VerificationWindowUm valor de três componentes é hh:mm:ss somente enquanto o primeiro número for 23 ou menos; a partir
de 24, o binder o lê como dias, então "24:00:00" significa vinte e quatro dias. Diferentemente do
ExpiresAfter, que aceita isso silenciosamente, aqui o teto de oito horas transforma o valor em uma
recusa de boot que aponta o erro.
Três recusas de boot
Enabled = true impede a inicialização, indicando a chave, quando:
SeedSecretestá ausente ou tem menos de 32 caracteres. Sem ela, as inscrições ficam ilegíveis, então um fator configurado pela metade não é um controle mais fraco — é um controle cuja força ninguém definiu.VerificationWindowé negativa ou maior que oito horas. Além de um turno de trabalho, o valor deixa de representar a presença de alguém diante do teclado e passa a representar uma sessão — que é justamente o motivo pelo qual uma janela deslizante foi descartada.ApproverPortal:Enabledé falso e nenhuma seçãoAuth:EntraIdestá configurada. São os dois únicos recursos que produzem uma sessão identificada, e o fator é comprovado dentro de um deles. Sem nenhum dos dois, ninguém conseguiria se inscrever, todo job de um perfil com aprovação configurada ficaria retido indefinidamente, e nenhum arquivo de pagamento seria assinado.
AllowedHosts
| Chave | Tipo | Padrão | Override por env | Observações |
|---|---|---|---|---|
AllowedHosts | string | * | AllowedHosts | Filtragem de host padrão do ASP.NET Core. Sobrescreva com uma lista separada por vírgulas se a instalação estiver atrás de um proxy reverso com um nome de host externo fixo. |
Variáveis de ambiente sem equivalente em JSON
| Variável | Finalidade |
|---|---|
BULK_SIGNER_CONFIG_DIR | Indica ao binário onde fica a configuração de produção quando o binário está em um local de instalação somente leitura. Definida pelos scripts de instalação. |
BULK_SIGNER_PKCS11_PIN | O PIN do HSM/token — lido da variável de ambiente cujo nome é configurado em Signing:Certificate:Pkcs11:PinEnvVar. |
BULK_SIGNER_ENCRYPTION_PASSWORD | Senha do PBKDF2 — lida da variável de ambiente cujo nome é configurado em Encryption:PasswordEnvVar. |
APPLICATIONINSIGHTS_CONNECTION_STRING | Variável padrão do Azure Monitor. Lida diretamente pelo exportador e aceita pelo validador de inicialização, de modo que Telemetry:ConnectionString pode ficar sem definir. Veja Telemetria. |
ASPNETCORE_ENVIRONMENT | Nome de ambiente padrão do ASP.NET Core (Development, Production). Os scripts de instalação definem Production. Com Production, o Signing:TrustLacunaTestRoot é recusado no boot — um host de homologação que confia na raiz de teste roda com outro nome, como Staging. |
ASPNETCORE_URLS | Padrão. Os scripts de instalação definem http://0.0.0.0:8080. |
ASPNETCORE_CONTENTROOT | Padrão. A instalação no Windows a define como C:\ProgramData\Lacuna\BulkSigner, para que os caminhos de arquivo sejam resolvidos em um disco gravável pelo operador. |
Verificando a configuração em tempo de execução
O banner de resumo de prontidão exibido na inicialização lista as configurações mais importantes para
tomar decisões (modo de host, ambiente, redirecionamento https, content root, raiz de armazenamento,
impressão digital da licença, origem do certificado, política de assinatura, status da criptografia,
intervalo de polling, modo do pipeline e uma linha operational store com o provider de banco de
dados). Uma chave digitada errado aparece ali como um valor padrão, em vez do valor que você
pretendia. Outras quatro linhas estão sempre presentes, e cada uma responde a uma pergunta: trust set
(se a raiz de teste da Lacuna é confiável — veja
Signing:TrustLacunaTestRoot),
web pki license e cloudhub (configurado ou não, nunca o valor) e customer logo (none, o
logotipo carregado ou o motivo de não ter sido carregado). As linhas ready e metrics mostram
(anonymous) ou (API key), então um Readiness:RequireApiKey ou Metrics:RequireApiKey que não
foi aplicado fica visível no boot.
Linhas que aparecem somente quando o recurso correspondente está configurado:
| Linha do banner | Aparece quando |
|---|---|
store status, store isolation | Database:Provider = SqlServer |
work share, azure credential, azure shares, input providers, work share owner | Storage:Provider = AzureFiles, ou qualquer pasta de entrada que o indique |
blob=… em uma linha de perfil | o certificado desse perfil é lido do Azure Blob Storage |
cnab240=on, approval=N/M, expires=… em uma linha de perfil | CheckCNAB240 / Approval nesse perfil |
prefixo DEGRADED · em uma linha de perfil, com uma linha FAIL ao lado | o certificado desse perfil não abriu |
O /api/ready retorna um corpo JSON com cada verificação — banco operacional, cada pasta de entrada,
licença, as linhas storage-share: e work-share-owner em um compartilhamento de trabalho remoto e uma
linha por perfil de assinatura degradado — com o veredito e sem detalhes; o /api/ready/details, com a
chave de API, acrescenta o detalhe de cada verificação. Um 503 com um corpo que lista a verificação
que falhou é o ciclo rápido de feedback para erros de configuração — veja
Diagnóstico de problemas.
Algumas linhas informam ok: false sem transformar a resposta em 503, porque um 503 tira a
instância do balanceador de carga, e cada uma delas tem uma solução que a própria instância oferece: a
linha do destino de log em tabela do Azure (uma indisponibilidade do log não deve virar uma
indisponibilidade da ingestão de arquivos), a de um perfil de assinatura degradado (é no dashboard que
ele é corrigido), uma linha profile-input-folder: para um perfil vinculado a uma pasta que este host
não configura e, no modo cluster, a linha cluster-instance de uma instância que se retirou. O
alerta deve ler checks[], e não só o ready de nível superior.
A seguir: Certificados — escolhendo e configurando uma origem de certificado. Anterior: Instalação.