stempler/hello-ip

A simple web application for authenticating users and maintaining an IP whitelist.

★ 0Forks 0PythonGitHub ↗Compare

README

hello-ip

A simple web application for authenticating users and maintaining an IP whitelist with configurable expiration and maximum entry limits.

Features

  • Web-based authentication interface
  • Persistent IP whitelist storage with expiration
  • Internal endpoint for retrieving whitelist as text file
  • Integration with Bunkerweb API (intended to be used for updating greylist)
  • Docker containerization with environment variable configuration
  • Support for multiple credentials
  • LDAP authentication support (with defaults targeting LLDAP)

AI disclaimer

Yes, this was vibe-coded. But it was also carefully reviewed and designed for minimum complexity.

Quick Start

Using Docker

You can either build the image locally or use the pre-built image from GitHub Container Registry.

Option 1: Use Pre-built Image from GHCR

docker pull ghcr.io/stempler/hello-ip:latest

Option 2: Build Locally

  1. Build the Docker image:
docker build -t hello-ip .
  1. Generate password hashes using the helper script:
python hash_password.py secret123 admin
python hash_password.py password456 user1
  1. Run the container with hashed credentials:
docker run -d \
  -p 8080:8080 \
  -p 8081:8081 \
  -v whitelist-data:/data \
  -e CREDENTIALS='{"admin": "pbkdf2:sha256:...", "user1": "pbkdf2:sha256:..."}' \
  -e BASE_PATH="/" \
  -e ENTRY_VALIDITY_HOURS=24 \
  -e MAX_ENTRIES=1000 \
  --name hello-ip \
  hello-ip
  1. Access the web interface at http://localhost:8080
  2. Retrieve the whitelist at http://localhost:8081/whitelist.txt

Environment Variables

Variable Description Default
BASE_PATH Base URL path for the application /
MAIN_PORT Port for main web interface 8080
INTERNAL_PORT Port for whitelist endpoint 8081
ENTRY_VALIDITY_HOURS Hours until whitelist entry expires 24
MAX_ENTRIES Maximum number of whitelist entries 1000
DATABASE_PATH SQLite database file path /data/whitelist.db
CREDENTIALS JSON string of credential IDs to password hashes {}
GUNICORN_WORKERS Number of gunicorn worker processes for main app 4
GUNICORN_THREADS Number of threads per worker 2
BUNKERWEB_ENABLED Enable BunkerWeb API integration false
BUNKERWEB_API_URL Base URL for BunkerWeb API ``
BUNKERWEB_USERNAME BunkerWeb API username for basic auth ``
BUNKERWEB_PASSWORD BunkerWeb API password for basic auth ``
BUNKERWEB_JOB_PLUGIN Job plugin name to trigger greylist
BUNKERWEB_JOB_NAME Job name to trigger greylist-download
BUNKERWEB_UNBAN_ENABLED Automatically unban whitelisted IPs in BunkerWeb false
LDAP_ENABLED Enable LDAP authentication false
LDAP_SERVER LDAP server URL ldap://localhost:3890
LDAP_BASE_DN Base DN for user searches dc=example,dc=com
LDAP_BIND_DN Service account DN for binding uid=admin,ou=people,dc=example,dc=com
LDAP_BIND_PASSWORD Service account password ``
LDAP_USER_DN_TEMPLATE Template for user DN uid={},ou=people,{}
LDAP_USER_FILTER Filter to find users (&(objectClass=person)(uid={}))
LDAP_USE_TLS Use STARTTLS for connection false
LDAP_FALLBACK_LOCAL Fall back to local credentials if LDAP fails true
LDAP_ALLOWED_GROUP Optional group name or DN. If set, only users in this group can authenticate (empty)
LDAP_GROUP_DN_TEMPLATE Template for constructing group DN from group name cn={},ou=groups,{}
LDAP_GROUP_OBJECT_CLASS LDAP objectClass for groups (e.g., groupOfNames, group, posixGroup) groupOfNames
LOG_LEVEL Python logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) INFO

