After migrating your site to HTTPS, you often discover that embedded resources like images, scripts, and stylesheets still reference old HTTP URLs hard-coded in your database or templates. Rather than running a database search-and-replace on thousands of URLs, the upgrade-insecure-requests directive tells browsers to automatically rewrite HTTP requests to HTTPS before loading any asset. This buys you time to update your CMS, templates, and content without users seeing mixed content warnings or broken resources.
This client-side Content Security Policy directive works in modern browsers (Chrome, Firefox, Safari, Edge) to protect users from accidentally loading unencrypted resources on secure pages. Enabling it in Nginx keeps passwords, session tokens, scripts, and images encrypted end-to-end. It’s a temporary bridge during HTTPS migration, not a permanent substitute for updating your URLs.
This guide explains how mixed content warnings occur, how to apply the directive globally or per domain in Nginx, and demonstrates practical patterns for testing and troubleshooting. You’ll learn when to use this approach versus other migration strategies.
Understanding the upgrade-insecure-requests Directive
On secure origins (HTTPS), the Content Security Policy directive upgrade-insecure-requests tells the browser to rewrite any subresource or form requests that use http:// to their https:// equivalents before sending them. Modern browsers perform this upgrade automatically without additional redirects or server involvement; legacy browsers that do not support the directive simply ignore it and load resources normally.
The directive works at the HTTP header level: Nginx sends the header in every response, and the browser applies the rule before making requests. You don’t need to change your application code or database. The always parameter ensures the header appears on error pages (404, 500, etc.) so browsers stay protected even when your application encounters problems.
When Mixed Content Warnings Occur
Mixed content occurs when an HTTPS page references resources (images, scripts, stylesheets, fonts) using HTTP URLs. Browsers classify this as a security risk because attackers can intercept or modify unencrypted assets even though the page itself loads over TLS. Modern browsers block mixed active content (scripts, iframes) by default and warn users about mixed passive content (images, media).
Common scenarios where mixed content appears: WordPress sites where image URLs were hard-coded when HTTP was the default, legacy content management systems storing asset paths in databases, external CDN URLs that don’t support HTTPS, or third-party embed codes (analytics, chat widgets, ads) still using HTTP endpoints.
How upgrade-insecure-requests Bridges the Migration Gap
The upgrade-insecure-requests directive tells browsers to automatically rewrite HTTP URLs to HTTPS before making the request, fixing mixed content transparently while you update hard-coded references in templates or databases. Think of it like URL rewrite rules in Apache or IIS, except the browser handles the upgrade after the TLS handshake completes instead of the server redirecting each request individually.
This directive complements server-level security but does not replace it. Always continue enforcing 301 HTTPS redirects in Nginx to catch users arriving on HTTP pages (see the URL redirect guide). Pair upgrade-insecure-requests with HSTS to force HTTPS at the domain level: HSTS ensures browsers always connect to your domain over HTTPS on subsequent visits, while upgrade-insecure-requests handles subresource URLs that still reference HTTP.
Use this directive as a temporary safety net during migration, not as a long-term solution. Set a deadline to finish updating your URLs, then remove the header once all hard-coded references are fixed. This keeps your security posture clean and ensures you’re not relying on client-side fixes for server-side URL problems.
- The header only protects visitors reaching your HTTPS page; always use 301 redirects on your HTTP server block to force HTTPS first.
- Subresource rewrites happen transparently in the browser, so hard-coded
http://URLs keep working while you update templates. - The directive works alongside other CSP rules like
default-src https:, building layered defense. - This is a migration tool, not a permanent fix; plan to update your CMS and templates and remove the header once assets are HTTPS-native.
Enable upgrade-insecure-requests Globally in Nginx
Configuring the header in the global http context ensures every virtual host on the server inherits the directive. Use this approach when all domains already serve valid HTTPS certificates and you want consistent policy across the entire server. If you host mixed HTTP and HTTPS sites, or need granular control per application, use the per-domain method shown in the next section instead.
Open the Main Nginx Configuration
Open a terminal window (press Ctrl + Alt + T on most desktops) and edit /etc/nginx/nginx.conf with root privileges so you can modify the global http block:
sudo nano /etc/nginx/nginx.conf
The sudo prefix elevates the command just like selecting Run as Administrator in Windows, ensuring you have permission to save changes.
Add the Header to the http Block
Place the add_header directive near other global headers in the http block. The always parameter ensures the header appears on error responses (404, 500, etc.) so browsers stay protected even when your application encounters problems.
Find the http block and add the directive:
http {
...
add_header Content-Security-Policy "upgrade-insecure-requests;" always;
...
}
Only enable this header once your site serves a valid TLS (Transport Layer Security) certificate. Browsers will rewrite legacy HTTP URLs to HTTPS automatically, so any unsecured endpoints must already answer on port 443.
Headers defined in the global
httpcontext are inherited by all servers, but more specificserver/locationblocks can override them. Nginx does not merge multipleContent-Security-Policyheaders; the most specific definition wins.
Reload Nginx and Verify the Header
Test the syntax before applying changes, then reload the service:
sudo nginx -t
sudo systemctl reload nginx
The syntax test catches missing semicolons or braces before Nginx reloads, and systemctl reload applies the change without dropping active connections.
Use curl to inspect the response headers. Replace the domain with your site:
curl -I https://yourdomain.com
You should see the Content Security Policy directive in the response:
HTTP/2 200 server: nginx content-security-policy: upgrade-insecure-requests; ...
Apply upgrade-insecure-requests to a Specific Server Block
Use per-server-block configuration when you host multiple domains with different security requirements, or when migrating sites to HTTPS gradually. This approach lets you enable the directive on production HTTPS sites while leaving development or legacy HTTP sites untouched.
Edit the Domain Server Block
Open the domain configuration, usually stored in /etc/nginx/sites-available/ on Debian-based systems.
sudo nano /etc/nginx/sites-available/yourdomain
Add the Directive in the Server Context
Setting the header inside server or location blocks keeps other virtual hosts untouched. Prefer placing CSP at the HTTPS server level for broad coverage; use location only when you intentionally scope by path. Include always if you want error pages to inherit the directive. Remember that a location-level add_header overrides higher-level definitions for the same header and does not merge values.
server {
listen 443 ssl;
server_name yourdomain.com www.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
add_header Content-Security-Policy "upgrade-insecure-requests;" always;
location / {
proxy_pass http://backend;
}
}
For cleaner output showing only the CSP header, use this filtered version:
curl -sSI https://yourdomain.com | tr -d '\r' | grep -i '^content-security-policy'
Test the Domain Response
Validate the configuration, reload the service, and test the specific domain:
sudo nginx -t
sudo systemctl reload nginx
curl -I https://yourdomain.com
The header should only appear for that specific domain. If you need to test against the server IP while bypassing DNS, use the --resolve flag to map the domain name to your server IP without certificate errors:
curl -I --resolve yourdomain.com:443:SERVER_IP https://yourdomain.com
Advanced upgrade-insecure-requests Patterns
Use the following examples when you need granular control over when the directive appears or how it interacts with other policies.
Disable Upgrades for Specific Services
In rare cases, a third-party service or legacy backend may not support HTTPS and you need temporary compatibility while seeking alternatives or upgrading the service. Use this pattern to disable upgrade-insecure-requests for specific paths only:
# Place both map directives in the http context, not inside a server block
map "" $csp_upgrade_all { default "upgrade-insecure-requests;"; }
map $uri $csp_upgrade {
~^/api/legacy-service "";
default $csp_upgrade_all;
}
server {
location / {
add_header Content-Security-Policy $csp_upgrade always;
}
}
This pattern disables upgrade-insecure-requests only for /api/legacy-service while all other paths benefit from the directive. Keep the map directives in the top-level http block, not inside individual server blocks. Replace the path with your specific endpoint. As soon as your service supports HTTPS, remove this exception and enforce upgrades site-wide.
Test in Report-Only Mode (No Blocking)
Validate behavior before enforcing it by sending a Report-Only header. Browsers log policy activity without blocking requests, which is useful during phased rollouts and QA:
server {
add_header Content-Security-Policy-Report-Only "upgrade-insecure-requests" always;
}
Test in Phased Rollout Mode (Specific Browsers)
All modern browsers support upgrade-insecure-requests natively, but if you want to test the directive with specific browsers before site-wide deployment, use this pattern. This is mainly useful for QA on internal testing or gradual rollouts to user segments:
# Define maps in the http context, not inside a server block
map $http_user_agent $csp_upgrade_ua {
~*chrome "upgrade-insecure-requests;";
~*firefox "upgrade-insecure-requests;";
default "";
}
server {
location / {
add_header Content-Security-Policy $csp_upgrade_ua always;
}
}
The pattern ~*chrome also matches Chromium-based browsers. Use user-agent targeting only for short-term testing, then remove it to keep policy consistent for everyone.
Protect Sensitive Paths
Apply the header only to confidential sections like account dashboards, payment forms, or administrative interfaces while leaving public marketing pages untouched. Use this when you have mixed security requirements across different application sections:
server {
location /secure-area/ {
add_header Content-Security-Policy "upgrade-insecure-requests;" always;
}
location / {
# public traffic stays unchanged
}
}
Combine with Additional Security Headers
Pair upgrade-insecure-requests with complementary policies to build defense-in-depth. HSTS forces browsers to always use HTTPS for your domain, X-Frame-Options prevents clickjacking attacks, and X-Content-Type-Options stops MIME type sniffing. Consolidate CSP into a single header at the final scope you want to apply, because Nginx does not merge multiple Content-Security-Policy headers and the most specific block overrides others. Reference the Nginx security header guide for comprehensive coverage of each header:
server {
listen 443 ssl;
# Consolidate CSP into one header string
add_header Content-Security-Policy "upgrade-insecure-requests; default-src https:" always;
# HSTS should be set at the HTTPS server level
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Optional and long-lived commitment: add "; preload" only when ALL subdomains are HTTPS
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
}
Block Non-Upgradable Resources
Once you’re confident all assets can be served over HTTPS and no endpoints require HTTP-only access, add block-all-mixed-content to reject any resource that cannot be upgraded. This forces resolution of remaining HTTP-only endpoints rather than silently failing:
add_header Content-Security-Policy "upgrade-insecure-requests; block-all-mixed-content" always;
When block-all-mixed-content is active, resources that fail the HTTPS upgrade attempt (HTTP endpoint doesn’t exist or doesn’t respond to HTTPS) are blocked entirely rather than loaded over HTTP. This visibility helps identify problematic assets you need to fix.
Monitor Mixed Content in Browser Console
Instead of server-side logging, monitor mixed content issues directly in browser developer tools where upgrade-insecure-requests actually operates. Open the browser console (F12 in most browsers) and watch the Console tab for CSP messages about upgraded requests. This shows you which resources browsers are automatically upgrading from HTTP to HTTPS, helping you prioritize template updates:
Upgrade Insecure Requests: Upgrading insecure resource request 'http://example.com/image.jpg' Mixed Content: The page at 'https://yourdomain.com/' was loaded over HTTPS, but requested an insecure image 'http://cdn.example.com/logo.png'. This content should also be served over HTTPS.
For server-side visibility, monitor your Nginx access logs for requests to port 443 and review the referrer field to identify pages still embedding HTTP URLs. Combine this with browser console monitoring during testing to catch issues before users encounter them.
Troubleshoot Missing or Incorrect Headers
If your curl test does not show the directive, or if headers appear inconsistent, work through these checks systematically:
- Confirm you edited the active configuration. Run
sudo nginx -Tto print all loaded files and verify the directive appears in the expected context (http/server/location). - Ensure you’re testing HTTPS. Test directly against your domain with
curl -I https://yourdomain.com(not http://). The header only appears on HTTPS responses. - Check for duplicate
add_headerdirectives lower in the config. Nginx does not merge multiple CSP headers; the most specific block overrides less specific ones. If your location block defines a different CSP, it overrides the http or server block. - Consolidate all CSP directives into a single
Content-Security-Policyheader at the final scope you want to apply. Multiple separate CSP headers are not combined; only the most specific one is sent. - Both
Content-Security-Policy "upgrade-insecure-requests"andContent-Security-Policy "upgrade-insecure-requests;"(with trailing semicolon) are accepted by browsers. - If using a CDN, reverse proxy, or load balancer, clear cached responses after config changes. Some proxies inject, strip, or cache CSP headers, so validate responses both at the origin and through the edge service.
Repeat the reload and verification steps once the checks pass. Consistent use of nginx -t before each reload prevents most header regressions.
Conclusion
The upgrade-insecure-requests directive eliminates mixed-content warnings by instructing browsers to automatically upgrade HTTP asset requests to HTTPS on secure pages. It’s a temporary bridge that protects users during HTTPS migration while you update hard-coded URLs in your CMS, templates, and database. Nginx makes it straightforward to enable globally across all virtual hosts or apply selectively to specific domains and paths.
Always combine upgrade-insecure-requests with 301 HTTPS redirects (to catch HTTP arrivals) and HSTS (to enforce HTTPS on future visits). Monitor browser console output during testing to identify remaining HTTP references. Most importantly, set a deadline to finish updating your URLs, then remove the header once all assets are HTTPS-native. This keeps your security posture clean and ensures you’re relying on proper URL updates rather than browser-side fixes.