Expect-CT Deprecation and Safe Removal
This guide is part of the Deprecated Security Headers & Legacy Browser Support reference. The Expect-CT response header once let a site opt in to Certificate Transparency (CT) enforcement before browsers required it — a certificate presented without valid Signed Certificate Timestamps (SCTs) would be rejected, and violations could be reported. That transitional job is finished: Chrome has enforced CT for all publicly-trusted certificates since 2018, Safari enforces its own CT policy, and Chrome removed Expect-CT support entirely in Chrome 107 (October 2022). Every browser that ever honored the header now ignores it. This page shows why it is obsolete, how to remove it from each platform without breaking anything, and what CT-log monitoring replaces it with.
Configuration Syntax & Exact Values
The header is defined by RFC 9163 and takes three directives. For completeness — and so you recognize what you are removing — the full grammar is:
Expect-CT: max-age=86400, enforce, report-uri="https://example.com/ct-report"
Breaking that down:
| Directive | Role | Effect today |
|---|---|---|
max-age |
Required. Seconds the policy is cached by the client. | Ignored — no client caches it. |
enforce |
Optional. Reject connections whose certificate lacks valid SCTs. | Ignored — enforcement is unconditional and built in. |
report-uri="..." |
Optional. URL to POST JSON violation reports to. | Ignored — no browser emits Expect-CT reports. |
A report-only policy omitted enforce:
Expect-CT: max-age=86400, report-uri="https://example.com/ct-report"
There is no correct value to set anymore. Every modern browser parses the header and discards it; Chrome, Edge, and other Chromium builds at version 107 or later do not even read it. The only correct action is deletion. Leaving it in place is not dangerous, but it is dead weight that misleads audits and implies a control that no longer exists.
Do not confuse Expect-CT with HSTS or Content Security Policy: those remain active, enforced controls. Expect-CT is inert.
Server-Side Configuration
The task is subtractive: find where Expect-CT is injected and stop injecting it. Because it may be added at the app, the web server, or the CDN, remove it at whichever layer set it — and verify at the edge, since that is the value the browser would have seen.
Nginx
If the header was set with add_header, delete that line. To defensively strip an Expect-CT arriving from an upstream application, hide it at the proxy:
server {
listen 443 ssl;
server_name example.com;
# 1. Delete any local add_header Expect-CT line entirely (do not leave it).
location / {
proxy_pass http://127.0.0.1:3000;
# Strips Expect-CT injected by the upstream app before the response
# leaves Nginx. proxy_hide_header is the reliable removal primitive.
proxy_hide_header Expect-CT;
}
}
If you use the headers_more module, more_clear_headers removes it regardless of which layer added it, including Nginx’s own add_header:
# Requires the ngx_headers_more module. Clears the header on every response,
# even one added elsewhere in the config.
more_clear_headers "Expect-CT";
After editing, reload rather than restart to avoid dropping connections: nginx -t && nginx -s reload.
Apache
Remove the setting line, and add an explicit unset to strip any upstream copy. The always condition matters because it applies the removal to error responses (4xx/5xx) as well as 200s — an app can attach the header to an error page only:
<VirtualHost *:443>
ServerName example.com
# Delete any prior "Header always set Expect-CT ..." line.
# 'always' applies the unset to ALL responses including 4xx/5xx, not just
# 2xx/3xx (which is all the default 'onsuccess' table would cover).
Header always unset Expect-CT
</VirtualHost>
Reload with apachectl configtest && apachectl graceful.
Cloudflare
If Cloudflare injected the header via a Transform Rule or the legacy Managed Transforms, delete that rule. To strip an Expect-CT forwarded from your origin, add an HTTP Response Header Modification rule:
{
"action": "rewrite",
"action_parameters": {
"headers": {
"Expect-CT": { "operation": "remove" }
}
},
"expression": "true",
"description": "Remove obsolete Expect-CT from all responses"
}
The "operation": "remove" is the removal verb; "expression": "true" applies it to every response the zone serves. Deploy through Rules → Transform Rules → Modify Response Header.
Node.js / Express (Helmet)
Helmet shipped an expectCt middleware that is removed in Helmet 6 and later. If you are on an older Helmet, disable it explicitly; if you set the header by hand, delete the res.setHeader call:
// Helmet 5 and earlier: turn the middleware off explicitly.
app.use(helmet({ expectCt: false }));
// If you set it manually anywhere, delete the line — do not leave it:
// res.setHeader('Expect-CT', 'max-age=86400, enforce');
Upgrading to Helmet 6+ removes the directive entirely, which is the cleaner fix.
Django / FastAPI
If you emitted the header from middleware, remove the assignment. A defensive stripping middleware guarantees no rogue upstream value survives:
# Django middleware — deletes Expect-CT from every response if present.
class RemoveExpectCTMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# response.headers is case-insensitive; pop is a no-op if absent.
response.headers.pop("Expect-CT", None)
return response
Diagnostic & Verification Steps
Confirm the header is gone at the edge, and confirm — separately — that Certificate Transparency is still being satisfied by your certificate. These are two different checks.
First, verify the header no longer ships. grep -i catches any casing:
curl -sI https://example.com | grep -i expect-ct
Expected output: nothing. An empty result means no Expect-CT header is present, which is the goal:
If it still appears, you removed it at the wrong layer — re-check the origin and the CDN independently by testing the origin IP directly:
curl -sI --resolve example.com:443:203.0.113.10 https://example.com | grep -i expect-ct
Second, confirm your certificate actually carries embedded SCTs, which is what CT enforcement checks. openssl prints the SCT list from the certificate:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -text | grep -A1 "CT Precertificate SCTs"
Expected output confirms SCTs are embedded — a certificate from any modern public CA will show this:
CT Precertificate SCTs:
Signed Certificate Timestamp:
Third, in DevTools → Security panel, load the page and inspect the certificate entry. Chrome shows Certificate Transparency: compliant for a valid public certificate. That status is produced by the built-in policy, not by any header you send.
Edge Cases, Security Implications & Safe Rollback
- Removing
Expect-CTdoes not remove CT enforcement. This is the trap that stalls removals: teams fear that deleting the header drops a security control. It does not. Chrome and Safari enforce CT for every publicly-trusted certificate regardless of headers. Your protection is unchanged; you are only deleting an instruction that browsers already ignore. The real control lives in the certificate’s SCTs and the browser’s validation logic. - A dangling
report-urimasks a dead endpoint. If your old policy pointedreport-uriat a reporting service, that endpoint has been receiving zero Expect-CT reports for years because no browser emits them. Do not treat its silence as “no CT problems.” Decommission it and move detection to CT-log monitoring instead (below). Firefox, notably, never enforced CT and never honoredExpect-CT, so it was never a source of these reports either. - CAA and short-lived certs change the monitoring picture. With browser enforcement automatic, the residual risk is mis-issuance — a CA issuing a certificate for your domain that you did not request.
Expect-CTnever protected against that; only monitoring the public CT logs does. Add aCAADNS record to constrain which CAs may issue for your domain, and subscribe a CT-log monitor to alert on any new certificate for your names. - Rollback. Restoring the header has no security value because it is inert, but the removal is trivially reversible if a downstream config validator flags the missing line: re-add the
add_header/Header always set/ middleware directive, reload the service, and purge the CDN cache. Because the change is subtractive and the header is ignored, there is no enforcement window to fear and no risk of locking clients out — unlike an HSTSmax-age, anExpect-CTmistake cannot brick a site.
The modern replacement for Expect-CT’s detection role is continuous CT-log monitoring. Every publicly-trusted certificate is logged to append-only Certificate Transparency logs; a monitor watches those logs for your domain and alerts on unexpected issuance.
Set up monitoring with a hosted service (Cloudflare’s Certificate Transparency alerts, Cert Spotter, Facebook’s CT monitoring, or a scheduled query against crt.sh) and pin issuance with a CAA record:
# Poll crt.sh for every certificate logged for your domain; alert on new IDs.
curl -s 'https://crt.sh/?q=example.com&output=json' \
| jq -r '.[] | "\(.id) \(.name_value) \(.issuer_name)"' | sort -u
# CAA DNS record constraining issuance to named CAs — a BIND zone-file line.
example.com. IN CAA 0 issue "letsencrypt.org"
Conclusion
Treat this as a low-risk cleanup, but stage it like any header change: remove Expect-CT in staging first and confirm with curl -sI that it is gone at both the origin and the edge, roll it to production, then stand up CT-log monitoring and a CAA record so the detection role the header pretended to fill is actually covered. Because the header is inert, there is no enforcement window and no lockout risk — the only diligence required is confirming that no audit tooling or dashboard still expects to see it before you delete the line for good.
Frequently Asked Questions
Is it safe to remove Expect-CT from production right now? Yes. Every browser that ever honored the header now ignores it, and Chrome removed support in version 107 (October 2022). Certificate Transparency enforcement continues unchanged because it is built into browser certificate validation, not driven by the header. Removal cannot break TLS or lock clients out.
Does deleting Expect-CT disable Certificate Transparency enforcement? No. Chrome and Safari enforce CT for all publicly-trusted certificates automatically. The header only ever existed to opt in early, before default enforcement shipped. Deleting it changes nothing about whether your certificate’s SCTs are checked.
Why is my report-uri endpoint silent — is CT working?
The silence tells you nothing about CT health. No browser has emitted Expect-CT reports since the header was deprecated, so a zero-traffic endpoint is expected, not reassuring. Decommission it and use CT-log monitoring to detect mis-issuance instead.
What replaces Expect-CT for catching mis-issued certificates?
Continuous CT-log monitoring. Every publicly-trusted certificate is logged to append-only CT logs; a monitor such as Cert Spotter, Cloudflare’s CT alerts, or a scheduled crt.sh query watches those logs for your domain and alerts on any certificate you did not request. Pair it with a CAA DNS record to constrain which CAs may issue.
Does Firefox enforce Certificate Transparency?
Firefox has never enforced CT and never honored Expect-CT, so it neither rejected non-compliant certificates nor sent reports. Removing the header has no effect on Firefox. Your CT protection in practice comes from Chromium-based browsers and Safari, plus the CT-log monitoring you run yourself.
Related
- Deprecated Security Headers & Legacy Browser Support — the parent reference for retiring obsolete headers.
- Removing X-Powered-By and Server Headers Safely — another subtractive hardening cleanup done at every hop.
- HTTP Strict Transport Security (HSTS) Deep Dive — the transport control that, unlike Expect-CT, remains active and enforced.