Pular para o conteúdo principal

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:

  1. appsettings.json (padrões embutidos)
  2. appsettings.{Environment}.json (por exemplo, appsettings.Production.json)
  3. appsettings.json + appsettings.{Environment}.json encontrados em BULK_SIGNER_CONFIG_DIR
  4. 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).

Mudou na 2.3.1 — o appsettings.json distribuído é neutro para produção

O 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​

MarcadorSignificado
REQUIREDO serviço se recusa a iniciar (ou a assinatura se recusa a rodar) sem um valor não vazio.
SECRETSensí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.

ChaveTipoPadrãoOverride por envObservações
Logging:LogLevel:DefaultstringInformationLogging__LogLevel__DefaultNível de log padrão.
Logging:LogLevel:Microsoft.AspNetCorestringWarningLogging__LogLevel__Microsoft.AspNetCoreReduz 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:Pathstringdata/logs/bulksigner-.logLogging__File__PathREQUIRED. Template de caminho do destino de arquivo. O - final antes de .log, somado à rotação diária, produz bulksigner-yyyyMMdd.log.
Logging:File:RollingIntervalstringDayLogging__File__RollingIntervalUm de Day, Hour, Minute, Infinite.
Logging:File:FileSizeLimitByteslong50000000Logging__File__FileSizeLimitBytesLimite por arquivo; acima dele, o destino passa a escrever em …_001.log. Faixa válida: 64 KB a 10 GB.
Logging:File:RetainedFileCountLimitint14Logging__File__RetainedFileCountLimitArquivos mais antigos são apagados conforme a rotação avança. Faixa válida: 1–365.
Logging:File:MinimumLevelstringInformationLogging__File__MinimumLevelUm de Verbose, Debug, Information, Warning, Error, Fatal.
Logging:File:WriteToConsolebooltrueLogging__File__WriteToConsoleQuando 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).

ChaveTipoPadrãoOverride por envObservações
Logging:AzureTable:EnabledboolfalseLogging__AzureTable__EnabledDesligado, 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:TableNamestringbulksignerlogsLogging__AzureTable__TableNamePrecisa 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:MinimumLevelstring(herda Logging:File:MinimumLevel)Logging__AzureTable__MinimumLevelRestringe 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:ServiceUristring—Logging__AzureTable__ServiceUriREQUIRED 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:Credentialstring—Logging__AzureTable__CredentialREQUIRED quando habilitado. ManagedIdentity, ServicePrincipal ou AccountKey. Não tem valor padrão — veja abaixo.
Logging:AzureTable:AccountNamestring—Logging__AzureTable__AccountNameObrigató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:AccountKeystring—Logging__AzureTable__AccountKeySECRET. Somente no modo AccountKey.
Logging:AzureTable:TenantIdstring—Logging__AzureTable__TenantIdSomente no modo ServicePrincipal.
Logging:AzureTable:AppIdstring—Logging__AzureTable__AppIdSomente no modo ServicePrincipal.
Logging:AzureTable:AppSecretstring—Logging__AzureTable__AppSecretSECRET. Somente no modo ServicePrincipal. Override por env recomendado.
Logging:AzureTable:QueueLimitint10000Logging__AzureTable__QueueLimitEventos 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:BatchSizeLimitint100Logging__AzureTable__BatchSizeLimitEventos 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:BatchPeriodSecondsint5Logging__AzureTable__BatchPeriodSecondsQuanto 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.

Nada limpa a tabela

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.

ChaveTipoPadrãoOverride por envObservações
Database:ProviderenumSqliteDatabase__ProviderSqlite 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:DefaultstringData Source=data/db/bulksigner.dbConnectionStrings__DefaultREQUIRED 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, com Sqlite, 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​

  1. 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.
  2. Um login com as permissões abaixo, mapeado para um usuário nesse banco de dados.
  3. Criptografia que o cliente aceite. O Encrypt tem padrão True no 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, acrescente TrustServerCertificate=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.

A variável de ambiente substitui o valor inteiro — não se mescla com o JSON

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.

perigo

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/ready fica 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 o READ_COMMITTED_SNAPSHOT está desligado. Quando está ligado, nada é informado.

Comportamento específico do engine que você não configura​

  • Com Sqlite, toda conexão recebe journal_mode=WAL, synchronous=NORMAL e busy_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 de Pipeline: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.

ChaveTipoPadrãoOverride por envObservações
Signing:PkiSdkLicensestring""Signing__PkiSdkLicenseREQUIRED, SECRET. String de licença do Lacuna PKI SDK (base64). Prefira a forma por variável de ambiente.
Signing:ProfileSecretsKeystring""Signing__ProfileSecretsKeySECRET. 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:TrustLacunaTestRootboolfalseSigning__TrustLacunaTestRootConfia 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:SourceenumPfxSigning__Certificate__SourceREQUIRED. 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.
Perder esta chave significa digitar novamente o segredo de certificado de cada perfil afetado

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 usa ASPNETCORE_ENVIRONMENT=Staging (ou qualquer nome diferente de Production). O Azure App Service usa Production por 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 set do 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​

