Browse Source

change authentication
improve reliability

zehe 3 tháng trước cách đây
mục cha
commit
20f56c405b
7 tập tin đã thay đổi với 384 bổ sung141 xóa
  1. 125 78
      README.md
  2. 35 3
      config.py
  3. 9 0
      emergency-access.service
  4. 5 5
      install.sh
  5. 147 38
      main.py
  6. 1 0
      requirements.txt
  7. 62 17
      test.py

+ 125 - 78
README.md

@@ -1,35 +1,37 @@
 # Emergency Access Server
 
-A fail-safe webserver that provides secure access to multiple decryption key parts with mandatory notification system integration. Designed for emergency scenarios where key retrieval must be monitored and logged.
+A fail-safe webserver that provides secure access to multiple decryption key parts with HTTP Basic Authentication and mandatory notification system integration. Designed for emergency scenarios where key retrieval must be monitored and logged.
 
 ## Features
 
-- **Multiple key support**: Configure multiple decryption keys with individual routes and notification backends
+- **Multiple key support**: Configure multiple decryption keys with individual authentication and notification backends
+- **HTTP Basic Authentication**: Secure username/password authentication for each key and health endpoint
 - **Fail-safe design**: All operations require successful notification delivery
-- **Dynamic endpoint system**: Emergency key access and health monitoring with configurable routes
+- **Dynamic endpoint system**: Emergency key access and health monitoring with simple, memorable paths
 - **dschep/ntfy integration**: Real-time notifications via multiple backends (Pushover, Pushbullet, Slack, etc.)
 - **Real-time log monitoring**: All application logs automatically sent to notification backends
-- **Configurable security**: Extremely long random endpoint paths (100+ characters) and file locations per key
+- **Password-based security**: PBKDF2-hashed passwords with individual credentials per key
 - **Caddy reverse proxy ready**: Runs on localhost for secure proxy setup
 - **Systemd integration**: Automatic startup and service management
 - **Comprehensive logging**: Detailed audit trail of all operations with live notifications
 
 ## Architecture
 
-The system consists of multiple key endpoints and a health monitoring endpoint:
+The system consists of multiple authenticated key endpoints and a health monitoring endpoint:
 
-1. **Emergency Key Endpoints** (e.g., `/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3`): 
+1. **Emergency Key Endpoints** (e.g., `/emergency-key-backup`, `/emergency-key-master`): 
    - Each serves a specific decryption key part
-   - Extremely long paths (100+ characters) to prevent accidental access
+   - Protected by HTTP Basic Authentication with unique credentials per key
    - Individual notification backends per key
    - Custom messages per key type
-   - Fails closed if notifications cannot be sent
+   - Fails closed if authentication or notifications fail
    - Supports unlimited number of keys
 
-2. **Health Check Endpoint** (`/health-check-f8d9e2a1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8`):
+2. **Health Check Endpoint** (`/health-check`):
+   - Protected by HTTP Basic Authentication with dedicated credentials
    - Verifies all system components required for emergency access
    - Tests both health and all key notification backends
-   - Validates file system access for dummy and all key files
+   - Validates file system access to dummy and all key files
    - Ensures complete emergency system readiness for all configured keys
    - Used for regular system verification
 