Credentials Format

The CREDENTIALS environment variable should be a JSON object mapping credential IDs to password hashes (not plain text passwords). Use the hash_password.py helper script to generate hashes.

Important: Never store plain text passwords in the CREDENTIALS environment variable. Always use password hashes generated by the helper script.

Generating Password Hashes

Use the hash_password.py script to generate password hashes:

# Generate a hash for a single password
python hash_password.py mypassword

# Generate a JSON object ready for CREDENTIALS env var
python hash_password.py mypassword admin
# Output: {"admin": "pbkdf2:sha256:600000$..."}

# Combine multiple credentials
python hash_password.py secret123 admin
python hash_password.py password456 user1
# Then combine the outputs into a single JSON object:
# {"admin": "pbkdf2:sha256:...", "user1": "pbkdf2:sha256:..."}

Example CREDENTIALS environment variable:

{"admin": "pbkdf2:sha256:600000$...", "user1": "pbkdf2:sha256:600000$..."}

API Endpoints

Main Application (Port 8080)

  • GET / - Authentication web interface
  • POST /auth - Authenticate and add IP to whitelist
    • Request body: {"credential_id": "admin", "password": "secret123"}
    • Returns 200 on success, 403 on authentication failure
  • GET /health - Health check endpoint

Internal Endpoint (Port 8081)

  • GET /whitelist.txt - Returns text file with valid IP addresses (one per line)
  • GET /whitelist.json - Returns JSON with all whitelist entry information
    • Query parameter ?all=true - Include expired entries (default: only valid entries)
    • Response format:
      {
        "count": 5,
        "valid_only": true,
        "entries": [
          {
            "ip": "192.168.1.1",
            "credential_id": "admin",
            "auth_time": "2024-01-15T10:30:00",
            "expires_at": "2024-01-16T10:30:00",
            "is_valid": true,
            "remaining_seconds": 86400
          }
        ]
      }

Development

Local Development with mise-en-place

This project uses mise-en-place (formerly rtx) for tool version management with automatic virtualenv activation.

  1. Install mise-en-place if you haven't already:
# Follow installation instructions at https://mise.jdx.dev/getting-started.html
  1. Install the required tools (this will install Python 3.11):
mise install
  1. Activate the mise environment (add to your shell config for automatic activation):
eval "$(mise activate)"
  1. Navigate to the project directory - mise will automatically:

    • Use Python 3.11
    • Create the .venv virtual environment if it doesn't exist
    • Activate the virtual environment automatically
  2. Install dependencies:

pip install --upgrade pip
pip install -r requirements.txt
  1. Generate password hashes and set environment variables:
# Generate password hash
python hash_password.py secret123 admin
# Output: {"admin": "pbkdf2:sha256:..."}

# Set environment variables (use the hash from above)
export CREDENTIALS='{"admin": "pbkdf2:sha256:..."}'
export DATABASE_PATH="./whitelist.db"
  1. Run the application:
python app.py

Note: The .mise.toml file configures Python 3.11 and uses automatic virtualenv activation. When you're in the project directory with mise activated, the virtual environment will be automatically created (if needed) and activated. No manual python -m venv or source .venv/bin/activate needed!

Local Development (without mise-en-place)

If you prefer not to use mise-en-place, ensure you have Python 3.11+ installed and follow steps 4-7 above.

Production Deployment

The Docker image uses gunicorn as a production WSGI server instead of Flask's development server. The application runs with:

  • Main app (port 8080): Production WSGI server with configurable workers
  • Internal app (port 8081): Lightweight server for internal whitelist endpoint

You can configure gunicorn workers and threads via environment variables:

-e GUNICORN_WORKERS=4 \
-e GUNICORN_THREADS=2

For local development, you can still use python app.py which uses Flask's development server (with the warning).

Security Considerations

  • Passwords are hashed using Werkzeug's secure password hashing
  • Credentials are stored only in environment variables (not in the database)
  • SQL injection prevention via parameterized queries
  • Separate ports for internal vs public access
  • Input validation for IP addresses
  • Non-root user in Docker container
  • Production WSGI server (gunicorn) in Docker image