ChaveTipoPadrãoOverride por envObservações
Signing:Certificate:Pfx:Pathstring""Signing__Certificate__Pfx__PathREQUIRED a menos que Blob ou S3 esteja definido — exatamente um local. Caminho absoluto para o arquivo .pfx/.p12.
Signing:Certificate:Pfx:Passwordstring""Signing__Certificate__Pfx__PasswordSECRET. String vazia é permitida para arquivos de teste sem senha. Prefira a forma por variável de ambiente.

Signing:Certificate:Pkcs11 — quando Source = Pkcs11​

ChaveTipoPadrãoOverride por envObservações
Signing:Certificate:Pkcs11:ModulePathstring""Signing__Certificate__Pkcs11__ModulePathREQUIRED. Caminho absoluto para o driver PKCS#11 do fabricante (.so/.dll/.dylib).
Signing:Certificate:Pkcs11:Thumbprintstring""Signing__Certificate__Pkcs11__ThumbprintREQUIRED. 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:PinEnvVarstringBULK_SIGNER_PKCS11_PINSigning__Certificate__Pkcs11__PinEnvVarNome 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.

ChaveTipoPadrãoOverride por envObservações
Signing:Certificate:WindowsStore:StoreLocationstringCurrentUserSigning__Certificate__WindowsStore__StoreLocationCurrentUser 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:StoreNamestringMySigning__Certificate__WindowsStore__StoreNameNome lógico do repositório. My é o repositório pessoal.
Signing:Certificate:WindowsStore:Thumbprintstring""Signing__Certificate__WindowsStore__ThumbprintREQUIRED. 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.

ChaveTipoPadrãoOverride por envObservações
Signing:Certificate:AzureKeyVault:Endpointstring""Signing__Certificate__AzureKeyVault__EndpointREQUIRED. URL do cofre. Precisa ser uma URL https:// absoluta — um nome DNS puro é recusado na inicialização.
Signing:Certificate:AzureKeyVault:AppIdstring""Signing__Certificate__AzureKeyVault__AppIdREQUIRED. ID de aplicativo (client) do registro de aplicativo do Microsoft Entra ID.
Signing:Certificate:AzureKeyVault:AppSecretstring""Signing__Certificate__AzureKeyVault__AppSecretREQUIRED, 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:KeyNamestring""Signing__Certificate__AzureKeyVault__KeyNameREQUIRED. Nome do objeto key no cofre que executa a assinatura. Um objeto certificate do cofre não é aceito.
Signing:Certificate:AzureKeyVault:CerPathstring""Signing__Certificate__AzureKeyVault__CerPathREQUIRED 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.

ChaveTipoPadrãoOverride por envObservações
Signing:Certificate:AwsKms:Regionstring""Signing__Certificate__AwsKms__RegionREQUIRED. 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:KeyIdstring""Signing__Certificate__AwsKms__KeyIdREQUIRED. 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:Credentialenum""Signing__Certificate__AwsKms__CredentialREQUIRED. 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:AccessKeyIdstring""Signing__Certificate__AwsKms__AccessKeyIdREQUIRED para AccessKey, recusada para InstanceRole. A metade identificadora do par; não é mascarada nos logs.
Signing:Certificate:AwsKms:SecretAccessKeystring""Signing__Certificate__AwsKms__SecretAccessKeyREQUIRED 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:CerPathstring""Signing__Certificate__AwsKms__CerPathREQUIRED 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.

ChaveTipoPadrãoOverride por envObservações
Signing:Certificate:<SRC>:Blob:Urlstring(não definido)Signing__Certificate__<SRC>__Blob__UrlREQUIRED 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:Credentialenum(não definido)Signing__Certificate__<SRC>__Blob__CredentialREQUIRED 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:TenantIdstring(não definido)Signing__Certificate__<SRC>__Blob__TenantIdREQUIRED 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:AppIdstring(não definido)Signing__Certificate__<SRC>__Blob__AppIdREQUIRED para ServicePrincipal. Precisa de Storage Blob Data Reader no container.
Signing:Certificate:<SRC>:Blob:AppSecretstring(não definido)Signing__Certificate__<SRC>__Blob__AppSecretREQUIRED para ServicePrincipal, SECRET. Forma por variável de ambiente recomendada.
Signing:Certificate:<SRC>:Blob:AccountKeystring(não definido)Signing__Certificate__<SRC>__Blob__AccountKeyREQUIRED 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.
Com Pfx, o blob é a chave de assinatura

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

Mudou na 2.1.0 — um certificado que não abre não impede mais o host de iniciar

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.

