Assinando documentos do Lacuna Signer no Amazon ECS Fargate
Esta página especifica uma implantação do Bulk Signer em instância única na Amazon Web Services que assina documentos criados no Lacuna Signer. Uma pessoa cria um documento em uma pasta do Signer vinculada a um perfil de assinatura do Bulk Signer; o Bulk Signer o assina com o certificado da organização, cuja chave privada está armazenada no AWS KMS, e o Signer mantém o documento assinado. A implantação é composta por:
- a imagem de container do Bulk Signer, executada como uma task do Amazon ECS no AWS Fargate, atrás de um Application Load Balancer;
- o banco de dados operacional no Amazon RDS for SQL Server;
- o compartilhamento de trabalho e o certificado de assinatura em um bucket do Amazon S3;
- a chave de assinatura no AWS KMS;
- um perfil de assinatura que usa o
método
SignerFolder; - o webhook
Signer:WebhookSecret, pelo qual o Signer notifica o Bulk Signer sobre documentos novos.
A infraestrutura é a especificada em Amazon ECS Fargate. Esta página repete aquele procedimento
por inteiro, para que ele possa ser executado em uma conta AWS vazia, e acrescenta a integração com o
Lacuna Signer. Em um ambiente já implantado a partir daquela página, aplicam-se somente os seguintes
passos: o passo 1, os dois segredos do Lacuna Signer no
passo 7, as configurações Signer no passo 9, uma nova implantação e os
passos 11 a 13.
O procedimento foi validado em um ambiente de homologação: um PDF criado no Lacuna Signer foi assinado pela task do Fargate com um certificado e-CNPJ cuja chave estava armazenada no AWS KMS, em um job com tempo total de processamento de 1,2 segundo.
O método SignerFolder e a chave Signer:WebhookSecret foram introduzidos na 2.16.0. O
provider de armazenamento S3, a
origem de certificado AwsKms e a chave
Auth:SessionKeyRing:Placement
foram introduzidos na 2.20.0. Imagens anteriores recusam a configuração desta página no boot.
O modo cluster é suportado somente no Azure App Service, e um compartilhamento de
trabalho no S3 é recusado quando Cluster:Enabled está definido. O serviço do ECS do passo 9 é
configurado para que duas tasks nunca sejam executadas ao mesmo tempo, inclusive durante uma
implantação. Implantações que exigem mais de uma instância devem usar o
Azure App Service (modo cluster).
Arquitetura
Conexões
Em relação a uma implantação que assina somente arquivos, três conexões são acrescentadas. Nenhuma delas é um recurso da AWS.
| Direção | Conteúdo | Credencial |
|---|---|---|
| Signer → balanceador de carga → task | O webhook: POST https://<host>/api/signer/notifications | Signer:WebhookSecret, enviado como Authorization: Bearer |
| Task → Signer | A varredura, a releitura de cada documento, e o início e a conclusão de cada assinatura | Signer:ApiKey, enviada como X-Api-Key |
| Task → KMS | kms:Sign, uma chamada por documento assinado | O task role |
Diferenças em relação à assinatura de arquivos
| Aspecto | Comportamento com o método SignerFolder |
|---|---|
| Conteúdo do documento | Nenhum arquivo passa pelo Bulk Signer. O Signer envia o hash a assinar, a task o assina no KMS, e o Signer incorpora a assinatura ao documento e o mantém. Nada entra em uma pasta monitorada e nada é gravado em work/output/. O documento assinado é baixado no Signer. |
| Identificação do titular | O Signer associa o participante do documento ao CPF contido no certificado do perfil. O certificado deve, portanto, ser um e-CPF ou um e-CNPJ da ICP-Brasil (que contém o CPF do responsável). |
| Formato da assinatura | O Signer seleciona o formato: PAdES para um PDF, XAdES para XML, CAdES para qualquer outro tipo. A configuração de formato do próprio perfil é ignorada. |
Função do Signer:WebhookSecret no desempenho
Mecanismos de descoberta
O Bulk Signer tem dois mecanismos para detectar um documento que aguarda a assinatura dele:
| Mecanismo | Funcionamento | Disponibilidade |
|---|---|---|
| Varredura | A cada Signer:FolderSweepIntervalSeconds (padrão: 300 segundos), a task lista os documentos pendentes de cada pasta vinculada e relê cada um no Signer. | Sempre ativa. Exige somente Signer:ApiKey. |
| Notificação | O Signer chama POST /api/signer/notifications quando um documento é criado, assinado ou aprovado. A task enfileira uma checagem para o documento citado na chamada, responde imediatamente e relê esse documento. | Somente quando Signer:WebhookSecret está definido. Sem ele, a rota retorna 404. |
Efeito da configuração
| Aspecto | Somente a varredura | Com Signer:WebhookSecret |
|---|---|---|
| Tempo até um documento ser detectado | Até o intervalo da varredura: de 0 a 5 minutos por padrão, 2,5 minutos em média. | Segundos. |
| Chamadas à API do Signer | Em cada passagem, uma listagem por pasta vinculada mais uma leitura por documento pendente nela, tenha havido mudança ou não. | Uma leitura por documento notificado. |
| Fluxos com vários participantes | Cada passagem de vez para o titular aguarda uma varredura. | A notificação da etapa anterior à do titular é o evento pelo qual a vez do titular é detectada, em segundos. |
O tempo de detecção também pode ser reduzido, sem o webhook, com um valor menor em
Signer:FolderSweepIntervalSeconds. O mínimo é de 60 segundos, e cada passagem repete a mesma listagem e
as mesmas leituras. No mínimo, o número de chamadas ao Signer é cinco vezes o do padrão, e um documento
ainda espera até um minuto. Com o webhook, o número de chamadas é proporcional ao número de eventos, e o
intervalo da varredura pode permanecer no padrão ou ser aumentado.
Alcance
| Propriedade | Especificação |
|---|---|
| Duração da assinatura | Não é afetada. A configuração altera o tempo até um documento ser detectado, não o tempo necessário para assiná-lo. No ambiente de homologação, um job levou 1,2 segundo, incluída a espera na fila. A vazão é determinada por Pipeline:MaxConcurrency e pela cota do KMS. |
| Garantia de entrega | É dada pela varredura, não pela notificação. Se o Signer não conseguir acessar o balanceador de carga, ou se uma entrega se perder, o documento é assinado na varredura seguinte. Um documento atrasa, nunca se perde. |
| Confiança na notificação | Nenhuma. Uma notificação identifica qual documento ler; nenhum outro conteúdo dela é usado. O documento é relido no Signer com Signer:ApiKey antes de um job ser criado. |
Propriedades de segurança
A rota é pública, e o segredo é o único elemento que distingue uma chamada do Signer de qualquer outra chamada.
| Propriedade | Especificação |
|---|---|
| Tamanho mínimo | 32 caracteres. Espaço em branco no início ou no fim é recusado no boot. |
| Comparação | Em tempo constante. |
| Chamada sem o segredo | 401. |
| Efeito de uma chamada forjada que apresente o segredo | No máximo, a task lê um documento no Signer. |
Pré-requisitos
| Requisito | Especificação |
|---|---|
| Ferramentas | AWS CLI v2 autenticado (aws sts get-caller-identity precisa responder), Docker para copiar a imagem e jq. Para a importação da chave no passo 3: PowerShell 7 e OpenSSL 3.x (incluído no Git for Windows). |
| Permissões do IAM | Permissão para criar recursos no ECR, ECS, IAM (iam:CreateRole, iam:PutRolePolicy, iam:PassRole), KMS, RDS, S3, Secrets Manager, Elastic Load Balancing, ACM, security groups do EC2 e CloudWatch Logs. A política PowerUserAccess, sozinha, é insuficiente: ela não tem iam:CreateRole. |
| Credenciais de registry | As credenciais que a Lacuna emitiu para a imagem de container. Veja Obtendo o produto. |
| Licença | A licença do PKI SDK. |
| DNS | Um hostname que o Signer consiga acessar pela internet e permissão de escrita na zona de DNS autoritativa para ele. Confirme a delegação com dig +short NS example.com: um domínio pode ter uma hosted zone do Route 53 na conta e ainda assim estar delegado a outro provedor. |
| Organização no Lacuna Signer | Uma organização em uma instância do Lacuna Signer e um usuário com o papel de administrador da organização. Esse papel é exigido para cadastrar o webhook. |
| Chave de API do Lacuna Signer | Uma chave de API dessa organização (item 1.2). |
| Material de assinatura | O PFX de um certificado e-CPF ou e-CNPJ da ICP-Brasil, com chave RSA 2048, 3072 ou 4096, e a senha dele. Um certificado que não contém CPF é recusado na criação do perfil. |
| Identificação do titular | O CPF contido no certificado e um e-mail do titular. Os documentos devem incluir o titular como participante exatamente com esse CPF. |
Variáveis de shell
Todos os comandos desta página são executados em uma mesma sessão de shell, inicializada com estas variáveis:
REGION=sa-east-1
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
NAME=bulksigner # prefixo de todos os recursos
BUCKET=$NAME-$ACCOUNT # nomes de bucket são globais
HOST=bulksigner.example.com # o hostname público
SIGNER=https://<endereço do Lacuna Signer>
PROFILE=aws-signer # o perfil que assina na pasta do Signer
VPC=$(aws ec2 describe-vpcs --region $REGION --filters Name=is-default,Values=true --query 'Vpcs[0].VpcId' --output text)
SUBNETS=$(aws ec2 describe-subnets --region $REGION --filters Name=vpc-id,Values=$VPC --query 'Subnets[].SubnetId' --output text | tr '\t' ',')
VPC_CIDR=$(aws ec2 describe-vpcs --region $REGION --vpc-ids $VPC --query 'Vpcs[0].CidrBlock' --output text)
| Variável | Conteúdo |
|---|---|
REGION | A região da AWS de todos os recursos. |
ACCOUNT | O ID da conta da AWS da identidade autenticada. |
NAME | O prefixo do nome de todos os recursos. |
BUCKET | O nome do bucket. Inclui o ID da conta porque nomes de bucket do S3 são únicos globalmente. |
HOST | O hostname público da implantação. O Signer faz chamadas a ele. |
SIGNER | O endereço da API do Lacuna Signer (item 1.1). |
PROFILE | O nome do perfil de assinatura vinculado à pasta do Signer. Não pode ser default. |
VPC, SUBNETS, VPC_CIDR | A VPC padrão, os IDs das sub-redes dela (separados por vírgula) e o bloco CIDR dela. |
Posicionamento de rede
O procedimento usa a VPC padrão e as sub-redes públicas dela. A task recebe um IP público para as chamadas de saída (ECR, KMS, S3, Secrets Manager, Lacuna Signer), e o security group dela admite tráfego de entrada somente do balanceador de carga. Em sub-redes privadas, a task exige um NAT gateway: o Lacuna Signer não é um serviço da AWS e não pode ser acessado por um VPC endpoint.
1. Preparação do Lacuna Signer
Os quatro itens a seguir são definidos no Lacuna Signer antes da criação de qualquer recurso na AWS.
1.1 Endereço da API
SIGNER é o endereço em que a API da instância do Signer responde, que não é necessariamente o endereço
da página inicial do produto. Sem uma chave, a API retorna 401:
curl -s -o /dev/null -w '%{http_code}\n' $SIGNER/api/documents # 401
1.2 Chave de API
Crie uma aplicação na organização e gere a chave dela, conforme especificado no passo 1 de Primeiros passos com a API do Signer.
| Propriedade | Especificação |
|---|---|
| Acesso ao menu Aplicações | Restrito ao administrador de sistema da instância. Sem esse acesso, solicite uma chave da organização ao administrador da instância do Signer. |
| Formato | <aplicação>|<segredo>. A parte da aplicação pode conter espaços. |
| Exibição | A chave é exibida uma única vez, na criação. |
| Escopo | A organização em que foi criada. O seletor de pastas do passo 11 lista somente as pastas dessa organização, e a task lê os documentos com essa chave. |
| Acesso exigido | Um papel que dê acesso às pastas e aos documentos da organização. |
1.3 Pasta
Em Pastas, selecione a organização e crie uma pasta com o ícone Criar pasta.
| Regra | Especificação |
|---|---|
| Vínculo | De um para um. Dois perfis não podem compartilhar uma pasta. |
| Alcance | A própria pasta. Um documento em uma subpasta dela não é assinado. |
As capturas de tela desta página usam uma pasta chamada AWSTest.
1.4 Política de assinatura em PDF
Se um PDF assinado com um certificado ICP-Brasil recebe a política da ICP-Brasil (AD-RB) ou uma assinatura
PAdES básica é determinado pela configuração UseBrazilianPdfSigningPolicies do Signer, e não pelo Bulk
Signer. A configuração pertence à instância do Signer:
| Instância do Signer | Onde a configuração é definida |
|---|---|
| On-premises | Nas configurações da instância. |
| Nuvem da Lacuna | Pela Lacuna. Solicite a confirmação do valor em vigor. |
Defina a política antes de o primeiro documento ser assinado.
2. O bucket
Crie o bucket, bloqueie todo acesso público, cancele uploads multipart incompletos após um dia e crie a pasta monitorada:
aws s3api create-bucket --bucket $BUCKET --region $REGION --create-bucket-configuration LocationConstraint=$REGION
aws s3api put-public-access-block --bucket $BUCKET --public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
aws s3api put-bucket-lifecycle-configuration --bucket $BUCKET --lifecycle-configuration \
'{"Rules":[{"ID":"abort-incomplete-multipart","Status":"Enabled","Filter":{"Prefix":""},"AbortIncompleteMultipartUpload":{"DaysAfterInitiation":1}}]}'
aws s3api put-object --bucket $BUCKET --key inputs/default/
O bucket usa três prefixos, que não podem ser aninhados:
| Prefixo | Conteúdo | Criado por | Usado pela pasta do Signer |
|---|---|---|---|
signing-material/ | O certificado de assinatura (.cer) | A importação, no passo 3 | Sim. Lido quando o perfil é aberto. |
work/ | O compartilhamento de trabalho | O Bulk Signer, no boot | Não. Ainda assim, o Bulk Signer não inicia sem um compartilhamento de trabalho. |
inputs/default/ | Uma pasta monitorada, para arquivos assinados pelo perfil default | O operador, antes do primeiro boot (o último comando acima) | Não. |
O Bulk Signer nunca cria uma pasta monitorada. No S3, uma pasta é um objeto marcador de tamanho zero.
O versionamento e o Object Lock devem permanecer desabilitados. As diferenças de comportamento de um compartilhamento de trabalho no S3 estão documentadas em Configuração.
3. A chave de assinatura e o certificado
Um arquivo PFX contém dois itens com destinos diferentes:
| Item | Destino | Propriedade |
|---|---|---|
| Chave privada | AWS KMS | O KMS nunca a devolve. |
| Certificado | O bucket S3, como um objeto .cer | Material público. |
Quando o perfil é aberto, o Bulk Signer lê os dois e recusa o par se as chaves públicas forem diferentes.
3.1 Importação com o script
O Import-PfxToKms.ps1 executa a importação completa e
solicita a senha do PFX:
pwsh ./Import-PfxToKms.ps1 -PfxPath ./signer.pfx -Region $REGION -Alias $NAME \
-S3Bucket $BUCKET -S3Key signing-material/$NAME.cer \
-SignerPrincipalArn arn:aws:iam::$ACCOUNT:role/$NAME-task
| Parâmetro | Valor | Descrição |
|---|---|---|
-PfxPath | ./signer.pfx | O arquivo PFX que contém a chave e o certificado. |
-Region | $REGION | A região da AWS em que a chave é criada. |
-Alias | $NAME | O alias do KMS. Os passos 9 e 11 o citam como alias/<nome>. O script é interrompido se o alias já existir. |
-S3Bucket | $BUCKET | O bucket que recebe o certificado. |
-S3Key | signing-material/$NAME.cer | A chave do objeto do certificado. |
-SignerPrincipalArn | O ARN do task role | A entidade de segurança (principal) excluída da negação de kms:Sign na política da chave. O role é criado no passo 7; um ARN pode ser citado antes de o role existir. |
Política da chave. A chave é criada com a política padrão do KMS mais uma negação explícita de
kms:Sign para qualquer outra entidade de segurança. Assim, um administrador da conta não consegue
assinar em nome do titular do certificado sem antes alterar a política da chave, e o CloudTrail registra
essa alteração.
3.2 Importação manual
O procedimento a seguir é equivalente ao script e expõe cada chamada. Execute-o em um diretório vazio, em uma máquina considerada confiável para conter o PFX.
Importação com o AWS CLI e o OpenSSL
1. Extraia o certificado e a chave privada. Cada comando solicita a senha do PFX. Um PFX protegido
com algoritmos legados exige -legacy nos dois comandos.
openssl pkcs12 -in signer.pfx -clcerts -nokeys | openssl x509 -outform DER -out signer.cer
openssl pkcs12 -in signer.pfx -nocerts -nodes | openssl pkcs8 -topk8 -nocrypt -outform DER -out private-key.der
openssl x509 -inform DER -in signer.cer -noout -text | grep 'Public-Key' # 2048, 3072 ou 4096
O signer.cer é material público. O private-key.der é a chave privada em texto claro e existe até o
item 7.
2. Crie uma chave do KMS sem material de chave. --origin EXTERNAL cria uma chave que aguarda uma
importação. A especificação da chave deve corresponder ao tamanho informado no item 1. A política é a
padrão do KMS mais a negação que reserva o kms:Sign para o task role:
cat > key-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{ "Sid": "EnableIamUserPermissions", "Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::$ACCOUNT:root" }, "Action": "kms:*", "Resource": "*" },
{ "Sid": "OnlyNamedPrincipalsMaySign", "Effect": "Deny",
"Principal": "*", "Action": "kms:Sign", "Resource": "*",
"Condition": { "StringNotEquals": { "aws:PrincipalArn": ["arn:aws:iam::$ACCOUNT:role/$NAME-task"] } } }
]
}
EOF
KEY_ID=$(aws kms create-key --region $REGION --origin EXTERNAL --key-spec RSA_2048 --key-usage SIGN_VERIFY \
--description "Bulk Signer signing key" --policy file://key-policy.json --query KeyMetadata.KeyId --output text)
aws kms create-alias --region $REGION --alias-name alias/$NAME --target-key-id $KEY_ID
3. Obtenha os parâmetros de importação. O KMS retorna uma chave pública RSA de uso único (a chave de encapsulamento) e um token de importação, ambos válidos por 24 horas:
aws kms get-parameters-for-import --region $REGION --key-id $KEY_ID \
--wrapping-algorithm RSA_AES_KEY_WRAP_SHA_256 --wrapping-key-spec RSA_4096 > import-params.json
jq -r .PublicKey import-params.json | base64 -d > wrapping-public-key.der
jq -r .ImportToken import-params.json | base64 -d > import-token.bin
4. Encapsule a chave privada. Uma chave privada RSA excede o tamanho que pode ser cifrado diretamente
com a chave de encapsulamento, então o KMS exige um algoritmo híbrido, RSA_AES_KEY_WRAP_SHA_256. A chave
privada é encapsulada com uma chave AES-256 aleatória, a chave AES é cifrada com a chave de
encapsulamento, e o arquivo de importação é a concatenação dos dois, com a chave AES cifrada primeiro.
openssl rand -out aes-key.bin 32
openssl enc -id-aes256-wrap-pad -K "$(xxd -p -c 64 aes-key.bin)" -iv A65959A6 \
-in private-key.der -out key-material-wrapped.bin
openssl pkeyutl -encrypt -in aes-key.bin -out aes-key-wrapped.bin -inkey wrapping-public-key.der -keyform DER -pubin \
-pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256
cat aes-key-wrapped.bin key-material-wrapped.bin > encrypted-key-material.bin
5. Importe o material de chave.
aws kms import-key-material --region $REGION --key-id $KEY_ID \
--encrypted-key-material fileb://encrypted-key-material.bin --import-token fileb://import-token.bin \
--expiration-model KEY_MATERIAL_DOES_NOT_EXPIRE
6. Compare a chave com o certificado. As duas linhas de saída devem ser idênticas. É a comparação que o Bulk Signer faz quando o perfil é aberto:
aws kms get-public-key --region $REGION --key-id $KEY_ID --query PublicKey --output text | base64 -d \
| openssl rsa -pubin -inform DER -noout -modulus
openssl x509 -inform DER -in signer.cer -noout -modulus
7. Faça upload do certificado e remova a chave privada.
aws s3 cp signer.cer s3://$BUCKET/signing-material/$NAME.cer --region $REGION
rm -f private-key.der aes-key.bin key-material-wrapped.bin aes-key-wrapped.bin encrypted-key-material.bin import-token.bin
O rm remove somente as entradas do diretório. O script sobrescreve cada arquivo com zeros antes de
excluí-lo.
3.3 Verificação
aws kms describe-key --region $REGION --key-id alias/$NAME \
--query 'KeyMetadata.{State:KeyState,Usage:KeyUsage,Spec:KeySpec,Origin:Origin}'
aws s3 ls s3://$BUCKET/signing-material/
| Item | Valor esperado |
|---|---|
State | Enabled |
Usage | SIGN_VERIFY |
Spec | RSA_2048, RSA_3072 ou RSA_4096, conforme a chave |
Origin | EXTERNAL |
| Listagem do S3 | Contém $NAME.cer |
O KMS nunca devolve a chave privada, mas a chave existiu fora de um HSM antes da importação. Mantenha exatamente uma cópia do PFX em backup offline, que é o único meio de importar a chave de novo, e exclua todas as demais. Para um certificado A3 da ICP-Brasil, cuja chave deve permanecer em hardware certificado, resolva a questão de conformidade antes de prosseguir.
Chave gerada no KMS. Uma chave criada dentro do KMS exige, em vez disso, uma CSR assinada pelo KMS e um certificado emitido para ela. Esse procedimento está documentado em Certificados.
4. A imagem
Copie a tag que a Lacuna especificou para a instalação para um repositório ECR da conta:
aws ecr create-repository --repository-name bulksigner --region $REGION \
--image-tag-mutability IMMUTABLE --image-scanning-configuration scanOnPush=true
IMAGE=$ACCOUNT.dkr.ecr.$REGION.amazonaws.com/bulksigner:<versão>
docker login <registry-da-lacuna> --username <usuário-do-registry> # solicita o token
docker pull --platform linux/amd64 <registry-da-lacuna>/bulksigner:<versão>
docker tag <registry-da-lacuna>/bulksigner:<versão> $IMAGE
aws ecr get-login-password --region $REGION \
| docker login --username AWS --password-stdin $ACCOUNT.dkr.ecr.$REGION.amazonaws.com
docker push $IMAGE
Copie uma versão explícita, e não latest. Os requisitos deste passo e a solução para a falha do
docker login The stub received bad data no Docker Desktop para Windows estão especificados no
passo 3 de Amazon ECS Fargate.
5. Security groups
Crie três security groups, cada um admitindo tráfego de entrada somente do anterior:
| Grupo | Regra de entrada | Origem |
|---|---|---|
$NAME-alb | TCP 443 e TCP 80 | 0.0.0.0/0 |
$NAME-task | TCP 8080 | $NAME-alb |
$NAME-db | TCP 1433 | $NAME-task |
SG_ALB=$(aws ec2 create-security-group --region $REGION --vpc-id $VPC --group-name $NAME-alb \
--description "Public HTTPS into the load balancer" --query GroupId --output text)
SG_TASK=$(aws ec2 create-security-group --region $REGION --vpc-id $VPC --group-name $NAME-task \
--description "Bulk Signer, reachable only from the load balancer" --query GroupId --output text)
SG_DB=$(aws ec2 create-security-group --region $REGION --vpc-id $VPC --group-name $NAME-db \
--description "RDS, reachable only from Bulk Signer" --query GroupId --output text)
aws ec2 authorize-security-group-ingress --region $REGION --group-id $SG_ALB --ip-permissions \
'IpProtocol=tcp,FromPort=443,ToPort=443,IpRanges=[{CidrIp=0.0.0.0/0}]' \
'IpProtocol=tcp,FromPort=80,ToPort=80,IpRanges=[{CidrIp=0.0.0.0/0}]'
aws ec2 authorize-security-group-ingress --region $REGION --group-id $SG_TASK \
--ip-permissions "IpProtocol=tcp,FromPort=8080,ToPort=8080,UserIdGroupPairs=[{GroupId=$SG_ALB}]"
aws ec2 authorize-security-group-ingress --region $REGION --group-id $SG_DB \
--ip-permissions "IpProtocol=tcp,FromPort=1433,ToPort=1433,UserIdGroupPairs=[{GroupId=$SG_TASK}]"
O webhook é uma chamada do Signer para o balanceador de carga. Se o grupo $NAME-alb for restrito a
endereços de origem específicos, os endereços a partir dos quais a instância do Signer faz chamadas
também devem ser admitidos; obtenha-os com o operador da instância. Se o Signer não puder ser admitido,
omita o webhook. A varredura, sozinha, assina todos os documentos, com o atraso especificado em
Função do Signer:WebhookSecret no desempenho.
6. O banco de dados
6.1 Instância
aws rds describe-db-engine-versions --region $REGION --engine sqlserver-ex \
--query "DBEngineVersions[?starts_with(EngineVersion,'16.')].EngineVersion" --output text
aws rds create-db-instance --region $REGION --db-instance-identifier $NAME \
--engine sqlserver-ex --engine-version <uma versão 16.00> --license-model license-included \
--db-instance-class db.t3.small --storage-type gp3 --allocated-storage 20 \
--master-username bsadmin --manage-master-user-password \
--vpc-security-group-ids $SG_DB --no-publicly-accessible \
--backup-retention-period 7 --storage-encrypted
aws rds wait db-instance-available --region $REGION --db-instance-identifier $NAME
DB_HOST=$(aws rds describe-db-instances --region $REGION --db-instance-identifier $NAME \
--query 'DBInstances[0].Endpoint.Address' --output text)
As versões 16.00 do engine são o SQL Server 2022. A edição Express é suficiente para um ambiente de
homologação; ambientes de produção exigem sqlserver-web ou sqlserver-se.
6.2 Segredo do login da aplicação
Gere a senha do login da aplicação diretamente em um segredo que também contém a connection string completa:
APP_PW=$(aws secretsmanager get-random-password --region $REGION --password-length 40 \
--exclude-punctuation --require-each-included-type --query RandomPassword --output text)
CS="Server=tcp:$DB_HOST,1433;Initial Catalog=BulkSigner;User ID=bulksigner;Password=$APP_PW;Encrypt=True;TrustServerCertificate=True;"
aws secretsmanager create-secret --region $REGION --name $NAME/sql-app \
--secret-string "$(jq -n --arg p "$APP_PW" --arg c "$CS" '{username:"bulksigner",password:$p,connectionString:$c}')"
unset APP_PW CS
6.3 Criação do banco de dados
O Bulk Signer cria as tabelas dele, mas nunca o banco de dados, e a instância é privada. Execute a
configuração como uma task avulsa do Fargate na mesma VPC, usando o sqlcmd da imagem
mcr.microsoft.com/mssql-tools, com o seguinte script:
#!/bin/sh
set -e
SQL=/opt/mssql-tools/bin/sqlcmd
run() { "$SQL" -S "tcp:$DB_HOST,1433" -U "$MASTER_USER" -P "$MASTER_PASSWORD" -N -C -b "$@"; }
run -d master -Q "IF DB_ID('BulkSigner') IS NULL CREATE DATABASE [BulkSigner];"
run -d master -Q "ALTER DATABASE [BulkSigner] SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE;"
run -d master -Q "IF SUSER_ID('bulksigner') IS NULL CREATE LOGIN [bulksigner] WITH PASSWORD = '$APP_PASSWORD', CHECK_POLICY = ON;"
run -d BulkSigner -Q "IF USER_ID('bulksigner') IS NULL CREATE USER [bulksigner] FOR LOGIN [bulksigner]; ALTER ROLE db_datareader ADD MEMBER [bulksigner]; ALTER ROLE db_datawriter ADD MEMBER [bulksigner]; ALTER ROLE db_ddladmin ADD MEMBER [bulksigner];"
A definição da task de configuração, a estrutura do segredo $NAME/sql-app, a limitação de
TrustServerCertificate=True e as condições confirmadas pelo código de saída 0 estão especificadas no
passo 5 de Amazon ECS Fargate.
7. Segredos e roles
7.1 Segredos
| Segredo | Conteúdo | Criado | Injetado como |
|---|---|---|---|
$NAME/sql-app | JSON com username, password e connectionString | Item 6.2 | ConnectionStrings__Default (chave connectionString) |
$NAME/auth-api-key | Uma chave aleatória de 48 caracteres: a X-API-Key da API REST e a chave de login do dashboard | Abaixo, gerado | Auth__ApiKey |
$NAME/pki-sdk-license | A licença do PKI SDK | Manualmente | Signing__PkiSdkLicense |
$NAME/signer-api-key | A chave de API do Lacuna Signer do item 1.2, no formato <aplicação>|<segredo> | Manualmente | Signer__ApiKey |
$NAME/signer-webhook-secret | O token que o Signer apresenta em uma notificação | Abaixo, gerado | Signer__WebhookSecret |
Os dois últimos segredos são específicos da integração com o Lacuna Signer.
Gere os dois valores aleatórios diretamente no Secrets Manager, para que não sejam exibidos nem registrados no histórico do shell:
aws secretsmanager create-secret --region $REGION --name $NAME/auth-api-key --secret-string \
"$(aws secretsmanager get-random-password --region $REGION --password-length 48 --exclude-punctuation --query RandomPassword --output text)"
aws secretsmanager create-secret --region $REGION --name $NAME/signer-webhook-secret --secret-string \
"$(aws secretsmanager get-random-password --region $REGION --password-length 48 --exclude-punctuation --require-each-included-type --query RandomPassword --output text)"
| Requisito do segredo do webhook | Motivo |
|---|---|
| Pelo menos 32 caracteres | Um valor menor é recusado no boot. |
| Sem espaço em branco no início ou no fim | Recusado no boot. |
| Sem pontuação | Não é exigido pelo Bulk Signer. Evita erros de transcrição no campo de token do Signer. |
Informe manualmente os dois valores emitidos externamente (a licença do PKI SDK e a chave de API do Signer), no console (Other type of secret → Plaintext, contendo somente o valor) ou a partir de um prompt que não ecoe a entrada. Armazene a chave de API do Signer exatamente como o Signer a exibiu, incluindo eventuais espaços na parte da aplicação.
7.2 Roles
Os dois roles usam a política de confiança a seguir, que restringe ecs-tasks.amazonaws.com à conta:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "ecs-tasks.amazonaws.com" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "aws:SourceAccount": "<conta>" },
"ArnLike": { "aws:SourceArn": "arn:aws:ecs:<região>:<conta>:*" }
}
}]
}
| Role | Permissões |
|---|---|
$NAME-exec | A política gerenciada da AWS AmazonECSTaskExecutionRolePolicy, mais secretsmanager:GetSecretValue em secret:$NAME/* e no segredo mestre gerenciado pelo RDS (este último somente para a task de configuração). Esse role lê a chave de API do Signer e o segredo do webhook para o container. |
$NAME-task | A política inline abaixo. |
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Action": ["kms:DescribeKey", "kms:GetPublicKey", "kms:Sign"],
"Resource": "arn:aws:kms:<região>:<conta>:key/<id da chave>" },
{ "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::<bucket>" },
{ "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:AbortMultipartUpload"],
"Resource": "arn:aws:s3:::<bucket>/*" }
]
}
A integração com o Lacuna Signer não exige nenhuma permissão adicional do IAM. O Signer é acessado
pela internet com a chave de API, e a assinatura usa a mesma ação kms:Sign de uma assinatura de arquivo.
8. HTTPS
8.1 Certificado
CERT=$(aws acm request-certificate --region $REGION --domain-name $HOST --validation-method DNS \
--query CertificateArn --output text)
aws acm describe-certificate --region $REGION --certificate-arn $CERT \
--query 'Certificate.DomainValidationOptions[0].ResourceRecord'
Crie o registro CNAME retornado na zona autoritativa e aguarde a validação:
aws acm wait certificate-validated --region $REGION --certificate-arn $CERT
8.2 Balanceador de carga, target group e listeners
LB=$(aws elbv2 create-load-balancer --region $REGION --name $NAME --type application --scheme internet-facing \
--subnets ${SUBNETS//,/ } --security-groups $SG_ALB --query 'LoadBalancers[0].LoadBalancerArn' --output text)
aws elbv2 modify-load-balancer-attributes --region $REGION --load-balancer-arn $LB \
--attributes Key=idle_timeout.timeout_seconds,Value=300 Key=routing.http.drop_invalid_header_fields.enabled,Value=true
TG=$(aws elbv2 create-target-group --region $REGION --name $NAME --protocol HTTP --port 8080 --vpc-id $VPC \
--target-type ip --health-check-path /api/ready --health-check-interval-seconds 30 \
--healthy-threshold-count 2 --unhealthy-threshold-count 3 --matcher HttpCode=200 \
--query 'TargetGroups[0].TargetGroupArn' --output text)
aws elbv2 modify-target-group-attributes --region $REGION --target-group-arn $TG \
--attributes Key=deregistration_delay.timeout_seconds,Value=30
aws elbv2 create-listener --region $REGION --load-balancer-arn $LB --protocol HTTPS --port 443 \
--certificates CertificateArn=$CERT --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
--default-actions Type=forward,TargetGroupArn=$TG
aws elbv2 create-listener --region $REGION --load-balancer-arn $LB --protocol HTTP --port 80 \
--default-actions 'Type=redirect,RedirectConfig={Protocol=HTTPS,Port=443,StatusCode=HTTP_301}'
A finalidade de cada configuração está especificada no passo 7 de Amazon ECS Fargate.
8.3 DNS
Aponte o hostname para o nome DNS do balanceador de carga, com um registro CNAME ou, se a zona estiver hospedada no Route 53, um registro de alias. O Signer faz chamadas a esse hostname e valida o certificado dele; o certificado do ACM atende a esse requisito.
9. O serviço
9.1 Grupo de logs e cluster
aws logs create-log-group --region $REGION --log-group-name /ecs/$NAME
aws logs put-retention-policy --region $REGION --log-group-name /ecs/$NAME --retention-in-days 30
aws ecs create-cluster --region $REGION --cluster-name $NAME --capacity-providers FARGATE
9.2 Definição de task
| Parâmetro | Valor |
|---|---|
cpu | 1024 |
memory | 2048 |
networkMode | awsvpc |
runtimePlatform.cpuArchitecture | X86_64 |
executionRoleArn | $NAME-exec |
taskRoleArn | $NAME-task |
| Nome do container | bulksigner |
| Imagem do container | $IMAGE (passo 4) |
| Porta do container | 8080 |
stopTimeout | 60 |
| Driver de log | awslogs, grupo /ecs/$NAME |
A definição do container traz o seguinte ambiente e os seguintes segredos:
"environment": [
{ "name": "ASPNETCORE_ENVIRONMENT", "value": "Production" },
{ "name": "Database__Provider", "value": "SqlServer" },
{ "name": "Auth__SessionKeyRing__Placement", "value": "OperationalStore" },
{ "name": "Hosting__ForwardedHeaders__Enabled", "value": "true" },
{ "name": "Hosting__ForwardedHeaders__KnownNetworks__0", "value": "<o CIDR da VPC>" },
{ "name": "Storage__Provider", "value": "S3" },
{ "name": "Storage__S3__BucketName", "value": "<bucket>" },
{ "name": "Storage__S3__Prefix", "value": "work" },
{ "name": "Storage__S3__Region", "value": "<região>" },
{ "name": "Storage__S3__Credential", "value": "InstanceRole" },
{ "name": "Storage__Inputs__0__Name", "value": "default" },
{ "name": "Storage__Inputs__0__Path", "value": "inputs/default" },
{ "name": "Storage__Inputs__0__PollIntervalSeconds", "value": "30" },
{ "name": "Signing__Certificate__Source", "value": "AwsKms" },
{ "name": "Signing__Certificate__AwsKms__Region", "value": "<região>" },
{ "name": "Signing__Certificate__AwsKms__KeyId", "value": "alias/<nome>" },
{ "name": "Signing__Certificate__AwsKms__Credential", "value": "InstanceRole" },
{ "name": "Signing__Certificate__AwsKms__S3__Bucket", "value": "<bucket>" },
{ "name": "Signing__Certificate__AwsKms__S3__Key", "value": "signing-material/<nome>.cer" },
{ "name": "Signing__Certificate__AwsKms__S3__Region", "value": "<região>" },
{ "name": "Signing__Certificate__AwsKms__S3__Credential", "value": "InstanceRole" },
{ "name": "Signer__Endpoint", "value": "https://<endereço do Lacuna Signer>" },
{ "name": "Signer__FolderSweepIntervalSeconds", "value": "300" }
],
"secrets": [
{ "name": "ConnectionStrings__Default", "valueFrom": "<ARN de $NAME/sql-app>:connectionString::" },
{ "name": "Auth__ApiKey", "valueFrom": "<ARN de $NAME/auth-api-key>" },
{ "name": "Signing__PkiSdkLicense", "valueFrom": "<ARN de $NAME/pki-sdk-license>" },
{ "name": "Signer__ApiKey", "valueFrom": "<ARN de $NAME/signer-api-key>" },
{ "name": "Signer__WebhookSecret", "valueFrom": "<ARN de $NAME/signer-webhook-secret>" }
]
9.3 Configuração do Bulk Signer
Um __ no nome de uma variável de ambiente corresponde a : na chave de configuração.
Conexão com o Lacuna Signer. Um host se conecta a uma organização do Signer, e todo perfil que usa o
método SignerFolder usa estas configurações
(Configuração).
| Variável de ambiente | Valor nesta implantação | Obrigatória | Descrição |
|---|---|---|---|
Signer__Endpoint | O endereço do item 1.1 | Sim | O endereço da API do Signer. |
Signer__ApiKey | Segredo $NAME/signer-api-key | Sim | Enviada como X-Api-Key em toda chamada ao Signer: a varredura, a releitura de cada documento, e o início e a conclusão de cada assinatura. Também determina a organização cujas pastas o formulário do perfil lista. |
Signer__WebhookSecret | Segredo $NAME/signer-webhook-secret | Não | Habilita POST /api/signer/notifications e é o token Bearer que o Signer deve apresentar nela. Sem essa chave, a rota retorna 404 e os documentos são detectados somente pela varredura (veja Função do Signer:WebhookSecret no desempenho). |
Signer__FolderSweepIntervalSeconds | 300 (o padrão) | Não | O intervalo entre listagens de cada pasta vinculada, de 60 a 86400 segundos. |
As seguintes regras valem para as chaves do Signer:
- Exigência conjunta. Definir qualquer chave em
Signer:, inclusive somente o segredo do webhook, tornaSigner:EndpointeSigner:ApiKeyobrigatórias no boot. - Chaves não usadas por este método.
Signer:PollIntervalSeconds,Signer:TimeoutHourseSigner:MaxConsecutiveApiFailuresvalem somente para o métodoLacunaSigner.
Demais configurações. As demais chaves são as da implantação base e estão especificadas na Referência de configuração de Amazon ECS Fargate. Os pontos a seguir são específicos desta implantação:
| Configuração | Relevância para a integração com o Lacuna Signer |
|---|---|
Signing:Certificate:* | O seed (carga inicial) do perfil de assinatura default, aplicado somente no primeiro boot contra um banco vazio. O perfil default não pode usar o método SignerFolder; o perfil criado no passo 11 cita a mesma chave e o mesmo .cer. Toda credencial é InstanceRole, então nenhum perfil armazena segredo. |
Hosting:ForwardedHeaders:* | Com os forwarded headers aplicados, a task observa o endereço do Signer em vez do endereço do balanceador de carga. O limite de requisições (rate limiting) da rota de notificações (RateLimiting:SignerNotification, 600 chamadas por minuto) é contado por endereço de origem; sem os forwarded headers, todas as origens compartilham uma única cota (Configuração). |
Storage:Inputs:0:* | A pasta monitorada para arquivos. Essas três chaves podem ser omitidas em uma implantação que assina somente documentos do Signer. |
ASPNETCORE_ENVIRONMENT, Signing:TrustLacunaTestRoot | Para um ambiente de homologação que assina com os certificados de teste da Lacuna, defina Staging e inclua Signing__TrustLacunaTestRoot=true (Configuração). |
9.4 Serviço
aws ecs create-service --region $REGION --cluster $NAME --service-name bulksigner --task-definition $NAME \
--desired-count 1 --launch-type FARGATE \
--deployment-configuration 'minimumHealthyPercent=0,maximumPercent=100,deploymentCircuitBreaker={enable=true,rollback=false}' \
--network-configuration "awsvpcConfiguration={subnets=[$SUBNETS],securityGroups=[$SG_TASK],assignPublicIp=ENABLED}" \
--load-balancers targetGroupArn=$TG,containerName=bulksigner,containerPort=8080 \
--health-check-grace-period-seconds 300
| Parâmetro | Valor | Requisito |
|---|---|---|
minimumHealthyPercent, maximumPercent | 0, 100 | Toda implantação para a task em execução antes de iniciar a nova. |
--desired-count | 1 | Não deve ser aumentado. Auto scaling não deve ser associado. |
deploymentCircuitBreaker | enable=true, rollback=false | Uma implantação que falha é interrompida em vez de fazer rollback, o que iniciaria uma segunda task. |
10. Verificação
curl -s https://$HOST/api/ready
KEY=$(aws secretsmanager get-secret-value --region $REGION --secret-id $NAME/auth-api-key --query SecretString --output text)
curl -s -H "X-API-Key: $KEY" https://$HOST/api/ready/details | jq '.checks[] | {name, ok, detail}'
Além das verificações especificadas no passo 9 de Amazon ECS Fargate (database,
storage-bucket:…, work-share-owner, pki-license), /api/ready/details informa duas verificações da
integração com o Lacuna Signer:
| Verificação | Resultado esperado neste ponto |
|---|---|
signer-notifications | accepted; none has arrived at this instance since it started |
signer-folder-sweep | ok: true. Depois da primeira passagem: the last pass at this instance finished without a failure at … |
Se signer-notifications informar not accepted (Signer:WebhookSecret is not set), o segredo não chegou
à task. Verifique a revisão da definição de task que o serviço está executando.
A rota de notificações pode ser testada sem credencial:
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://$HOST/api/signer/notifications
| Resposta | Significado |
|---|---|
401 (signer-notification.unauthorized) | Esperado. A rota está habilitada e exige o token Bearer. |
404 (signer-notification.not-configured) | Nenhum segredo de webhook está definido. |
11. O perfil de assinatura
O perfil default recebe os uploads que não citam perfil e as pastas monitoradas sem vínculo, e o método
SignerFolder não aceita arquivos. A pasta do Signer exige, portanto, um perfil dedicado.
11.1 Criação
Abra o dashboard em https://<host>/, entre com a chave armazenada em $NAME/auth-api-key e abra
Perfis de assinatura → Novo perfil (/profiles/_new).
| Campo | Valor |
|---|---|
| Nome | aws-signer (o valor de $PROFILE). O nome não pode ser alterado depois da criação. |
| Método de assinatura | Pasta do Lacuna Signer (este host assina documentos criados no Signer) |
| Pasta do Lacuna Signer | A pasta do item 1.3, localizada pela busca por nome. O seletor lista as pastas da organização a que a chave de API pertence e guarda o ID da pasta. |
| Formato da assinatura | Ignorado por este método. O Signer seleciona o formato. |
| Origem do certificado | AwsKms |
| Onde está o arquivo? | Em um objeto do Amazon S3 |
| Região da AWS, Chave do KMS, Modo de credencial da AWS | $REGION, alias/$NAME, InstanceRole |
| Bucket do S3, Chave do objeto no S3, Região do S3, Modo de credencial do S3 | $BUCKET, signing-material/$NAME.cer, $REGION, InstanceRole |
Quando o método é selecionado, o formulário oculta as configurações que o Signer determina (a pasta de entrada, a verificação, a criptografia e a checagem CNAB240) e as salva desabilitadas.
Selecione Criar perfil. O certificado é aberto durante o salvamento: a task lê o .cer do S3,
solicita a chave pública ao KMS e compara os dois. O salvamento é recusado, e nenhum perfil é criado, nos
seguintes casos:
| Condição | Causa |
|---|---|
| Chave do KMS inacessível | Um alias incorreto, ou um task role sem kms:GetPublicKey na chave. |
| Certificado sem CPF | O certificado não é um e-CPF nem um e-CNPJ. |
| Pasta já vinculada | Outro perfil assina na pasta selecionada. |
Um perfil salvo entra em vigor imediatamente. Nenhum reinício é necessário.
11.2 Verificação
curl -s -H "X-API-Key: $KEY" https://$HOST/api/profiles/$PROFILE | jq
A resposta deve informar o método SignerFolder e degraded = false. Na página Perfis de
assinatura, a situação do perfil é pronto.
11.3 Certificado separado para o perfil
Para usar um certificado diferente do certificado do perfil default, repita o passo 3 com outro alias e
outra chave de objeto. Inclua o ARN da nova chave na instrução kms: do task role antes de criar o
perfil; do contrário, o salvamento é recusado em kms:GetPublicKey.
12. O webhook no Lacuna Signer
12.1 Cadastro
Copie o segredo para a área de transferência, para que ele não seja exibido. O comando da área de
transferência é clip no Windows, pbcopy no macOS e xclip -selection clipboard no Linux:
aws secretsmanager get-secret-value --region $REGION --secret-id $NAME/signer-webhook-secret \
--query SecretString --output text | tr -d '\r\n' | clip
No Lacuna Signer, com um administrador da organização:
- Abra Organizações e selecione a organização.
- Na seção Integração, abaixo do card Webhooks, selecione o botão +. Um novo painel Webhook N é aberto. Uma organização pode ter vários webhooks; um webhook existente de outro sistema não é afetado.
- Preencha os campos conforme especificado abaixo.
- Selecione Salvar, que permanece desabilitado até o token ser informado.
- Limpe a área de transferência.
| Campo | Valor | Requisito |
|---|---|---|
| URL | https://<host>/api/signer/notifications | O hostname público do passo 8, não o nome DNS do balanceador de carga na AWS. O esquema deve ser https: o listener da porta 80 responde com um redirecionamento, e clientes HTTP costumam reenviar um POST redirecionado como GET, o que não entrega nenhuma notificação. |
| Tipo de autenticação | Bearer | X-Api-Key não deve ser usado. Esse header é a credencial REST do próprio Bulk Signer, e uma notificação que o apresenta é recusada. |
| Token de Autenticação | O valor de $NAME/signer-webhook-secret | Sem a palavra Bearer. O Signer acrescenta o prefixo. |
O conteúdo que o Signer envia e o comportamento dele de autenticação e de novas tentativas estão especificados em Webhooks.
12.2 Correspondência entre o Lacuna Signer e o Bulk Signer
A configuração completa do lado do Signer (passos 1 e 12):
| Item no Lacuna Signer | Local | Item correspondente no Bulk Signer |
|---|---|---|
| A aplicação e a chave de API dela | Aplicações, na instância | Signer:ApiKey |
| A pasta | Pastas, na organização | A Pasta do Lacuna Signer do perfil |
| A URL do webhook | Organizações → Integração | O hostname público, seguido de /api/signer/notifications |
| O token do webhook, do tipo Bearer | Organizações → Integração | Signer:WebhookSecret |
UseBrazilianPdfSigningPolicies | As configurações da instância | Nenhum. A configuração determina sozinha a política das assinaturas em PDF. |
12.3 Comportamento das entregas
| Comportamento | Especificação |
|---|---|
| Seleção de eventos | Não disponível. O Signer entrega todo evento da organização a todo webhook da organização. |
| Eventos processados pelo Bulk Signer | DocumentsCreated, DocumentSigned e DocumentApproved. Todo outro evento recebe 204, para que o Signer não o tente de novo. |
| Novas tentativas | O Signer tenta de novo uma entrega que não recebe uma resposta 2xx, até dez vezes, ao longo de várias horas (Retentativas). |
| Efeito de um token incorreto | Cada entrega recebe 401, e o resultado unauthorized de bulksigner_signer_notifications_total aumenta. |
13. Teste de assinatura
13.1 Criação do documento
No Lacuna Signer:
- Selecione Novo documento, faça upload de um PDF e selecione a organização e a pasta vinculada.
- Inclua o titular do certificado como participante do tipo Signatário, com o nome, o CPF contido no certificado e o e-mail. O titular pode ser o único participante ou vir depois de outros; a task só age quando a ação pendente é a do titular.
- Selecione Enviar.
13.2 Resultado esperado
Em segundos, a página Jobs do dashboard lista um job com o nome do documento, marcado como Lacuna
Signer, no perfil aws-signer:
O job passa pelos seguintes status:
| Status | Significado |
|---|---|
| Na fila | O documento foi detectado e o job foi criado. |
| Processando | A task releu o documento, iniciou a assinatura e assinou o hash no KMS. |
| Verificando | A assinatura foi entregue ao Signer, que a valida e a incorpora ao documento. |
| Concluído | O Signer aceitou a assinatura. |
No Lacuna Signer, a ação do titular está concluída e o documento contém a assinatura. Se o titular era o último participante, o status do documento na pasta é Concluído:
O histórico de eventos do CloudTrail mostra, em alguns minutos, a chamada Sign do task role na chave.
13.3 Verificação pela linha de comando
curl -s -H "X-API-Key: $KEY" "https://$HOST/api/jobs?profile=$PROFILE" | jq
curl -s -H "X-API-Key: $KEY" https://$HOST/api/ready/details | jq -r '.checks[] | select(.name=="signer-notifications") | .detail'
curl -s -H "X-API-Key: $KEY" https://$HOST/api/metrics \
| grep -E 'bulksigner_signer_(notifications_total|folder_signatures_total|folder_jobs_enqueued_total)'
| Evidência | Valor esperado |
|---|---|
Verificação signer-notifications | accepted; the last arrived at this instance at <hora> |
bulksigner_signer_folder_signatures_total{outcome="signed"} | Incrementado uma vez por assinatura aceita pelo Signer. |
bulksigner_signer_notifications_total | Conta as entregas, por resultado. |
Um documento assinado somente minutos depois de criado foi detectado pela varredura, o que indica que a
notificação não chegou. Nesse caso, a verificação signer-notifications ainda informa que nenhuma
notificação chegou, ou o resultado unauthorized está aumentando. Veja Sintomas.
Operação
As operações da implantação base estão especificadas no passo 11 de Amazon ECS Fargate. As operações a seguir são específicas da integração com o Lacuna Signer:
| Operação | Procedimento | Observações |
|---|---|---|
| Implantação (atualização de versão ou rotação de segredo) | Pause o pipeline (POST /api/pipeline/pause), aguarde até não haver job em Processing ou Verifying, atualize o serviço e retome (POST /api/pipeline/resume). | Uma task interrompida durante uma assinatura obriga o boot seguinte a perguntar ao Signer como o job terminou, e alguns desses jobs terminam Failed com signer-folder.interrupted (Integração com o Lacuna Signer). A varredura é executada com o pipeline pausado: documentos novos viram jobs e aguardam na fila. |
| Rotação do segredo do webhook | Armazene um novo valor aleatório em $NAME/signer-webhook-secret, force uma nova implantação e depois informe o novo valor no webhook no Signer. | O mesmo valor existe dos dois lados. Entre as duas mudanças, as notificações recebem 401; o Signer as tenta de novo, e a varredura detecta os documentos restantes. Nenhum documento se perde. |
| Rotação da chave de API do Signer | Crie a nova chave no Signer, armazene-a em $NAME/signer-api-key, force uma nova implantação e só então revogue a chave anterior. | Com uma chave revogada, toda varredura falha, e a verificação signer-folder-sweep falha após três passagens consecutivas com falha. A verificação não afeta o /api/ready, então o balanceador de carga mantém a task. |
| Renovação do certificado | Importe o novo PFX com um novo alias (passo 3), inclua a chave no task role, defina a nova chave e o novo .cer na página do perfil (Editar certificado) e reinicie. | Se o novo certificado contiver um CPF diferente, os documentos pendentes no Signer para o CPF anterior não têm ação para o novo titular. Cada um é registrado uma vez como Failed com signer-folder.no-holder-action. Avise os usuários que criam documentos antes da mudança. |
| Mudança de pasta | Salve a nova pasta no painel Certificado do perfil. | Vale sem reinício. Os documentos que permanecerem na pasta anterior deixam de ser assinados pelo perfil. |
| Desativação | Remova o webhook no Signer e depois desabilite o perfil na página dele ou altere o método dele. | Remover as variáveis Signer__* enquanto um perfil ainda usa o método não impede o boot. O perfil fica degradado, e o motivo cita as duas chaves que faltam. |
Contratos de interface
Endpoint de notificações
POST /api/signer/notifications é chamado pelo Lacuna Signer e autenticado com
Authorization: Bearer <Signer:WebhookSecret>. O contrato completo está especificado em
API REST.
| Resposta | Código | Condição |
|---|---|---|
202 | — | Um documento em uma pasta vinculada foi enfileirado para checagem, ou já estava enfileirado. |
204 | — | Qualquer outra entrega autenticada: um evento que não pode deixar um documento aguardando assinatura, uma pasta sem vínculo, a pasta de um perfil desabilitado ou um corpo que não pode ser lido. |
401 | signer-notification.unauthorized | O token Bearer está ausente ou incorreto, ou outro esquema foi usado. |
404 | signer-notification.not-configured | Signer:WebhookSecret não está definido. |
429 | — | O limite de requisições da rota (RateLimiting:SignerNotification) foi excedido. O Signer tenta a entrega de novo. |
503 | signer-notification.unavailable | O banco operacional não está utilizável na instância, ou há checagens demais enfileiradas. O Signer tenta a entrega de novo. |
As checagens enfileiradas ficam em memória. Um reinício descarta as que ainda não foram feitas, e a varredura seguinte detecta os documentos correspondentes.
Outros endpoints usados nesta página
Requisições autenticadas enviam X-API-Key: <Auth:ApiKey>.
| Método | Rota | Autenticação | Usado por |
|---|---|---|---|
GET | /api/ready, /api/ready/details | Anônimo; chave de API para os detalhes | Verificação (passos 10 e 13). |
GET | /api/profiles/{name} | Chave de API | Verificação do perfil (passo 11). |
GET | /api/jobs?profile={name} | Chave de API | Teste de assinatura (passo 13). |
GET | /api/metrics | Chave de API | Teste de assinatura (passo 13). |
POST | /api/pipeline/pause, /api/pipeline/resume | Chave de API | Implantação (Operação). |
GET | <Signer__Endpoint>/api/documents | Nenhuma | Confirma o endereço da API do Signer: 401 sem uma chave (item 1.1). |
Chamadas à AWS feitas pela task para o perfil
| Serviço | Ação | Recurso | Quando é chamada |
|---|---|---|---|
| Amazon S3 | s3:GetObject | signing-material/<nome>.cer | Quando o perfil é aberto: durante o salvamento no passo 11 e em cada boot. |
| AWS KMS | kms:DescribeKey, kms:GetPublicKey | A chave de assinatura | Quando o perfil é aberto. |
| AWS KMS | kms:Sign | A chave de assinatura | Uma vez por documento assinado. |
Referência de erros
Códigos de erro
| Código | Origem | Significado | Solução |
|---|---|---|---|
signer-notification.unauthorized | Rota de notificações, 401 | O token cadastrado no Signer é diferente do segredo, foi informado com a palavra Bearer, ou o tipo de autenticação não é Bearer. | Corrija o webhook no Signer (passo 12). |
signer-notification.not-configured | Rota de notificações, 404 | O segredo do webhook não chegou à task. | Verifique a revisão da definição de task que o serviço executa e se o execution role consegue ler o segredo. |
signer-folder.no-holder-action | Job, Failed | O documento está na pasta vinculada, mas não tem ação de assinatura para o CPF do certificado: o titular está ausente, foi incluído somente como aprovador ou foi incluído sem o CPF. | Inclua o titular como signatário, com o CPF. A varredura seguinte detecta a nova ação. Não admite nova tentativa. |
signer-folder.signature-refused | Job, Failed | O Signer recusou a assinatura. Os resultados da validação ficam registrados no histórico do job; a causa usual é o certificado (revogado, expirado ou que não é o do participante). | Corrija a causa informada pelo Signer e tente o job de novo. |
signer-folder.interrupted | Job, Failed | A task foi interrompida durante a assinatura. | Verifique o documento no Signer e tente o job de novo. Uma nova tentativa termina Canceled se a ação do titular já estiver concluída. |
ThrottlingException, KeyUnavailableException | AWS KMS, registrado no job | A cota de requisições do KMS, compartilhada por todos os chamadores da conta e da região, foi excedida, ou a chave estava indisponível. | Veja Solução de problemas. |
Um job que termina Canceled não tem código: o documento foi cancelado, recusado ou expirou no Signer,
ou já tinha sido assinado pelo titular, antes de a task assiná-lo. Não é uma falha. Todos os códigos
signer-folder.* estão especificados em
Solução de problemas.
Sintomas
| Sintoma | Causa | Solução |
|---|---|---|
A task para no boot e o log cita Signer:Endpoint ou Signer:ApiKey. | Uma chave em Signer: está definida sem essas duas. Definir somente Signer__WebhookSecret é suficiente para exigir as duas. | Defina as duas chaves (passo 9). |
A task para no boot e o log cita Signer:WebhookSecret. | O segredo tem menos de 32 caracteres, ou tem espaço em branco no início ou no fim, em geral uma quebra de linha no fim do valor do segredo. | Gere o segredo de novo (item 7.1). |
signer-notifications informa accepted; none has arrived… depois que um documento foi criado. | O Signer não acessa o balanceador de carga: a URL cadastrada no Signer está incorreta ou usa http://, ou o security group $NAME-alb não admite os endereços do Signer. | Corrija a URL (passo 12) ou o security group (passo 5). |
| Os documentos são assinados, mas minutos depois de criados. | A notificação não chega, e os documentos são detectados pela varredura. | Veja Solução de problemas. |
| O seletor de pastas informa que o Signer está inacessível, ou lista as pastas de outra organização. | Signer__Endpoint não é o endereço da API do Signer, ou Signer__ApiKey pertence a outra organização. | Corrija a configuração (passos 1 e 9). |
| Criar perfil é recusado com uma mensagem sobre o CPF do certificado. | O certificado não contém CPF. | Use um e-CPF ou e-CNPJ da ICP-Brasil. Veja Solução de problemas. |
| Criar perfil é recusado com uma mensagem sobre o KMS ou o S3. | Falta ao task role kms:GetPublicKey na chave ou s3:GetObject no .cer, o ARN do task role não está excluído da negação da política da chave, ou o .cer não corresponde à chave. | Corrija o role ou a política da chave (passos 3 e 7). Veja Solução de problemas. |
A verificação signer-folder-sweep falha. | Três passagens consecutivas falharam. A causa usual é uma Signer:ApiKey revogada ou incorreta. | Veja Solução de problemas. |
Os erros da implantação base que não envolvem o Lacuna Signer estão especificados na Referência de erros de Amazon ECS Fargate.
Limitações
| Não oferecido | Alternativa |
|---|---|
| Um arquivo assinado no S3 | O Signer mantém o documento. Baixe-o no Signer; nada é gravado em work/output/. |
| Verificação do resultado pelo Bulk Signer | Nenhuma. A verificação fica desabilitada neste método, porque o Signer valida a assinatura ao incorporá-la ao documento. |
| A direção oposta: um arquivo enviado ao Signer para uma pessoa assinar | O método LacunaSigner, que usa as mesmas Signer:Endpoint e Signer:ApiKey, não exige nenhuma outra mudança na AWS e é selecionado por perfil. Veja Integração com o Lacuna Signer. |
| Mais de uma instância, verificação do certificado do servidor de banco de dados, métricas no CloudWatch, backups feitos pelo Bulk Signer | As limitações de Amazon ECS Fargate valem sem alteração. |