Database Schema

The application uses SQLite with one table:

  • whitelist_entries: Stores IP addresses, credential IDs, authentication time, and expiration time

Note: Credentials are not stored in the database. They are configured via the CREDENTIALS environment variable and verified at runtime.

LDAP Authentication

The application supports LDAP as an alternative authentication backend. When enabled, users can authenticate against an LDAP server instead of (or in addition to) local credentials.

LDAP Configuration

Default values are configured to work with LLDAP (Light LDAP).

Variable Default Description
LDAP_ENABLED false Enable LDAP authentication
LDAP_SERVER ldap://localhost:3890 LDAP server URL (LLDAP default port is 3890)
LDAP_BASE_DN dc=example,dc=com Base DN for user searches
LDAP_BIND_DN uid=admin,ou=people,dc=example,dc=com Service account DN for initial binding
LDAP_BIND_PASSWORD (empty) Service account password
LDAP_USER_DN_TEMPLATE uid={},ou=people,{} Template for constructing user DN (placeholders: username, base_dn)
LDAP_USER_FILTER (&(objectClass=person)(uid={})) LDAP filter to find users (placeholder: username)
LDAP_USE_TLS false Use STARTTLS for secure connection
LDAP_FALLBACK_LOCAL true Fall back to local credentials if LDAP auth fails
LDAP_ALLOWED_GROUP (empty) Optional group name or DN. If set, only users in this group can authenticate
LDAP_GROUP_DN_TEMPLATE cn={},ou=groups,{} Template for constructing group DN from group name (placeholders: group_name, base_dn)

Group-Based Access Control

When LDAP_ALLOWED_GROUP is configured, authentication is restricted to users who are members of the specified LDAP group. This provides an additional layer of access control on top of password authentication.

Requirements:

  • LDAP_BIND_DN and LDAP_BIND_PASSWORD must be configured (service account is required for group membership checks)
  • The group must exist in your LDAP directory

Group Format:

  • Group name (CN): whitelist-users → will construct DN as cn=whitelist-users,ou=groups,{base_dn} using LDAP_GROUP_DN_TEMPLATE
  • Full DN: cn=whitelist-users,ou=groups,dc=example,dc=com → used as-is

How it works:

  1. The service account binds and searches for the user to obtain their DN
  2. The application then searches for the allowed group and checks if the user's DN is in the group's member attribute
  3. If the user is not in the group, authentication is denied immediately (fail-fast)
  4. If the user is in the group, password authentication proceeds by verifying the user's password

Note: Group membership is checked after finding the user's DN but before password verification. This provides efficient fail-fast behavior: users not in the allowed group are rejected without attempting password verification.

Authentication Flow

  1. If LDAP_ENABLED=true:
    • Service account binds and searches for the user's DN
    • If LDAP_ALLOWED_GROUP is configured, check if the user is a member of the allowed group
    • If user is not in the allowed group, authentication fails immediately
    • If user is in the allowed group (or no group restriction), proceed to password verification by binding as the user
  2. If LDAP authentication fails and LDAP_FALLBACK_LOCAL=true, it tries local credentials
  3. If LDAP_ENABLED=false, only local credentials are used

Example: LLDAP Configuration with Group Access Control

docker run -d \
  -e LDAP_ENABLED=true \
  -e LDAP_SERVER=ldap://lldap:3890 \
  -e LDAP_BASE_DN=dc=example,dc=com \
  -e LDAP_BIND_DN=uid=admin,ou=people,dc=example,dc=com \
  -e LDAP_BIND_PASSWORD=admin_password \
  -e LDAP_ALLOWED_GROUP=whitelist-users \
  -e LDAP_GROUP_DN_TEMPLATE=cn={},ou=groups,{} \
  ...

Note: When using LDAP_ALLOWED_GROUP, you must also configure LDAP_BIND_DN and LDAP_BIND_PASSWORD as the service account is required to check group membership.

Example: Basic LLDAP Configuration

docker run -d \
  -p 8080:8080 \
  -p 8081:8081 \
  -v whitelist-data:/data \
  -e LDAP_ENABLED=true \
  -e LDAP_SERVER="ldap://lldap:3890" \
  -e LDAP_BASE_DN="dc=example,dc=com" \
  -e LDAP_BIND_DN="uid=admin,ou=people,dc=example,dc=com" \
  -e LDAP_BIND_PASSWORD="admin_password" \
  -e LDAP_FALLBACK_LOCAL=false \
  --name hello-ip \
  hello-ip

Notes

  • The LDAP_USER_DN_TEMPLATE uses {} as placeholders: the first {} is replaced with the username, the second with LDAP_BASE_DN
  • If LDAP_BIND_DN and LDAP_BIND_PASSWORD are provided, the application will use them to search for users; otherwise, it constructs the user DN directly from the template
  • LLDAP uses port 3890 by default (not the standard LDAP port 389)
  • For LDAPS (LDAP over SSL), use ldaps:// in the server URL instead of enabling LDAP_USE_TLS

BunkerWeb Integration

The application supports optional integration with the BunkerWeb API to automatically trigger jobs when the whitelist changes.

Configuration

To enable BunkerWeb integration, set the following environment variables:

-e BUNKERWEB_ENABLED=true \
-e BUNKERWEB_API_URL="http://bunkerweb:8888" \
-e BUNKERWEB_USERNAME="api_user" \
-e BUNKERWEB_PASSWORD="api_password" \
-e BUNKERWEB_JOB_PLUGIN="greylist" \
-e BUNKERWEB_JOB_NAME="greylist-download" \
-e BUNKERWEB_UNBAN_ENABLED=true

Optional Configuration:

  • BUNKERWEB_UNBAN_ENABLED: When set to true, automatically unban the whitelisted IP address in BunkerWeb after triggering the job. This ensures newly whitelisted IPs are immediately unbanned. Default: false.

How It Works

  1. Authentication: When a whitelist entry is added, the application authenticates with the BunkerWeb API using basic authentication (username/password) at the /auth endpoint.

  2. Token Management: The authentication token is cached and reused for subsequent API calls. The token is refreshed automatically when it expires.

  3. Cache Clearing: Before triggering the job, the application clears relevant cache files to ensure the job uses fresh data.

  4. Job Triggering: After clearing cache, the application triggers the configured job by sending a POST request to /jobs/run with the following payload:

    {
      "jobs": [
        {
          "plugin": "greylist",
          "name": "greylist-download"
        }
      ]
    }
  5. IP Unbanning (Optional): If BUNKERWEB_UNBAN_ENABLED is set to true, after triggering the job, the application will automatically unban the whitelisted IP address by sending a DELETE request to /bans. This ensures that newly whitelisted IPs are immediately unbanned in BunkerWeb.

  6. Concurrency Handling: All BunkerWeb API calls are serialized using a lock mechanism to prevent conflicts when multiple whitelist updates occur simultaneously.

  7. Error Handling: BunkerWeb integration failures are logged but do not affect whitelist operations. The application continues to function normally even if BunkerWeb is unavailable.

Example Docker Run with BunkerWeb

docker run -d \
  -p 8080:8080 \
  -p 8081:8081 \
  -v whitelist-data:/data \
  -e CREDENTIALS='{"admin": "pbkdf2:sha256:..."}' \
  -e BUNKERWEB_ENABLED=true \
  -e BUNKERWEB_API_URL="http://bunkerweb:8080" \
  -e BUNKERWEB_USERNAME="bunkerweb_user" \
  -e BUNKERWEB_PASSWORD="bunkerweb_pass" \
  -e BUNKERWEB_JOB_PLUGIN="greylist" \
  -e BUNKERWEB_JOB_NAME="greylist-download" \
  -e BUNKERWEB_UNBAN_ENABLED=true \
  --name hello-ip \
  hello-ip

License

MIT

Contributors

stempler

Issues