ChaveTipoPadrãoOverride por envObservações
Signing:Certificate:<SRC>:S3:Bucketstring(não definido)Signing__Certificate__<SRC>__S3__BucketREQUIRED quando o bloco está presente. Exibido no banner de inicialização como s3://<Bucket>/<Key>.
Signing:Certificate:<SRC>:S3:Keystring(não definido)Signing__Certificate__<SRC>__S3__KeyREQUIRED quando o bloco está presente. A chave do objeto; diferencia maiúsculas de minúsculas.
Signing:Certificate:<SRC>:S3:Regionstring(não definido)Signing__Certificate__<SRC>__S3__RegionREQUIRED a menos que ServiceUrl esteja definido. Por exemplo, sa-east-1. Nunca é obtida do ambiente do host.
Signing:Certificate:<SRC>:S3:Credentialenum(não definido)Signing__Certificate__<SRC>__S3__CredentialREQUIRED 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:AccessKeyIdstring(não definido)Signing__Certificate__<SRC>__S3__AccessKeyIdREQUIRED para AccessKey, recusada para InstanceRole. Não é mascarada nos logs.
Signing:Certificate:<SRC>:S3:SecretAccessKeystring(não definido)Signing__Certificate__<SRC>__S3__SecretAccessKeyREQUIRED 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:ServiceUrlstring(não definido)Signing__Certificate__<SRC>__S3__ServiceUrlUm endpoint compatível com S3 (MinIO, Ceph). http:// ou https:// absoluto.
Signing:Certificate:<SRC>:S3:ForcePathStyleboolfalseSigning__Certificate__<SRC>__S3__ForcePathStyleEndereç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).

Mudou na 2.1.0 — esta seção é um seed de uso único, não a fonte da verdade

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 chamado default é derivado do bloco Signing:Certificate existente mais o Encryption:Enabled e 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. O Signing:Certificate é ignorado. Cada entrada é validada como se fosse o bloco global de certificado — as mesmas regras de Pfx / Pkcs11 / WindowsStore / AzureKeyVault / AwsKms se 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.

