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. Common scenarios include a WordPress site with http://cdn.example.com/ image URLs embedded in 500 posts, legacy HTML templates using <script src="http://..."> tags, or database records containing HTTP asset paths that would take weeks to find and update manually. Rather than running a risky 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 client-side Content Security Policy (CSP) directive, a security standard that controls what resources browsers can load, works in modern browsers (Chrome 43+, Firefox 48+, Safari 10.1+, Edge 17+) 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 that buys you time to update your CMS, templates, and content without users seeing mixed content warnings or broken resources, 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. The content-security-policy: upgrade-insecure-requests; line confirms the header is active and browsers will upgrade HTTP requests automatically:
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;
}
}
This example includes a reverse proxy configuration forwarding requests to a backend server, demonstrating how the CSP header protects proxied applications.
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. The CSP header should only appear for domains configured with the directive (port 443 in this example). See our port configuration guide if you need non-standard HTTPS ports:
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. These patterns combine CSP with Nginx features like map directives and conditional logic. For URL transformation needs beyond CSP, see our rewrite rules guide.
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. Consider adding rate limiting and performance optimizations like gzip compression to complete your security and performance stack:
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, or Ctrl+Shift+I on Linux) 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.
What successful upgrades look like in Chrome/Edge DevTools:
Upgrade Insecure Requests: Upgrading insecure resource request 'http://example.com/image.jpg'
What mixed content warnings look like (when upgrade fails):
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.
If you see mixed content warnings after enabling upgrade-insecure-requests, the resource either doesn’t exist at the HTTPS URL or the server doesn’t support HTTPS. You’ll need to fix the source or find an alternative CDN. 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
When your curl test doesn’t show the directive, or headers appear inconsistent, work through these checks systematically. Understanding Nginx’s add_header inheritance rules (from nginx.org documentation) is critical: directives are inherited from parent contexts only if no add_header exists at the current level.
Header Not Appearing
Run this diagnostic command to see exactly what Nginx loaded:
sudo nginx -T | grep -A 5 -B 5 "Content-Security-Policy"
This shows all CSP headers and their surrounding context blocks. If you see multiple definitions, the most specific one (location > server > http) wins.
Common causes:
- Testing HTTP instead of HTTPS: The header only appears on HTTPS responses. Use
curl -I https://yourdomain.com(not http://). - Location block overriding: If a location block has any
add_headerdirective, it cancels inheritance from parent contexts. You must redefine all headers you want in that location block. - Wrong configuration file: Nginx might be loading a different config. Check
nginx -Voutput for--conf-pathto confirm which file is active.
Browsers Still Show Mixed Content Warnings
If the header appears in curl but browsers still show mixed content warnings:
- Clear browser cache: Browsers cache CSP policies. Hard refresh with Ctrl+Shift+R (Linux/Windows) or Cmd+Shift+R (Mac).
- HTTPS resource doesn’t exist: The directive upgrades the URL, but if
https://example.com/image.jpgreturns 404 whilehttp://example.com/image.jpgworks, browsers can’t load it. Check browser console for failed requests. - Check browser support: Confirm your browser version supports upgrade-insecure-requests (Chrome 43+, Firefox 48+, Safari 10.1+, Edge 17+).
CDN or Reverse Proxy Stripping Headers
For Cloudflare, AWS CloudFront, or similar CDN services:
# Test origin directly (bypass CDN)
curl -I https://origin-ip-address -H "Host: yourdomain.com"
# Test through CDN
curl -I https://yourdomain.com
When the header appears at origin but not through CDN, check your CDN’s header passthrough settings. Some CDNs strip or modify CSP headers for caching optimization.
Multiple CSP Headers Appearing
Nginx can send multiple CSP headers if defined at different levels with the new add_header_inherit merge; directive (Nginx 1.29.3+). For older versions, consolidate all CSP directives into a single header at one scope level:
# Correct: Single header with multiple directives
add_header Content-Security-Policy "upgrade-insecure-requests; default-src https:" always;
# Wrong: Multiple headers (only last one applies on older Nginx)
add_header Content-Security-Policy "upgrade-insecure-requests" always;
add_header Content-Security-Policy "default-src https:" always;
Repeat the reload and verification steps once the checks pass. Consistent use of nginx -t before each reload prevents most header regressions.
Real-World Migration Workflow
Follow this tested workflow when migrating an existing site to HTTPS with upgrade-insecure-requests as your safety net:
Phase 1: Pre-Migration Preparation
- Install SSL certificate: Obtain and configure your certificate before enabling any directives.
- Test HTTPS access: Verify
https://yourdomain.comloads correctly without mixed content warnings on a test page. - Back up configuration: Save your current Nginx config:
sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.backup
Step 2: Enable Safety Directives
- Add upgrade-insecure-requests: Enable globally or per-domain following the examples above.
- Test configuration: Run
sudo nginx -tto verify syntax. - Reload Nginx: Apply with
sudo systemctl reload nginx. - Verify header presence: Use curl to confirm the CSP header appears.
Stage 3: Test and Monitor
- Browse your site with DevTools open: Press F12 and watch the Console tab for upgrade messages.
- Check critical pages: Test your homepage, product pages, blog posts, and any pages with embedded media.
- Document remaining HTTP URLs: List any resources that fail to upgrade (404 errors on HTTPS versions).
- Monitor for 1-2 weeks: Watch server logs and user reports for issues.
Final Step: Permanent Migration
- Update database URLs: For WordPress, use a plugin like Better Search Replace or WP-CLI to change
http://tohttps://in your database. - Update templates: Replace hard-coded HTTP URLs in theme files, plugins, and custom code.
- Fix external CDNs: Migrate to HTTPS-capable CDNs for any resources that don’t support HTTPS.
- Add HSTS: Once all resources are HTTPS-native, add Strict-Transport-Security header (see our security headers guide).
- Remove upgrade-insecure-requests: After confirming no more HTTP URLs exist, remove the CSP directive and test thoroughly.
Set a deadline for the final step. Don’t leave upgrade-insecure-requests enabled indefinitely; it masks problems instead of fixing them. Most sites complete migration within 30-60 days.
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.