Strapi login fails with a 500, and the server log says:
Error: Cannot send secure cookie over unencrypted connectionYour site is on HTTPS. The padlock is there. The certificate is valid. And yet.
What is happening
The error comes from the cookies library that Koa uses. Its logic is one condition:
if (!secure && opts && opts.secure) {
throw new Error('Cannot send secure cookie over unencrypted connection')
}Something asked for a cookie with the Secure flag, and the framework believes the current connection is not encrypted. It refuses, because sending a Secure cookie over plaintext would be a security bug.
The important word is believes. In a typical deployment:
Browser ──HTTPS──> Nginx / ALB / Cloudflare ──HTTP──> Strapi (127.0.0.1:1337)TLS is terminated at the proxy. The connection Strapi actually receives is plain HTTP on loopback, which is fine and normal, but Strapi has no way to know the original request was HTTPS unless you tell it to look.
So: the connection is genuinely encrypted end-to-end from the user's perspective, and Strapi is genuinely looking at an unencrypted socket. Both are true. You need to bridge them.
The fix
Strapi side
export default ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
url: env('PUBLIC_URL', 'https://cms.example.com'),
proxy: {
koa: true,
},
app: {
keys: env.array('APP_KEYS'),
// Documented location for these two:
proxyIpHeader: 'X-Forwarded-For',
maxIpsCount: 1,
},
});Where do
proxyIpHeaderandmaxIpsCountactually live? The docs list them underapp, and the full-configuration example puts them there. Strapi's server source reads them fromserver.proxy.ipHeaderandserver.proxy.maxIpsCount, and theProxytype declares both keys. If IP-header trust matters to you, set them in both places and verify with the debug middleware below rather than trusting either location:proxy: { koa: true, ipHeader: 'X-Forwarded-For', maxIpsCount: 1, },
proxy.koa: true sets Koa's app.proxy. With it, Koa trusts X-Forwarded-Proto, X-Forwarded-Host, and X-Forwarded-For, so ctx.protocol returns https and the secure cookie is allowed.
The v4 → v5 change
In Strapi v4 this was a top-level boolean:
// Strapi v4 module.exports = ({ env }) => ({ proxy: true, });In Strapi 5,
proxyis an object withglobal,fetch,http,https, andkoakeys, and the Koa setting moved toproxy.koa. A v4 config carried over unchanged setsproxyto a boolean where an object is expected, and the Koa behaviour is not applied. This is a common upgrade regression, and it presents as exactly this error appearing after a v5 migration on a deployment that had worked for years.The same breaking change moved v4's
server.globalProxy. It becomes eitherproxy.global(now covering HTTP/HTTPS requests as well asstrapi.fetch) orproxy.fetch(identical to the v4 behaviour). There is no codemod for any of this; the migration is manual.
Then rebuild, because changes to config/server require it:
NODE_ENV=production npm run buildProxy side
proxy.koa: true tells Strapi to trust the forwarded headers. It does not create them. Your proxy has to send them.
Nginx:
location / {
proxy_pass http://strapi;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # ← this one
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}Apache:
RequestHeader set X-Forwarded-Proto "https"
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:1337/
ProxyPassReverse / http://127.0.0.1:1337/Traefik, Caddy, AWS ALB, Cloudflare, and Google Cloud Load Balancing all set X-Forwarded-Proto by default. If you are behind one of those, you probably only need the Strapi-side change.
Docker Compose with an Nginx container: make sure it is the outermost proxy setting the header, not an inner one that only ever sees HTTP.
Verify it
# Should return the header your proxy is sending back through Strapi
curl -I https://cms.example.com/_health
# From inside the Strapi host, simulate what the proxy sends
curl -I -H "X-Forwarded-Proto: https" -H "Host: cms.example.com" \
http://127.0.0.1:1337/_healthThen log in to /admin. If the error is gone, the chain is correct.
For a definitive check, log the protocol from a temporary middleware:
export default () => {
return async (ctx, next) => {
strapi.log.info(`protocol=${ctx.protocol} secure=${ctx.secure} xfp=${ctx.get('x-forwarded-proto')}`);
await next();
};
};protocol=https secure=true means it is working. protocol=http with xfp=https means the header arrives but proxy.koa is not applied. xfp= empty means the proxy is not sending it.
Remove the middleware afterwards.
Do not "fix" it by disabling secure cookies
The tempting shortcut is to set secure: false on the cookie. Do not. A session cookie without the Secure flag is sent over plain HTTP, which means any downgrade, whether a stray http:// link, a hostile network, or a captive portal, leaks the session token. You would be turning a configuration error into a real vulnerability.
The same applies to your own custom cookies. If you set cookies in a controller, keep them secure in production and let the proxy configuration make that possible:
ctx.cookies.set('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 1000 * 60 * 60 * 24 * 14,
});Security note on maxIpsCount
Trusting X-Forwarded-For without bounding it lets a client spoof their own IP by sending the header themselves, because your proxy appends to it rather than replacing it, so a forged value ends up first in the list. app.maxIpsCount says how many entries to read from the right, which is how many proxies you actually have:
1: a single reverse proxy (Nginx, ALB).2: Cloudflare in front of Nginx.0: Koa's default, meaning unlimited. Unsafe once you have enabled proxy trust.
Set it to match your real topology. This matters for rate limiting, IP allowlists, and audit logs.
What else proxy.koa fixes
Once Strapi knows the real protocol and host, several other things quietly start working:
- Password reset and email confirmation links point at your domain, not
localhost:1337. - OAuth and SSO callback URLs resolve correctly.
- Client IPs in logs are real addresses instead of
127.0.0.1. - Rate limiting and IP-based policies operate on the actual client.
If you have been living with any of those, this one key is probably the cause.
Related: admin login over plain HTTP
From v5.24.0 onwards the admin panel stores session data in secure, HTTP-only cookies. Browsers will not store or send those over plain HTTP, so an admin panel served without TLS cannot complete a login at all, a different symptom from the error above with the same root cause. Terminate TLS in front of Strapi and forward X-Forwarded-Proto. Local development against the built-in server still works, because the development configuration does not mark the cookies secure.
A closely related known issue on v5.24.0+: login succeeds, the API returns 200, and the UI stays stuck on the login page behind an ALB or Nginx. proxy: { koa: true } plus the forwarded headers is the documented fix for that too.
Related
If your admin panel loads blank and the console mentions localhost:1337, that is a different problem: the admin bundle was built without your public URL. See the admin panel connection guide. For the complete proxy setup, see deploying Strapi behind Nginx.


![Fixing TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string in Strapi](https://automatic-life-0194aa0342.media.strapiapp.com/strapi_err_invalid_arg_type_path_must_be_string_ea76191703.png)