ChaveTipoPadrãoOverride por envObservações
Signing:Profiles[].Namestringn/aSigning__Profiles__0__NameREQUIRED. 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[].EnabledbooltrueSigning__Profiles__0__EnabledSe 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[].Formatenumn/aSigning__Profiles__0__FormatREQUIRED 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[].MethodenumLocalSigning__Profiles__0__MethodLocal (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[].VerifybooltrueSigning__Profiles__0__VerifyQuando 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[].EncryptboolfalseSigning__Profiles__0__EncryptQuando 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[].ValidateCertificatebooltrueSigning__Profiles__0__ValidateCertificateQuando 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[].PreserveFileExtensionboolfalseSigning__Profiles__0__PreserveFileExtensionQuando 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[].SaveAsPemboolfalseSigning__Profiles__0__SaveAsPemQuando 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[].SimpleP7sboolfalseSigning__Profiles__0__SimpleP7sNovo 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[].CheckCNAB240boolfalseSigning__Profiles__0__CheckCNAB240Quando 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[].CheckCnab240PaymentDatesbooltrueSigning__Profiles__0__CheckCnab240PaymentDatesNovo 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[].ApprovalaninhadoausenteSigning__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.*aninhadon/aSigning__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.Namestringn/aSigning__Profiles__0__Signer__NameREQUIRED quando Method = LacunaSigner. Nome de exibição do participante para quem o Lacuna Signer enviará o documento.
Signing:Profiles[].Signer.Emailstringn/aSigning__Profiles__0__Signer__EmailREQUIRED quando Method = LacunaSigner. E-mail do participante — precisa conter @.
Signing:Profiles[].Signer.Identifierstringn/aSigning__Profiles__0__Signer__IdentifierREQUIRED quando Method = LacunaSigner. Identificador nacional do participante (CPF no Brasil).
Signing:Profiles[].SignerFolder.Idstring (GUID)n/aSigning__Profiles__0__SignerFolder__IdNovo 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.

ChaveTipoPadrãoOverride por envObservações
…Approval.MinimumApproversint1Signing__Profiles__0__Approval__MinimumApproversO 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.ExpiresAfterTimeSpanausenteSigning__Profiles__0__Approval__ExpiresAfterPrazo 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.SignersstringProfileKeySigning__Profiles__0__Approval__SignersO 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[].Namestringn/a…__Approvers__0__NameREQUIRED. Nome de exibição. É o que o registro de auditoria mostra para este aprovador.
…Approval.Approvers[].Emailstringn/a…__Approvers__0__EmailREQUIRED, 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[].Cpfstringn/a…__Approvers__0__CpfREQUIRED. 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.
Escreva ExpiresAfter com o componente de dias

Um 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 página de aprovação por job é anônima

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​

Novo na 2.16.0

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.Id fazem 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 o Signer:Endpoint e a Signer:ApiKey deste host.
  • Verify, Encrypt, ValidateCertificate ou CheckCnab240 iguais a true — 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 que Verify e ValidateCertificate têm padrão true; por isso, uma entrada SignerFolder declara as duas como false.
  • Um bloco Approval — o fluxo do documento é decidido no Signer.
  • Uma pasta monitorada vinculada a ele — um Storage:Inputs[].Profile que 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​

ChaveTipoPadrãoOverride por envObservações
Storage:Inputs[].Profilestring?null (→ "default")Storage__Inputs__0__ProfileOpcional, 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.
Mudou na 2.2.0 — o perfil escolhe a pasta

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.

aviso

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.

Mudou na 2.1.0 — a seção é avaliada por si só, não pelos perfis existentes

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.

ChaveTipoPadrãoOverride por envObservações
Signer:Endpointstring""Signer__EndpointREQUIRED 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:ApiKeystring""Signer__ApiKeyREQUIRED, SECRET quando algum perfil usa LacunaSigner ou SignerFolder. Formato esperado: application-id|secret. O valor literal é removido dos logs.
Signer:WebhookSecretstring"" (notificações desligadas)Signer__WebhookSecretNovo 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:PollIntervalSecondsint30Signer__PollIntervalSecondsCom que frequência o worker de polling percorre cada linha AwaitingSigner. Faixa válida: 1–3600.
Signer:TimeoutHoursint168 (7 dias)Signer__TimeoutHoursQuanto tempo um job pode ficar em AwaitingSigner antes de falhar com code = signer.timeout. Faixa válida: 1–8760.
Signer:MaxConsecutiveApiFailuresint5Signer__MaxConsecutiveApiFailuresLimite 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:FolderSweepIntervalSecondsint300 (5 minutos)Signer__FolderSweepIntervalSecondsNovo 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.

ChaveTipoPadrãoOverride por envObservações
WebPki:Licensestring""WebPki__LicenseObrigató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.

ChaveTipoPadrãoOverride por envObservações
CloudHub:ApiKeystring""CloudHub__ApiKeySECRET. 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:Endpointstringhttps://cloudhub.lacunasoftware.com/CloudHub__EndpointOnde 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:PublicBaseUrlstring""CloudHub__PublicBaseUrlREQUIRED 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.

ChaveTipoPadrãoOverride por envObservações
Encryption:EnabledboolfalseEncryption__EnabledChave geral. Quando verdadeira, o worker criptografa o artefato assinado com AES-256-GCM entre a verificação e a promoção.
Encryption:Passwordstring""Encryption__PasswordSECRET. 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:PasswordEnvVarstringBULK_SIGNER_ENCRYPTION_PASSWORDEncryption__PasswordEnvVarNome da variável de ambiente que fornece a senha. Se não estiver vazia no boot, ela prevalece sobre Encryption:Password.
Encryption:Saltstring""Encryption__SaltREQUIRED 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:Iterationsint600000Encryption__IterationsContagem de iterações do PBKDF2-HMAC-SHA256. Rejeitada abaixo de 10000.
perigo

A perda da senha é irrecuperável — não há endpoint de descriptografia no servidor nem custódia de chaves. Veja Criptografia.

Auth​

ChaveTipoPadrãoOverride por envObservações
Auth:ApiKeystring""Auth__ApiKeyREQUIRED, 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:CookieNamestringlbs-authAuth__CookieNameNome do cookie emitido por /api/auth/login. SameSite=Strict, HttpOnly, marcado como seguro quando a requisição foi HTTPS.
Auth:ApiKeyHeaderstringX-API-KeyAuth__ApiKeyHeaderHeader 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.

ChaveTipoPadrãoOverride por envObservações
Auth:SessionKeyRing:PlacementstringausenteAuth__SessionKeyRing__PlacementDataRoot 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:

  • DataRoot com Cluster:Enabled = true. Um ring dentro de Storage:Root pertence 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.
  • OperationalStore com Database:Provider = Sqlite (inclusive com o provider ausente, que é SQLite). O arquivo do banco fica no mesmo Storage:Root que keys/, então a mudança não sobrevive a nada a que keys/ não sobreviveria, e, no Windows, abre mão da criptografia DPAPI em troca de nada. Use SqlServer ou deixe o ring em keys/.

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 com OperationalStore começ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 de ApproverPortal: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 em keys/, 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á.

ChaveTipoPadrãoOverride por envObservações
Auth:EntraId:TenantIdstring(ausente)Auth__EntraId__TenantIdREQUIRED 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:ClientIdstring(ausente)Auth__EntraId__ClientIdREQUIRED 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:ClientSecretstring(ausente)Auth__EntraId__ClientSecretREQUIRED 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​

OndeSeção ausente (padrão)Seção presente
/loginFormulário de chave de APIBotão Entrar com a Microsoft. O formulário de chave de API deixa de aparecer.
POST /api/auth/loginTroca a chave de API por um cookieNão emite cookie nem para uma chave correta. Desligado, não apenas escondido.
Cookies de operador existentesVálidos durante a janela deslizante de 8 horasDeixam de atender às políticas imediatamente. Planeje a transição como um "desconectar todo mundo".
REST X-API-KeyFuncionaInalterado. Clientes automatizados nunca percebem o modo.
Páginas de operadorQualquer cookie autenticadoExige a app role Administrator.
/approvals e a página de aprovação por jobSomente o link do portal do aprovadorLink ou uma sessão com a role Approver; o pool congelado continua determinando quais jobs ficam visíveis.
Identificação registrada na aprovaçãoSelfDeclaredEmail / LinkDerivedEmailAcrescenta EntraIdEmail para decisões tomadas em uma sessão do Entra.
SairLimpa o cookieLimpa 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 roleDá acesso aPágina de destino após o login
AdministratorTodas as páginas e ações de operador que o cookie de chave de API concede hoje. Sem níveis./
ApproverSomente 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.

Atualizando de uma versão anterior à 2.2.1

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​

ChaveTipoPadrãoOverride por envObservações
Storage:RootstringdataStorage__RootREQUIRED. 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:ProviderenumLocalFileSystemStorage__ProviderLocalFileSystem, 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:AzureFilesobjetoausenteStorage__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:AzureBlobobjetoausenteStorage__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:S3objetoausenteStorage__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 herda Storage:Provider quando 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/ e error/ — é um único lugar, em um único provider (Storage:Provider). A promoção, a movimentação para a pasta de erro e a devolução .reject movem 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 com error/ 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.

ProviderPasta de entradaCompartilhamento de trabalhoLease de um arquivoUma movimentação éCom Cluster:EnabledUma pasta existe quando
LocalFileSystem (padrão)Observada pelo FileSystemWatcher; polling só se a pasta indicar um intervaloDiretórios dentro de Storage:RootControle interno deste processoUma renomeaçãoRecusadoO produto a cria
AzureFilesPollingUm compartilhamento, com prefixo Directory opcionalMantido pelo serviçoUma renomeação dentro do compartilhamentoAdmitidoO produto a cria
AzureBlobPollingUm container, com Prefix opcionalMantido pelo serviço (um lease de blob)Uma cópia no servidor seguida de uma exclusão — recuperável, não atômicaAdmitidoO blob marcador dela existe — o de uma pasta de entrada nunca é criado pelo produto
S3PollingUm bucket, com Prefix opcionalNenhum — toda exclusão é condicionada a um ETag, e uma cópia preparada é comparada com a impressão digital dela antes da assinaturaUma cópia no servidor seguida de uma exclusão — recuperável, não atômicaRecusadoO 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)​

  • Name precisa 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.
  • Path nã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 chamada default em {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/.

ChaveTipoPadrãoOverride por envObservações
Storage:AzureFiles:AccountNamestringn/aStorage__AzureFiles__AccountNameREQUIRED quando um provider resolve para AzureFiles. Nome da conta sem sufixo; o endpoint é https://<AccountName>.file.core.windows.net.
Storage:AzureFiles:ShareNamestringn/aStorage__AzureFiles__ShareNameREQUIRED. O compartilhamento de trabalho. Somente protocolo SMB — um compartilhamento NFS é recusado no boot, com a indicação do nome.
Storage:AzureFiles:Directorystring?null (raiz do compartilhamento)Storage__AzureFiles__DirectoryPrefixo 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:Credentialenumn/aStorage__AzureFiles__CredentialREQUIRED 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 / :AppIdstringn/aStorage__AzureFiles__TenantId, …__AppIdSomente no modo ServicePrincipal.
Storage:AzureFiles:AppSecretstringn/aStorage__AzureFiles__AppSecretSECRET. Somente no modo ServicePrincipal. Permitido na configuração; override por ambiente recomendado.
Storage:AzureFiles:AccountKeystringn/aStorage__AzureFiles__AccountKeySECRET. 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.

Um quarto item aparece no compartilhamento de trabalho, e não é uma pasta

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.

ChaveTipoPadrãoOverride por envObservações
Storage:Inputs[N].Providerenum?herda Storage:ProviderStorage__Inputs__N__ProviderLocalFileSystem, 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].Pathstringn/aStorage__Inputs__N__PathREQUIRED. 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:*objetoherda Storage:AzureFiles campo a campoStorage__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].PollIntervalSecondsint?herda WatchedFolder:PollIntervalSecondsStorage__Inputs__N__PollIntervalSecondsNa 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.
Se uma pasta faz polling e com que frequência são duas questões separadas

