---
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**. 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`](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).

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.

> **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: uma pessoa 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 | **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](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. |

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

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

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

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

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

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

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

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

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

```json
{
  "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. |

```json
{
  "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

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

```json
"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](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.4 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. |

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

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

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

[![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** |

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