Skip to content

Yundera/Nginx-hash-lock

Repository files navigation

NGINX Hash Lock

A lightweight NGINX-based authentication proxy that protects web applications using hash-based authentication and username/password authentication.

What It Does

NGINX Hash Lock sits in front of your application and provides flexible authentication options:

  1. Hash Authentication: Block requests unless they include ?hash=YOUR_SECRET_HASH
  2. Username/Password Authentication: Show a login page requiring credentials
  3. Both Methods: Accept either hash parameter OR valid login session
  4. No Authentication: Optionally disable security entirely

Quick Start

Environment Variables

Core Settings:

environment:
  BACKEND_HOST: "your-app"               # Required: Backend service hostname
  BACKEND_PORT: "8080"                   # Required: Backend service port
  LISTEN_PORT: "80"                      # Required: Port NGINX listens on (80 recommended for clean subdomains)

Authentication Options:

  # Hash-based authentication (automatically provided by CasaOS)
  AUTH_HASH: $AUTH_HASH                  # Optional: For hash-based auth (CasaOS provides this)
                                         # Important: Also add /?hash=$AUTH_HASH to x-casaos.index

  # Username/Password authentication
  USER: "admin"                          # Optional: Username for login page
  PASSWORD: "your-secure-password"       # Optional: Password for login page
  SESSION_DURATION_HOURS: "720"          # Optional: Session duration in hours (default: 720 = 30 days)

Bypass Options:

  ALLOWED_EXTENSIONS: "js,css,png,ico"   # Optional: Allow static files without auth
  ALLOWED_PATHS: "login,api/health"      # Optional: Allow specific paths without auth
  ALLOW_HASH_CONTENT_PATHS: "true"       # Optional: Allow /[40-hex-char]/* paths without auth (for Stremio, etc.)

Proxy Behavior (Advanced):

  # These have sensible defaults - only override if needed
  PROXY_BUFFERING: "off"                 # Default: off. Use "on" for caching/rate-limiting support
  PROXY_REQUEST_BUFFERING: "off"         # Default: off. Use "on" if backend needs full body before processing
  PROXY_CONNECT_TIMEOUT: "300s"          # Default: 300s. Time to establish backend connection
  PROXY_SEND_TIMEOUT: "300s"             # Default: 300s. Timeout between write operations to backend
  PROXY_READ_TIMEOUT: "300s"             # Default: 300s. Timeout between read operations from backend
  CLIENT_MAX_BODY_SIZE: "0"              # Default: 0 (unlimited). Use "10G" or "100M" to limit uploads

Authentication Modes

The system automatically selects the authentication mode based on which environment variables are configured:

AUTH_HASH USER/PASSWORD Mode Behavior
✅ Defined ❌ Undefined Hash Only Require ?hash= parameter, show 403 page on failure
❌ Undefined ✅ Defined Credentials Only Show login page, require username/password, no hash option
✅ Defined ✅ Defined Both Methods Accept either hash parameter OR valid login session
❌ Undefined ❌ Undefined No Authentication Allow all requests (security disabled)

Important for CasaOS Deployments

$AUTH_HASH is automatically provided by Yundera's CasaOS - you don't need to manually configure it. However, you must:

  1. Include $AUTH_HASH in the environment (CasaOS will populate it)
  2. Add ?hash=$AUTH_HASH to the index in x-casaos metadata

Example:

environment:
  AUTH_HASH: $AUTH_HASH  # CasaOS provides this automatically

x-casaos:
  index: /?hash=$AUTH_HASH  # Important! Pass hash to URL

This ensures the Dashboard button automatically includes the authentication hash.

Critical: Container Naming for Subdomain Routing

The NGINX Hash Lock container MUST have the same name as the app. The mesh-router routes subdomains based on container name matching the app name in docker-compose.yml.

Correct Setup:

name: myapp                          # App name

services:
  myapp:                             # ← Service name matches app name
    image: ghcr.io/yundera/nginx-hash-lock:latest
    container_name: myapp            # ← Container name matches app name
    environment:
      BACKEND_HOST: "myapp-backend"  # ← Points to backend
      ...

  myapp-backend:                     # ← Backend has different name
    image: your-actual-app:latest
    container_name: myapp-backend

x-casaos:
  main: myapp                        # ← Main service is the nginx proxy

Why this matters:

  • Subdomain myapp-username.nsl.sh routes to container named myapp
  • If the backend has the app name, traffic bypasses NGINX Hash Lock entirely
  • The nginx proxy must "claim" the app name for proper routing

Docker Compose Examples

Example 1: Hash-Only Authentication (CasaOS)

services:
  hashlock:
    image: ghcr.io/yundera/nginx-hash-lock:latest
    environment:
      AUTH_HASH: $AUTH_HASH            # CasaOS provides this
      BACKEND_HOST: "myapp"
      BACKEND_PORT: "8080"
      LISTEN_PORT: "80"
    expose:
      - 80
    depends_on:
      - myapp

  myapp:
    image: your-app:latest

