{"id":1803,"date":"2025-11-13T20:44:46","date_gmt":"2025-11-13T17:44:46","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/cookies-that-behave-samesitelax-strict-secure-and-httponly-done-right-on-nginx-apache-and-in-your-app\/"},"modified":"2025-11-13T20:44:46","modified_gmt":"2025-11-13T17:44:46","slug":"cookies-that-behave-samesitelax-strict-secure-and-httponly-done-right-on-nginx-apache-and-in-your-app","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/cookies-that-behave-samesitelax-strict-secure-and-httponly-done-right-on-nginx-apache-and-in-your-app\/","title":{"rendered":"Cookies That Behave: SameSite=Lax\/Strict, Secure, and HttpOnly Done Right on Nginx, Apache, and in Your App"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><p>A couple of Mondays ago, I walked into a gnarly login loop. You know the kind: user signs in, gets bounced to the dashboard, then wham\u2014right back to the login page like nothing happened. I\u2019ve seen countless versions of this, but this time the villain was subtle: cookie attributes. Specifically, a missing SameSite setting and a Secure flag that wasn\u2019t actually secure because the app was sitting behind a proxy that wasn\u2019t telling the framework it was HTTPS.<\/p>\n<p>Ever had that moment when everything looks fine in code, yet the browser whispers \u201cNope\u201d under its breath? That\u2019s cookies. They\u2019re tiny, but they have rules\u2014and the browser enforces those rules at the door.<\/p>\n<p>In this guide, we\u2019ll talk about how to set <strong>SameSite=Lax\/Strict<\/strong>, <strong>Secure<\/strong>, and <strong>HttpOnly<\/strong> the right way. We\u2019ll do it on Nginx, on Apache, and inside your app code. We\u2019ll look at why these flags exist, where they\u2019ll save your skin, and where they can break your login flow if you\u2019re not careful. I\u2019ll share configs and examples I actually use, plus a playbook for migrating without disrupting your users.<\/p>\n<div id=\"toc_container\" class=\"toc_transparent no_bullets\"><p class=\"toc_title\">\u0130&ccedil;indekiler<\/p><ul class=\"toc_list\"><li><a href=\"#Why_These_Cookie_Flags_Matter_And_When_They_Bite\"><span class=\"toc_number toc_depth_1\">1<\/span> Why These Cookie Flags Matter (And When They Bite)<\/a><\/li><li><a href=\"#Picking_the_Right_Defaults_Without_Overthinking_It\"><span class=\"toc_number toc_depth_1\">2<\/span> Picking the Right Defaults Without Overthinking It<\/a><\/li><li><a href=\"#Nginx_Setting_and_Fixing_Cookie_Flags_at_the_Edge\"><span class=\"toc_number toc_depth_1\">3<\/span> Nginx: Setting and Fixing Cookie Flags at the Edge<\/a><ul><li><a href=\"#When_your_app_already_sets_the_right_flags\"><span class=\"toc_number toc_depth_2\">3.1<\/span> When your app already sets the right flags<\/a><\/li><li><a href=\"#Older_Nginx_trick_appending_via_proxy_cookie_path\"><span class=\"toc_number toc_depth_2\">3.2<\/span> Older Nginx trick: appending via proxy_cookie_path<\/a><\/li><li><a href=\"#Setting_a_cookie_directly_from_Nginx\"><span class=\"toc_number toc_depth_2\">3.3<\/span> Setting a cookie directly from Nginx<\/a><\/li><\/ul><\/li><li><a href=\"#Apache_The_Headers_That_Save_Your_Day\"><span class=\"toc_number toc_depth_1\">4<\/span> Apache: The Headers That Save Your Day<\/a><\/li><li><a href=\"#Set_It_in_Your_App_Real_Examples_That_Dont_Flake\"><span class=\"toc_number toc_depth_1\">5<\/span> Set It in Your App: Real Examples That Don\u2019t Flake<\/a><ul><li><a href=\"#Nodejs_Express\"><span class=\"toc_number toc_depth_2\">5.1<\/span> Node.js (Express)<\/a><\/li><li><a href=\"#PHP_native\"><span class=\"toc_number toc_depth_2\">5.2<\/span> PHP (native)<\/a><\/li><li><a href=\"#Django\"><span class=\"toc_number toc_depth_2\">5.3<\/span> Django<\/a><\/li><li><a href=\"#Go_nethttp\"><span class=\"toc_number toc_depth_2\">5.4<\/span> Go (net\/http)<\/a><\/li><li><a href=\"#One_more_thing_cookie_name_prefixes\"><span class=\"toc_number toc_depth_2\">5.5<\/span> One more thing: cookie name prefixes<\/a><\/li><\/ul><\/li><li><a href=\"#Avoiding_Common_Traps_and_Odd_Behaviors\"><span class=\"toc_number toc_depth_1\">6<\/span> Avoiding Common Traps and Odd Behaviors<\/a><\/li><li><a href=\"#NginxApache_vs_App_Code_Where_Should_You_Set_Flags\"><span class=\"toc_number toc_depth_1\">7<\/span> Nginx\/Apache vs. App Code: Where Should You Set Flags?<\/a><\/li><li><a href=\"#A_Quick_Migration_Plan_That_Wont_Panic_Your_Users\"><span class=\"toc_number toc_depth_1\">8<\/span> A Quick Migration Plan That Won\u2019t Panic Your Users<\/a><\/li><li><a href=\"#Debugging_The_Browser_Is_Telling_You_Whats_Wrong\"><span class=\"toc_number toc_depth_1\">9<\/span> Debugging: The Browser Is Telling You What\u2019s Wrong<\/a><\/li><li><a href=\"#CORS_OAuth_and_Cross-Site_Reality\"><span class=\"toc_number toc_depth_1\">10<\/span> CORS, OAuth, and Cross-Site Reality<\/a><\/li><li><a href=\"#Hardening_Tips_That_Pay_Off_for_Years\"><span class=\"toc_number toc_depth_1\">11<\/span> Hardening Tips That Pay Off for Years<\/a><\/li><li><a href=\"#A_Note_on_Performance_and_Deliverability\"><span class=\"toc_number toc_depth_1\">12<\/span> A Note on Performance and Deliverability<\/a><\/li><li><a href=\"#Putting_It_All_Together_A_Friendly_Checklist\"><span class=\"toc_number toc_depth_1\">13<\/span> Putting It All Together: A Friendly Checklist<\/a><\/li><li><a href=\"#WrapUp_Calm_Cookies_Happy_Users\"><span class=\"toc_number toc_depth_1\">14<\/span> Wrap\u2011Up: Calm Cookies, Happy Users<\/a><\/li><\/ul><\/div>\n<h2 id=\"section-1\"><span id=\"Why_These_Cookie_Flags_Matter_And_When_They_Bite\">Why These Cookie Flags Matter (And When They Bite)<\/span><\/h2>\n<p>Think of cookies like VIP passes. They get your user past the rope and straight into the app, but only if the pass is legit and used in the right place. Browsers check the fine print\u2014what we call attributes\u2014before honoring a cookie. The big four for sessions and auth flows are <strong>Secure<\/strong>, <strong>HttpOnly<\/strong>, and <strong>SameSite<\/strong> (with <strong>Lax<\/strong>, <strong>Strict<\/strong>, or <strong>None<\/strong> as values).<\/p>\n<p><strong>Secure<\/strong> means \u201conly send this cookie over HTTPS.\u201d It\u2019s non-negotiable for anything sensitive. If you\u2019re on HTTP in production in 2025, that\u2019s a whole other conversation\u2014but assume Secure for auth, always. <strong>HttpOnly<\/strong> means JavaScript can\u2019t read the cookie. That\u2019s a huge shield against \u201csteal the session via XSS\u201d attacks. You still need to prevent XSS, but HttpOnly limits the blast radius if something slips through.<\/p>\n<p><strong>SameSite<\/strong> is the one that surprises teams most often. With <strong>Lax<\/strong>, your cookie will be sent on regular navigations (like clicking a link to your site) but not on cross-site POSTs or iframes. With <strong>Strict<\/strong>, the browser only sends the cookie when you\u2019re already on the same site\u2014it\u2019s the paranoid option. And <strong>None<\/strong> means \u201ctreat it like the old days,\u201d but only if you also add Secure. Modern browsers won\u2019t honor SameSite=None without Secure. If you\u2019re curious about the detailed semantics, the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Set-Cookie\" rel=\"nofollow noopener\" target=\"_blank\">MDN Set-Cookie reference<\/a> is an excellent, plain-English resource.<\/p>\n<p>Here\u2019s the thing: browsers changed the default. If you don\u2019t specify SameSite, it behaves like Lax in most modern browsers. That\u2019s a smart default for security, but it trips up OAuth and embedded app flows. When your login provider posts back to your site, you might be doing a cross-site POST or loading an iframe\u2014Lax will refuse to send the cookie in those moments. That\u2019s the login loop I met on Monday. If you want the longer story on the change itself, I like the clarity in <a href=\"https:\/\/web.dev\/samesite-cookies-explained\/\" rel=\"nofollow noopener\" target=\"_blank\">SameSite cookies explained<\/a>.<\/p>\n<h2 id=\"section-2\"><span id=\"Picking_the_Right_Defaults_Without_Overthinking_It\">Picking the Right Defaults Without Overthinking It<\/span><\/h2>\n<p>Let me share the simple rule I use with teams: for session cookies, go <strong>Secure + HttpOnly + SameSite=Lax<\/strong>. Nine times out of ten, that\u2019s exactly what you want. It protects you from casual CSRF by refusing cross-site POSTs while still letting a user click a link from an email and land in their account without hiccups. For admin panels or apps that never need to take traffic from other domains, I often go <strong>SameSite=Strict<\/strong> and call it a day\u2014just know that sharing links around (like from chat) still works because it\u2019s a top-level navigation, but some flows that rely on cross-origin embeds won\u2019t.<\/p>\n<p>When would you use <strong>SameSite=None<\/strong>? In two situations: cross-site single sign-on and embedded experiences. If your app lives inside an iframe on a different domain, or you\u2019re doing OAuth with a provider that posts back from their domain, you need None\u2014<strong>and<\/strong> you must include Secure. There\u2019s no way around it. Some browsers also limit third-party cookies aggressively for privacy reasons, so for embedded apps, you may need to think beyond cookies (message passing, tokens scoped to the parent, or prompting the user to open in a new tab to establish first-party context).<\/p>\n<p>One quick reminder: SameSite helps with CSRF, but it\u2019s not a replacement for CSRF tokens on state-changing requests. Good defense stacks up, not swaps out. If you want a crisp refresher, the <a href=\"https:\/\/owasp.org\/www-community\/attacks\/csrf\" rel=\"nofollow noopener\" target=\"_blank\">OWASP CSRF page<\/a> is a great sanity check.<\/p>\n<h2 id=\"section-3\"><span id=\"Nginx_Setting_and_Fixing_Cookie_Flags_at_the_Edge\">Nginx: Setting and Fixing Cookie Flags at the Edge<\/span><\/h2>\n<h3><span id=\"When_your_app_already_sets_the_right_flags\">When your app already sets the right flags<\/span><\/h3>\n<p>If your app code is correct, great\u2014Nginx can step back and chill. But I still like having guardrails at the edge, especially when I\u2019m proxying legacy apps or third-party backends that I don\u2019t fully control. One of the handiest tools is <strong>proxy_cookie_flags<\/strong>, which lets you force flags onto cookies coming from upstream.<\/p>\n<pre class=\"language-nginx line-numbers\"><code class=\"language-nginx\">http {\n    # ...\n    server {\n        listen 443 ssl http2;\n        server_name example.com;\n\n        # Enforce HTTPS for Secure cookies &amp; general good hygiene\n        # (Consider HSTS once you're confident)\n\n        location \/ {\n            proxy_pass http:\/\/app;\n\n            # Add flags to specific cookies by name\n            proxy_cookie_flags session_id secure httponly samesite=lax;\n\n            # Or match multiple cookies with a regex\n            proxy_cookie_flags ~*^(__Secure-|__Host-|session|sid) secure httponly samesite=lax;\n        }\n    }\n}\n<\/code><\/pre>\n<p>If you\u2019re not sure which names your app uses, you can temporarily log Set-Cookie headers in a staging environment and then add a regex. The regex approach is my favorite because it catches variations (like \u201csid\u201d vs \u201csession_id\u201d) without needing to micromanage names.<\/p>\n<h3><span id=\"Older_Nginx_trick_appending_via_proxy_cookie_path\">Older Nginx trick: appending via proxy_cookie_path<\/span><\/h3>\n<p>In older Nginx versions that don\u2019t have <strong>proxy_cookie_flags<\/strong>, you can append attributes using a path rewrite. It\u2019s a bit of a hack, but it\u2019s reliable:<\/p>\n<pre class=\"language-nginx line-numbers\"><code class=\"language-nginx\">location \/ {\n    proxy_pass http:\/\/app;\n\n    # Append flags to every Set-Cookie by rewriting the Path attribute\n    proxy_cookie_path \/ &quot;\/; Secure; HttpOnly; SameSite=Lax&quot;;\n}\n<\/code><\/pre>\n<p>Just be careful not to double-append. If your upstream already sets SameSite or Secure, you might end up with duplicates. Browsers usually pick the last occurrence, but I prefer to keep things tidy by standardizing upstream or using <strong>proxy_cookie_flags<\/strong> where available.<\/p>\n<h3><span id=\"Setting_a_cookie_directly_from_Nginx\">Setting a cookie directly from Nginx<\/span><\/h3>\n<p>Sometimes you want a small edge cookie\u2014maybe for a feature flag, a short-lived banner dismissal, or a routing hint. You can set it directly:<\/p>\n<pre class=\"language-nginx line-numbers\"><code class=\"language-nginx\">location = \/welcome {\n    add_header Set-Cookie &quot;welcome=1; Path=\/; Secure; HttpOnly; SameSite=Lax&quot; always;\n    try_files \/welcome.html =404;\n}\n<\/code><\/pre>\n<p>Two gotchas: first, don\u2019t set cookies on responses that should be cached by a CDN unless you\u2019ve got cache rules that respect personalization. Second, cookies are per-header; if you add multiple cookies, send multiple Set-Cookie headers\u2014don\u2019t jam them into one line.<\/p>\n<p>And yes, all of this rides on having solid TLS. If you\u2019re cleaning up certificates anyway, I\u2019ve had great results with serving dual ECDSA and RSA certificates for both speed and compatibility. If you\u2019re curious, I wrote about it here: <a href=\"https:\/\/www.dchost.com\/blog\/en\/nginx-apachede-ecdsa-rsa-ikili-ssl-uyumluluk-mu-hiz-mi-ikisini-birden-nasil-alirsin\/\">serving Dual ECDSA + RSA Certificates on Nginx and Apache<\/a>.<\/p>\n<h2 id=\"section-4\"><span id=\"Apache_The_Headers_That_Save_Your_Day\">Apache: The Headers That Save Your Day<\/span><\/h2>\n<p>On Apache, the workhorse is <strong>mod_headers<\/strong>. You can append flags to Set-Cookie headers with a simple rule. It\u2019s elegant when you need a quick, global fix, especially for apps you can\u2019t edit.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">&lt;IfModule mod_headers.c&gt;\n    # Only when HTTPS is on, to avoid Secure on HTTP\n    Header always edit Set-Cookie ^(.*)$ &quot;$1; HttpOnly; Secure; SameSite=Lax&quot; env=HTTPS\n&lt;\/IfModule&gt;\n<\/code><\/pre>\n<p>If you worry about adding flags twice, you can tighten it up with a case-insensitive regex that tries to avoid cookies that already include attributes. It\u2019s not perfect, but it helps in mixed environments:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">&lt;IfModule mod_headers.c&gt;\n    # Append only when missing (best effort)\n    Header always edit Set-Cookie \n        &quot;(?i)^((?!;s*httponly)(?!;s*secure)(?!;s*samesite).*)$&quot; \n        &quot;$1; HttpOnly; Secure; SameSite=Lax&quot; env=HTTPS\n&lt;\/IfModule&gt;\n<\/code><\/pre>\n<p>Inside a VirtualHost, keep it close to your app proxying to make it obvious:<\/p>\n<pre class=\"language-apache line-numbers\"><code class=\"language-apache\">&lt;VirtualHost *:443&gt;\n    ServerName example.com\n    ProxyPass        \/ http:\/\/app\/\n    ProxyPassReverse \/ http:\/\/app\/\n\n    &lt;IfModule mod_headers.c&gt;\n        Header always edit Set-Cookie ^(.*)$ &quot;$1; HttpOnly; Secure; SameSite=Lax&quot; env=HTTPS\n    &lt;\/IfModule&gt;\n&lt;\/VirtualHost&gt;\n<\/code><\/pre>\n<p>Just like with Nginx, I generally set flags at the source (the app) and use Apache as a safety net. It keeps your behavior consistent if you change reverse proxies or add a CDN later. If you\u2019re running a load balancer in front, especially one that does sticky sessions with a routing cookie, make sure those cookies are also getting the right flags so your users don\u2019t bounce around. I went deep on that in <a href=\"https:\/\/www.dchost.com\/blog\/en\/haproxy-ile-l4-l7-yuk-dengeleme-nasil-sifir-kesinti-sunar-health-check-sticky-sessions-ve-tls-passthroughu-sade-sade-konusalim\/\">Zero\u2011Downtime HAProxy: sticky sessions that behave<\/a>.<\/p>\n<h2 id=\"section-5\"><span id=\"Set_It_in_Your_App_Real_Examples_That_Dont_Flake\">Set It in Your App: Real Examples That Don\u2019t Flake<\/span><\/h2>\n<h3><span id=\"Nodejs_Express\">Node.js (Express)<\/span><\/h3>\n<p>Express makes this easy, but there\u2019s one catch: if you\u2019re behind a proxy (Nginx\/Apache\/ELB), tell Express to trust it so Secure cookies don\u2019t get downgraded by mistake.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">const express = require('express');\nconst app = express();\n\n\/\/ Behind a reverse proxy (important for Secure)\napp.set('trust proxy', 1);\n\napp.get('\/login', (req, res) =&gt; {\n  res.cookie('session_id', 'abc123', {\n    httpOnly: true,\n    secure: true,          \/\/ requires HTTPS\n    sameSite: 'lax',       \/\/ 'strict' or 'none' also valid\n    path: '\/',\n    maxAge: 60 * 60 * 1000 \/\/ 1 hour\n  });\n  res.send('Logged in');\n});\n<\/code><\/pre>\n<p>For OAuth callbacks that require cross-site context, set <code>sameSite: 'none'<\/code> and keep <code>secure: true<\/code>. If you do this only for the short-lived login state cookie (and keep your long session cookie at Lax), you get the best of both worlds.<\/p>\n<h3><span id=\"PHP_native\">PHP (native)<\/span><\/h3>\n<p>Modern PHP has a clean options array. Use it\u2014no more string concatenation gymnastics.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">setcookie('session_id', $value, [\n  'expires'  =&gt; time() + 3600,\n  'path'     =&gt; '\/',\n  'domain'   =&gt; 'example.com', \/\/ or omit for host-only\n  'secure'   =&gt; true,\n  'httponly' =&gt; true,\n  'samesite' =&gt; 'Lax',\n]);\n<\/code><\/pre>\n<p>If you\u2019re writing a cookie that must go cross-site (like an OAuth state cookie), set <code>'samesite' =&gt; 'None'<\/code> and keep <code>'secure' =&gt; true<\/code>. For session libraries, mirror these settings in your <code>php.ini<\/code> or framework config so everything aligns.<\/p>\n<h3><span id=\"Django\">Django<\/span><\/h3>\n<p>Django centralizes cookie policy nicely. Put it in settings, and most of your session and CSRF behavior just snaps into place.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># settings.py\nSESSION_COOKIE_SECURE = True\nSESSION_COOKIE_HTTPONLY = True\nSESSION_COOKIE_SAMESITE = 'Lax'  # 'Strict' or 'None' (with SECURE)\n\nCSRF_COOKIE_SECURE = True\nCSRF_COOKIE_SAMESITE = 'Lax'\n<\/code><\/pre>\n<p>If you need a custom cookie, set attributes explicitly when issuing it in a view. For cross-site SSO, consider using a short-lived cookie with SameSite=None just for the handshake.<\/p>\n<h3><span id=\"Go_nethttp\">Go (net\/http)<\/span><\/h3>\n<p>Go gives you the keys, but you have to set every attribute yourself. Good news: it\u2019s straightforward and explicit.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">http.SetCookie(w, &amp;http.Cookie{\n    Name:     &quot;session_id&quot;,\n    Value:    value,\n    Path:     &quot;\/&quot;,\n    Secure:   true,\n    HttpOnly: true,\n    SameSite: http.SameSiteLaxMode, \/\/ StrictMode or NoneMode\n    MaxAge:   3600,\n})\n<\/code><\/pre>\n<p>For <code>SameSite=None<\/code>, use <code>http.SameSiteNoneMode<\/code> and make sure <code>Secure<\/code> is true.<\/p>\n<h3><span id=\"One_more_thing_cookie_name_prefixes\">One more thing: cookie name prefixes<\/span><\/h3>\n<p>If you want to lock things down further, consider the <strong>__Host-<\/strong> and <strong>__Secure-<\/strong> name prefixes. <strong>__Host-<\/strong> requires the cookie to be set with Secure, <strong>Path=\/<\/strong>, and without a Domain attribute\u2014making it host-only and harder to spoof across subdomains. <strong>__Secure-<\/strong> requires Secure. They\u2019re a quiet upgrade that pay off when you\u2019re sharing infrastructure across subdomains.<\/p>\n<h2 id=\"section-6\"><span id=\"Avoiding_Common_Traps_and_Odd_Behaviors\">Avoiding Common Traps and Odd Behaviors<\/span><\/h2>\n<p>I\u2019ve lost hours to these, so you don\u2019t have to. First, remember that <strong>SameSite=Lax<\/strong> still sends cookies on top-level navigations (like clicking a link). It blocks cross-site POSTs and most iframes, but it won\u2019t protect a state-changing GET. So don\u2019t put \u201cdelete account\u201d behind a GET URL. Pair Lax with anti-CSRF tokens for POSTs and keep state changes off GET entirely.<\/p>\n<p>Second, if you\u2019re doing OAuth via a provider domain and your callback involves a POST or an iframe, <strong>SameSite=Lax<\/strong> might stop your cookie at the door. If you see a login loop only when coming from a particular identity provider, that\u2019s your clue to set <strong>SameSite=None<\/strong> for the short-lived handshake cookie.<\/p>\n<p>Third, if Secure is set but the cookie still doesn\u2019t arrive, your app might not realize it\u2019s behind HTTPS. Classic bug: Express or Rails thinks the request is HTTP because the reverse proxy didn\u2019t set <code>X-Forwarded-Proto: https<\/code> or your framework isn\u2019t configured to trust the proxy. Fix that, and suddenly Secure cookies behave.<\/p>\n<p>Finally, don\u2019t hand your CDN a personalized response to cache. If a response sets a cookie, be explicit with cache headers and vary rules. If you want to dive into safe, user-friendly caching patterns, I wrote about <a href=\"https:\/\/www.dchost.com\/blog\/en\/kesinti-caninizi-sikmasin-stale-while-revalidate-ve-stale-if-error-nasil-hayat-kurtarir\/\">stale-while-revalidate and stale-if-error making caching feel effortless<\/a>. Cookies and caching can get along; you just have to introduce them politely.<\/p>\n<h2 id=\"section-7\"><span id=\"NginxApache_vs_App_Code_Where_Should_You_Set_Flags\">Nginx\/Apache vs. App Code: Where Should You Set Flags?<\/span><\/h2>\n<p>My philosophy: set flags in your app where the cookie is created, then use your proxy (Nginx\/Apache) as a seatbelt. The app knows the context\u2014like which cookies need SameSite=None for SSO\u2014and can be precise. Your proxy acts as a safety net for legacy paths, third-party apps you\u2019re proxying, or the odd misconfiguration that sneaks in during a refactor.<\/p>\n<p>There are times when the edge is the only realistic fix. I\u2019ve had to patch marketing tools that drop cookies without HttpOnly by using Nginx\u2019s <strong>proxy_cookie_flags<\/strong>. It\u2019s not perfect, but it\u2019s better than leaving a cookie readable to JavaScript when it doesn\u2019t need to be. Just document these edge rules so you remember why they exist in six months.<\/p>\n<h2 id=\"section-8\"><span id=\"A_Quick_Migration_Plan_That_Wont_Panic_Your_Users\">A Quick Migration Plan That Won\u2019t Panic Your Users<\/span><\/h2>\n<p>Migrations go smoother when you separate the cookies by purpose. First, identify your primary session cookie. Switch it to <strong>Secure + HttpOnly + SameSite=Lax<\/strong> and roll it out on a canary environment or a single region. Watch your error rates and login metrics. If your app supports multiple login providers, test each end-to-end.<\/p>\n<p>Next, handle the special flows. If you\u2019re doing OAuth or embedding your app, create a distinct, short-lived cookie for that handshake with <strong>SameSite=None + Secure<\/strong>. Don\u2019t change your main session cookie to None unless you absolutely must. That keeps your day-to-day browsing protected while still allowing the specific cross-site move you need.<\/p>\n<p>When names or scopes need to change (say, you\u2019re switching from a subdomain cookie to a host-only one with <strong>__Host-<\/strong>), consider a new cookie name and a temporary compatibility read of the old cookie. You can then sunset the legacy cookie after a comfortable window\u2014typically a few weeks, depending on your user base and session lifetimes. Remember that <strong>Expires<\/strong> and <strong>Max-Age<\/strong> influence persistence; explicit Max-Age is what I default to for predictability.<\/p>\n<h2 id=\"section-9\"><span id=\"Debugging_The_Browser_Is_Telling_You_Whats_Wrong\">Debugging: The Browser Is Telling You What\u2019s Wrong<\/span><\/h2>\n<p>The best tool for cookie debugging is the browser itself. Open DevTools, head to the Application\/Storage tab, and look at your cookies for the site. You\u2019ll see which attributes are actually set\u2014and if the cookie is marked as \u201cThis cookie was blocked due to SameSite\u201d on network requests, the browser will tell you in the console or Network panel.<\/p>\n<p>I like inspecting the Network tab for the exact request that fails. Filter for your callback or POST, click the request, and check \u201cRequest Headers\u201d to see whether the Cookie header includes what you expect. Then look at the response headers to confirm the Set-Cookie line. When I\u2019m unsure whether a proxy is stripping or rewriting headers, a quick <code>curl -I https:\/\/example.com\/login<\/code> in staging helps confirm what\u2019s getting sent from the server versus what the browser chooses to store.<\/p>\n<p>Also, verify the <strong>Domain<\/strong> and <strong>Path<\/strong> you set line up with where you need the cookie. If you omit Domain, the cookie is host-only\u2014sometimes that\u2019s good (tighter scope), sometimes not (your API is on a subdomain). A small but useful tweak is preferring host-only cookies plus <strong>__Host-<\/strong> where possible, and using domain-wide cookies only when you truly need them across subdomains.<\/p>\n<h2 id=\"section-10\"><span id=\"CORS_OAuth_and_Cross-Site_Reality\">CORS, OAuth, and Cross-Site Reality<\/span><\/h2>\n<p>There\u2019s a dance between cookies and CORS. If your frontend on one domain calls your API on another and you want the browser to send cookies, your fetch\/XHR must include <code>credentials: 'include'<\/code>, your server must reply with <code>Access-Control-Allow-Credentials: true<\/code>, and your cookie likely needs <strong>SameSite=None + Secure<\/strong> depending on the exact flow. You also need explicit <code>Access-Control-Allow-Origin<\/code> to the calling domain\u2014no wildcard with credentials.<\/p>\n<p>For OAuth, I split cookies by purpose. The main session stays at <strong>SameSite=Lax<\/strong>. A separate, short-lived state cookie involved in the cross-site handshake uses <strong>SameSite=None + Secure<\/strong>. After the handshake, I re-issue the main session cookie as Lax and drop the state cookie. That keeps your daily browsing safer while still making SSO smooth.<\/p>\n<p>And if you\u2019re handling payments, compliance checklists care that session data stays confidential and transport is locked down. It\u2019s all part of a bigger picture: encryption, key rotation, safe storage, and tight scoping. For the hosting side of that world, I\u2019ve put together a practical guide here: <a href=\"https:\/\/www.dchost.com\/blog\/en\/e%e2%80%91ticarette-pci-dssi-dert-etmeden-nasil-uyumlu-kalirsin-hosting-tarafinda-gercekten-ne-yapmak-gerekir\/\">PCI DSS for E\u2011Commerce, without the panic<\/a>.<\/p>\n<h2 id=\"section-11\"><span id=\"Hardening_Tips_That_Pay_Off_for_Years\">Hardening Tips That Pay Off for Years<\/span><\/h2>\n<p>A few habits have aged really well for me. First, make <strong>Secure + HttpOnly<\/strong> your non-negotiable baseline for any cookie that touches auth or session state. Second, prefer <strong>SameSite=Lax<\/strong>, and reserve <strong>SameSite=None<\/strong> for the few places that truly need it. Third, use <strong>__Host-<\/strong> for your main session cookie when feasible: it forces host-only scope, Secure, and Path=\/, which shuts a lot of doors evil twins might try to slip through on sibling subdomains.<\/p>\n<p>Fourth, don\u2019t forget the TLS story behind Secure. The browser won\u2019t send a Secure cookie over HTTP. If you\u2019re cleaning up your HTTPS setup, you might enjoy my write-up on <a href=\"https:\/\/www.dchost.com\/blog\/en\/nginx-apachede-ecdsa-rsa-ikili-ssl-uyumluluk-mu-hiz-mi-ikisini-birden-nasil-alirsin\/\">serving both ECDSA and RSA certs without drama<\/a>. Fifth, be explicit about caching. If a response sets cookies or varies by cookie, mark it accordingly. I\u2019ve seen \u201cwhy am I seeing someone else\u2019s cart?\u201d bugs that were just an over-eager cache ignoring Set-Cookie and Vary.<\/p>\n<p>Finally, document your cookie strategy. A simple comment at the top of your session middleware that says \u201cMain session: Secure, HttpOnly, SameSite=Lax. OAuth state: Secure, SameSite=None, short-lived. Admin cookies: Strict.\u201d has saved me more hours than any tool. People change jobs; README files don\u2019t.<\/p>\n<h2 id=\"section-12\"><span id=\"A_Note_on_Performance_and_Deliverability\">A Note on Performance and Deliverability<\/span><\/h2>\n<p>Cookies add bytes to every request to their scope. Don\u2019t store anything heavy in them, and avoid setting them on top-level domains if the cookie isn\u2019t relevant for all paths and subdomains. Small cookies, tight scope, and short lifetimes where possible\u2014these little choices add up. If you\u2019re routing traffic across balancers or doing canary checks, a tiny cookie for stickiness is fine. Just give it Secure and, where appropriate, HttpOnly. If you\u2019re evolving your edge stack, you might like how cleanly <a href=\"https:\/\/www.dchost.com\/blog\/en\/haproxy-ile-l4-l7-yuk-dengeleme-nasil-sifir-kesinti-sunar-health-check-sticky-sessions-ve-tls-passthroughu-sade-sade-konusalim\/\">HAProxy handles health checks and sticky sessions<\/a> when you want full control.<\/p>\n<h2 id=\"section-13\"><span id=\"Putting_It_All_Together_A_Friendly_Checklist\">Putting It All Together: A Friendly Checklist<\/span><\/h2>\n<p>I\u2019m not a fan of rigid checklists, but this one has become muscle memory for me. First: sessions\u2014Secure, HttpOnly, SameSite=Lax. Second: OAuth\/SSO handshake\u2014SameSite=None + Secure, short lifetime, then drop it. Third: admin-only flows\u2014consider SameSite=Strict if you never embed or cross-post.<\/p>\n<p>Fourth: set the right flags in your app, and use Nginx\/Apache to enforce or patch as needed. Fifth: verify TLS is solid; if your app is behind a proxy, set trust headers correctly so frameworks recognize HTTPS. Sixth: test in real browsers\u2014Chrome, Firefox, Safari\u2014because their cookie policies can differ in just enough places to surprise you. If you need a deep-dive reference while testing, I keep the <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Headers\/Set-Cookie\" rel=\"nofollow noopener\" target=\"_blank\">MDN Set-Cookie page<\/a> pinned.<\/p>\n<h2 id=\"section-14\"><span id=\"WrapUp_Calm_Cookies_Happy_Users\">Wrap\u2011Up: Calm Cookies, Happy Users<\/span><\/h2>\n<p>I still remember the first time I flipped a cookie to SameSite=Strict on an admin tool and watched CSRF attempts just\u2026 stop. It felt like locking a door that had quietly been ajar for years. On the other hand, I\u2019ve also watched a login loop drive a support team up the wall until we realized the callback needed SameSite=None. That\u2019s cookies\u2014small, mighty, and occasionally moody.<\/p>\n<p>If you take nothing else from this, let it be this: make <strong>Secure + HttpOnly + SameSite=Lax<\/strong> your baseline, carve out <strong>SameSite=None<\/strong> only for the flows that truly need it, and let your reverse proxy help you enforce good behavior. Test your login and checkout flows end-to-end, including in incognito windows and with blocking extensions, and keep DevTools open to see what the browser is actually doing. The browser tells the truth; you just have to ask it the right way.<\/p>\n<p>And if you\u2019re cleaning up the rest of your stack while you\u2019re here, you might enjoy a calm walk through <a href=\"https:\/\/www.dchost.com\/blog\/en\/e%e2%80%91ticarette-pci-dssi-dert-etmeden-nasil-uyumlu-kalirsin-hosting-tarafinda-gercekten-ne-yapmak-gerekir\/\">PCI on the hosting side<\/a> or a practical strategy for <a href=\"https:\/\/www.dchost.com\/blog\/en\/kesinti-caninizi-sikmasin-stale-while-revalidate-ve-stale-if-error-nasil-hayat-kurtarir\/\">caching that doesn\u2019t break personalization<\/a>. Hope this was helpful! If it saves you from one midnight login loop, I\u2019ll call that a win. See you in the next post.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>A couple of Mondays ago, I walked into a gnarly login loop. You know the kind: user signs in, gets bounced to the dashboard, then wham\u2014right back to the login page like nothing happened. I\u2019ve seen countless versions of this, but this time the villain was subtle: cookie attributes. Specifically, a missing SameSite setting and [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1804,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-1803","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-teknoloji"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1803","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=1803"}],"version-history":[{"count":0,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1803\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/1804"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=1803"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=1803"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=1803"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}