---
title: "Implantando no Amazon ECS Fargate"
description: "Procedimento de implantação do Bulk Signer como uma instância única no Amazon ECS Fargate, com RDS for SQL Server, um bucket S3 e a chave de assinatura no AWS KMS, 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"
locale: "pt-BR"
product: "Bulk Signer"
last_updated: "2026-11-11"
---

# Implantando no Amazon ECS Fargate

Esta página especifica uma implantação do Bulk Signer em instância única na Amazon Web Services. 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 uma pasta monitorada de entrada em **um bucket do Amazon S3**;
- a chave de assinatura no **AWS KMS**.

Este não é um tipo de instalação à parte. É o [alvo Docker](https://docs.lacunasoftware.com/articles/bulk-signer/installation#docker--compose) executado
por um orquestrador gerenciado, e cada configuração desta página é uma chave de configuração comum. O
procedimento foi validado de ponta a ponta em um ambiente de homologação: um PDF foi enviado por HTTPS,
assinado em PAdES na política AD-RB da ICP-Brasil com um certificado e-CNPJ cuja chave estava armazenada
no AWS KMS, e recuperado do S3.

> **Info: Versão mínima: 2.20.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 S3 não oferece lease, então não é
> possível distinguir uma segunda instância ativa de uma instância que parou de responder. O serviço do ECS
> do passo 8 é 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

![O Bulk Signer no Amazon ECS Fargate: uma task atrás de um Application Load Balancer, com RDS for SQL Server, S3, AWS KMS, Secrets Manager e CloudWatch Logs](https://docs.lacunasoftware.com/images/bulk-signer/aws-single-instance-architecture.svg)

### Roles do IAM

| Role | Assumido por | Finalidade |
|---|---|---|
| Execution role (`$NAME-exec`) | Amazon ECS | Baixa a imagem, escreve o stream de log e lê os segredos injetados no ambiente do container. |
| Task role (`$NAME-task`) | Bulk Signer | A identidade com que a aplicação é executada. É a credencial `InstanceRole` citada por todas as configurações de S3 e KMS desta página. |

Nenhum dos dois roles armazena uma chave de acesso de longa duração. A única credencial sujeita a rotação
manual é a senha do login SQL na connection string, porque o RDS for SQL Server não oferece autenticação
de banco de dados pelo IAM.

### Seleção de componentes

| Componente | Seleção | Justificativa |
|---|---|---|
| Computação | **ECS Fargate**, uma task, 1 vCPU / 2 GB | A imagem é executada sem alterações. Uma task do Fargate não tem disco que persista além da própria task, o que determina as quatro linhas seguintes. |
| Banco operacional | **RDS for SQL Server 2022** | O SQLite exige um disco durável. A edição Express é suficiente para um ambiente de homologação e limita o banco a 10 GB; ambientes de produção exigem a edição Web ou Standard. |
| Compartilhamento de trabalho e entradas | **Um bucket S3** (`Storage:Provider = S3`) | Não há disco persistente disponível. O bucket guarda `processing/`, `output/` e `error/` sob um prefixo e a pasta monitorada sob outro. |
| Key ring de sessão | **Banco operacional** (`Auth:SessionKeyRing:Placement = OperationalStore`) | Com qualquer outro posicionamento, o key ring é gravado no disco da task, e cada implantação encerra todas as sessões. |
| Chave de assinatura | **AWS KMS** (`Source = AwsKms`) | A chave nunca sai do KMS, e cada assinatura é uma chamada `kms:Sign` registrada pelo AWS CloudTrail. |
| Certificado (`.cer`) | **Um objeto do S3** no mesmo bucket | Não há caminho em disco disponível para um arquivo de certificado. O objeto é lido uma vez, no boot. |
| Logs | **stdout → CloudWatch Logs** | O log em arquivo continua sendo escrito, mas em um disco descartado junto com a task. |
| Backup do banco | **Backups automáticos do RDS** | `Backup:Enabled` é recusado com `Database:Provider = SqlServer`. |

## Pré-requisitos

| Requisito | Especificação |
|---|---|
| Ferramentas | **AWS CLI v2** autenticado (`aws sts get-caller-identity` precisa responder) e **Docker** para copiar a imagem. Para a importação da chave no passo 2: **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). |
| Material de assinatura | O **PFX** cuja chave vai assinar, ou uma chave do KMS e o certificado que uma Autoridade Certificadora (AC) emitiu para ela. |
| Licenças e chaves | A **licença do PKI SDK** (obrigatória). A chave de API do Lacuna Signer e a licença do Web PKI, se essas integrações forem usadas. |
| DNS | Um **hostname** e permissão de escrita na zona de DNS autoritativa para ele. |

> **Aviso: Identifique a zona de DNS autoritativa**
>
> Um domínio pode ter uma hosted zone do Route 53 na conta e ainda assim estar delegado a outro provedor
> (Azure DNS, Cloudflare, um registrador). Registros escritos em uma zona não autoritativa nunca são
> resolvidos, e o certificado do ACM solicitado no passo 7 permanece em *Pending validation*
> indefinidamente. Confirme a delegação em um resolvedor público antes de começar:
>
> ```bash
> dig +short NS example.com
> ```

### 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
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. |
| `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; nenhum NAT gateway é necessário. Em produção, coloque a task e o banco em
sub-redes privadas, atrás de um NAT gateway ou de VPC endpoints. Nenhuma outra configuração desta página
muda.

## 1. O bucket

Crie o bucket, bloqueie todo acesso público e configure a interrupção de uploads multipart incompletos
após um dia:

```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}}]}'
```

Crie a pasta monitorada. O Bulk Signer nunca cria uma pasta monitorada, então ela precisa existir antes do
primeiro boot. No S3, uma pasta é um objeto marcador de tamanho zero:

```bash
aws s3api put-object --bucket $BUCKET --key inputs/default/
```

O bucket usa três prefixos:

| Prefixo | Conteúdo | Criado por |
|---|---|---|
| `work/` | O compartilhamento de trabalho: `processing/`, `output/` e `error/` | O Bulk Signer, no boot |
| `inputs/default/` | A pasta monitorada | O operador, antes do primeiro boot (o objeto marcador acima) |
| `signing-material/` | O certificado de assinatura (`.cer`) | O script de importação, no passo 2 |

Os prefixos não podem se aninhar. Uma pasta monitorada dentro do prefixo do compartilhamento de trabalho
é recusada no boot.

**O versionamento e o Object Lock devem permanecer desligados.** Com o versionamento ligado, toda entrada
excluída e toda cópia preparada são mantidas como versão anterior, inclusive arquivos de pagamento. As
demais diferenças de comportamento de um compartilhamento de trabalho no S3 (ausência de lease,
movimentação implementada como uma cópia seguida de uma exclusão e verificação de fingerprint antes de
cada assinatura) estão documentadas em
[Configuração](https://docs.lacunasoftware.com/articles/bulk-signer/configuration#the-work-share-in-an-s3-bucket--storageprovider--s3) e em
[Instalação](https://docs.lacunasoftware.com/articles/bulk-signer/installation#armazenamento-no-azure-blob-storage-e-no-s3-opcional).

## 2. A chave de assinatura

Para um PFX existente, o [`Import-PfxToKms.ps1`](https://docs.lacunasoftware.com/articles/bulk-signer/samples#powershell-7--import-pfxtokmsps1) executa a
importação completa:

1. Cria no KMS uma chave RSA de assinatura com material de chave importado.
2. Encapsula a chave privada localmente, no formato que o KMS exige.
3. Importa o material de chave encapsulado.
4. Compara a chave pública do KMS com o certificado, com a mesma verificação que o Bulk Signer faz no
   boot.
5. Faz upload do arquivo `.cer` para o S3.

O script 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. O passo 8 o cita como `alias/<nome>` em `Signing:Certificate:AwsKms:KeyId`. 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 6; 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. Para fazer uma assinatura de teste pela linha de comando, inclua o usuário do IAM
correspondente na lista de exceções.

> **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 e as verificações que o Bulk Signer faz ao abrir a chave
estão documentados em [Certificados](https://docs.lacunasoftware.com/articles/bulk-signer/certificates#configuração-na-aws).

**Requisitos da chave**, seja qual for a origem:

| Propriedade | Valor exigido |
|---|---|
| Tipo de chave | RSA 2048, 3072 ou 4096 |
| Uso de chave | **Sign and verify** |

## 3. A imagem

Copie a tag que a Lacuna indicou para instalação para um repositório do ECR na 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>     # pede 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
```