É 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. contabil sobrescreve ShareName e nada mais, então AccountName, Credential e 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 AccountKey gera 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 cujo Path seja 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 como Completed.
  • As duas pastas remotas definem o próprio intervalo, e a local deliberadamente não define nenhum. É por não definir intervalo que o legacy continua orientado a eventos.
  • O banner confirma, exibindo azure credential = AccountKey, work share = contosofiles/bulksigner, o provider e o local de cada pasta (input providers) e azure 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.

ChaveTipoPadrãoOverride por envObservações
Storage:Inputs[N].Pathstringn/aStorage__Inputs__N__PathREQUIRED. 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:ContainerUrlstringherda Storage:AzureBlob:ContainerUrlStorage__Inputs__N__AzureBlob__ContainerUrlREQUIRED. 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:CredentialenumherdaStorage__Inputs__N__AzureBlob__CredentialREQUIRED 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 / :AppIdstringherda…__AzureBlob__TenantId, …__AppIdSomente no modo ServicePrincipal.
Storage:Inputs[N].AzureBlob:AppSecretstringherdaStorage__Inputs__N__AzureBlob__AppSecretSECRET. Somente no modo ServicePrincipal; override por ambiente recomendado. Mascarado em todos os logs.
Storage:Inputs[N].AzureBlob:AccountKeystringherdaStorage__Inputs__N__AzureBlob__AccountKeySECRET. 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].PollIntervalSecondsint?herda WatchedFolder:PollIntervalSecondsStorage__Inputs__N__PollIntervalSecondsNa 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 com If-None-Match: * sobre um blob existente, uma gravação e uma exclusão com If-Match em 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 e Storage: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/ready mostra uma linha vermelha storage-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/ready e na página Sistema, cada um indicando o comando az storage blob upload … --metadata hdi_isfolder=true acima 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/.