@@ -116,33 +118,43 @@ Configuration structure:
     "port": 1127
   },
   "routes": {
-    "health_route": "/health-check-f8d9e2a1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8"
+    "health_route": "/health-check",
+    "health_username": "health_monitor",
+    "health_password_hash": "salt:hash_generated_by_password_tool"
   },
   "files": {
     "dummy_file": "/etc/emergency-access/dummy.txt"
   },
   "keys": {
     "backup_key": {
-      "route": "/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3",
+      "route": "/emergency-key-backup",
       "file": "/etc/emergency-access/backup-key.txt",
+      "username": "emergency_backup",
+      "password_hash": "salt:hash_generated_by_password_tool",
       "backends": ["matrix_sec", "pushover_emergency"],
       "message": "🚨 EMERGENCY: Backup decryption key accessed from server"
     },
     "master_key": {
-      "route": "/emergency-key-master-x3k8m9n2p5q7r1t4u6v9w2y5z8b1c4d7e0f3g6h9j2k5l8m1n4o7p0q3r6s9t2u5v8w1x4y7z0a3b6c9d2e5f8",
+      "route": "/emergency-key-master",
       "file": "/etc/emergency-access/master-key.txt",
+      "username": "emergency_master",
+      "password_hash": "salt:hash_generated_by_password_tool",
       "backends": ["matrix_sec", "pushover_critical", "slack_emergency"],
       "message": "🚨 CRITICAL: Master decryption key accessed from server"
     },
     "recovery_key": {
-      "route": "/emergency-key-recovery-q5w7r8t1u3i6o9p0a2s4d6f8g0h2j4k7l9z1x3c5v7b9n1m3q6w8e0r2t4y6u8i0o2p4a6s8d0f2g4h6j8k0l2z4x6c8v0",
+      "route": "/emergency-key-recovery",
       "file": "/etc/emergency-access/recovery-key.txt",
+      "username": "emergency_recovery",
+      "password_hash": "salt:hash_generated_by_password_tool",
       "backends": ["matrix_sec", "email_emergency"],
       "message": "🚨 EMERGENCY: Recovery decryption key accessed from server"
     },
     "admin_key": {
-      "route": "/emergency-key-admin-z9x7c5v3b1n8m6k4j2h0g9f7d5s3a1p0o9i8u7y6t5r4e3w2q1z0x9c8v7b6n5m4k3j2h1g0f9d8s7a6p5o4i3u2y1t0r9",
+      "route": "/emergency-key-admin",
       "file": "/etc/emergency-access/admin-key.txt",
+      "username": "emergency_admin",
+      "password_hash": "salt:hash_generated_by_password_tool",
       "backends": [
         "matrix_sec",
         "pushover_critical",
@@ -169,12 +181,16 @@ Configuration structure:
 - `port`: Listen port (default: `1127`)
 
 #### Route Settings
-- `health_route`: Path for health checks - must be 100+ characters long to prevent accidental access
+- `health_route`: Path for health checks (e.g., `/health-check`)
+- `health_username`: Username for health endpoint authentication
+- `health_password_hash`: PBKDF2 password hash for health endpoint
 
 #### Keys Configuration
 Each key in the `keys` object supports:
-- `route`: Unique random path for this specific key - must be 100+ characters long to prevent accidental access
+- `route`: Path for this specific key (e.g., `/emergency-key-backup`)
 - `file`: Path to the key file for this specific key
+- `username`: Username for this key's authentication
+- `password_hash`: PBKDF2 password hash for this key
 - `backends`: List of notification backend names for this specific key
 - `message`: Custom message sent when this specific key is accessed
 
@@ -194,9 +210,21 @@ Each key in the `keys` object supports:
 - `slack_critical`: Slack backend for critical alerts
 - Any backend name configured in `/etc/emergency-access/ntfy.yml`
 
-### Key and Dummy Files
+### Key Files and Authentication Setup
 
-1. **Create key files for each configured key**:
+1. **Generate secure passwords and hashes**:
+   ```bash
+   # Generate passwords for all configured keys
+   python generate_passwords.py --keys backup master recovery admin
+   
+   # Interactive password setup
+   python generate_passwords.py --interactive
+   
+   # Update existing config with new passwords
+   python generate_passwords.py --keys backup master --update-config config.json
+   ```
+
+2. **Create key files for each configured key**:
    ```bash
    # Backup key
    echo "YOUR_BACKUP_KEY_PART_HERE" | sudo tee /etc/emergency-access/backup-key.txt
@@ -219,13 +247,19 @@ Each key in the `keys` object supports:
    sudo chmod 600 /etc/emergency-access/admin-key.txt
    ```
 
-2. **Create dummy file**:
+3. **Create dummy file**:
    ```bash
    echo "system_healthy" | sudo tee /etc/emergency-access/dummy.txt
    sudo chown emergency-access:emergency-access /etc/emergency-access/dummy.txt
    sudo chmod 644 /etc/emergency-access/dummy.txt
    ```
 
+4. **Store credentials securely**:
+   - Save usernames and passwords in a secure password manager
+   - Document which credentials correspond to which keys
+   - Keep emergency access credentials separate from regular system credentials
+   - Consider printing credentials and storing in a physical safe for true emergency scenarios
+
 ## dschep/ntfy Backend Setup
 
 The system uses a dedicated ntfy configuration file at `/etc/emergency-access/ntfy.yml`. Configure your notification backends in this file and reference them by name in the main configuration.
@@ -335,8 +369,8 @@ curl http://localhost:1127/health-check-b8e3f4a2
 ### Emergency Key Access
 
 ```bash
-# Access backup key
-curl https://your-domain.com/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3
+# Access backup key with HTTP Basic Auth
+curl -u emergency_backup:PASSWORD https://your-domain.com/emergency-key-backup
 
 # Expected response:
 {
@@ -347,8 +381,8 @@ curl https://your-domain.com/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7
   "notified_backends": ["matrix_sec", "pushover_emergency"]
 }
 
-# Access master key
-curl https://your-domain.com/emergency-key-master-x3k8m9n2p5q7r1t4u6v9w2y5z8b1c4d7e0f3g6h9j2k5l8m1n4o7p0q3r6s9t2u5v8w1x4y7z0a3b6c9d2e5f8
+# Access master key with HTTP Basic Auth
+curl -u emergency_master:PASSWORD https://your-domain.com/emergency-key-master
 
 # Expected response:
 {
@@ -359,8 +393,8 @@ curl https://your-domain.com/emergency-key-master-x3k8m9n2p5q7r1t4u6v9w2y5z8b1c4
   "notified_backends": ["matrix_sec", "pushover_critical", "slack_emergency"]
 }
 
-# Access admin key
-curl https://your-domain.com/emergency-key-admin-z9x7c5v3b1n8m6k4j2h0g9f7d5s3a1p0o9i8u7y6t5r4e3w2q1z0x9c8v7b6n5m4k3j2h1g0f9d8s7a6p5o4i3u2y1t0r9
+# Access admin key with HTTP Basic Auth
+curl -u emergency_admin:PASSWORD https://your-domain.com/emergency-key-admin
 
 # Expected response:
 {
@@ -370,13 +404,18 @@ curl https://your-domain.com/emergency-key-admin-z9x7c5v3b1n8m6k4j2h0g9f7d5s3a1p
   "timestamp": 1703123456.789,
   "notified_backends": ["matrix_sec", "pushover_critical", "slack_emergency", "email_critical"]
 }
+
+# Authentication failure response:
+{
+  "error": "Invalid credentials"
+}
 ```
 
 ### Health Check
 
 ```bash
-# Regular health monitoring
-curl https://your-domain.com/health-check-f8d9e2a1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8
+# Regular health monitoring with HTTP Basic Auth
+curl -u health_monitor:PASSWORD https://your-domain.com/health-check
 
 # Expected response (all systems operational):
 {
@@ -594,29 +633,30 @@ python test.py
 # Run quick configuration tests only
 python test.py --quick
 
-# Test specific endpoints (replace with your actual paths)
-curl http://localhost:1127/emergency-key-backup-[YOUR_SECURE_PATH]
-curl http://localhost:1127/emergency-key-master-[YOUR_SECURE_PATH]
-curl http://localhost:1127/health-check-[YOUR_SECURE_PATH]
+# Generate passwords for configuration
+python generate_passwords.py --keys backup master recovery
 
-# Generate new configuration with secure paths
-python generate_secure_paths.py --keys 3 --output config
+# Test specific endpoints with authentication
+curl -u emergency_backup:PASSWORD http://localhost:1127/emergency-key-backup
+curl -u emergency_master:PASSWORD http://localhost:1127/emergency-key-master
+curl -u health_monitor:PASSWORD http://localhost:1127/health-check
 ```
 
 ## Multiple Key Implementation Details
 
-### Path Security
+### Authentication Security
 
-#### Path Length Requirements
-- **Minimum**: 100 characters
-- **Recommended**: 120+ characters
-- **Example length**: `/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3` (130 chars)
+#### HTTP Basic Authentication
+- **Username/Password**: Each key and health endpoint has unique credentials
+- **PBKDF2 Hashing**: Passwords are hashed using PBKDF2 with SHA-256 and 100,000 iterations
+- **Salt Protection**: Each password hash includes a unique cryptographic salt
+- **No Plaintext Storage**: Only password hashes are stored in configuration
 
-#### Why Ultra-Long Paths?
-1. **Prevent accidental access**: Impossible to guess or stumble upon
-2. **Reduce false positives**: No accidental emergency notifications
-3. **Security through obscurity**: Additional layer of protection
-4. **URL safety**: Only lowercase letters and numbers
+#### Why HTTP Basic Auth?
+1. **Standard Protocol**: Built into HTTP, supported by all clients
+2. **Simple & Reliable**: No complex token management or infrastructure required
+3. **Emergency Friendly**: Works with basic tools like curl in emergency situations
+4. **Fail-Safe Compatible**: Authentication failure prevents access, maintaining security
 
 ### Dynamic Route Registration
 The application dynamically registers routes for each configured key:
@@ -660,55 +700,59 @@ for key_id, key_config in config.keys.items():
 
 ### Generate Secure Configuration
 
-Use the included path generator for cryptographically secure paths:
+Use the included password generator for secure authentication:
 
 ```bash
-# Generate new configuration with secure paths
-python generate_secure_paths.py --keys 3 --output config
+# Generate passwords for specific keys
+python generate_passwords.py --keys backup master recovery
 
-# Generate complete setup including Caddy config and setup scripts
-python generate_secure_paths.py --keys 4 --domain your-domain.com --output all
+# Interactive password setup
+python generate_passwords.py --interactive
 
-# Generate just secure paths for existing configuration
-python generate_secure_paths.py --keys 2 --output paths
+# Generate and update existing config
+python generate_passwords.py --keys backup master --update-config config.json
+
+# Hash a specific password
+python generate_passwords.py --hash-password "my_secure_password"
 ```
 
-The path generator creates:
-- **Configuration file**: Complete JSON config with secure paths
-- **Caddy configuration**: Reverse proxy setup with path restrictions
-- **Setup script**: Automated key file creation with proper permissions
-- **Security validation**: Ensures all paths meet minimum length requirements
+The password generator creates:
+- **Secure Passwords**: Cryptographically secure random passwords
+- **PBKDF2 Hashes**: Salted password hashes for configuration
+- **Configuration Updates**: Automatically updates config files with hashes
+- **Credential Management**: Saves credentials securely for reference
 
 ### Security Best Practices
 
-#### Path Storage
-- Store paths in secure password manager
-- Document which path corresponds to which key
-- Never store paths in plain text logs
-- Use the path generator for cryptographically secure random paths
+#### Credential Storage
+- Store usernames and passwords in secure password manager
+- Document which credentials correspond to which keys
+- Never store passwords in plain text logs or configuration files
+- Use the password generator for cryptographically secure passwords
 
 #### Access Monitoring
-- Monitor reverse proxy logs for path access
-- Set up alerts for any key access
+- Monitor reverse proxy logs for authentication attempts
+- Set up alerts for failed authentication attempts
+- Set up alerts for any successful key access
 - Regularly audit notification delivery
-- Implement rate limiting to prevent brute force attempts
+- Implement rate limiting to prevent brute force attacks
 
 #### Emergency Procedures
-1. **Key Compromise**: Change the path, not the key content
-2. **False Positive**: Investigate immediately, check logs
-3. **Path Leak**: Generate new paths and update configuration
+1. **Credential Compromise**: Change the password immediately, update configuration
+2. **Failed Authentication**: Investigate immediately, check logs for brute force attempts
+3. **Password Leak**: Generate new passwords and update configuration
 4. **Notification Failure**: Validate all backends before deployment
 
-### Migration Considerations
+### Security Architecture
 
-This implementation removes legacy single-key support for enhanced security:
+This implementation uses HTTP Basic Authentication for robust security:
 
-#### Clean Architecture
-- Removed all legacy single-key configuration support
-- Simplified configuration structure with only `keys` section
-- All paths must be 100+ characters for security
-- Each key requires explicit configuration
-- No backward compatibility to ensure security by default
+#### Authentication-First Design
+- All endpoints require HTTP Basic Authentication
+- Each key has unique username and password
+- Health endpoint has separate authentication credentials
+- Password hashes use PBKDF2 with SHA-256 and strong salts
+- No security through obscurity - proper authentication instead
 
 ## License
 
@@ -721,7 +765,7 @@ For issues and questions:
 2. Verify configuration: `/etc/emergency-access/config.json`
 3. Test notification systems independently
 4. Monitor service status: `systemctl status emergency-access`
-5. Use path generator: `python generate_secure_paths.py --help`
+5. Use password generator: `python generate_passwords.py --help`
 
 ### Validation Commands
 ```bash
@@ -734,6 +778,9 @@ sudo -u emergency-access cat /etc/emergency-access/backup-key.txt
 # Test notifications
 ntfy -c /etc/emergency-access/ntfy.yml -b matrix_sec send "Test message"
 
-# Generate new secure paths
-python generate_secure_paths.py --keys 3 --path-length 120
+# Generate secure passwords
+python generate_passwords.py --keys backup master recovery
+
+# Test authentication
+curl -u emergency_backup:PASSWORD http://localhost:1127/emergency-key-backup
 ```

+ 35 - 3
config.py

@@ -1,23 +1,27 @@
 import json
 import os
 import yaml
+import hashlib
+import secrets
 from typing import Dict, List, Any, Optional
 
 class KeyConfig:
     """Configuration for a single key"""
     def __init__(self, key_id: str, config_data: Dict[str, Any], global_config: Dict[str, Any]):
         self.key_id = key_id
-        self.route = config_data.get('route', '')
+        self.route = config_data.get('route', f'/emergency-key-{key_id}')
         self.file_path = config_data.get('file', '')
         self.backends = config_data.get('backends', [])
         self.message = config_data.get('message', f'🚨 EMERGENCY: Key {key_id} accessed from server')
+        self.username = config_data.get('username', f'emergency_{key_id}')
+        self.password_hash = config_data.get('password_hash', '')
 
-        if not self.route:
-            raise Exception(f"Route not configured for key '{key_id}'")
         if not self.file_path:
             raise Exception(f"File path not configured for key '{key_id}'")
         if not self.backends:
             raise Exception(f"No notification backends configured for key '{key_id}'")
+        if not self.password_hash:
+            raise Exception(f"Password hash not configured for key '{key_id}'. Use generate_password_hash() to create one.")
 
 class Config:
     def __init__(self, config_path: str = None):
@@ -28,6 +32,23 @@ class Config:
         self.config = self._load_config()
         self._keys = self._load_keys()
 
+    @staticmethod
+    def generate_password_hash(password: str) -> str:
+        """Generate a password hash for use in configuration"""
+        salt = secrets.token_hex(16)
+        password_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000)
+        return f"{salt}:{password_hash.hex()}"
+
+    @staticmethod
+    def verify_password(password: str, password_hash: str) -> bool:
+        """Verify a password against its hash"""
+        try:
+            salt, stored_hash = password_hash.split(':')
+            password_hash_computed = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000)
+            return password_hash_computed.hex() == stored_hash
+        except Exception:
+            return False
+
     def _load_config(self) -> Dict[str, Any]:
         """Load configuration from file"""
         try:
@@ -68,6 +89,17 @@ class Config:
     def health_route(self) -> str:
         return self.config.get('routes', {}).get('health_route', '/health-check')
 
+    @property
+    def health_username(self) -> str:
+        return self.config.get('routes', {}).get('health_username', 'health_monitor')
+
+    @property
+    def health_password_hash(self) -> str:
+        health_auth = self.config.get('routes', {}).get('health_password_hash', '')
+        if not health_auth:
+            raise Exception("Health endpoint password hash not configured")
+        return health_auth
+
     @property
     def dummy_file_path(self) -> str:
         dummy_file = self.config.get('files', {}).get('dummy_file')

+ 9 - 0
emergency-access.service

@@ -15,8 +15,11 @@ ExecStart=/opt/emergency-access/venv/bin/python /opt/emergency-access/main.py
 ExecReload=/bin/kill -HUP $MAINPID
 Restart=always
 RestartSec=5
+StartLimitIntervalSec=300
+StartLimitBurst=5
 StandardOutput=journal
 StandardError=journal
+SyslogIdentifier=emergency-access
 
 # Security settings
 NoNewPrivileges=true
@@ -34,6 +37,12 @@ LockPersonality=true
 MemoryDenyWriteExecute=true
 RestrictSUIDSGID=true
 
+# Monitoring and health
+WatchdogSec=30
+NotifyAccess=none
+KillMode=mixed
+TimeoutStopSec=10
+
 # Network restrictions - allow localhost only (Caddy reverse proxy)
 IPAddressDeny=any
 IPAddressAllow=localhost

+ 5 - 5
install.sh

@@ -109,7 +109,7 @@ install_application() {
     if [[ ! -f "$CONFIG_DIR/config.json" ]]; then
         cp config.json.example "$CONFIG_DIR/config.json"
         print_status "Copied example configuration to $CONFIG_DIR/config.json"
-        print_warning "Please edit $CONFIG_DIR/config.json with your backend names and secure paths"
+        print_warning "Please edit $CONFIG_DIR/config.json with your backend names and authentication credentials"
     else
         print_warning "Configuration file already exists, skipping copy"
     fi
@@ -221,10 +221,10 @@ print_final_instructions() {
     print_status "Installation complete!"
     echo
     print_warning "IMPORTANT: Before starting the service:"
-    echo "1. Edit $CONFIG_DIR/config.json with your backend names and secure paths"
+    echo "1. Edit $CONFIG_DIR/config.json with your backend names and authentication credentials"
     echo "2. Edit $CONFIG_DIR/ntfy.yml with your notification backend configurations"
     echo "3. Create key files for each configured key (e.g., backup-key.txt, master-key.txt)"
-    echo "4. Use generate_secure_paths.py to create ultra-secure access paths"
+    echo "4. Use generate_passwords.py to create secure authentication credentials"
     echo "5. Test the configuration"
     echo
     print_status "Service management commands:"
@@ -239,8 +239,8 @@ print_final_instructions() {
     echo "  ntfy config:      $CONFIG_DIR/ntfy.yml"
     echo "  Key files:        $CONFIG_DIR/[key-name]-key.txt (create as configured)"
     echo ""
-    print_status "Path generator:"
-    echo "  Generate paths:   python generate_secure_paths.py --keys 3"
+    print_status "Password generator:"
+    echo "  Generate passwords: python generate_passwords.py --keys backup master"
     echo "  Dummy file:       $CONFIG_DIR/dummy.txt"
     echo "  Log file:         $LOG_FILE"
     echo

+ 147 - 38
main.py

@@ -5,9 +5,14 @@ import sys
 import logging
 import time
 import tempfile
-from flask import Flask, jsonify
+from flask import Flask, jsonify, request
+from werkzeug.security import check_password_hash
+from werkzeug.serving import WSGIRequestHandler
+from functools import wraps
 from config import Config
 from typing import List, Tuple
+import base64
+import signal
 
 # Configure logging with custom handler
 class NtfyLogHandler(logging.Handler):
@@ -56,45 +61,107 @@ app = Flask(__name__)
 # Global config instance will be initialized in main
 config = None
 
+def require_auth(key_config=None, is_health=False):
+    """Decorator for HTTP Basic Authentication"""
+    def decorator(f):
+        @wraps(f)
+        def decorated_function(*args, **kwargs):
+            auth = request.authorization
+
+            if not auth:
+                return jsonify({'error': 'Authentication required'}), 401, {
+                    'WWW-Authenticate': 'Basic realm="Emergency Access"'
+                }
+
+            # Determine expected credentials
+            if is_health:
+                expected_username = config.health_username
+                expected_password_hash = config.health_password_hash
+                auth_type = "health check"
+            else:
+                expected_username = key_config.username
+                expected_password_hash = key_config.password_hash
+                auth_type = f"key '{key_config.key_id}'"
+
+            # Verify credentials
+            if (auth.username != expected_username or
+                not Config.verify_password(auth.password, expected_password_hash)):
+                logger.warning(f"Authentication failed for {auth_type}: invalid credentials from {request.remote_addr}")
+                return jsonify({'error': 'Invalid credentials'}), 401, {
+                    'WWW-Authenticate': 'Basic realm="Emergency Access"'
+                }
+
+            logger.info(f"Authentication successful for {auth_type}")
+            return f(*args, **kwargs)
+        return decorated_function
+    return decorator
+
 def send_ntfy_notification(backends: List[str], message: str, title: str = None) -> Tuple[bool, List[str]]:
     """
     Send notification using dschep/ntfy with dedicated config file
     Returns: (success, successful_backends)
     """
     successful_backends = []
+    max_retries = 3
+    retry_delay = 1
 
     for backend in backends:
-        try:
-            # Import ntfy here to avoid import issues during startup
-            try:
-                import ntfy
-                from ntfy.config import load_config
-            except ImportError:
-                raise Exception("ntfy package not available. Please install with: pip install ntfy")
-
-            # Load the ntfy config file
-            ntfy_config = load_config(config.ntfy_config_path)
+        success = False
+        last_error = None
 
-            ntfy_config["backends"] = [backend]
-
-            # Send notification using the backend name from our config file
-            if title:
-                ret = ntfy.notify(message, title=title, config=ntfy_config)
-            else:
-                ret = ntfy.notify(message, config=ntfy_config, title="Note")
-
-            if ret == 0:
-                successful_backends.append(backend)
-                logger.info(f"Notification sent successfully via {backend}")
-            else:
-                logger.error(f"Failed to send notification via {backend}")
-                raise Exception(f"Failed to send notification via {backend}")
+        for attempt in range(max_retries):
+            try:
+                # Import ntfy here to avoid import issues during startup
+                try:
+                    import ntfy
+                    from ntfy.config import load_config
+                except ImportError:
+                    raise Exception("ntfy package not available. Please install with: pip install ntfy")
+
+                # Load the ntfy config file
+                ntfy_config = load_config(config.ntfy_config_path)
+                ntfy_config["backends"] = [backend]
+
+                # Add timeout to prevent hanging
+                import signal
+                def timeout_handler(signum, frame):
+                    raise Exception("Notification timeout")
+
+                signal.signal(signal.SIGALRM, timeout_handler)
+                signal.alarm(15)  # 15 second timeout
+
+                try:
+                    # Send notification using the backend name from our config file
+                    if title:
+                        ret = ntfy.notify(message, title=title, config=ntfy_config)
+                    else:
+                        ret = ntfy.notify(message, config=ntfy_config, title="Note")
+                finally:
+                    signal.alarm(0)  # Cancel timeout
+
+                if ret == 0:
+                    successful_backends.append(backend)
+                    logger.info(f"Notification sent successfully via {backend}")
+                    success = True
+                    break
+                else:
+                    raise Exception(f"ntfy returned error code {ret}")
+
+            except ImportError as e:
+                logger.error(f"ntfy package not available for backend {backend}: {e}")
+                last_error = e
+                break  # Don't retry import errors
+            except Exception as e:
+                last_error = e
+                if attempt < max_retries - 1:
+                    logger.warning(f"Notification attempt {attempt + 1} failed for {backend}: {str(e)}, retrying...")
+                    time.sleep(retry_delay * (attempt + 1))  # Exponential backoff
+                else:
+                    logger.error(f"All notification attempts failed for {backend}: {str(e)}")
 
-        except ImportError:
-            logger.error(f"ntfy package not available for backend {backend}")
-        except Exception as e:
-            logger.error(f"Failed to send notification to {backend}: {str(e)}")
-            raise
+        if not success and last_error:
+            # Don't raise exception - just log failure and continue with other backends
+            logger.error(f"Failed to send notification to {backend} after {max_retries} attempts: {str(last_error)}")
 
     success = len(successful_backends) > 0
     return success, successful_backends
@@ -127,6 +194,7 @@ def read_file_safely(file_path: str) -> Tuple[bool, str]:
 
 def create_key_handler(key_config):
     """Create a key access handler for a specific key configuration"""
+    @require_auth(key_config=key_config)
     def get_key_part():
         """Emergency key access endpoint"""
         logger.warning(f"EMERGENCY: Key access attempt detected for key '{key_config.key_id}'")
@@ -177,6 +245,7 @@ def create_key_handler(key_config):
     return get_key_part
 
 
+@require_auth(is_health=True)
 def health_check():
     """Health check endpoint that verifies both health monitoring and all key request functionality"""
     logger.info("Health check requested")
@@ -364,7 +433,19 @@ def validate_setup():
     logger.info("System validation completed successfully")
     return True
 
+class ProductionRequestHandler(WSGIRequestHandler):
+    """Custom request handler with improved error handling"""
+
+    def log_error(self, format, *args):
+        """Override to use our logger"""
+        logger.error(f"HTTP Error: {format % args}")
+
+    def log_message(self, format, *args):
+        """Override to use our logger"""
+        logger.info(f"HTTP: {format % args}")
+
 if __name__ == '__main__':
+    config = None
     try:
         # Load configuration
         config = Config()
@@ -388,7 +469,7 @@ if __name__ == '__main__':
             handler = create_key_handler(key_config)
             endpoint_name = f'get_key_part_{key_id}'
             app.add_url_rule(key_config.route, endpoint_name, handler, methods=['GET'])
-            logger.info(f"Registered key route for '{key_id}': {key_config.route}")
+            logger.info(f"Registered authenticated key route for '{key_id}': {key_config.route}")
 
         # Add health check route
         app.add_url_rule(config.health_route, 'health_check', health_check, methods=['GET'])
@@ -397,14 +478,42 @@ if __name__ == '__main__':
         logger.info(f"Health route: {config.health_route}")
         logger.info(f"Configured {len(config.keys)} key(s)")
 
-        # Run the server on local port for Caddy reverse proxy
-        app.run(
-            host=config.server_host,
-            port=config.server_port,
-            debug=False,
-            threaded=True
-        )
+        # Use production-ready server with better error handling
+        try:
+            from waitress import serve
+            logger.info("Using Waitress WSGI server for production")
+            serve(
+                app,
+                host=config.server_host,
+                port=config.server_port,
+                threads=6,
+                connection_limit=100,
+                cleanup_interval=30,
+                channel_timeout=120
+            )
+        except ImportError:
+            logger.warning("Waitress not available, falling back to Flask dev server")
+            app.run(
+                host=config.server_host,
+                port=config.server_port,
+                debug=False,
+                threaded=True,
+                request_handler=ProductionRequestHandler
+            )
 
+    except KeyboardInterrupt:
+        logger.info("Received keyboard interrupt, shutting down gracefully")
     except Exception as e:
         logger.error(f"Failed to start server: {str(e)}")
+        if config and hasattr(config, 'ntfy_backends_health'):
+            try:
+                send_ntfy_notification(
+                    config.ntfy_backends_health,
+                    f"🚨 Emergency Access server failed to start: {str(e)}",
+                    "Emergency Access CRITICAL"
+                )
+            except Exception:
+                pass
         sys.exit(1)
+    finally:
+        logger.info("Emergency Access server shutdown complete")

+ 1 - 0
requirements.txt

@@ -1,4 +1,5 @@
 flask>=2.0,<3.0
 ntfy[matrix,telegram]>=2.7.0
 pyyaml>=6.0
+waitress>=2.1.0
 gunicorn>=21.0

+ 62 - 17
test.py

@@ -30,14 +30,18 @@ def test_multikey_config():
         },
         "keys": {
             "backup_key": {
-                "route": "/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3",
+                "route": "/emergency-key-backup",
                 "file": "/tmp/backup-key.txt",
+                "username": "emergency_backup",
+                "password_hash": "salt123:hash123",
                 "backends": ["test_backend1", "test_backend2"],
                 "message": "Backup key accessed"
             },
             "master_key": {
-                "route": "/emergency-key-master-x3k8m9n2p5q7r1t4u6v9w2y5z8b1c4d7e0f3g6h9j2k5l8m1n4o7p0q3r6s9t2u5v8w1x4y7z0a3b6c9d2e5f8",
+                "route": "/emergency-key-master",
                 "file": "/tmp/master-key.txt",
+                "username": "emergency_master",
+                "password_hash": "salt456:hash456",
                 "backends": ["test_backend1", "test_backend3"],
                 "message": "Master key accessed"
             }
@@ -73,26 +77,30 @@ def test_multikey_config():
 
         # Test backup key
         backup_key = keys["backup_key"]
-        assert backup_key.route == "/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3"
+        assert backup_key.route == "/emergency-key-backup"
+        assert backup_key.username == "emergency_backup"
+        assert backup_key.password_hash == "salt123:hash123"
         assert backup_key.file_path == "/tmp/backup-key.txt"
         assert backup_key.backends == ["test_backend1", "test_backend2"]
         assert backup_key.message == "Backup key accessed"
 
         # Test master key
         master_key = keys["master_key"]
-        assert master_key.route == "/emergency-key-master-x3k8m9n2p5q7r1t4u6v9w2y5z8b1c4d7e0f3g6h9j2k5l8m1n4o7p0q3r6s9t2u5v8w1x4y7z0a3b6c9d2e5f8"
+        assert master_key.route == "/emergency-key-master"
+        assert master_key.username == "emergency_master"
+        assert master_key.password_hash == "salt456:hash456"
         assert master_key.file_path == "/tmp/master-key.txt"
         assert master_key.backends == ["test_backend1", "test_backend3"]
         assert master_key.message == "Master key accessed"
 
         # Test key lookup methods
-        found_key = config.get_key_by_route("/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3")
+        found_key = config.get_key_by_route("/emergency-key-backup")
         assert found_key is not None
         assert found_key.key_id == "backup_key"
 
         found_key = config.get_key_by_id("master_key")
         assert found_key is not None
-        assert found_key.route == "/emergency-key-master-x3k8m9n2p5q7r1t4u6v9w2y5z8b1c4d7e0f3g6h9j2k5l8m1n4o7p0q3r6s9t2u5v8w1x4y7z0a3b6c9d2e5f8"
+        assert found_key.route == "/emergency-key-master"
 
         print("✅ Multi-key configuration test passed")
 
@@ -146,23 +154,23 @@ def test_key_config_validation():
     """Test key configuration validation"""
     print("Testing key configuration validation...")
 
-    # Test missing route
-    try:
-        KeyConfig("test", {"file": "/tmp/test.txt"}, {})
-        assert False, "Should have raised exception for missing route"
-    except Exception as e:
-        assert "Route not configured" in str(e)
-
     # Test missing file
     try:
-        KeyConfig("test", {"route": "/test"}, {})
+        KeyConfig("test", {"username": "test", "password_hash": "salt:hash", "backends": ["test"]}, {})
         assert False, "Should have raised exception for missing file"
     except Exception as e:
         assert "File path not configured" in str(e)
 
+    # Test missing password hash
+    try:
+        KeyConfig("test", {"file": "/tmp/test.txt", "backends": ["test"]}, {})
+        assert False, "Should have raised exception for missing password hash"
+    except Exception as e:
+        assert "Password hash not configured" in str(e)
+
     # Test missing backends
     try:
-        KeyConfig("test", {"route": "/test", "file": "/tmp/test.txt"}, {})
+        KeyConfig("test", {"file": "/tmp/test.txt", "password_hash": "salt:hash"}, {})
         assert False, "Should have raised exception for missing backends"
     except Exception as e:
         assert "No notification backends configured" in str(e)
@@ -173,6 +181,8 @@ def test_key_config_validation():
         {
             "route": "/test",
             "file": "/tmp/test.txt",
+            "username": "test_user",
+            "password_hash": "salt:hash",
             "backends": ["backend1"],
             "message": "Test message"
         },
@@ -182,11 +192,41 @@ def test_key_config_validation():
     assert key_config.key_id == "test"
     assert key_config.route == "/test"
     assert key_config.file_path == "/tmp/test.txt"
+    assert key_config.username == "test_user"
+    assert key_config.password_hash == "salt:hash"
     assert key_config.backends == ["backend1"]
     assert key_config.message == "Test message"
 
     print("✅ Key configuration validation test passed")
 
+def test_password_functions():
+    """Test password generation and verification functions"""
+    print("Testing password generation and verification...")
+
+    from config import Config
+
+    # Test password hash generation
+    password = "test_password_123"
+    password_hash = Config.generate_password_hash(password)
+
+    # Hash should be in format "salt:hash"
+    assert ':' in password_hash
+    salt, hash_part = password_hash.split(':')
+    assert len(salt) == 32  # 16 bytes hex = 32 chars
+    assert len(hash_part) == 64  # 32 bytes hex = 64 chars
+
+    # Test password verification - correct password
+    assert Config.verify_password(password, password_hash) == True
+
+    # Test password verification - incorrect password
+    assert Config.verify_password("wrong_password", password_hash) == False
+
+    # Test password verification - malformed hash
+    assert Config.verify_password(password, "invalid_hash") == False
+    assert Config.verify_password(password, "no:colon:format:hash") == False
+
+    print("✅ Password generation and verification test passed")
+
 def test_app_integration():
     """Test Flask app integration with multiple keys"""
     print("Testing Flask app integration...")
@@ -220,14 +260,18 @@ def test_app_integration():
             },
             "keys": {
                 "backup_key": {
-                    "route": "/emergency-key-backup-a7f9d2e1b4c7f3e6d9a2b5c8e1f4d7a0b3c6e9f2a5d8b1c4e7f0a3d6b9c2e5f8a1d4b7c0e3f6d9a2b5c8e1f4a7b0d3",
+                    "route": "/emergency-key-backup",
                     "file": backup_key_file,
+                    "username": "emergency_backup",
+                    "password_hash": "salt123:hash123",
                     "backends": ["test_backend1"],
                     "message": "Backup key accessed"
                 },
                 "master_key": {
-                    "route": "/emergency-key-master-x3k8m9n2p5q7r1t4u6v9w2y5z8b1c4d7e0f3g6h9j2k5l8m1n4o7p0q3r6s9t2u5v8w1x4y7z0a3b6c9d2e5f8",
+                    "route": "/emergency-key-master",
                     "file": master_key_file,
+                    "username": "emergency_master",
+                    "password_hash": "salt456:hash456",
                     "backends": ["test_backend2"],
                     "message": "Master key accessed"
                 }
@@ -288,6 +332,7 @@ def main():
         test_multikey_config()
         test_invalid_config()
         test_key_config_validation()
+        test_password_functions()
         test_app_integration()
 
         print("\n🎉 All tests passed! Multi-key functionality is working correctly.")