x-casaos:
  main: hashlock
  index: /?hash=$AUTH_HASH             # IMPORTANT: Include hash in URL
  webui_port: 80

CasaOS Dashboard button: Automatically opens with authentication hash

Example 2: Username/Password Only (CasaOS)

services:
  hashlock:
    image: ghcr.io/yundera/nginx-hash-lock:latest
    environment:
      USER: $USER                      # Set in CasaOS or compose
      PASSWORD: $PASSWORD              # Set in CasaOS or compose
      SESSION_DURATION_HOURS: "168"    # 1 week
      BACKEND_HOST: "myapp"
      BACKEND_PORT: "8080"
      LISTEN_PORT: "80"
    expose:
      - 80
    depends_on:
      - myapp

  myapp:
    image: your-app:latest

x-casaos:
  main: hashlock
  index: /                             # No hash needed - shows login page
  webui_port: 80

CasaOS Dashboard button: Opens login page → Enter credentials → 1-week session

Example 3: Both Methods (Hash OR Login) - CasaOS

services:
  hashlock:
    image: ghcr.io/yundera/nginx-hash-lock:latest
    environment:
      AUTH_HASH: $AUTH_HASH            # Option 1: CasaOS hash
      USER: $USER                      # Option 2: Password auth
      PASSWORD: $PASSWORD
      SESSION_DURATION_HOURS: "720"    # 30 days
      BACKEND_HOST: "myapp"
      BACKEND_PORT: "8080"
      LISTEN_PORT: "80"
    expose:
      - 80
    depends_on:
      - myapp

  myapp:
    image: your-app:latest

x-casaos:
  main: hashlock
  index: /?hash=$AUTH_HASH             # Dashboard uses hash (quick access)
  webui_port: 80

CasaOS Dashboard button: Opens with hash (quick access) Alternative: Visit without hash → Login page → Enter credentials → 30-day session

How It Works

Hash Authentication Mode

  1. With correct hash: https://yourapp.example.com/?hash=my-secret-123 → Access granted
  2. Without hash: Returns 403 Forbidden with custom error page

Username/Password Mode

  1. First visit: Shows login page
  2. Enter credentials: Username and password validated (2-second delay on failure for anti-brute-force)
  3. Session created: Secure cookie with configurable expiration (default: 30 days)
  4. Subsequent visits: Automatic access with valid session cookie

Both Methods Mode

  1. With hash parameter: Instant access (no login required)
  2. With valid session: Access granted
  3. Without either: Redirected to login page

Optional Features

Allow Static Assets (No Hash Required)

Useful for CSS, JavaScript, images:

ALLOWED_EXTENSIONS: "js,css,png,ico,svg,woff,woff2"

Now /styles/app.css works without a hash, but /admin still requires it.

Allow Public Paths (No Hash Required)

Useful for login pages or public APIs:

ALLOWED_PATHS: "login,about,api/health,api/public"

Now /login and /api/health work without a hash, but /dashboard still requires it.

Important - Reserved Paths:

  • /nhl-auth/ is reserved for internal authentication endpoints and cannot be used in ALLOWED_PATHS
  • /login is reserved for the login page
  • All other paths are available for use in ALLOWED_PATHS
  • The /auth path is now available for your application (previously reserved)

Hash Content Paths (For Stremio and Media Servers)

Some applications like Stremio use 40-character hexadecimal paths for content:

  • /8187fed409fc90636a87a44b706ade4865e83bc9/video.mp4
  • /bca2d44dcd7655ecfdffe81659a569d3525f0195/0

These paths are dynamically generated and the hash itself acts as the access token. To allow these paths without requiring additional authentication:

environment:
  ALLOW_HASH_CONTENT_PATHS: "true"

