Security Headers
The Granit.Http.SecurityHeaders module provides centralized HTTP security response header
management. It suppresses the Kestrel Server header to prevent server fingerprinting
and injects defensive headers on every response per the
OWASP Secure Headers Project.
Included in Bundle.Essentials — active by default for all Granit applications.
Standards
Section titled “Standards”| Standard | Reference |
|---|---|
| OWASP Secure Headers Project | Response header best practices |
| OWASP ASVS V14.4 | HTTP Security Headers |
| ISO 27001 A.8.9 | Configuration management |
| CWE-200 | Information Exposure |
Headers managed
Section titled “Headers managed”| Header | Default value | Purpose |
|---|---|---|
Server | (removed) | Prevent fingerprinting |
X-Content-Type-Options | nosniff | Prevent MIME sniffing |
X-Frame-Options | DENY | Prevent clickjacking |
Strict-Transport-Security | max-age=31536000; includeSubDomains | Force HTTPS (1 year) |
Referrer-Policy | strict-origin-when-cross-origin | Control referrer leakage |
Permissions-Policy | camera=(), microphone=(), geolocation=(), payment=(), accelerometer=(), gyroscope=(), magnetometer=(), usb=() | Restrict browser features |
X-XSS-Protection | 0 | Disable legacy XSS auditor |
Cross-Origin-Opener-Policy | same-origin | Spectre isolation |
Cross-Origin-Resource-Policy | same-origin | Resource sharing control |
Cross-Origin-Embedder-Policy | (not set) | Optional Spectre isolation |
Content-Security-Policy | default-src 'none'; base-uri 'none'; frame-ancestors 'none' | Composed per-endpoint from CspOptions base + registered ICspContributor implementations |
The module is included in Bundle.Essentials. Add the middleware in Program.cs:
app.UseGranitExceptionHandling(); // 1st — catch errorsapp.UseGranitSecurityHeaders(); // 2nd — headers on ALL responsesapp.UseRouting();app.UseAuthentication();app.UseAuthorization();Configuration
Section titled “Configuration”All headers are configurable via appsettings.json:
{ "Http:SecurityHeaders": { "SuppressServerHeader": true, "EnableContentTypeOptions": true, "XFrameOptions": "DENY", "ReferrerPolicy": "strict-origin-when-cross-origin", "DisableXssProtection": true, "PermissionsPolicy": "camera=(), microphone=(), geolocation=(), payment=(), accelerometer=(), gyroscope=(), magnetometer=(), usb=()", "EnableHsts": true, "HstsMaxAgeSeconds": 31536000, "HstsIncludeSubDomains": true, "HstsPreload": false, "CrossOriginOpenerPolicy": "same-origin", "CrossOriginEmbedderPolicy": null, "CrossOriginResourcePolicy": "same-origin" }}Disabling a header
Section titled “Disabling a header”Set the header value to null or the enable flag to false:
{ "Http:SecurityHeaders": { "XFrameOptions": null, "EnableContentTypeOptions": false }}Content Security Policy
Section titled “Content Security Policy”CSP in Granit is composed per-endpoint rather than configured as a single
application-wide string. The composer starts from a typed base policy
(SecurityHeaders:Csp in configuration) and then lets every package that owns
a UI surface declare its own relaxations through an ICspContributor — without
the consuming application having to know about them.
Default policy
Section titled “Default policy”The base policy is API-grade strict, suitable for JSON-only endpoints:
default-src 'none'; base-uri 'none'; frame-ancestors 'none'Every other directive is empty by default and inherits from default-src 'none'
per CSP semantics. UI surfaces that need relaxations (e.g. Scalar, an admin UI,
the BFF login flow) ship a contributor scoped to their own route.
Typed configuration
Section titled “Typed configuration”CspOptions exposes one strongly-typed property per directive. Bind from the
SecurityHeaders:Csp section:
{ "Http:SecurityHeaders": { "Csp": { "DefaultSrc": ["'self'"], "ScriptSrc": ["'self'", "'unsafe-inline'"], "FontSrc": ["'self'", "https://fonts.scalar.com"] } }}| Property | Directive |
|---|---|
DefaultSrc | default-src |
ScriptSrc, ScriptSrcElem, ScriptSrcAttr | script-src, script-src-elem, script-src-attr |
StyleSrc, StyleSrcElem, StyleSrcAttr | style-src, style-src-elem, style-src-attr |
FontSrc, ImgSrc, ConnectSrc, MediaSrc, ObjectSrc, ManifestSrc | font-src, img-src, connect-src, media-src, object-src, manifest-src |
FrameSrc, WorkerSrc, ChildSrc | frame-src, worker-src, child-src |
BaseUri, FormAction, FrameAncestors | base-uri, form-action, frame-ancestors |
ReportUri, ReportTo | report-uri, report-to |
ReportOnly | Emit Content-Security-Policy-Report-Only instead of the enforcing header |
UpgradeInsecureRequests | Emit upgrade-insecure-requests |
RawOverride | Emergency escape hatch — emitted verbatim, contributors skipped |
CSP contributors
Section titled “CSP contributors”A contributor is the unit of CSP composition: a small class — usually internal sealed —
that declares “on routes matching X, add Y to the policy.” The composer collects
every registered contributor, runs them against the matched endpoint, merges
their additions into the base policy, and emits a single header per response.
The contracts ship in Granit.Http.SecurityHeaders.Abstractions — a
zero-runtime-dependency contracts package. A package that wants to declare a
relaxation references only the Abstractions package, not the runtime, mirroring
the Microsoft.Extensions.Logging.Abstractions pattern.
Example contributor for a hypothetical /admin UI:
internal sealed class AdminUiCspContributor : ICspContributor{ public void Contribute(HttpContext context, CspBuilder builder) { if (context.GetEndpoint()?.Metadata.GetMetadata<AdminUiMetadata>() is null) { return; }
builder .AddScriptSrc("'self'", "'unsafe-inline'") .AddStyleSrc("'self'", "'unsafe-inline'"); }}Registration happens in the package’s Use* extension, after the activation gate:
public static IApplicationBuilder UseAdminUi(this IApplicationBuilder app){ var registry = app.ApplicationServices.GetService<ICspContributorRegistry>(); registry?.Add(new AdminUiCspContributor()); // ... map admin endpoints with WithMetadata(new AdminUiMetadata()) ... return app;}The registry locks on the first call from the composer (i.e. the first request
served). Registering a contributor after that throws InvalidOperationException
with an actionable message — register during application configuration, not
lazily on first use.
Disabling a contributor from configuration
Section titled “Disabling a contributor from configuration”When an internal policy is stricter than the framework default, opt a contributor out without forking the owning package:
{ "Http:SecurityHeaders": { "DisabledContributors": ["ScalarCspContributor"] }}Matched against ICspContributor.Name, which defaults to the runtime type name
via a default interface method. Override Name only when the type name is not
stable across refactors.
Worked example — Keycloak silent SSO
Section titled “Worked example — Keycloak silent SSO”Consumers using the Keycloak JS adapter need /silent-check-sso.html to be
embeddable in a same-origin iframe. The strict default frame-ancestors 'none'
blocks that. Ship a small contributor in the consuming application:
internal sealed class SilentCheckSsoCspContributor : ICspContributor{ public void Contribute(HttpContext context, CspBuilder builder) { if (!context.Request.Path.Equals( "/silent-check-sso.html", StringComparison.OrdinalIgnoreCase)) { return; }
builder.AddFrameAncestors("'self'"); }}var registry = app.Services.GetService<ICspContributorRegistry>();registry?.Add(new SilentCheckSsoCspContributor());This relaxes frame-ancestors only on that one route; the rest of the app
keeps the strict default. For modern browsers, CSP frame-ancestors supersedes
the legacy X-Frame-Options — the global XFrameOptions: "DENY" setting stays
unchanged.
Audit endpoint
Section titled “Audit endpoint”Granit.Http.SecurityHeaders.Endpoints ships a single audit endpoint so
security auditors can introspect the effective per-route CSP from outside the
deployment. The package is opt-in — reference it explicitly and map the
endpoint in Program.cs:
app.MapGranitSecurityHeadersAudit();// GET /security-headers/csp// Gated by DiagnosticsPermissions.Monitoring.ReadThe endpoint reuses the existing Diagnostics permission — no new permission, no new localisation. Returns a JSON snapshot shaped like:
{ "baseDirectives": { "default-src": ["'none'"], "base-uri": ["'none'"], "frame-ancestors": ["'none'"] }, "rawOverride": null, "reportOnly": false, "contributors": [ { "name": "ScalarCspContributor", "fullTypeName": "Granit.Http.ApiDocumentation.Internal.ScalarCspContributor" } ], "endpoints": [ { "httpMethods": ["GET"], "pattern": "/scalar", "displayName": "Scalar", "headerName": "Content-Security-Policy", "composedCsp": "default-src 'self'; script-src 'self' 'unsafe-inline'; ..." } ]}HSTS preload
Section titled “HSTS preload”To submit your domain to the HSTS Preload List:
{ "Http:SecurityHeaders": { "HstsPreload": true, "HstsIncludeSubDomains": true, "HstsMaxAgeSeconds": 31536000 }}Cross-Origin-Embedder-Policy
Section titled “Cross-Origin-Embedder-Policy”COEP is not set by default because it can break cross-origin resources (fonts,
images, scripts). Enable it only when all cross-origin resources include
Cross-Origin-Resource-Policy:
{ "Http:SecurityHeaders": { "CrossOriginEmbedderPolicy": "require-corp" }}Kestrel-only caveat
Section titled “Kestrel-only caveat”The Server header suppression applies only when the application runs on Kestrel.
When deployed behind a reverse proxy (nginx, Envoy, YARP), the proxy manages its
own Server header. Ensure the proxy is also configured to suppress it.
See also
Section titled “See also”- Security Overview — authentication, authorization, encryption modules
- BFF Security — clickjacking protection on BFF auth endpoints
- API Documentation — Scalar’s bundled
ICspContributorand how to disable it - HTTP Conventions — CORS, caching, API versioning