ChaveTipoPadrãoOverride por envObservações
Storage:AzureBlob:ContainerUrlstringn/aStorage__AzureBlob__ContainerUrlREQUIRED. 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:Prefixstring?null (raiz do container)Storage__AzureBlob__PrefixOpcional, 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:Credentialenumn/aStorage__AzureBlob__CredentialREQUIRED 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/ ou error/, 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 .reject e a movimentação para a pasta de erro copiam com o Copy Blob From URL síncrono e If-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. Com ManagedIdentity ou ServicePrincipal, 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 em output/. A recuperação na inicialização conclui um job cujo artefato assinado já está em output/, e completa em uma única pasta o error/<id>/ copiado pela metade de um job que falhou, em vez de dividi-lo. Só a devolução .reject não é reconciliada (ela acontece depois que o job terminou); a cópia que sobrar dela em processing/ é 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.

ChaveTipoPadrãoOverride por envObservações
Storage:Inputs[N].Pathstringn/aStorage__Inputs__N__PathREQUIRED. 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:BucketNamestringherda Storage:S3:BucketNameStorage__Inputs__N__S3__BucketNameREQUIRED. Um único nome de bucket, sem /.
Storage:Inputs[N].S3:RegionstringherdaStorage__Inputs__N__S3__RegionREQUIRED 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:ServiceUrlstringherdaStorage__Inputs__N__S3__ServiceUrlUm 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:ForcePathStylebool?herda; senão, falseStorage__Inputs__N__S3__ForcePathStyleEndereç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:CredentialenumherdaStorage__Inputs__N__S3__CredentialREQUIRED 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:AccessKeyIdstringherdaStorage__Inputs__N__S3__AccessKeyIdSomente no modo AccessKey. Não é um segredo por si só.
Storage:Inputs[N].S3:SecretAccessKeystringherdaStorage__Inputs__N__S3__SecretAccessKeySECRET. 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].PollIntervalSecondsint?herda WatchedFolder:PollIntervalSecondsStorage__Inputs__N__PollIntervalSecondsNa 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 — PutObject e CopyObject com If-None-Match: * sobre um objeto existente, PutObject e DeleteObject com If-Match em 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 um 400 que recusa a requisição como inválida) em vez de avaliá-la, causa uma recusa de boot que indica essa condição e Storage: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/ready mostra uma linha vermelha storage-bucket:<endpoint>/<bucket> (um retrato do boot) ao lado da linha ao vivo da própria pasta. O probe precisa de s3:PutObject e s3:DeleteObject sob 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/ready e na página Sistema; um marcador ausente indica o comando aws s3api put-object acima para aquele bucket e prefixo, e nada o recria.
  • As falhas são classificadas pelo Code de 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 do Storage:Inputs[N].Provider da 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/.

ChaveTipoPadrãoOverride por envObservações
Storage:S3:BucketNamestringn/aStorage__S3__BucketNameREQUIRED.
Storage:S3:Prefixstring?null (raiz do bucket)Storage__S3__PrefixOpcional, 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 / ForcePathStyleStorage__S3__…Como em uma pasta: Region é REQUIRED a menos que ServiceUrl esteja definido, sem valor padrão.
Storage:S3:Credential (+ AccessKeyId, SecretAccessKey)enumn/aStorage__S3__Credential, Storage__S3__SecretAccessKeyREQUIRED 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/ ou error/, 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 .reject e a movimentação para a pasta de erro fazem um CopyObject com If-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 do CopyObject é copiado em partes e concluído com o mesmo If-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 para error/ 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 durante AwaitingApproval — continua falhando com approval.content-changed.
  • O envelope assinado de um aprovador (<arquivo>.approvals.p7m) é substituído por um PutObject condicionado com If-Match ao ETag com que foi lido — e criado com If-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-owner do /api/ready continua 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 de Storage: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​

ChaveTipoPadrãoOverride por envObservações
Pipeline:PollIntervalSecondsint2Pipeline__PollIntervalSecondsCom 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:MaxConcurrencyint1Pipeline__MaxConcurrencyLimite 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:RejectAlreadyProcessedFileNamesbooltruePipeline__RejectAlreadyProcessedFileNamesNovo 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.

ChaveTipoPadrãoOverride por envObservações
WatchedFolder:StabilityPollIntervalMsint500WatchedFolder__StabilityPollIntervalMsIntervalo entre verificações de estabilidade. Faixa válida: 50–10000.
WatchedFolder:StabilityRequiredSamplesint3WatchedFolder__StabilityRequiredSamplesAmostras idênticas consecutivas necessárias antes do enfileiramento. Faixa válida: 1–100.
WatchedFolder:StabilityConcurrencyint8WatchedFolder__StabilityConcurrencyQuantos 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:StabilityTimeoutSecondsint60WatchedFolder__StabilityTimeoutSecondsEspera máxima antes de desistir de um arquivo que nunca se estabiliza. Faixa válida: 1–3600.
WatchedFolder:PollIntervalSecondsint?30WatchedFolder__PollIntervalSecondsCom 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:IgnoredExtensionsarray[".tmp", ".part", ".crdownload", ".swp"]n/a (use a configuração)Extensões de arquivo que o observador ignora por completo.
WatchedFolder:IgnoredPrefixesarray[".", "~$"]n/a (use a configuração)Prefixos de nome de arquivo que o observador ignora (dotfiles, arquivos de lock do Office).

