---
title: "Assinando documentos do Lacuna Signer no Amazon ECS Fargate"
description: "Procedimento de implantação do Bulk Signer no Amazon ECS Fargate com RDS for SQL Server, S3 e AWS KMS e de assinatura de documentos criados em uma pasta do Lacuna Signer com o método SignerFolder e o webhook Signer:WebhookSecret, com os parâmetros de configuração, os contratos de interface e os códigos de erro envolvidos."
canonical: "https://docs.lacunasoftware.com/articles/bulk-signer/aws-signer-folder"
locale: "pt-BR"
product: "Bulk Signer"
last_updated: "2026-11-11"
---

# 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**. Usuário ou API 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`](https://docs.lacunasoftware.com/articles/bulk-signer/lacuna-signer#assinando-em-uma-pasta-do-lacuna-signer--signerfolder);
- o webhook `Signer:WebhookSecret`, pelo qual o Signer notifica o Bulk Signer sobre documentos novos.

A infraestrutura é a especificada em [Amazon ECS Fargate](https://docs.lacunasoftware.com/articles/bulk-signer/aws). 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](https://docs.lacunasoftware.com/articles/bulk-signer/aws-signer-folder#prepare-signer), os dois segredos do Lacuna Signer no
[passo 7](https://docs.lacunasoftware.com/articles/bulk-signer/aws-signer-folder#secrets-and-roles), as configurações `Signer` no [passo 9](https://docs.lacunasoftware.com/articles/bulk-signer/aws-signer-folder#service), uma nova implantação e os
[passos 11 a 13](https://docs.lacunasoftware.com/articles/bulk-signer/aws-signer-folder#profile).

> **Dica: A mesma implantação com CloudFormation**
>
> [Amazon ECS Fargate + Lacuna Signer (CloudFormation)](https://docs.lacunasoftware.com/articles/bulk-signer/aws-signer-folder-cloudformation) cria a
> infraestrutura da AWS dos passos 2 e 5 a 9 com uma única stack do CloudFormation, em vez dos comandos desta
> página. Os recursos e a configuração são os mesmos.

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. Em 11/11/2026, o ambiente foi recriado somente a partir desta
página, em uma estação Windows com o Git Bash, e dois documentos criados no Signer foram assinados pelo
webhook, o primeiro em 1,7 segundo.

> **Info: Versão mínima: 2.20.0**
>
> O método `SignerFolder` e a chave `Signer:WebhookSecret` foram introduzidos na 2.16.0. O
> [provider de armazenamento S3](https://docs.lacunasoftware.com/articles/bulk-signer/configuration#the-work-share-in-an-s3-bucket--storageprovider--s3), a
> [origem de certificado `AwsKms`](https://docs.lacunasoftware.com/articles/bulk-signer/certificates#origem--awskms) e a chave
> [`Auth:SessionKeyRing:Placement`](https://docs.lacunasoftware.com/articles/bulk-signer/configuration#authsessionkeyring--where-the-session-key-ring-is-kept)
> foram introduzidos na 2.20.0. Imagens anteriores recusam a configuração desta página no boot.

> **Perigo: Restrição de instância única**
>
> O [modo cluster](https://docs.lacunasoftware.com/articles/bulk-signer/high-availability) é 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)](https://docs.lacunasoftware.com/articles/bulk-signer/azure).

## Arquitetura

[![Assinatura em uma pasta do Lacuna Signer na AWS: usuário ou API cria um documento no Signer, o Signer notifica a task do Fargate pelo load balancer, a task assina com a chave no AWS KMS e devolve a assinatura ao Signer](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-architecture-pt.svg)](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-architecture-pt.svg)

### 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`. |

[![Quanto tempo um documento espera: com Signer ele é assinado segundos depois de criado; só com a varredura ele espera a próxima passagem, até cinco minutos por padrão](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-webhook-vs-sweep-pt.svg)](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-webhook-vs-sweep-pt.svg)

### 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`](https://docs.lacunasoftware.com/articles/bulk-signer/configuration) e pela [cota do KMS](https://docs.lacunasoftware.com/articles/bulk-signer/certificates#cota-e-throttling). |
| 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 | Um shell **bash** (no Windows, o Git Bash do Git for Windows), o **AWS CLI v2** autenticado (`aws sts get-caller-identity` precisa responder), o **Docker** para copiar a imagem e o **`jq`**, que não vem com o Windows nem com o Git for Windows (`winget install jqlang.jq`; `brew install jq` no macOS; `apt install jq` no Linux). 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](https://docs.lacunasoftware.com/articles/bulk-signer/installation#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. |
| Microsoft Entra ID (opcional) | Somente para o login dos operadores com a conta Microsoft ([item 9.3](https://docs.lacunasoftware.com/articles/bulk-signer/aws-signer-folder#entra-id)): um tenant, o módulo `Microsoft.Graph.Applications` do PowerShell e uma conta com o papel **Cloud Application Administrator** ou superior. |

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

```bash
export MSYS_NO_PATHCONV=1             # somente no Git Bash; veja a nota abaixo
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), sem barra no final. |
| `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. |
| `MSYS_NO_PATHCONV` | Desliga a conversão de caminhos do Git Bash. Sem ela, um argumento que começa com `/` é reescrito como um caminho do Windows antes de chegar ao AWS CLI: `--health-check-path /api/ready` vira `C:/Program Files/Git/api/ready`, a criação do target group no item 8.2 falha, e os comandos seguintes falham com `$TG` vazio. O mesmo vale para `--log-group-name /ecs/$NAME` no item 9.1. Em outros shells, a linha não tem efeito. |

### 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`:

```bash
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](https://docs.lacunasoftware.com/articles/signer/get-started#passo-1-crie-a-aplicação-e-a-chave-de-api).

| 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](https://docs.lacunasoftware.com/articles/signer/on-premises/settings) 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:

```bash
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](https://docs.lacunasoftware.com/articles/bulk-signer/configuration#the-work-share-in-an-s3-bucket--storageprovider--s3).

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

[![Importação de um PFX: a chave privada é encapsulada na sua estação e importada em uma chave do AWS KMS, o certificado público é enviado ao S3, e a task do Fargate confere um com o outro](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-key-import-pt.svg)](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-key-import-pt.svg)

### 3.1 Importação com o script

O [`Import-PfxToKms.ps1`](https://docs.lacunasoftware.com/articles/bulk-signer/samples#powershell-7--import-pfxtokmsps1) executa a importação completa e
solicita a senha do PFX:

```bash
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.

```bash
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:

```bash
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:

```bash
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.

```bash
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.**

```bash
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:

```bash
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.**

```bash
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

```bash
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` |

> **Aviso: A chave é tão segura quanto cada cópia do PFX**
>
> 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](https://docs.lacunasoftware.com/articles/bulk-signer/certificates#configuração-na-aws).

## 4. A imagem

Copie a tag que a Lacuna especificou para a instalação para um repositório ECR da conta. A Lacuna informa,
com as credenciais do registry, três valores:

| Variável | Conteúdo |
|---|---|
| `REGISTRY` | Somente o host do registry, por exemplo `<nome>.azurecr.io`. É o argumento do `docker login`, que não aceita um caminho. |
| `REPOSITORY` | O caminho da imagem dentro do registry, por exemplo `lacuna/bulksigner`. |
| `VERSION` | A versão a instalar, por exemplo `2.20.0`. Use uma versão explícita, e não `latest`. |

```bash
REGISTRY=<host do registry da Lacuna>
REPOSITORY=<caminho da imagem no registry>
VERSION=<versão>
IMAGE=$ACCOUNT.dkr.ecr.$REGION.amazonaws.com/bulksigner:$VERSION

aws ecr create-repository --repository-name bulksigner --region $REGION \
  --image-tag-mutability IMMUTABLE --image-scanning-configuration scanOnPush=true

docker login $REGISTRY --username <usuário-do-registry>     # solicita o token
docker pull --platform linux/amd64 $REGISTRY/$REPOSITORY:$VERSION
docker tag $REGISTRY/$REPOSITORY:$VERSION $IMAGE
```

Envie a imagem ao ECR. No Linux e no macOS:

```bash
aws ecr get-login-password --region $REGION \
  | docker login --username AWS --password-stdin $ACCOUNT.dkr.ecr.$REGION.amazonaws.com
docker push $IMAGE
```

**No Docker Desktop para Windows**, o `docker login` no ECR falha com
`error storing credentials … The stub received bad data`: o token do ECR excede o tamanho que o
Gerenciador de Credenciais do Windows aceita. Um diretório `--config` vazio não resolve, porque o Docker
Desktop continua usando o Gerenciador de Credenciais. Grave o token diretamente no `config.json` de um
diretório temporário e envie a imagem com ele:

```bash
mkdir -p .docker-ecr
aws ecr get-authorization-token --region $REGION --query 'authorizationData[0].authorizationToken' --output text \
  | jq -R --arg r "$ACCOUNT.dkr.ecr.$REGION.amazonaws.com" '{auths: {($r): {auth: .}}}' > .docker-ecr/config.json
docker --config .docker-ecr push $IMAGE
rm -rf .docker-ecr
```

O diretório contém um token de acesso ao ECR válido por 12 horas; o `rm` o remove. Confirme o envio:

```bash
aws ecr describe-images --region $REGION --repository-name bulksigner --query 'imageDetails[].imageTags' --output text
```

## 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` |

```bash
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}]"
```

> **Observação: A porta 443 deve permanecer acessível ao Signer**
>
> 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](https://docs.lacunasoftware.com/articles/bulk-signer/aws-signer-folder#why-webhook-secret).

## 6. O banco de dados

### 6.1 Instância

```bash
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:

```bash
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
```

A connection string inclui `TrustServerCertificate=True` porque a imagem do Bulk Signer não confia na
autoridade certificadora do Amazon RDS; essa limitação está especificada no
[passo 5 de Amazon ECS Fargate](https://docs.lacunasoftware.com/articles/bulk-signer/aws#5-o-banco-de-dados).

O banco de dados `BulkSigner` e o login `bulksigner` ainda não existem. Eles são criados por uma task
avulsa no [item 9.2](https://docs.lacunasoftware.com/articles/bulk-signer/aws-signer-folder#create-database), que depende do execution role do passo 7 e do cluster do item 9.1.

## 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` |
| `$NAME/entra-client-secret` | O client secret do registro de aplicativo no Microsoft Entra ID (opcional) | Item 9.3 | `Auth__EntraId__ClientSecret` |

`$NAME/signer-api-key` e `$NAME/signer-webhook-secret` 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:

```bash
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

| Role | Assumido por | Permissões |
|---|---|---|
| `$NAME-exec` | O Amazon ECS | 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 do item 9.2). Esse role lê os segredos do item 7.1 para o container. |
| `$NAME-task` | O Bulk Signer | `kms:DescribeKey`, `kms:GetPublicKey` e `kms:Sign` na chave do passo 3, `s3:ListBucket` no bucket e leitura, gravação e exclusão de objetos no bucket. |

Os dois roles usam a mesma política de confiança, que restringe `ecs-tasks.amazonaws.com` à conta:

```bash
cat > trust-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "ecs-tasks.amazonaws.com" },
    "Action": "sts:AssumeRole",
    "Condition": {
      "StringEquals": { "aws:SourceAccount": "$ACCOUNT" },
      "ArnLike": { "aws:SourceArn": "arn:aws:ecs:$REGION:$ACCOUNT:*" }
    }
  }]
}
EOF
aws iam create-role --role-name $NAME-exec --assume-role-policy-document file://trust-policy.json --query Role.Arn
aws iam create-role --role-name $NAME-task --assume-role-policy-document file://trust-policy.json --query Role.Arn
```

O execution role:

```bash
RDS_SECRET=$(aws rds describe-db-instances --region $REGION --db-instance-identifier $NAME \
  --query 'DBInstances[0].MasterUserSecret.SecretArn' --output text)
cat > exec-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "secretsmanager:GetSecretValue",
    "Resource": ["arn:aws:secretsmanager:$REGION:$ACCOUNT:secret:$NAME/*", "$RDS_SECRET"]
  }]
}
EOF
aws iam attach-role-policy --role-name $NAME-exec \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam put-role-policy --role-name $NAME-exec --policy-name read-deployment-secrets \
  --policy-document file://exec-policy.json
```

O task role:

```bash
KEY_ID=$(aws kms describe-key --region $REGION --key-id alias/$NAME --query KeyMetadata.KeyId --output text)
cat > task-policy.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Allow", "Action": ["kms:DescribeKey", "kms:GetPublicKey", "kms:Sign"],
      "Resource": "arn:aws:kms:$REGION:$ACCOUNT:key/$KEY_ID" },
    { "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/*" }
  ]
}
EOF
aws iam put-role-policy --role-name $NAME-task --policy-name bulksigner-app --policy-document file://task-policy.json
```

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

```bash
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:

```bash
aws acm wait certificate-validated --region $REGION --certificate-arn $CERT
```

### 8.2 Balanceador de carga, target group e listeners

```bash
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](https://docs.lacunasoftware.com/articles/bulk-signer/aws#7-https).

### 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

```bash
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 Criação do banco de dados

O Bulk Signer cria as tabelas dele, mas nunca o banco de dados, e a instância do RDS é privada. A
configuração é executada como uma task avulsa do Fargate na mesma VPC, com o `sqlcmd` da imagem
`mcr.microsoft.com/mssql-tools`, usando o usuário mestre. Grave o script:

```bash
cat > dbsetup.sh <<'EOF'
#!/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];"
EOF
```

A imagem não contém o script. A definição de task a seguir o entrega ao container codificado em base64,
na linha de comando, e lê as duas senhas do Secrets Manager:

```bash
SQL_APP_ARN=$(aws secretsmanager describe-secret --region $REGION --secret-id $NAME/sql-app --query ARN --output text)
jq -n --arg family "$NAME-dbsetup" --arg exec "arn:aws:iam::$ACCOUNT:role/$NAME-exec" \
  --arg cmd "echo $(base64 -w0 dbsetup.sh) | base64 -d | sh" --arg db "$DB_HOST" \
  --arg master "$RDS_SECRET:password::" --arg app "$SQL_APP_ARN:password::" \
  --arg logs "/ecs/$NAME" --arg region "$REGION" '{
  family: $family, networkMode: "awsvpc", requiresCompatibilities: ["FARGATE"], cpu: "256", memory: "512",
  executionRoleArn: $exec,
  containerDefinitions: [{
    name: "sqlcmd", image: "mcr.microsoft.com/mssql-tools", essential: true,
    entryPoint: ["sh", "-c"], command: [$cmd],
    environment: [{name: "DB_HOST", value: $db}, {name: "MASTER_USER", value: "bsadmin"}],
    secrets: [{name: "MASTER_PASSWORD", valueFrom: $master}, {name: "APP_PASSWORD", valueFrom: $app}],
    logConfiguration: {logDriver: "awslogs",
      options: {"awslogs-group": $logs, "awslogs-region": $region, "awslogs-stream-prefix": "dbsetup"}}
  }]}' > dbsetup-taskdef.json
aws ecs register-task-definition --region $REGION --cli-input-json file://dbsetup-taskdef.json \
  --query taskDefinition.taskDefinitionArn
```

Execute a task uma vez, aguarde o fim e leia o código de saída:

```bash
SETUP_TASK=$(aws ecs run-task --region $REGION --cluster $NAME --task-definition $NAME-dbsetup --launch-type FARGATE \
  --network-configuration "awsvpcConfiguration={subnets=[$SUBNETS],securityGroups=[$SG_TASK],assignPublicIp=ENABLED}" \
  --query 'tasks[0].taskArn' --output text)
aws ecs wait tasks-stopped --region $REGION --cluster $NAME --tasks $SETUP_TASK
aws ecs describe-tasks --region $REGION --cluster $NAME --tasks $SETUP_TASK \
  --query 'tasks[0].{exit:containers[0].exitCode,reason:stoppedReason}'
```

| Resultado | Significado |
|---|---|
| `exit` = `0` | O banco `BulkSigner` existe com `READ_COMMITTED_SNAPSHOT` ligado (o RDS, como qualquer SQL Server fora do Azure, não o liga por padrão), e o login `bulksigner` tem `db_datareader`, `db_datawriter` e `db_ddladmin`, e não `db_owner`. O log fica vazio: o `sqlcmd` não escreve nada quando um comando é bem-sucedido. |
| `exit` diferente de `0` | O primeiro comando que falhou está no log: `aws logs tail /ecs/$NAME --region $REGION --log-stream-name-prefix dbsetup`. |
| `exit` vazio e `reason` com `CannotPullContainerError` ou `ResourceInitializationError` | A task não iniciou: a imagem não foi baixada ou um segredo não pôde ser lido pelo execution role (item 7.2). |

O script pode ser executado de novo sem efeito. Não o execute com o serviço em funcionamento: o
`ROLLBACK IMMEDIATE` encerra as transações abertas no banco.

### 9.3 Login pelo Microsoft Entra ID (opcional)

Por padrão, a página de login do dashboard pede a chave `$NAME/auth-api-key`. Com uma seção
`Auth:EntraId`, ela oferece **Entrar com a Microsoft** no lugar, e dois app roles controlam o acesso:
**Administrator** para o dashboard do operador e **Approver** para as páginas do aprovador
([Configuração](https://docs.lacunasoftware.com/articles/bulk-signer/configuration#authentraid--login-opcional-pelo-microsoft-entra-id)). O `X-API-Key` da API REST não muda. Para usar a chave de API no
dashboard, pule este item.

> **Aviso: Configure antes da primeira implantação**
>
> As sessões criadas pelo login com a chave de API deixam de funcionar quando a seção `Auth:EntraId` é
> incluída. Configurado neste ponto, o Entra ID entra na primeira definição de task (item 9.4), e o perfil
> do passo 11 já é criado com a conta Microsoft. Para incluí-lo em um ambiente já implantado, registre uma
> nova revisão da definição de task e faça uma implantação ([Operação](https://docs.lacunasoftware.com/articles/bulk-signer/aws-signer-folder#day-two)).

**1. Crie o registro de aplicativo.** Atribua ao ambiente um registro dedicado, não compartilhado com
outro ambiente. O [`New-BulkSignerEntraApp.ps1`](https://docs.lacunasoftware.com/articles/bulk-signer/samples#powershell-7--new-bulksignerentraappps1)
cria um registro de tenant único, com a URI de redirecionamento `https://$HOST/signin-oidc`, os app roles
`Administrator` e `Approver`, a claim `email` e um aplicativo empresarial com **Assignment required**:

```bash
pwsh ./New-BulkSignerEntraApp.ps1 -BaseUrl https://$HOST -DisplayName "Lacuna Bulk Signer (AWS)"
```

O script recusa um nome de exibição que já existe no tenant. Ao final, ele exibe uma única vez o bloco
`Auth__EntraId__TenantId`, `Auth__EntraId__ClientId` e `Auth__EntraId__ClientSecret`.

**2. Armazene os valores.** O client secret vai para o Secrets Manager por um prompt que não ecoa a
entrada; o tenant e o client não são segredos:

```bash
read -rsp 'Client secret: ' ENTRA_SECRET; echo
aws secretsmanager create-secret --region $REGION --name $NAME/entra-client-secret \
  --secret-string "$ENTRA_SECRET" --query ARN --output text
unset ENTRA_SECRET
TENANT_ID=<o valor de Auth__EntraId__TenantId>
CLIENT_ID=<o valor de Auth__EntraId__ClientId>
```

Limpe o console em seguida. O execution role já lê `secret:$NAME/*`.

**3. Atribua as pessoas.** No centro de administração do Entra, em **Enterprise applications → o
aplicativo → Users and groups**, atribua o app role **Administrator** a cada operador. Uma conta sem
atribuição não consegue entrar, e uma conta que se autentica sem nenhum dos dois roles é recusada.

| Item | Descrição |
|---|---|
| Dependência dos forwarded headers | O login monta a URI de redirecionamento a partir do esquema que o Bulk Signer vê. As configurações `Hosting:ForwardedHeaders` do item 9.4 são obrigatórias; sem elas, a URI é `http://…/signin-oidc`, e a Microsoft a recusa com `AADSTS50011`. |
| Expiração do client secret | O tempo de vida padrão do script é de doze meses. Um segredo expirado só falha na hora do login, com um erro `AADSTS`. Faça a rotação no mesmo segredo e force uma nova implantação. |

### 9.4 Definição de task

| Parâmetro | Valor |
|---|---|
| `cpu`, `memory` | `1024`, `2048` |
| `networkMode` | `awsvpc` |
| `runtimePlatform` | `X86_64`, `LINUX` |
| `executionRoleArn`, `taskRoleArn` | `$NAME-exec`, `$NAME-task` |
| Container | `bulksigner`, com a imagem `$IMAGE` (passo 4), a porta `8080` e `stopTimeout` de `60` |
| Logs | `awslogs`, grupo `/ecs/$NAME` |

O comando a seguir monta a definição completa, com o ambiente e os segredos do container, e a registra.
As entradas `Auth__EntraId__*` são incluídas somente quando `TENANT_ID` está definida (item 9.3):

```bash
arn() { aws secretsmanager describe-secret --region $REGION --secret-id "$1" --query ARN --output text; }
jq -n --arg name "$NAME" --arg account "$ACCOUNT" --arg region "$REGION" --arg image "$IMAGE" \
  --arg bucket "$BUCKET" --arg cidr "$VPC_CIDR" --arg signer "$SIGNER" \
  --arg sql "$(arn $NAME/sql-app)" --arg auth "$(arn $NAME/auth-api-key)" --arg pki "$(arn $NAME/pki-sdk-license)" \
  --arg sak "$(arn $NAME/signer-api-key)" --arg webhook "$(arn $NAME/signer-webhook-secret)" \
  --arg tenant "${TENANT_ID:-}" --arg client "${CLIENT_ID:-}" \
  --arg entra "$([ -n "${TENANT_ID:-}" ] && arn $NAME/entra-client-secret)" '{
  family: $name, networkMode: "awsvpc", requiresCompatibilities: ["FARGATE"], cpu: "1024", memory: "2048",
  runtimePlatform: {cpuArchitecture: "X86_64", operatingSystemFamily: "LINUX"},
  executionRoleArn: "arn:aws:iam::\($account):role/\($name)-exec",
  taskRoleArn: "arn:aws:iam::\($account):role/\($name)-task",
  containerDefinitions: [{
    name: "bulksigner", image: $image, essential: true, stopTimeout: 60,
    portMappings: [{containerPort: 8080, protocol: "tcp"}],
    logConfiguration: {logDriver: "awslogs",
      options: {"awslogs-group": "/ecs/\($name)", "awslogs-region": $region, "awslogs-stream-prefix": "bulksigner"}},
    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: $cidr},
      {name: "Storage__Provider", value: "S3"},
      {name: "Storage__S3__BucketName", value: $bucket},
      {name: "Storage__S3__Prefix", value: "work"},
      {name: "Storage__S3__Region", value: $region},
      {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: $region},
      {name: "Signing__Certificate__AwsKms__KeyId", value: "alias/\($name)"},
      {name: "Signing__Certificate__AwsKms__Credential", value: "InstanceRole"},
      {name: "Signing__Certificate__AwsKms__S3__Bucket", value: $bucket},
      {name: "Signing__Certificate__AwsKms__S3__Key", value: "signing-material/\($name).cer"},
      {name: "Signing__Certificate__AwsKms__S3__Region", value: $region},
      {name: "Signing__Certificate__AwsKms__S3__Credential", value: "InstanceRole"},
      {name: "Signer__Endpoint", value: $signer},
      {name: "Signer__FolderSweepIntervalSeconds", value: "300"}
    ] + (if $tenant == "" then [] else [
      {name: "Auth__EntraId__TenantId", value: $tenant},
      {name: "Auth__EntraId__ClientId", value: $client}
    ] end)),
    secrets: ([
      {name: "ConnectionStrings__Default", valueFrom: "\($sql):connectionString::"},
      {name: "Auth__ApiKey", valueFrom: $auth},
      {name: "Signing__PkiSdkLicense", valueFrom: $pki},
      {name: "Signer__ApiKey", valueFrom: $sak},
      {name: "Signer__WebhookSecret", valueFrom: $webhook}
    ] + (if $tenant == "" then [] else [
      {name: "Auth__EntraId__ClientSecret", valueFrom: $entra}
    ] end))
  }]}' > taskdef.json
aws ecs register-task-definition --region $REGION --cli-input-json file://taskdef.json \
  --query taskDefinition.taskDefinitionArn
```

Um ARN vazio no `taskdef.json` indica um segredo do item 7.1 que não foi criado; o `register-task-definition`
o recusa. Para um ambiente de homologação que assina com certificados de teste da Lacuna, troque
`Production` por `Staging` e inclua `Signing__TrustLacunaTestRoot=true` (item 9.5).

### 9.5 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](https://docs.lacunasoftware.com/articles/bulk-signer/configuration#signer--conexão-com-o-lacuna-signer)).

| 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](https://docs.lacunasoftware.com/articles/bulk-signer/aws-signer-folder#why-webhook-secret)). |
| `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,
  torna `Signer:Endpoint` e `Signer:ApiKey` obrigatórias no boot.
- **Chaves não usadas por este método.** `Signer:PollIntervalSeconds`, `Signer:TimeoutHours` e
  `Signer:MaxConsecutiveApiFailures` valem somente para o método `LacunaSigner`.

**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](https://docs.lacunasoftware.com/articles/bulk-signer/aws#referência-de-configuração). 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](https://docs.lacunasoftware.com/articles/bulk-signer/configuration#hosting)). |
| `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](https://docs.lacunasoftware.com/articles/bulk-signer/configuration#signingtrustlacunatestroot--certificados-de-teste-para-homologação)). |

### 9.6 Serviço

```bash
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. |

O primeiro boot aplica as migrations do banco. Aguarde o serviço estabilizar:

```bash
aws ecs wait services-stable --region $REGION --cluster $NAME --services bulksigner
```

## 10. Verificação

```bash
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](https://docs.lacunasoftware.com/articles/bulk-signer/aws#9-verificação) (`database`,
`input-folder:default`, `storage-bucket:…`, `work-share-owner`, `pki-license`), `/api/ready/details` informa
a verificação `cloudhub`, com `ok: true` e o detalhe `not configured (CloudHub:ApiKey)`, que é o valor
normal nesta implantação, e 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:

```bash
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. |

Com o Entra ID (item 9.3), siga o primeiro redirecionamento do login sem entrar:

```bash
curl -s -o /dev/null -w '%{redirect_url}\n' https://$HOST/api/auth/entra-login
```

A URL deve apontar para `login.microsoftonline.com/<tenant>/oauth2/v2.0/authorize`, com o ID do
aplicativo e `redirect_uri=https%3A%2F%2F<host>%2Fsignin-oidc`. O esquema `https` é o valor a confirmar.

## 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>/` e entre: com **Entrar com a Microsoft**, usando uma conta com o app
role **Administrator**, se o item 9.3 foi aplicado; do contrário, com a chave armazenada em
`$NAME/auth-api-key`, copiada para a área de transferência sem ser exibida:

```bash
aws secretsmanager get-secret-value --region $REGION --secret-id $NAME/auth-api-key \
  --query SecretString --output text | tr -d '\r\n' | clip
```

Abra **Perfis de assinatura → Novo perfil** (`/profiles/_new`).

[![A página Novo perfil de assinatura com o nome aws-signer, o método Pasta do Lacuna Signer e uma pasta escolhida no seletor](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-profile-form-pt.png)](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-profile-form-pt.png)

| 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** |
| **Endpoint compatível com S3** | Vazio. O campo é para um armazenamento compatível com S3, como o MinIO. |
| **Endereçamento do bucket por caminho** | Desligado, pelo mesmo motivo. |

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.

[![Os campos de certificado do novo perfil: Em um objeto do Amazon S3, a região da AWS, o alias da chave do KMS, InstanceRole, e o bucket, a chave do objeto, a região e o modo de credencial do S3](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-profile-kms-pt.png)](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-profile-kms-pt.png)

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

```bash
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:

```bash
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:

1. Abra **Organizações** e selecione a organização.
2. 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.
3. Preencha os campos conforme especificado abaixo.
4. Selecione **Salvar**, que permanece desabilitado até o token ser informado.
5. Limpe a área de transferência.

[![A seção Integração de uma organização no Lacuna Signer: um novo webhook com a URL terminada em /api/signer/notifications, o tipo de autenticação Bearer e o campo Token de Autenticação](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-signer-webhook-pt.png)](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-signer-webhook-pt.png)

| 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](https://docs.lacunasoftware.com/articles/signer/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](https://docs.lacunasoftware.com/articles/signer/webhooks#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:

1. Selecione **Novo documento**, faça upload de um PDF e selecione a organização e **a pasta vinculada**.
2. 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.
3. Selecione **Enviar**.

[![A página Novo documento do Lacuna Signer: a organização, a pasta AWSTest e um participante do tipo Signatário com os campos Nome, CPF e Email](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-signer-new-document-pt.png)](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-signer-new-document-pt.png)

### 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`:

[![A página Jobs com o job Doc1, marcado como Lacuna Signer e application/pdf, no perfil aws-signer com status Concluído](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-jobs-pt.png)](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-jobs-pt.png)

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

[![A página do job: tempo total de processamento de 1,2 segundo e a linha do tempo Na fila, Processando, Verificando, Concluído, terminando com Signed at Lacuna Signer](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-job-timeline-pt.png)](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-job-timeline-pt.png)

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

[![A pasta vinculada no Lacuna Signer, listando o documento Doc1 com o status Concluído](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-signer-completed-pt.png)](https://docs.lacunasoftware.com/images/bulk-signer/aws-signer-folder-signer-completed-pt.png)

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

```bash
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](https://docs.lacunasoftware.com/articles/bulk-signer/aws-signer-folder#symptoms).

## Operação

As operações da implantação base estão especificadas no
[passo 11 de Amazon ECS Fargate](https://docs.lacunasoftware.com/articles/bulk-signer/aws#11-operação). 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](https://docs.lacunasoftware.com/articles/bulk-signer/lacuna-signer#quando-a-execução-que-o-assinava-termina)). 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. |
| Inclusão do Entra ID em um ambiente implantado | Execute o item 9.3, gere de novo o `taskdef.json` do item 9.4 com `TENANT_ID` e `CLIENT_ID` definidas, registre a nova revisão e implante como na primeira linha desta tabela. | As sessões criadas pelo login com a chave de API deixam de funcionar. Os perfis existentes não mudam. |
| 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](https://docs.lacunasoftware.com/articles/bulk-signer/rest-api#notificações-do-signer).

| 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](https://docs.lacunasoftware.com/articles/bulk-signer/aws-signer-folder#day-two)). |
| `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. |
| `error storing credentials … The stub received bad data` | Docker Desktop para Windows, no `docker login` do ECR | O Gerenciador de Credenciais do Windows recusa o tamanho do token do ECR. | Envie a imagem com o diretório `.docker-ecr` do passo 4. Um diretório `--config` vazio não resolve. |
| `Health check path 'C:/Program Files/Git/api/ready' must begin with a '/' character` | AWS CLI no Git Bash, item 8.2 | O Git Bash converteu `/api/ready` em um caminho do Windows. | Execute `export MSYS_NO_PATHCONV=1` ([Variáveis de shell](https://docs.lacunasoftware.com/articles/bulk-signer/aws-signer-folder#variáveis-de-shell)) e repita o item 8.2 a partir do `create-target-group`. |
| `AADSTS50011` | Microsoft Entra ID, no login | A URI de redirecionamento enviada é `http://…` ou não corresponde ao registro de aplicativo. | Confirme as configurações `Hosting:ForwardedHeaders` do item 9.4 e a URI `https://<host>/signin-oidc` do registro (item 9.3). |
| `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](https://docs.lacunasoftware.com/articles/bulk-signer/troubleshooting#jobs-do-aws-kms-falham-com-throttlingexception-ou-keyunavailableexception). |

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](https://docs.lacunasoftware.com/articles/bulk-signer/troubleshooting#um-job-de-pasta-do-lacuna-signer-termina-failed-ou-canceled).

### 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](https://docs.lacunasoftware.com/articles/bulk-signer/troubleshooting#documentos-do-lacuna-signer-só-são-encontrados-no-intervalo-da-varredura-e-não-em-segundos). |
| 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](https://docs.lacunasoftware.com/articles/bulk-signer/troubleshooting#um-perfil-de-pasta-do-signer-está-degradado-dizendo-que-o-certificado-não-tem-cpf). |
| **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](https://docs.lacunasoftware.com/articles/bulk-signer/troubleshooting#um-perfil-awskms-está-degradado-na-inicialização). |
| 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](https://docs.lacunasoftware.com/articles/bulk-signer/troubleshooting#a-linha-signer-folder-sweep-em-apireadydetails-está-vermelha). |

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](https://docs.lacunasoftware.com/articles/bulk-signer/aws#referência-de-erros).

## 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](https://docs.lacunasoftware.com/articles/bulk-signer/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](https://docs.lacunasoftware.com/articles/bulk-signer/aws#limitações) valem sem alteração. |
