Password protecting your static site with NGINX

December 26, 2024

I'm building a website for my wedding next year! One of the constraints is the need to password-protect the site to prevent certain details, such as the location and date, from being publicly accessible.

This problem is particularly interesting because the website is primarily static. Introducing a heavy layer of authentication would slow things down, and we also need to secure most assets on the page. This is necessary because the JavaScript running on the site might inadvertently expose sensitive information.

Problem Constraints

Here are the key constraints I wanted to address:

  1. Only users with a specific password can access the website's contents.
  2. Users should be able to access the site through a link without manually entering the password.
  3. Users only need to authenticate once; after that, they can freely browse the site.
  4. The password mechanism embedded in the link must not leak the password.

To solve this, I wanted a lightweight solution that wouldn't complicate the static nature of the site or impact its performance. My first thought was to leverage NGINX, the middleware I use to serve static assets quickly. While I’m fairly new to NGINX, I had a hunch it could handle this setup.

High-Level Solution

Here's the approach I developed:

  1. Links to the site include a query parameter that contains the password.
  2. The server detects the query parameter, sets a cookie with the password, and redirects to the same URL without the query parameter.
  3. If the server detects the correct password cookie, it serves the content. Otherwise, it returns a 403 Forbidden error.

This solution turned out to be fairly clean to implement using NGINX. Below is the configuration I ended up with:

server {
  set $password '<password>';

  location / {
    # Handle query parameter authentication
    if ($arg_pw = $password) {
      add_header Set-Cookie "pw=$arg_pw; Path=/; HttpOnly; Secure; SameSite=Lax";
      add_header Cache-Control "no-cache, no-store";
      return 302 $scheme://$host$uri;
    }

    # Check for the password cookie
    if ($cookie_pw != $password) {
      add_header Cache-Control "no-cache, no-store";
      return 403;
    }

    # Serve static files
    try_files $uri $uri/ =404;
  }
}

Debugging Challenges

Here are a couple of issues I ran into during implementation:

1. Caching

Browsers sometimes cache the resolved target of a redirect, ignoring its side effects (e.g., setting a cookie). To prevent this, I explicitly set Cache-Control headers to disable caching during the redirect and the 403 response.

2. Cookie SameSite Policy

Initially, I used SameSite=Strict, but this caused issues when navigating from external sites (e.g., Discord links). Switching to SameSite=Lax resolved this, allowing the password cookie to be sent in such cases.

Final Thoughts

I'm really happy with how this turned out! It satisfies all the constraints without adding unnecessary complexity or server dependencies. For a basic password authentication mechanism, this lightweight solution works really well.

© 2025 Eugene Che
Built with ❤️.