Security Model

  • Main site (/, /settings, etc.) → Requires login or ?hash=AUTH_HASH
  • Content paths (/[40-hex-chars]/*) → Accessible if you know the content hash

This is similar to how signed URLs work on cloud storage services - the hash IS the authentication for that specific content.

Example: Stremio with Hash Content Paths

services:
  stremio:
    image: ghcr.io/yundera/nginx-hash-lock:latest
    environment:
      AUTH_HASH: $AUTH_HASH
      USER: "admin"
      PASSWORD: "stremio"
      BACKEND_HOST: "stremiocommunity"
      BACKEND_PORT: "8080"
      LISTEN_PORT: "80"
      ALLOW_HASH_CONTENT_PATHS: "true"  # Required for video streaming
    expose:
      - 80

  stremiocommunity:
    image: tsaridas/stremio-docker:latest

Real-World Examples: Protected Terminal (CasaOS)

Hash-Only Authentication (safe-terminal-app-nginxhashlock.yml)

Quick access via URL hash parameter - Dashboard button includes hash automatically:

services:
  yunderaterminal:
    image: ghcr.io/yundera/nginx-hash-lock:latest
    environment:
      AUTH_HASH: $AUTH_HASH            # CasaOS provides this
      BACKEND_HOST: "ttyd"
      BACKEND_PORT: "7681"
      LISTEN_PORT: "80"
    expose:
      - 80
    depends_on:
      - ttyd

  ttyd:
    image: tsl0922/ttyd:latest
    command: ["ttyd", "--writable", "chroot", "/host", "bash"]

x-casaos:
  main: yunderaterminal
  index: /?hash=$AUTH_HASH             # IMPORTANT: Pass hash to URL
  webui_port: 80

CasaOS Dashboard: Automatically opens with hash → Instant access

Password Authentication (safe-terminal-app-nginxhashpass.yml)

Session-based login with username/password:

services:
  yunderaterminalpass:
    image: ghcr.io/yundera/nginx-hash-lock:latest
    environment:
      USER: $USER                      # Set in CasaOS
      PASSWORD: $PASSWORD              # Set in CasaOS
      SESSION_DURATION_HOURS: "720"    # 30 days
      BACKEND_HOST: "ttydpass"
      BACKEND_PORT: "7681"
      LISTEN_PORT: "80"
    expose:
      - 80
    depends_on:
      - ttydpass

  ttydpass:
    image: tsl0922/ttyd:latest
    command: ["ttyd", "--writable", "chroot", "/host", "bash"]

x-casaos:
  main: yunderaterminalpass
  index: /                             # No hash - show login page
  webui_port: 80

CasaOS Dashboard: Opens login page → Enter credentials → 30-day session

Dual Authentication (safe-terminal-app-nginxhashboth.yml)

Accept BOTH hash OR password for maximum flexibility:

services:
  yunderaterminalboth:
    image: ghcr.io/yundera/nginx-hash-lock:latest
    environment:
      AUTH_HASH: $AUTH_HASH            # Option 1: CasaOS hash (Dashboard)
      USER: $USER                      # Option 2: Login page
      PASSWORD: $PASSWORD
      SESSION_DURATION_HOURS: "168"    # 1 week
      BACKEND_HOST: "ttydboth"
      BACKEND_PORT: "7681"
      LISTEN_PORT: "80"
    expose:
      - 80
    depends_on:
      - ttydboth

  ttydboth:
    image: tsl0922/ttyd:latest
    command: ["ttyd", "--writable", "chroot", "/host", "bash"]

x-casaos:
  main: yunderaterminalboth
  index: /?hash=$AUTH_HASH             # Dashboard uses hash for quick access
  webui_port: 80

CasaOS Dashboard: Opens with hash → Instant access Alternative: Visit without hash parameter → Login page → 1-week session

Security Features

Username/Password Authentication

  • Session-based authentication: Secure httpOnly cookies prevent XSS attacks
  • Anti-brute-force protection: 2-second delay on failed login attempts
  • Configurable session duration: Set SESSION_DURATION_HOURS to control session lifetime
  • Automatic session cleanup: Expired sessions are automatically removed from memory

Hash Authentication

  • URL parameter validation: Simple and effective for trusted environments
  • No server-side state: Stateless authentication

Security Notes

  • Hash is visible in URLs: This is simple authentication, not encryption. Use HTTPS in production.
  • Use HTTPS in production: Prevents hash and cookie exposure over network
  • Strong passwords: Use strong passwords for username/password authentication
  • Session security: Sessions are stored in memory and cleared on container restart
  • Rotate credentials: Change AUTH_HASH or PASSWORD if compromised
  • Not a replacement for OAuth/SAML: Use for simple cases or as an additional protection layer

Files

Core Files

  • Dockerfile - Debian NGINX container with Node.js
  • nginx.conf - NGINX configuration template with auth_request support
  • entrypoint.sh - Configures authentication mode and starts services
  • 403.html - Custom error page for hash authentication failures
  • login.html - Login page for username/password authentication

Authentication Service

  • auth-service/app.js - Express.js authentication service
  • auth-service/package.json - Node.js dependencies

Configuration Details

The entrypoint script automatically:

  1. Determines authentication mode based on environment variables
  2. Starts the Node.js auth service if credentials are configured
  3. Configures hash content paths bypass if ALLOW_HASH_CONTENT_PATHS=true
  4. Generates appropriate NGINX configuration for the selected auth mode
  5. Configures optional allowed paths/extensions
  6. Starts NGINX with the generated configuration

No manual configuration needed - just set environment variables and run.

Services Started Based on Configuration

Configuration Auth Service (port 9999) NGINX
Hash only
Credentials only
Both methods
No authentication

Technical Architecture

Authentication Flow

Hash-Only Mode:

Request → NGINX auth_request to auth service → Check session cookie
  ├─ Valid session → Backend
  └─ No/invalid session → Check ?hash parameter
      ├─ Valid hash → Create session cookie → Backend
      └─ Invalid/missing → Return 403 Forbidden

Credentials-Only Mode:

Request → NGINX auth_request to auth service → Check session cookie
  ├─ Valid session → Backend
  └─ No/invalid session → Redirect to /login → Validate credentials → Set cookie → Backend

Both Methods Mode:

Request → NGINX auth_request to auth service → Check session cookie
  ├─ Valid session → Backend
  └─ No/invalid session → Check ?hash parameter
      ├─ Valid hash → Backend
      └─ Invalid/missing → Redirect to /login

Hash Content Paths Mode (when ALLOW_HASH_CONTENT_PATHS=true):

Request matching /[40-hex-chars]/* pattern → Direct proxy to backend (no auth)
Other requests → Normal authentication flow

Session Management

  • Sessions stored in-memory (Node.js auth service)
  • Automatic cleanup of expired sessions every hour
  • Session IDs are cryptographically secure (32 random bytes)
  • Sessions survive nginx reload but not container restart

Application Compatibility

NGINX Hash Lock has been tested with:

  • Stremio - Media streaming (use ALLOW_HASH_CONTENT_PATHS=true)
  • Jellyfin/Emby - Media servers with transcoding
  • Plex - Media server with remote access
  • qBittorrent - Download manager
  • Transmission - Torrent client
  • File browsers - Filebrowser, FileShelter
  • Code servers - VS Code Server, code-server
  • Terminal apps - ttyd, wetty, gotty

The ALLOW_HASH_CONTENT_PATHS feature is useful for:

  • Media servers that use 40-character hex paths for content
  • Applications where the content hash acts as an access token
  • Stremio and similar streaming applications

Supported Features

Feature Status Notes
Standard HTTP/1.1 apps Fully supported
WebSocket connections Automatic detection and upgrade
Video/audio streaming Buffering disabled by default
Large file uploads Unlimited by default
Server-Sent Events (SSE) Proper headers configured
Long-polling requests 5-minute timeouts
REST APIs All methods supported

Known Limitations

Feature Status Reason
gRPC Requires grpc_pass directive and HTTP/2 - fundamentally different from HTTP proxying
HTTP/2 to backend Uses HTTP/1.1 for backend connections (sufficient for 99% of apps)
Headers with underscores ⚠️ Ignored by default (nginx default behavior)

Note on gRPC: Applications using gRPC (some CI/CD tools, Kubernetes services) cannot be proxied through NGINX Hash Lock. gRPC requires a completely different nginx configuration using grpc_pass instead of proxy_pass.

Proxy Behavior Configuration

NGINX Hash Lock is designed to work with any application out of the box. The defaults prioritize compatibility over performance.

Environment Variables Reference

Variable Default Description
PROXY_BUFFERING off Response buffering. off = streaming-friendly, on = better for caching
PROXY_REQUEST_BUFFERING off Request buffering. off = large uploads work, on = backend gets full body first
PROXY_CONNECT_TIMEOUT 300s Time allowed to establish connection with backend
PROXY_SEND_TIMEOUT 300s Timeout between successive write operations to backend
PROXY_READ_TIMEOUT 300s Timeout between successive read operations from backend
CLIENT_MAX_BODY_SIZE 0 Maximum upload size. 0 = unlimited, or use 10G, 100M, etc.

When to Override Defaults

Most apps need no configuration - the defaults handle:

  • Video/audio streaming (Stremio, Jellyfin, Plex)
  • Large file uploads (ConvertX, file managers)
  • WebSocket connections (terminals, real-time apps)
  • Server-Sent Events (SSE)
  • Long-polling requests

Override only if:

Scenario Setting
Need nginx-level caching PROXY_BUFFERING=on
Need nginx rate-limiting PROXY_BUFFERING=on
Backend requires full request before processing PROXY_REQUEST_BUFFERING=on
Want to limit upload sizes CLIENT_MAX_BODY_SIZE=10G
Very long operations (>5 min) PROXY_READ_TIMEOUT=3600s

What's Fixed Automatically

These issues are handled without configuration:

Feature Implementation
WebSocket support Correct Connection header via nginx map directive
Forwarded headers X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port
SSE support X-Accel-Buffering: no header
Backend redirects Proper redirect rewriting

Building & Publishing

The Docker image is automatically built and published to GitHub Container Registry via GitHub Actions on every push to main.

Image location: ghcr.io/yundera/nginx-hash-lock:latest

For manual builds (development only):

docker build -t krizcold/nginxhashlock:dev .
docker push krizcold/nginxhashlock:dev

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors