Borda HTTP fechada por padrão para APIs ASP.NET Core: proxy confiável explícito, hosts permitidos, HSTS e headers de segurança, limite de requisições com resposta 429 padronizada e CORS por allowlist.
- ✨ Por que usar
- 📥 Instalação
- 🚀 Início rápido
- ⚙️ Configuração
- 🚦 Limite de requisições
- 🌐 Proxy reverso
- 🧱 O que derruba a subida
- 📈 Observabilidade
- 🧩 Diagrama de arquitetura
- ⚡ Compatibilidade
- 🛡️ Limites conhecidos
| Sem TEC.Http | Com TEC.Http |
|---|---|
| CORS, limite e HSTS reescritos em cada serviço, cada um de um jeito | Uma linha no Program.cs e uma seção Http no appsettings |
AllowAnyOrigin com credenciais, AllowedHosts: "*" em produção |
A aplicação não sobe com essas configurações |
X-Forwarded-For aceito de qualquer cliente: o IP é forjável e o limite por IP é burlado |
O header só vale se vier de um proxy listado; senão é ignorado |
| 429 com corpo e formato próprios (ou 503 padrão do limiter) | 429 no ApiResponse do TEC.Core, com Retry-After e traceId |
O componente usa só o ASP.NET Core (e o TEC.Core para o envelope de erro): nenhuma dependência de terceiros.
dotnet nuget add source https://nuget.pkg.github.com/tudoemcodigo/index.json -n tec-interno -u <usuario> -p <PAT>
dotnet add package TEC.Httpusing TEC.Http;
builder.Services.AddTecHttp(builder.Configuration);
var app = builder.Build();
app.UseTecObservability(); // TEC.Observability: correlation id primeiro
app.UseTecExceptionHandler(); // TEC.Cqrs.AspNetCore
app.UseTecHttp(); // proxy, HSTS, headers, CORS, limite por IP: ANTES da autenticação
app.UseAuthentication();
app.UseTecRateLimitingPerIdentity(); // DEPOIS da autenticação
app.UseAuthorization();Important
A ordem importa. O limite por IP precisa vir antes de UseAuthentication: é ele que contém tentativas de credencial
(API key e token inválidos). O limite por identidade só existe depois da autenticação.
Seção Http. Os valores abaixo são os padrões (a seção inteira é opcional).
{
"AllowedHosts": "api.exemplo.com",
"Http": {
"Forwarding": { "Enabled": false, "KnownProxies": [], "KnownNetworks": [], "ForwardLimit": 1 },
"Hsts": { "Enabled": true, "MaxAgeDays": 365, "IncludeSubDomains": false, "Preload": false },
"SecurityHeaders": { "Enabled": true },
"RateLimiting": {
"Enabled": true,
"PerIp": { "PermitLimit": 100, "WindowSeconds": 60 },
"PerIdentity": { "PermitLimit": 600, "WindowSeconds": 60 }
},
"Cors": {
"Origins": [], "Methods": [], "Headers": [], "ExposedHeaders": [],
"AllowCredentials": false, "MaxAgeSeconds": 600
}
}
}| Item | Comportamento |
|---|---|
| CORS | Sem Origins, nenhuma origem é liberada (é o estado de partida: servidores não usam CORS). Formato https://host[:porta], sem caminho, barra final nem curinga. * só em Development e sem credenciais. http só para localhost em Development. Methods vazio: GET, POST, PUT, PATCH, DELETE. Headers vazio: Content-Type, Authorization, X-Api-Key, Idempotency-Key, If-Match. Cada item de Methods, Headers e ExposedHeaders precisa ser um token HTTP (sem espaço, vírgula ou quebra de linha, até 128 caracteres). * vale só como item único e nunca com AllowCredentials. Para o navegador ler o Retry-After do 429, inclua Retry-After em ExposedHeaders. |
| Listas | Origins, Methods, Headers, ExposedHeaders, KnownProxies e KnownNetworks são lidas por índice (Http:Cors:Origins:0; em variável de ambiente, Http__Cors__Origins__0). Um valor único na própria chave (ex.: Http__Cors__Origins=https://a,https://b) derruba a subida com a chave esperada na mensagem, em vez de deixar a lista vazia em silêncio. |
| HSTS | Só fora de Development e em HTTPS. IncludeSubDomains e Preload vêm desligados: sem configuração explícita o header é só max-age. Em produção, ligue Http:Hsts:IncludeSubDomains depois de inventariar os subdomínios (todos, inclusive os internos, precisam servir HTTPS). Http:Hsts:Preload é um passo deliberado de release (praticamente irreversível; a submissão em hstspreload.org é manual) e exige IncludeSubDomains = true e MaxAgeDays >= 365; senão a subida falha com a chave na mensagem. A validação vale também em Development e com Enabled: false, embora o header não saia ali. Valor que não seja true ou false também derruba a subida. |
| Headers | X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer. Não sobrescrevem header já definido pela aplicação. |
| AllowedHosts | Fora de Development, vazio ou * derruba a subida. Liste também o host que o orquestrador usa nas sondas de saúde (muitos chamam pelo IP do pod, e o filtro responde 400). |
- Janela deslizante em memória, por instância: com N instâncias o limite efetivo é N vezes o configurado. Se precisar de
limite global, use o gateway ou um limiter distribuído. Não há ponto de extensão público hoje (YAGNI): a criação do limiter
e as chaves de partição ficam isoladas em
RateLimitMiddlewareeClientPartition, onde um limiter distribuído entraria sem mudar o contrato de configuração. - Estágio por IP (toda requisição): IPv6 é agrupado por prefixo /64 (um cliente controla o /64 inteiro; sem isso, rotacionar o sufixo burlaria o limite). Sem IP remoto (por exemplo, conexão por Unix domain socket) o limite vale por conexão: clientes diferentes não dividem um balde só, e cada conexão continua limitada. Um aviso é registrado uma vez (EventId 6002). Se nem o id da conexão existir, a requisição cai em um balde compartilhado (falha fechada, nunca sem limite).
- Preflight CORS: com
Cors:Originsconfigurado, oOPTIONSde preflight é respondido pelo CORS antes do limite e não conta no balde do IP. Sem CORS configurado não há preflight a responder:OPTIONSé uma requisição comum e conta. - Estágio por identidade (só autenticadas): chave
tec_uid(identidade normalizada do TEC.Security),subouNameIdentifier. Autenticado sem identificador usa um balde próprio. Chaves longas viram hash (memória limitada). - Resposta 429:
ApiResponsecomerrors[0].code = "http.rate_limit_exceeded",traceId,Cache-Control: no-storeeRetry-Afterem segundos (nunca 0). Para origem liberada, o 429 sai com os headers CORS, então o navegador consegue lê-lo. - Desligar (
RateLimiting:Enabled: false) é decisão explícita, para quando há gateway à frente.
Os headers X-Forwarded-* são escolhidos pelo cliente. O componente não detecta se há proxy: você declara.
- Sem proxy (padrão):
X-Forwarded-Foré ignorado; o IP é o da conexão. Forjar o header não burla o limite. - Atrás de proxy: ligue
Forwardinge listeKnownProxiese/ouKnownNetworks(CIDR). Loopback não é confiável por padrão; liste-o se o proxy roda na mesma máquina. Sem isso, todo cliente aparece com o IP do proxy e divide o mesmo balde. X-Forwarded-Prototambém é aplicado, entãoRequest.IsHttpse o HSTS funcionam atrás de quem termina o TLS.- Header
X-Forwarded-Forrecebido comForwardingdesligado: a requisição segue, o contadortec.http.forwarded_headers.ignoredsobe e um aviso é registrado uma vez. É o sinal de proxy sem configuração.
A configuração é validada na subida (UseTecHttp e ValidateOnStart); erro derruba a aplicação, não a primeira requisição.
AllowedHostsausente ou*fora de Development.Forwardingligado sem proxy nem rede; IP ou CIDR inválido; prefixo/0;ForwardLimitfora de 1 a 5.- Origem CORS fora do formato;
*fora de Development ou comAllowCredentials;httpque não seja localhost em Development. - Item de
Methods,HeadersouExposedHeadersque não seja token HTTP;*misturado com outros itens ou comAllowCredentials. - Lista configurada com valor único em vez de itens por índice.
- Limite fora de 1 a 1.000.000 ou janela fora de 1 a 86.400 s;
MaxAgeSecondsfora de 0 a 86.400. - Valor presente e ilegível (ex.:
"abc"num número): nunca é trocado pelo padrão em silêncio.
Medidor TEC.Http (registre-o no OpenTelemetry com AddMeter("TEC.Http")):
| Métrica | Quando |
|---|---|
tec.http.ratelimit.rejected (tag stage: ip ou identity) |
Requisição recusada com 429 |
tec.http.forwarded_headers.ignored |
X-Forwarded-For recebido com Forwarding desligado |
Componentes e ligações do TEC.Http, gerados pelo GitDiagram a partir da main em 02/11/2026. O diagrama é gerado por IA a partir de uma amostra do código: use-o como mapa de leitura, não como contrato.
flowchart TD
subgraph group_configuration["Configuration and validation"]
node_options["HTTP options<br/>[TecHttpOptions.cs]"]
node_reader["Configuration reader"]
node_validator["Startup validation"]
end
subgraph group_edge["HTTP edge protections"]
node_registration["Service registration"]
node_proxy["Trusted proxy forwarding"]
node_hosts["Allowed hosts"]
node_hsts["HSTS"]
node_headers["Security headers"]
node_cors["CORS allowlist"]
end
subgraph group_limiting["Rate limiting"]
node_iplimit["Per-IP limiter"]
node_identity["Per-identity limiter"]
end
subgraph group_internal["Shared internals"]
node_partition["Partition keys<br/>[ClientPartition.cs]"]
node_response["429 response"]
node_metrics["HTTP metrics<br/>[TecHttpMetrics.cs]"]
node_logging["HTTP logging<br/>[TecHttpLog.cs]"]
end
node_app(("API application"))
node_request(("Incoming requests"))
node_auth["Authentication middleware"]
node_app -->|"registers services"| node_registration
node_registration -->|"reads configuration"| node_reader
node_reader -->|"populates"| node_options
node_registration -->|"registers validation"| node_validator
node_validator -->|"validates"| node_options
node_app -->|"adds middleware"| node_proxy
node_request -->|"passes through"| node_proxy
node_request -->|"host checked"| node_hosts
node_request -->|"passes through"| node_hsts
node_request -->|"passes through"| node_headers
node_request -->|"passes through"| node_cors
node_request -->|"passes through"| node_iplimit
node_auth -->|"precedes"| node_identity
node_request -->|"passes through"| node_identity
node_iplimit -->|"gets IP key"| node_partition
node_identity -->|"gets identity key"| node_partition
node_iplimit -->|"writes rejection"| node_response
node_identity -->|"writes rejection"| node_response
node_iplimit -->|"records rejections"| node_metrics
node_identity -->|"records rejections"| node_metrics
node_proxy -->|"records ignored headers"| node_metrics
node_proxy -->|"logs ignored headers"| node_logging
classDef toneNeutral fill:#f8fafc,stroke:#334155,stroke-width:1.5px,color:#0f172a
classDef toneBlue fill:#dbeafe,stroke:#2563eb,stroke-width:1.5px,color:#172554
classDef toneAmber fill:#fef3c7,stroke:#d97706,stroke-width:1.5px,color:#78350f
classDef toneMint fill:#dcfce7,stroke:#16a34a,stroke-width:1.5px,color:#14532d
classDef toneRose fill:#ffe4e6,stroke:#e11d48,stroke-width:1.5px,color:#881337
classDef toneIndigo fill:#e0e7ff,stroke:#4f46e5,stroke-width:1.5px,color:#312e81
classDef toneTeal fill:#ccfbf1,stroke:#0f766e,stroke-width:1.5px,color:#134e4a
class node_options,node_reader,node_validator toneBlue
class node_registration,node_proxy,node_hosts,node_hsts,node_headers,node_cors toneAmber
class node_iplimit,node_identity,node_app,node_request toneMint
class node_partition,node_response,node_metrics,node_logging toneRose
class node_auth toneIndigo
.NET 10, compatível com Native AOT e trimming (configuração lida sem reflexão).
- Limite por instância (ver acima).
- Preflights não contam no limite (o CORS responde 204 antes dele, inclusive para origem não liberada). É uma resposta
barata, sem rota nem autenticação; contenção de volume contra
OPTIONSem massa fica com o gateway. - Atrás de proxy por Unix domain socket, o ASP.NET não consegue atestar o proxy (não há IP), então
Forwardingnão se aplica e o limite vale por conexão do proxy: se ele abre uma conexão por requisição, o limite por IP perde efeito. Limite também no proxy, ou use TCP comForwarding. - Sem CSP: a API não serve HTML; se servir (ex.: documentação), defina a política na aplicação.
- O componente não faz limite de tamanho de corpo nem de tempo de requisição: use
Kestrel(MaxRequestBodySize,RequestHeadersTimeout) e o gateway. - Atrás de proxy, o IP só é confiável se o proxy sobrescreve (não repassa) o
X-Forwarded-Forrecebido do cliente.