Upload​

ChaveTipoPadrãoOverride por envObservações
Upload:EnabledbooltrueUpload__EnabledNovo 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:MaxByteslong104857600 (100 MiB)Upload__MaxBytesLimite 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​

ChaveTipoPadrãoOverride por envObservações
Dashboard:PollIntervalSecondsint5Dashboard__PollIntervalSecondsIntervalo 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.

ChaveTipoPadrãoOverride por envObservações
Branding:CustomerLogo:Pathstring—Branding__CustomerLogo__PathCaminho 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:Urlstring""Branding__CustomerLogo__Blob__UrlURL 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:Credentialstring—Branding__CustomerLogo__Blob__CredentialManagedIdentity, 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 / AppSecretstring—Branding__CustomerLogo__Blob__…Somente no modo ServicePrincipal; os três são obrigatórios. AppSecret é SECRET — defina-o pelo ambiente.
Branding:CustomerLogo:Blob:AccountKeystring—Branding__CustomerLogo__Blob__AccountKeySomente 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.

ChaveTipoPadrãoOverride por envObservações
ApproverPortal:EnabledboolfalseApproverPortal__EnabledChave geral. Quando falsa, nenhum link é resolvido, nenhuma sessão é emitida, e a única forma de aprovação é o link por job.
ApproverPortal:LinkSecretstring—ApproverPortal__LinkSecretREQUIRED 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:DecidedLookbackTimeSpan90.00:00:00ApproverPortal__DecidedLookbackAté 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:SessionLifetimeTimeSpan30.00:00:00ApproverPortal__SessionLifetimeTempo 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:PollIntervalTimeSpan00:00:10ApproverPortal__PollIntervalCom 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.

Mudou na 2.1.0 — um aviso, não mais uma recusa

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​

ChaveTipoPadrãoOverride por envObservações
Console:Dashboard:EnabledbooltrueConsole__Dashboard__EnabledSe 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.

