✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

Strapi login fails with a 500, and the server log says:

Error: Cannot send secure cookie over unencrypted connection

Your 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 proxyIpHeader and maxIpsCount actually live? The docs list them under app, and the full-configuration example puts them there. Strapi's server source reads them from server.proxy.ipHeader and server.proxy.maxIpsCount, and the Proxy type 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, proxy is an object with global, fetch, http, https, and koa keys, and the Koa setting moved to proxy.koa. A v4 config carried over unchanged sets proxy to 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 either proxy.global (now covering HTTP/HTTPS requests as well as strapi.fetch) or proxy.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 build

Proxy 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/_health

Then 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.

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.

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.

Further reading

Theodore Kelechukwu OnyejiakuDevRel and Community | Software Developer | Technical Writer

Theodore is a Technical Writer and a full-stack software developer. He loves writing technical articles, building solutions, and sharing his expertise.

Fixing TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string in Strapi
EcosystemIntermediate·5 min read

Fixing TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string in Strapi

Strapi crashes on restart after you create a component, with a path error from the component loader. The Windows bug behind it, and how to fix it.

·September 9, 2026
Finding and using Strapi design system icons in your plugin
EcosystemIntermediate·6 min read

Finding and using Strapi design system icons in your plugin

@strapi/icons ships hundreds of icons as React components with no visual index. How to browse them, use them in a plugin, and generate a reference.

·September 9, 2026
Uploading large files to Strapi: every limit you have to raise
EcosystemIntermediate·6 min read

Uploading large files to Strapi: every limit you have to raise

"The uploaded file exceeds the maximum allowed asset size" comes from one of four independent limits. Each one, and the order to change them in.

·September 9, 2026