| Requisito | Motivo |
|---|---|
| Copie uma versão explícita, não `latest`. | A tag enviada aqui é a tag que a definição de task fixa, e uma atualização é uma implantação agendada (passo 11). |
| Use um repositório na própria conta. | O ECS baixa a imagem toda vez que uma task inicia. Um repositório local garante que um reinício não dependa da disponibilidade de um registry externo. |
| Plataforma `linux/amd64`. | A definição de task especifica a arquitetura `X86_64` (passo 8). |

> **Observação: Docker Desktop no Windows: "The stub received bad data"**
>
> O token de login do ECR excede o tamanho que o Gerenciador de Credenciais do Windows aceita, então o
> `docker login` no ECR pode falhar com `error storing credentials … The stub received bad data`. Faça o
> login e o push com um diretório de configuração do Docker temporário (`docker --config <pasta vazia>
> login …` e `docker --config <mesma pasta> push …`) e exclua o diretório em seguida.

## 4. 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}]"
```

Como `$NAME-task` admite somente o balanceador de carga, nenhum outro host consegue acessar a porta do
Bulk Signer para forjar forwarded headers. É essa condição que permite ao passo 8 confiar em forwarded
headers vindos de toda a faixa da VPC.

## 5. O banco de dados

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

| Parâmetro | Valor | Observações |
|---|---|---|
| `--engine` | `sqlserver-ex` | Edição Express. Em produção, use `sqlserver-web` ou `sqlserver-se` e avalie o Multi-AZ. |
| `--engine-version` | Uma versão `16.00` | As versões `16.00` do engine são o SQL Server 2022. O primeiro comando lista as disponíveis na região. |
| `--db-instance-class` | `db.t3.small` | |
| `--storage-type`, `--allocated-storage` | `gp3`, `20` | |
| `--master-username` | `bsadmin` | Usado somente pela task de configuração do item 5.3. |
| `--manage-master-user-password` | — | Armazena a senha mestre em um segredo do Secrets Manager que o próprio RDS controla e rotaciona. |
| `--no-publicly-accessible` | — | A instância só é acessível de dentro da VPC. |
| `--backup-retention-period` | `7` | Dias de backups automáticos. |
| `--storage-encrypted` | — | Criptografa o armazenamento em repouso. |

### 5.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. `ConnectionStrings:Default` precisa da string completa, e o ECS consegue injetar uma chave de um
segredo, mas não concatenar duas:

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

O segredo `$NAME/sql-app` tem a seguinte estrutura:

| Chave | Conteúdo | Consumidor |
|---|---|---|
| `username` | `bulksigner` | — |
| `password` | A senha gerada (40 caracteres, sem pontuação) | A task de configuração do item 5.3 (`APP_PASSWORD`) |
| `connectionString` | A connection string abaixo | O serviço (`ConnectionStrings__Default`) |

| Propriedade da connection string | Valor |
|---|---|
| `Server` | `tcp:<endpoint do RDS>,1433` |
| `Initial Catalog` | `BulkSigner` |
| `User ID` | `bulksigner` |
| `Password` | A senha gerada |
| `Encrypt` | `True` |
| `TrustServerCertificate` | `True` (veja o aviso abaixo) |

> **Aviso: TrustServerCertificate=True é uma limitação conhecida**
>
> A imagem do Bulk Signer não confia na autoridade certificadora do Amazon RDS. Sem essa propriedade, o
> primeiro login falha com *The certificate chain was issued by an authority that is not trusted*. Dentro
> de uma VPC cujo security group do banco admite somente a task, uma interceptação é um risco menor do que
> em uma rede compartilhada; ainda assim, o certificado do servidor de banco de dados não é verificado.

### 5.3 Criação do banco de dados

O Bulk Signer cria as tabelas, mas nunca o banco
([Escolhendo onde fica o banco operacional](https://docs.lacunasoftware.com/articles/bulk-signer/installation#escolhendo-onde-fica-o-banco-operacional)), e
a instância é privada. Execute a configuração como uma task avulsa do Fargate na mesma VPC, com o
`sqlcmd` da imagem `mcr.microsoft.com/mssql-tools` e este 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 de task de configuração exige:

| Item | Tipo | Valor |
|---|---|---|
| Imagem | — | `mcr.microsoft.com/mssql-tools` |
| `DB_HOST` | Variável de ambiente | O endpoint do RDS (`$DB_HOST`) |
| `MASTER_USER` | Variável de ambiente | `bsadmin` |
| `MASTER_PASSWORD` | Segredo | `"<ARN do segredo gerenciado pelo RDS>:password::"` |
| `APP_PASSWORD` | Segredo | `"<ARN de $NAME/sql-app>:password::"` |
| Execution role | — | `$NAME-exec` (passo 6) |
| Logs | — | O grupo `awslogs` do serviço, `/ecs/$NAME` (passo 8) |
| Rede | — | As sub-redes da task e o security group `$NAME-task` |

Crie o execution role (passo 6) e o grupo de logs (passo 8) antes de executar a task de configuração.
Execute-a uma vez com `aws ecs run-task` e leia o log. **Código de saída 0** confirma que:

- o banco `BulkSigner` existe com `READ_COMMITTED_SNAPSHOT` ligado, que o RDS, como qualquer SQL Server
  fora do Azure, não liga por padrão;
- o login `bulksigner` existe e tem `db_datareader`, `db_datawriter` e `db_ddladmin`, e **não**
  `db_owner`.

## 6. Segredos e roles

### 6.1 Segredos

Gere o `Auth:ApiKey`, que é o `X-API-Key` da API REST e, a menos que o login pelo Microsoft Entra ID esteja
ligado (passo 10), a chave de login do dashboard:

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

Informe manualmente os valores emitidos pela Lacuna, no console (**Other type of secret → Plaintext**,
somente com o valor) ou por um prompt que não exibe o que é digitado. Não os registre no histórico do
shell.

O inventário completo de segredos da implantação:

| Segredo | Conteúdo | Criado em | Injetado como |
|---|---|---|---|
| `$NAME/sql-app` | JSON com `username`, `password` e `connectionString` | Item 5.2 | `ConnectionStrings__Default` (chave `connectionString`) |
| `$NAME/auth-api-key` | Uma chave aleatória de 48 caracteres | Item 6.1 | `Auth__ApiKey` |
| `$NAME/pki-sdk-license` | A licença do PKI SDK | Item 6.1, manualmente | `Signing__PkiSdkLicense` |
| `$NAME/signer-api-key` | A chave de API do Lacuna Signer, no formato `<aplicação>\|<segredo>`; a parte da aplicação pode ter espaços | Item 6.1, manualmente | `Signer__ApiKey` |
| `$NAME/webpki-license` | A licença do Web PKI | Item 6.1, manualmente | `WebPki__License` |
| `$NAME/entra-client-secret` | O client secret do Entra ID | Passo 10 (opcional) | `Auth__EntraId__ClientSecret` |
| Segredo mestre gerenciado pelo RDS | A senha de `bsadmin` | Item 5.1, pelo RDS | `MASTER_PASSWORD` (somente na task de configuração) |

### 6.2 Roles

Os dois roles usam a seguinte política de confiança, 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, somente para a task de configuração). |
| `$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>/*" }
  ]
}
```

O momento em que cada ação é chamada está especificado em
[Chamadas à AWS feitas pela task](https://docs.lacunasoftware.com/articles/bulk-signer/aws#chamadas-à-aws-feitas-pela-task).

**ECS Exec (opcional).** Para abrir um shell na task em execução para diagnóstico, inclua as quatro ações
de canal `ssmmessages:*` no task role e passe `--enable-execute-command` ao criar o serviço no passo 8.

## 7. HTTPS

### 7.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 (veja [Pré-requisitos](https://docs.lacunasoftware.com/articles/bulk-signer/aws#pré-requisitos)) e aguarde a
validação:

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

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

| Recurso | Configuração | Valor | Finalidade |
|---|---|---|---|
| Balanceador de carga | Esquema | `internet-facing` | Ponto de entrada público. |
| Balanceador de carga | `idle_timeout.timeout_seconds` | `300` | Mantém aberto o WebSocket do dashboard entre atualizações. |
| Balanceador de carga | `routing.http.drop_invalid_header_fields.enabled` | `true` | Remove headers HTTP com nomes inválidos antes do encaminhamento. |
| Target group | Protocolo, porta, tipo de destino | `HTTP`, `8080`, `ip` | Tasks do Fargate no modo `awsvpc` se registram pelo endereço IP. |
| Target group | Health check | `/api/ready`, a cada 30 s; 2 saudáveis, 3 não saudáveis; `200` esperado | Veja o contrato abaixo. |
| Target group | `deregistration_delay.timeout_seconds` | `30` | |
| Listener HTTPS | Porta, política, ação | `443`, `ELBSecurityPolicy-TLS13-1-2-2021-06`, encaminhar ao target group | |
| Listener HTTP | Porta, ação | `80`, redirecionar para HTTPS com `301` | |

**Contrato do health check.** O `GET /api/ready` é anônimo e responde `503` até o schema do banco ser
aplicado. Assim, o balanceador de carga não encaminha tráfego a uma task que ainda não consegue atender
requisições. Veja [Endpoints HTTP](https://docs.lacunasoftware.com/articles/bulk-signer/aws#endpoints-http).

### 7.3 DNS

Aponte o hostname para o nome DNS do balanceador de carga, com um registro CNAME ou, se a zona estiver no
Route 53, com um registro de alias.

## 8. O serviço

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

### 8.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 3) |
| 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://<o endereço do seu Lacuna Signer>" }
],
"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": "WebPki__License",            "valueFrom": "<ARN de $NAME/webpki-license>" }
]
```

Cada chave está especificada na [Referência de configuração](https://docs.lacunasoftware.com/articles/bulk-signer/aws#referência-de-configuração). As seguintes
regras valem para esta implantação:

- **Key ring de sessão.** `Auth__SessionKeyRing__Placement = OperationalStore` armazena o key ring no
  banco. Sem essa configuração, o key ring é gravado no disco da task, e cada implantação encerra as
  sessões de todos os operadores e aprovadores. O primeiro boot com ela cria um key ring novo, o que
  encerra todas as sessões uma vez
  ([Configuração](https://docs.lacunasoftware.com/articles/bulk-signer/configuration#authsessionkeyring--where-the-session-key-ring-is-kept)).
- **Forwarded headers.** Os endereços IP do balanceador de carga mudam e não podem ser listados um a um,
  então a faixa inteira da VPC é considerada confiável; o security group do passo 4 garante que somente o
  balanceador acessa a porta. Sem essas configurações, o Bulk Signer vê o esquema como `http`, os cookies
  dele não são marcados como seguros, uma aprovação registra o endereço do balanceador em vez do endereço
  do aprovador, e o login pelo Entra ID (passo 10) falha ([Configuração](https://docs.lacunasoftware.com/articles/bulk-signer/configuration#hosting)).
- **Pasta monitorada.** A entrada não especifica provider e, portanto, herda o S3 e o bucket do
  compartilhamento de trabalho. `PollIntervalSeconds` é obrigatório, porque objetos novos em um bucket só
  são detectados por listagem.
- **Seed do perfil de assinatura.** `Signing:Certificate` é o seed (carga inicial) do perfil de assinatura
  `default`, aplicado somente no primeiro boot contra um banco vazio. Alterações posteriores são feitas
  na página **Perfis de assinatura** do dashboard. Toda credencial é `InstanceRole`, então nenhum perfil
  armazena segredo e `Signing:ProfileSecretsKey` não é necessário.
- **Lacuna Signer.** As chaves do `Signer` são opcionais e devem ser definidas em conjunto: definir
  `Signer__Endpoint` torna `Signer__ApiKey` obrigatório no boot. Omita as duas para executar sem o Lacuna
  Signer. O endpoint é o endereço em que a própria API do Signer responde (`GET <endpoint>/api/documents`
  retorna `401` sem uma chave), que não é necessariamente o endereço da página inicial do produto.
  Verifique o endpoint com essa requisição.
- **Homologação.** Para um ambiente que assina com os certificados de teste da Lacuna, defina
  `ASPNETCORE_ENVIRONMENT` como `Staging` e inclua `Signing__TrustLacunaTestRoot=true`
  ([Configuração](https://docs.lacunasoftware.com/articles/bulk-signer/configuration#signingtrustlacunatestroot--certificados-de-teste-para-homologação)).
  Nenhuma das duas configurações é necessária para um certificado de produção da ICP-Brasil.

### 8.3 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. O padrão do ECS inicia a nova task primeiro, e duas tasks no mesmo compartilhamento de trabalho no S3 não podem ser detectadas. Cada implantação, portanto, causa uma breve indisponibilidade. |
| `--desired-count` | `1` | Não deve ser aumentado, pelo mesmo motivo. Não associe auto scaling. |
| `--health-check-grace-period-seconds` | `300` | O primeiro boot aplica todas as migrations do banco e testa o bucket. O período de carência impede que o balanceador de carga pare a task antes de ela ficar pronta. |
| `deploymentCircuitBreaker` | `enable=true`, `rollback=false` | Uma implantação que falha é interrompida em vez de fazer rollback, o que iniciaria uma segunda task. |
| `assignPublicIp` | `ENABLED` | Necessário para as chamadas de saída a partir de sub-redes públicas (veja [Posicionamento de rede](https://docs.lacunasoftware.com/articles/bulk-signer/aws#posicionamento-de-rede)). |
| `--load-balancers` | Target group, container `bulksigner`, porta `8080` | Registra a task no target group do passo 7. |

## 9. Verificação

### 9.1 Prontidão e perfil

```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
curl -s -H "X-API-Key: $KEY" https://$HOST/api/profiles
```

Em um primeiro boot bem-sucedido, o `/api/ready/details` informa as seguintes verificações:

| Verificação | Resultado esperado |
|---|---|
| `database` | Acessível, citando o host do RDS e `BulkSigner`. |
| `storage-bucket:…` | Acessível, com **toda requisição condicional recusada como esperado**. É a verificação de inicialização que confirma que o S3 respeita as requisições condicionais de que o compartilhamento de trabalho depende. |
| `input-folder:default` | Em execução, em `s3://…/inputs/default`. |
| `work-share-owner` | *Written at startup, not held*. É o valor normal no S3, não uma falha. |
| `pki-license` | Carregada. |

O `/api/profiles` lista o perfil `default` com `certificateSource` = `AwsKms` e `degraded` = `false`.
Isso confirma que a task leu o certificado do S3, acessou o KMS com o role dela e encontrou a chave
correspondente ao certificado.

### 9.2 Teste de assinatura

1. Envie um PDF: `POST /api/files`, multipart, campo `file`. A resposta é `202 Accepted` com o `jobId`.
2. Consulte `GET /api/jobs/{id}` até que `status` seja `Completed`.
3. Baixe o arquivo assinado: `GET /api/jobs/{id}/output`.

```bash
JOB=$(curl -s -H "X-API-Key: $KEY" -F "file=@sample.pdf" https://$HOST/api/files | jq -r .jobId)
curl -s -H "X-API-Key: $KEY" https://$HOST/api/jobs/$JOB | jq -r .status
curl -s -H "X-API-Key: $KEY" -o sample.signed.pdf https://$HOST/api/jobs/$JOB/output
```

Evidências esperadas:

- O histórico do job registra a saída como `s3://…/work/output/<nome>.signed.pdf`.
- O histórico de eventos do CloudTrail mostra a chamada `Sign` da task em alguns minutos.

Um nome de arquivo concluído fica reservado. Um novo envio com o mesmo nome retorna `409` com
`file.already-processed` até o job ser excluído na página **Jobs** do dashboard.

### 9.3 Avisos de inicialização esperados

Os seguintes avisos do log de inicialização são esperados nesta topologia e não indicam falha:

| Aviso | Explicação |
|---|---|
| Marcador do compartilhamento de trabalho *written, not held* | O S3 não oferece lease. |
| Key ring armazenado sem criptografia | O key ring é armazenado no banco e protegido pelo controle de acesso do banco. |
| `ValidateCertificate=false` no perfil criado pelo seed | A configuração pode ser ligada na página do perfil. |

## 10. Login pelo Microsoft Entra ID (opcional)

Por padrão, a página de login do dashboard pede a chave de API. Quando uma seção `Auth:EntraId` está
configurada, a página 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.

### 10.1 Registro de aplicativo

Atribua ao ambiente **um registro de aplicativo dedicado**, não um compartilhado com outro ambiente, para
que o acesso seja controlado pelas atribuições de role dele e o client secret dele siga um calendário de
rotação próprio. O [`New-BulkSignerEntraApp.ps1`](https://docs.lacunasoftware.com/articles/bulk-signer/samples#powershell-7--new-bulksignerentraappps1) o
cria:

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

| Parâmetro | Valor | Descrição |
|---|---|---|
| `-BaseUrl` | `https://$HOST` | A URL base pública; a URI de redirecionamento é derivada dela. |
| `-DisplayName` | `Lacuna Bulk Signer (AWS)` | O nome de exibição do registro de aplicativo. |

O script mostra o client secret uma única vez. Armazene-o no Secrets Manager como
`$NAME/entra-client-secret` (o execution role já lê `secret:$NAME/*`) e limpe o console. Atribua os
usuários no centro de administração do Entra, em **Enterprise applications** → o aplicativo → **Users and
groups**. Um usuário sem atribuição de role não consegue entrar.

### 10.2 Configuração

Inclua as seguintes entradas na definição de task, registre a nova revisão e atualize o serviço:

```json
{ "name": "Auth__EntraId__TenantId", "value": "<ID do diretório (tenant)>" },
{ "name": "Auth__EntraId__ClientId", "value": "<ID do aplicativo (client)>" }
```

```json
{ "name": "Auth__EntraId__ClientSecret", "valueFrom": "<ARN de $NAME/entra-client-secret>" }
```

### 10.3 Pré-condições e efeitos

| Item | Descrição |
|---|---|
| Dependência dos forwarded headers | O login monta a URI de redirecionamento a partir do esquema que o Bulk Signer vê. Sem as configurações de forwarded headers do passo 8, ele envia `http://…/signin-oidc`, que a Microsoft recusa com `AADSTS50011`. |
| Encerramento de sessões | Sessões criadas pelo login com chave de API param de funcionar assim que a seção existe. |
| 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. |

### 10.4 Verificação

Siga o primeiro redirecionamento sem entrar:

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

A URL precisa apontar para `login.microsoftonline.com/<tenant>/oauth2/v2.0/authorize`, com o ID do
aplicativo e `redirect_uri=https://<host>/signin-oidc`. O esquema **`https`** em `redirect_uri` é o valor a
confirmar.

## 11. Operação

| Operação | Procedimento | Observações |
|---|---|---|
| Atualização de versão | Envie uma nova tag de imagem (passo 3), registre uma revisão da definição de task que a cite e execute `aws ecs update-service --cluster $NAME --service bulksigner --task-definition $NAME`. | A task em execução para antes de a nova iniciar. As migrations do banco rodam no boot da nova task. |
| Rotação de segredo | Atualize o segredo e execute `aws ecs update-service … --force-new-deployment`. | Um segredo rotacionado só chega ao container quando a task é substituída. |
| Rotação da senha do banco | Execute `ALTER LOGIN` no servidor, depois atualize `$NAME/sql-app` e force uma nova implantação. | As duas mudanças devem ser feitas nessa ordem. |
| Renovação do certificado | Execute o script de importação com o novo PFX e um novo alias, faça upload do novo `.cer`, defina `KeyId` com o novo alias e reinicie. | Mudanças de certificado valem a partir de um reinício. |
| Pausa | Execute `aws ecs update-service … --desired-count 0` e `aws rds stop-db-instance`. | O RDS reinicia automaticamente uma instância parada após sete dias. O balanceador de carga e o endereço IPv4 público continuam sendo cobrados. |
| Desativação | Exclua os recursos na ordem abaixo. | |

Ordem de desativação:

1. O serviço (escalado para 0 e depois excluído).
2. O cluster.
3. Os listeners, o balanceador de carga e o target group.
4. Os registros de DNS.
5. O certificado do ACM.
6. A instância do RDS.
7. Os security groups.
8. Os roles do IAM.
9. Os segredos.
10. O bucket (esvaziado antes).
11. O repositório do ECR.
12. A chave do KMS, por último, com `schedule-key-deletion` e um período de espera de 7 a 30 dias.

As assinaturas já feitas continuam verificáveis após a exclusão da chave, porque a verificação usa a
chave pública do certificado. A exclusão da chave elimina a capacidade de assinar com ela, a não ser que o
backup do PFX seja importado de novo.

## Referência de configuração

Todas as chaves são definidas como variáveis de ambiente na definição de task (passo 8). Um `__` no nome
de uma variável de ambiente corresponde a `:` na chave de configuração.

### Configurações principais

| Variável de ambiente | Valor nesta implantação | Obrigatória | Descrição |
|---|---|---|---|
| `ASPNETCORE_ENVIRONMENT` | `Production` (`Staging` em homologação) | Sim | O ambiente de hospedagem. |
| `Database__Provider` | `SqlServer` | Sim | O provider do banco operacional. |
| `ConnectionStrings__Default` | Segredo `$NAME/sql-app`, chave `connectionString` | Sim | A connection string do SQL Server (item 5.2). |
| `Auth__ApiKey` | Segredo `$NAME/auth-api-key` | Sim | O `X-API-Key` da API REST; também a chave de login do dashboard quando o Entra ID não está configurado. |
| `Auth__SessionKeyRing__Placement` | `OperationalStore` | Sim, nesta topologia | Armazena o key ring de sessão no banco, para que as sessões sobrevivam às implantações. |
| `Hosting__ForwardedHeaders__Enabled` | `true` | Sim | Aplica os forwarded headers do balanceador de carga. |
| `Hosting__ForwardedHeaders__KnownNetworks__0` | `$VPC_CIDR` | Sim | A rede de onde os forwarded headers são considerados confiáveis. |
| `Signing__PkiSdkLicense` | Segredo `$NAME/pki-sdk-license` | Sim | A licença do PKI SDK. |
| `WebPki__License` | Segredo `$NAME/webpki-license` | Se o Web PKI for usado | A licença do Web PKI. |

### Compartilhamento de trabalho e pasta monitorada

| Variável de ambiente | Valor nesta implantação | Obrigatória | Descrição |
|---|---|---|---|
| `Storage__Provider` | `S3` | Sim | Coloca o compartilhamento de trabalho no S3. |
| `Storage__S3__BucketName` | `$BUCKET` | Sim | O bucket. |
| `Storage__S3__Prefix` | `work` | Sim | O prefixo do compartilhamento de trabalho. Não pode conter uma pasta monitorada. |
| `Storage__S3__Region` | `$REGION` | Sim | A região do bucket. |
| `Storage__S3__Credential` | `InstanceRole` | Sim | Autentica com o task role. |
| `Storage__Inputs__0__Name` | `default` | Sim | O nome da pasta monitorada. |
| `Storage__Inputs__0__Path` | `inputs/default` | Sim | O prefixo da pasta no bucket. O objeto marcador precisa existir (passo 1). |
| `Storage__Inputs__0__PollIntervalSeconds` | `30` | Sim, no S3 | O intervalo entre listagens da pasta. |

### Certificado de assinatura (seed do perfil `default`)

| Variável de ambiente | Valor nesta implantação | Obrigatória | Descrição |
|---|---|---|---|
| `Signing__Certificate__Source` | `AwsKms` | Sim | A origem do certificado. |
| `Signing__Certificate__AwsKms__Region` | `$REGION` | Sim | A região da chave. |
| `Signing__Certificate__AwsKms__KeyId` | `alias/$NAME` | Sim | A chave, pelo alias (passo 2). |
| `Signing__Certificate__AwsKms__Credential` | `InstanceRole` | Sim | Autentica no KMS com o task role. |
| `Signing__Certificate__AwsKms__S3__Bucket` | `$BUCKET` | Sim | O bucket que contém o certificado. |
| `Signing__Certificate__AwsKms__S3__Key` | `signing-material/$NAME.cer` | Sim | A chave do objeto do certificado. |
| `Signing__Certificate__AwsKms__S3__Region` | `$REGION` | Sim | A região do bucket do certificado. |
| `Signing__Certificate__AwsKms__S3__Credential` | `InstanceRole` | Sim | Autentica no S3 com o task role. |
| `Signing__TrustLacunaTestRoot` | `true` | Somente em homologação | Confia na raiz de teste da Lacuna, para certificados de teste. |

Essas chaves só são lidas no primeiro boot contra um banco vazio. Depois disso, o perfil é editado no
dashboard.

### Lacuna Signer (opcional)

| Variável de ambiente | Valor nesta implantação | Obrigatória | Descrição |
|---|---|---|---|
| `Signer__Endpoint` | `https://<endereço do Lacuna Signer>` | Não | O endereço da API do Signer. |
| `Signer__ApiKey` | Segredo `$NAME/signer-api-key` | Sim, quando `Signer__Endpoint` está definido | A chave de API do Signer, `<aplicação>\|<segredo>`. A ausência dela interrompe a task no boot. |

### Microsoft Entra ID (opcional)

| Variável de ambiente | Valor nesta implantação | Obrigatória | Descrição |
|---|---|---|---|
| `Auth__EntraId__TenantId` | O ID do diretório (tenant) | Para o login pelo Entra ID | O tenant. |
| `Auth__EntraId__ClientId` | O ID do aplicativo (client) | Para o login pelo Entra ID | O registro de aplicativo. |
| `Auth__EntraId__ClientSecret` | Segredo `$NAME/entra-client-secret` | Para o login pelo Entra ID | O client secret. |

### Configurações que não devem ser usadas

| Configuração | Motivo |
|---|---|
| `Cluster:Enabled` | Recusada com um compartilhamento de trabalho no S3. |
| `Backup:Enabled` | Recusada com `Database:Provider = SqlServer`; os backups automáticos do RDS a substituem. |

## Contratos de interface

### Endpoints HTTP

Os endpoints usados nesta página. Requisições autenticadas enviam `X-API-Key: <Auth:ApiKey>`. Respostas
de erro são corpos `ProblemDetails` (RFC 9457) com um campo `code` estável; clientes devem decidir pelo
`code`, não pelo `title` ([API REST](https://docs.lacunasoftware.com/articles/bulk-signer/rest-api#envelope-de-erro)).

| Método | Rota | Autenticação | Resposta | Usado por |
|---|---|---|---|---|
| `GET` | `/api/ready` | Anônima (a menos que `Readiness:RequireApiKey = true`) | `200`, ou `503` quando uma verificação bloqueante falha. Corpo: `{ ready, checks: [{ name, ok }] }`, sem detalhe. | O health check do target group (passo 7); a verificação (passo 9). |
| `GET` | `/api/ready/details` | Chave de API | O mesmo relatório com o `detail` de cada verificação, e a mesma regra de `200` / `503`. | A verificação (passo 9). |
| `GET` | `/api/profiles` | Chave de API | `200`, listando todos os perfis de assinatura, com `certificateSource` e `degraded`. | A verificação (passo 9). |
| `POST` | `/api/files` | Chave de API | `202 Accepted` com `jobId`, `fileName`, `format` e `status` (`Queued`). | O teste de assinatura (passo 9). |
| `GET` | `/api/jobs/{id}` | Chave de API | `200` com o job, incluindo `status` e um array `history`. | O teste de assinatura (passo 9). |
| `GET` | `/api/jobs/{id}/output` | Chave de API | O arquivo assinado. `409 job.output-unavailable` se o job não estiver `Completed`; `410 job.output-gone` se o arquivo saiu de `output/`. | O teste de assinatura (passo 9). |
| `GET` | `/api/auth/entra-login` | Anônima | Um redirecionamento para o endpoint de login da Microsoft. | A verificação do Entra ID (passo 10). |
| `GET` | `/api/metrics` | Chave de API (quando `Metrics:RequireApiKey = true`, o padrão) | Formato de exposição do Prometheus. | Não configurado nesta página (veja [Limitações](https://docs.lacunasoftware.com/articles/bulk-signer/aws#limitações)). |

A API completa está especificada em [API REST](https://docs.lacunasoftware.com/articles/bulk-signer/rest-api).

### Endpoint externo

| Método | Rota | Resposta esperada | Finalidade |
|---|---|---|---|
| `GET` | `<Signer__Endpoint>/api/documents` | `401` sem uma chave | Confirma que `Signer__Endpoint` é o endereço da API do Signer. |

### Chamadas à AWS feitas pela task

| Serviço | Ação | Recurso | Quando é chamada |
|---|---|---|---|
| AWS KMS | `kms:DescribeKey`, `kms:GetPublicKey` | A chave de assinatura | Uma vez, no boot, quando o certificado é aberto. |
| AWS KMS | `kms:Sign` | A chave de assinatura | Uma vez por arquivo assinado. |
| Amazon S3 | `s3:GetObject` | `signing-material/<nome>.cer` | Uma vez, no boot. |
| Amazon S3 | `s3:ListBucket` | O bucket | A cada `PollIntervalSeconds`, para detectar arquivos na pasta monitorada; também pelas verificações de inicialização. |
| Amazon S3 | `s3:GetObject`, `s3:PutObject`, `s3:DeleteObject`, `s3:AbortMultipartUpload` | Objetos do bucket | Durante o processamento dos jobs. Uma movimentação entre prefixos é uma cópia seguida de uma exclusão. |

As cotas de requisições do KMS são compartilhadas por todos os chamadores da conta e da região.

## Referência de erros

### Códigos de erro

| Código | Origem | Código de status HTTP | Significado | Solução |
|---|---|---|---|---|
| `file.already-processed` | API do Bulk Signer | `409` | Um job `Completed` ou ativo já tem esse nome de arquivo. | Exclua o job na página **Jobs** ou renomeie o arquivo. |
| `job.output-unavailable` | API do Bulk Signer | `409` | O job não está `Completed`. | Consulte `GET /api/jobs/{id}` até `Completed`. |
| `job.output-gone` | API do Bulk Signer | `410` | O job está `Completed`, mas o arquivo dele não está mais em `output/`. | — |
| `job.staged-copy-changed` | Job do Bulk Signer | — (registrado no job) | A cópia preparada no bucket foi alterada entre a preparação e a assinatura. Nada foi assinado. | Garanta que somente o Bulk Signer escreva em `work/`. |
| `ThrottlingException`, `KeyUnavailableException` | AWS KMS | — (registrado no job) | A cota de requisições do KMS foi excedida, ou a chave estava indisponível. | Veja [Diagnóstico de problemas](https://docs.lacunasoftware.com/articles/bulk-signer/troubleshooting#jobs-do-aws-kms-falham-com-throttlingexception-ou-keyunavailableexception). |
| `AADSTS50011` | Microsoft Entra ID | — | A URI de redirecionamento não corresponde ao registro de aplicativo; ela é `http://`. | Configure os forwarded headers do passo 8 com a faixa correta. |
| Erro `AADSTS` no login, após um período de funcionamento normal | Microsoft Entra ID | — | O client secret expirou. | Faça a rotação do segredo em `$NAME/entra-client-secret` e force uma nova implantação. |
| *The certificate chain was issued by an authority that is not trusted* | Cliente do SQL Server | — | A imagem não confia no certificado do servidor RDS. | Inclua `TrustServerCertificate=True` na connection string (item 5.2). |
| `error storing credentials … The stub received bad data` | Docker no Windows | — | O Gerenciador de Credenciais do Windows recusa o tamanho do token do ECR. | Use um diretório `--config` temporário (passo 3). |

A lista completa de códigos de erro da API está em [API REST](https://docs.lacunasoftware.com/articles/bulk-signer/rest-api#envelope-de-erro).

### Sintomas

| Sintoma | Causa | Solução |
|---|---|---|
| O certificado do ACM permanece em *Pending validation*. | O CNAME de validação foi escrito em uma zona que não é autoritativa para o domínio. | Confira os registros NS públicos e escreva o registro na zona autoritativa. |
| A task para no boot, e o log dela cita `Signer:ApiKey`. | `Signer__Endpoint` está definido sem `Signer__ApiKey`. | Defina as duas chaves ou nenhuma. |
| `/api/ready` retorna `503` e a verificação `database` está falhando. | A task está em execução, mas o schema não foi aplicado. Se o detalhe cita uma cadeia de certificados não confiável, falta `TrustServerCertificate=True` na connection string. Caso contrário, o banco ainda não existe, ou o security group do banco não admite o grupo da task. | Corrija a connection string (item 5.2), execute a task de configuração (item 5.3) ou corrija o security group (passo 4). |
| `input-folder:default` não está em execução. | Falta o objeto marcador `inputs/default/`, ou o task role não tem `s3:ListBucket` no bucket. | Crie o marcador (passo 1) ou corrija o task role (passo 6). Veja [Diagnóstico de problemas](https://docs.lacunasoftware.com/articles/bulk-signer/troubleshooting#uma-pasta-de-entrada-do-blob-ou-do-s3-aparece-como-inexistente). |
| O perfil `default` está degradado. | O objeto do certificado no S3 não pode ser lido, o KMS recusou o task role ou o `.cer` não corresponde à chave do KMS. | Confirme que o ARN do task role está na lista de exceções da negação na política da chave. O motivo aparece no `/api/ready/details` e na página do perfil; veja [Diagnóstico de problemas](https://docs.lacunasoftware.com/articles/bulk-signer/troubleshooting#um-perfil-awskms-está-degradado-na-inicialização). |
| Todos os usuários são desconectados a cada implantação. | `Auth__SessionKeyRing__Placement` não é `OperationalStore`. | Defina a chave (passo 8). |

## Limitações

| Não oferecido | Alternativa |
|---|---|
| Mais de uma instância | [Azure App Service (modo cluster)](https://docs.lacunasoftware.com/articles/bulk-signer/azure). |
| Verificação do certificado do servidor de banco de dados | Nenhuma nesta topologia; veja `TrustServerCertificate=True` no item 5.2. |
| Métricas no CloudWatch | O `/api/metrics` serve texto do Prometheus atrás da chave de API. O Amazon Managed Service for Prometheus ou o agente do CloudWatch podem coletá-lo; nenhum dos dois é configurado nesta página. |
| Backup feito pelo Bulk Signer | Os backups automáticos do RDS. |