ChaveTipoPadrãoOverride por envObservações
LogViewer:EnabledbooltrueLogViewer__EnabledChave 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:MaxEntriesint20LogViewer__MaxEntriesTamanho 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:RefreshIntervalSecondsint5LogViewer__RefreshIntervalSecondsIntervalo 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:Levelsstring[]["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​

ChaveTipoPadrãoOverride por envObservações
Readiness:RequireApiKeyboolfalseReadiness__RequireApiKeyNovo 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.
Mudou na 2.6.0 — o probe anônimo responde com um veredito, não com uma descrição

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​

ChaveTipoPadrãoOverride por envObservações
Metrics:RequireApiKeybooltrueMetrics__RequireApiKeyQuando 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​

ChaveTipoPadrãoOverride por envObservações
Statistics:EnabledbooltrueStatistics__EnabledChave 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.

ChaveTipoPadrãoOverride por envObservações
Backup:EnabledboolfalseBackup__EnabledChave 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:DestinationstringDiskBackup__DestinationDisk, 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:IntervalHoursint?(ausente)Backup__IntervalHoursCom 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:RetainCountint14Backup__RetainCountQuantos 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:Pathstring—Backup__Disk__PathREQUIRED 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:BucketNamestring—Backup__S3__BucketNameREQUIRED quando Destination = S3.
Backup:S3:Regionstring—Backup__S3__RegionREQUIRED 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:Prefixstring""Backup__S3__PrefixPrefixo de chave dentro do bucket. Normalizado — bulksigner, bulksigner/ e /bulksigner/ significam a mesma coisa.
Backup:S3:Credentialstring—Backup__S3__CredentialREQUIRED 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:AccessKeyIdstring—Backup__S3__AccessKeyIdREQUIRED 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:SecretAccessKeystring—Backup__S3__SecretAccessKeyREQUIRED quando Credential = AccessKey. SECRET. Defini-lo com InstanceRole causa uma recusa de boot.
Backup:S3:ServiceUrlstring—Backup__S3__ServiceUrlUm 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:ForcePathStyleboolfalseBackup__S3__ForcePathStyleEndereç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:ContainerUrlstring—Backup__AzureBlob__ContainerUrlREQUIRED 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:Prefixstring""Backup__AzureBlob__PrefixPrefixo de nome de blob dentro do container. Normalizado como o do S3.
Backup:AzureBlob:Credentialstring—Backup__AzureBlob__CredentialREQUIRED 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 / AppIdstring—Backup__AzureBlob__TenantId, …REQUIRED quando Credential = ServicePrincipal.
Backup:AzureBlob:AppSecretstring—Backup__AzureBlob__AppSecretREQUIRED 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:AccountKeystring—Backup__AzureBlob__AccountKeyREQUIRED 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.

ChaveTipoPadrãoOverride por envObservações
Cluster:EnabledboolfalseCluster__EnabledChave 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:HeartbeatSecondsint15Cluster__HeartbeatSecondsCom 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:StaleAfterSecondsint60Cluster__StaleAfterSecondsQuanto 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:Provider precisa ser SqlServer. 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 — AzureFiles ou AzureBlob (AzureBlob desde 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 pasta default, criada automaticamente sem configuração, é local, então uma primeira execução com a chave ligada também é recusada. O S3 é recusado para os dois, com a indicação de Storage:Provider ou do Storage:Inputs[N].Provider da 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 de Storage:Root pertence a um único host.
  • As origens de certificado por host Pkcs11 e WindowsStore sã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. Use Pfx (de preferência lido de um blob ou de um objeto S3), AzureKeyVault ou AwsKms. Um bloco Certificate obsoleto em um perfil com Method = LacunaSigner continua 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 de Signing:Profiles[] ou para o bloco legado Signing: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-instance vermelha 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 é assumido Cluster:StaleAfterSeconds depois 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.

Mudou na 2.5.0 — deslocada, não mais recusada

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.

ChaveTipoPadrãoOverride por envObservações
Telemetry:EnabledboolfalseTelemetry__EnabledChave geral. Quando true, uma connection string é obrigatória — a inicialização falha sem ela.
Telemetry:ConnectionStringstring?nullTelemetry__ConnectionStringSECRET. Connection string do Application Insights. Deixe-a sem definir para fornecê-la pela variável de ambiente padrão APPLICATIONINSIGHTS_CONNECTION_STRING.
Telemetry:RoleNamestringLacuna.BulkSignerTelemetry__RoleNameInformado 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.

ChaveTipoPadrãoOverride por envObservações
RateLimiting:EnabledbooltrueRateLimiting__EnabledChave geral. Desabilite em instalações em rede fechada.
RateLimiting:Upload:PermitsPerWindowint30RateLimiting__Upload__PermitsPerWindowRequisições permitidas por janela em POST /api/files.
RateLimiting:Upload:WindowSecondsint60RateLimiting__Upload__WindowSecondsDuração da janela da política de upload.
RateLimiting:Upload:QueueLimitint0RateLimiting__Upload__QueueLimitQuantas requisições acima do limite esperam, em vez de serem rejeitadas imediatamente. 0 = rejeitar imediatamente.
RateLimiting:Actions:PermitsPerWindowint60RateLimiting__Actions__PermitsPerWindowRequisições permitidas por janela nos endpoints de ação (nova tentativa, cancelar, nova varredura, limpeza, pausar, retomar).
RateLimiting:Actions:WindowSecondsint60RateLimiting__Actions__WindowSecondsDuração da janela da política de ações.
RateLimiting:Actions:QueueLimitint0RateLimiting__Actions__QueueLimitTamanho da fila da política de ações.
RateLimiting:Approval:*mesmo formato10 por 60 sRateLimiting__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 formato10 por 60 sRateLimiting__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 formato600 por 60 sRateLimiting__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​

ChaveTipoPadrãoOverride por envObservações
Hosting:RequireHttpsboolfalseHosting__RequireHttpsControla 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:EnabledboolfalseHosting__ForwardedHeaders__EnabledLê 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:TrustAnyProxyboolfalseHosting__ForwardedHeaders__TrustAnyProxyAceita 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:KnownProxiesstring[][]Hosting__ForwardedHeaders__KnownProxies__0Endereç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:KnownNetworksstring[][]Hosting__ForwardedHeaders__KnownNetworks__0Faixas 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:ForwardLimitint1Hosting__ForwardedHeaders__ForwardLimitQuantas 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.

ChaveTipoPadrãoOverride por envObservações
ApproverSecondFactor:EnabledboolfalseApproverSecondFactor__EnabledChave 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:SeedSecretstring—ApproverSecondFactor__SeedSecretREQUIRED 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:VerificationWindowTimeSpan00:20:00ApproverSecondFactor__VerificationWindowPor 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:IssuerstringLacuna Bulk SignerApproverSecondFactor__IssuerO 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.
Sempre escreva o componente de dias em VerificationWindow

Um 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:

  1. SeedSecret está 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.
  2. 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.
  3. ApproverPortal:Enabled é falso e nenhuma seção Auth:EntraId está 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​

ChaveTipoPadrãoOverride por envObservações
AllowedHostsstring*AllowedHostsFiltragem 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ávelFinalidade
BULK_SIGNER_CONFIG_DIRIndica 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_PINO PIN do HSM/token — lido da variável de ambiente cujo nome é configurado em Signing:Certificate:Pkcs11:PinEnvVar.
BULK_SIGNER_ENCRYPTION_PASSWORDSenha do PBKDF2 — lida da variável de ambiente cujo nome é configurado em Encryption:PasswordEnvVar.
APPLICATIONINSIGHTS_CONNECTION_STRINGVariá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_ENVIRONMENTNome 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_URLSPadrão. Os scripts de instalação definem http://0.0.0.0:8080.
ASPNETCORE_CONTENTROOTPadrã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 bannerAparece quando
store status, store isolationDatabase:Provider = SqlServer
work share, azure credential, azure shares, input providers, work share ownerStorage:Provider = AzureFiles, ou qualquer pasta de entrada que o indique
blob=… em uma linha de perfilo certificado desse perfil é lido do Azure Blob Storage
cnab240=on, approval=N/M, expires=… em uma linha de perfilCheckCNAB240 / Approval nesse perfil
prefixo DEGRADED · em uma linha de perfil, com uma linha FAIL ao ladoo 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.

Ver em Markdown