浏览代码

add key management

zehe 3 月之前
父节点
当前提交
349736c18d
共有 5 个文件被更改,包括 1215 次插入4 次删除
  1. 72 0
      README.md
  2. 485 0
      add_key.py
  3. 88 0
      example_key_setup.sh
  4. 26 4
      generate_passwords.py
  5. 544 0
      manage_keys.py

+ 72 - 0
README.md

@@ -49,6 +49,78 @@ Run the automated installation script as root:
 sudo ./install.sh
 sudo ./install.sh
 ```
 ```
 
 
+The installation script will:
+- Install Python dependencies
+- Create service user and directories
+- Set up the systemd service
+- Install key management tools
+- Create example configuration files
+
+### Key Management Tools
+
+The system includes three key management scripts:
+
+#### 1. `add_key.py` - Add New Keys
+Add new emergency access keys to your configuration:
+
+```bash
+# Interactive key addition (recommended)
+python add_key.py --interactive
+
+# Add key programmatically
+python add_key.py --key-id backup \
+  --file /etc/emergency-access/backup-key.txt \
+  --backends matrix_sec,email \
+  --create-file
+
+# List existing keys
+python add_key.py --list
+
+# Remove a key
+python add_key.py --remove backup --remove-file
+```
+
+#### 2. `manage_keys.py` - Key File Operations
+Manage key files, test access, and validate configuration:
+
+```bash
+# List all key files and their status
+python manage_keys.py --list-files
+
+# Validate all key files
+python manage_keys.py --validate
+
+# Test access to a specific key
+python manage_keys.py --test-key backup --password "your_password"
+
+# Test health endpoint
+python manage_keys.py --test-health --password "health_password"
+
+# Create/rotate key files
+python manage_keys.py --rotate-key backup --key-type ssh
+python manage_keys.py --create-key-file /path/to/key.txt --key-type api
+
+# Generate key content
+python manage_keys.py --generate-content ssh
+```
+
+#### 3. `generate_passwords.py` - Password Management
+Generate secure passwords and update configuration:
+
+```bash
+# Generate passwords for specific keys
+python generate_passwords.py --keys backup master recovery
+
+# Interactive password setup
+python generate_passwords.py --interactive
+
+# Update configuration with new passwords
+python generate_passwords.py --keys backup --update-config config.json
+
+# Hash a specific password
+python generate_passwords.py --hash-password "my_secure_password"
+```
+
 ### Manual Installation
 ### Manual Installation
 
 
 1. **Install system dependencies**:
 1. **Install system dependencies**:

+ 485 - 0
add_key.py

@@ -0,0 +1,485 @@
+#!/usr/bin/env python3
+
+"""
+Add Key Script for Emergency Access Server
+Adds new emergency access keys to the configuration with proper validation
+"""
+
+import json
+import yaml
+import os
+import sys
+import argparse
+import secrets
+import string
+import hashlib
+import shutil
+from pathlib import Path
+from typing import Dict, Any, Optional
+
+
+def generate_secure_password(length: int = 64) -> str:
+    """Generate a cryptographically secure password"""
+    alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
+    return ''.join(secrets.choice(alphabet) for _ in range(length))
+
+
+def generate_password_hash(password: str) -> str:
+    """Generate a password hash using PBKDF2 with SHA-256"""
+    salt = secrets.token_hex(16)
+    password_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000)
+    return f"{salt}:{password_hash.hex()}"
+
+
+def load_config(config_path: str) -> Dict[str, Any]:
+    """Load configuration from file"""
+    try:
+        with open(config_path, 'r') as f:
+            if config_path.endswith(('.yaml', '.yml')):
+                return yaml.safe_load(f)
+            else:
+                return json.load(f)
+    except FileNotFoundError:
+        raise Exception(f"Configuration file {config_path} not found")
+    except Exception as e:
+        raise Exception(f"Failed to load configuration: {str(e)}")
+
+
+def save_config(config: Dict[str, Any], config_path: str) -> None:
+    """Save configuration to file"""
+    # Create backup
+    backup_path = f"{config_path}.backup"
+    if os.path.exists(config_path):
+        shutil.copy2(config_path, backup_path)
+        print(f"📁 Created backup: {backup_path}")
+
+    try:
+        with open(config_path, 'w') as f:
+            if config_path.endswith(('.yaml', '.yml')):
+                yaml.safe_dump(config, f, indent=2, default_flow_style=False)
+            else:
+                json.dump(config, f, indent=2)
+        print(f"💾 Configuration saved to: {config_path}")
+    except Exception as e:
+        # Restore backup if save failed
+        if os.path.exists(backup_path):
+            shutil.move(backup_path, config_path)
+        raise Exception(f"Failed to save configuration: {str(e)}")
+
+
+def validate_key_id(key_id: str, existing_keys: Dict[str, Any]) -> None:
+    """Validate key ID format and uniqueness"""
+    if not key_id:
+        raise ValueError("Key ID cannot be empty")
+
+    if not key_id.replace('_', '').replace('-', '').isalnum():
+        raise ValueError("Key ID must contain only letters, numbers, underscores, and hyphens")
+
+    if key_id in existing_keys:
+        raise ValueError(f"Key ID '{key_id}' already exists")
+
+
+def validate_file_path(file_path: str) -> None:
+    """Validate file path and create directory if needed"""
+    path = Path(file_path)
+
+    # Create parent directory if it doesn't exist
+    path.parent.mkdir(parents=True, exist_ok=True)
+
+    # Check if we can write to the directory
+    if not os.access(path.parent, os.W_OK):
+        raise ValueError(f"Cannot write to directory: {path.parent}")
+
+
+def validate_backends(backends: list, available_backends: Optional[list] = None) -> None:
+    """Validate notification backends"""
+    if not backends:
+        raise ValueError("At least one notification backend must be specified")
+
+    if available_backends:
+        invalid_backends = [b for b in backends if b not in available_backends]
+        if invalid_backends:
+            raise ValueError(f"Invalid backends: {invalid_backends}")
+
+
+def get_available_backends(config: Dict[str, Any]) -> list:
+    """Extract available backends from ntfy config"""
+    try:
+        ntfy_config_path = config.get('notifications', {}).get('config_path', '/etc/emergency-access/ntfy.yml')
+        if os.path.exists(ntfy_config_path):
+            with open(ntfy_config_path, 'r') as f:
+                ntfy_config = yaml.safe_load(f)
+                if 'backends' in ntfy_config:
+                    return list(ntfy_config['backends'].keys())
+    except Exception:
+        pass
+    return []
+
+
+def create_key_file(file_path: str, content: str = None) -> None:
+    """Create the key file with content or placeholder"""
+    path = Path(file_path)
+
+    if content is None:
+        content = f"# Emergency Access Key File\n# Generated on: {os.popen('date').read().strip()}\n# TODO: Replace this with your actual key content\n"
+
+    with open(path, 'w') as f:
+        f.write(content)
+
+    # Set secure permissions (owner read/write only)
+    os.chmod(path, 0o600)
+    print(f"🔑 Created key file: {file_path} (permissions: 600)")
+
+
+def add_key_interactive(config: Dict[str, Any], config_path: str) -> None:
+    """Interactive key addition"""
+    print("\n" + "=" * 60)
+    print("INTERACTIVE KEY ADDITION")
+    print("=" * 60)
+
+    existing_keys = config.get('keys', {})
+    available_backends = get_available_backends(config)
+
+    # Get key ID
+    while True:
+        key_id = input("\nEnter key ID (e.g., 'backup', 'master', 'recovery'): ").strip()
+        try:
+            validate_key_id(key_id, existing_keys)
+            break
+        except ValueError as e:
+            print(f"❌ {e}")
+
+    # Get route (optional, defaults to /emergency-key-{key_id})
+    default_route = f"/emergency-key-{key_id}"
+    route = input(f"Enter route (default: {default_route}): ").strip()
+    if not route:
+        route = default_route
+
+    # Get file path
+    while True:
+        default_file = f"/etc/emergency-access/{key_id}-key.txt"
+        file_path = input(f"Enter key file path (default: {default_file}): ").strip()
+        if not file_path:
+            file_path = default_file
+
+        try:
+            validate_file_path(file_path)
+            break
+        except ValueError as e:
+            print(f"❌ {e}")
+
+    # Get username (optional, defaults to emergency_{key_id})
+    default_username = f"emergency_{key_id}"
+    username = input(f"Enter username (default: {default_username}): ").strip()
+    if not username:
+        username = default_username
+
+    # Get password
+    import getpass
+    while True:
+        password = getpass.getpass("Enter password (leave empty to generate): ").strip()
+        if not password:
+            password = generate_secure_password(64)
+            print(f"🔐 Generated password: {password}")
+            break
+        elif len(password) < 12:
+            print("❌ Password must be at least 12 characters")
+        else:
+            break
+
+    password_hash = generate_password_hash(password)
+
+    # Get notification backends
+    if available_backends:
+        print(f"\nAvailable backends: {', '.join(available_backends)}")
+
+    while True:
+        backends_input = input("Enter notification backends (comma-separated): ").strip()
+        backends = [b.strip() for b in backends_input.split(',') if b.strip()]
+
+        try:
+            validate_backends(backends, available_backends if available_backends else None)
+            break
+        except ValueError as e:
+            print(f"❌ {e}")
+
+    # Get custom message (optional)
+    default_message = f"🚨 EMERGENCY: Key {key_id} accessed from server"
+    message = input(f"Enter notification message (default: {default_message}): ").strip()
+    if not message:
+        message = default_message
+
+    # Create key file
+    create_file = input(f"\nCreate key file at {file_path}? (y/N): ").strip().lower()
+    if create_file == 'y':
+        key_content = input("Enter key content (leave empty for placeholder): ").strip()
+        create_key_file(file_path, key_content if key_content else None)
+
+    # Add key to config
+    add_key_to_config(config, key_id, {
+        'route': route,
+        'file': file_path,
+        'username': username,
+        'password_hash': password_hash,
+        'backends': backends,
+        'message': message
+    })
+
+    save_config(config, config_path)
+
+    print(f"\n✅ Successfully added key '{key_id}'")
+    print(f"   Route: {route}")
+    print(f"   File: {file_path}")
+    print(f"   Username: {username}")
+    print(f"   Password: {password}")
+    print(f"   Backends: {', '.join(backends)}")
+
+
+def add_key_to_config(config: Dict[str, Any], key_id: str, key_config: Dict[str, Any]) -> None:
+    """Add key configuration to config"""
+    if 'keys' not in config:
+        config['keys'] = {}
+
+    config['keys'][key_id] = key_config
+
+
+def add_key_programmatic(config: Dict[str, Any], config_path: str, args) -> None:
+    """Add key programmatically using command line arguments"""
+    existing_keys = config.get('keys', {})
+    available_backends = get_available_backends(config)
+
+    # Validate inputs
+    validate_key_id(args.key_id, existing_keys)
+    validate_file_path(args.file)
+    validate_backends(args.backends, available_backends if available_backends else None)
+
+    # Generate password if not provided
+    if args.password:
+        password = args.password
+    else:
+        password = generate_secure_password(64)
+        print(f"🔐 Generated password: {password}")
+
+    password_hash = generate_password_hash(password)
+
+    # Set defaults
+    route = args.route or f"/emergency-key-{args.key_id}"
+    username = args.username or f"emergency_{args.key_id}"
+    message = args.message or f"🚨 EMERGENCY: Key {args.key_id} accessed from server"
+
+    # Create key file if requested
+    if args.create_file:
+        create_key_file(args.file, args.key_content)
+
+    # Add key to config
+    add_key_to_config(config, args.key_id, {
+        'route': route,
+        'file': args.file,
+        'username': username,
+        'password_hash': password_hash,
+        'backends': args.backends,
+        'message': message
+    })
+
+    save_config(config, config_path)
+
+    print(f"\n✅ Successfully added key '{args.key_id}'")
+    print(f"   Route: {route}")
+    print(f"   File: {args.file}")
+    print(f"   Username: {username}")
+    print(f"   Password: {password}")
+    print(f"   Backends: {', '.join(args.backends)}")
+
+
+def list_keys(config: Dict[str, Any]) -> None:
+    """List existing keys"""
+    keys = config.get('keys', {})
+
+    if not keys:
+        print("No keys configured")
+        return
+
+    print("\n" + "=" * 60)
+    print("CONFIGURED KEYS")
+    print("=" * 60)
+
+    for key_id, key_config in keys.items():
+        print(f"\n📋 Key ID: {key_id}")
+        print(f"   Route: {key_config.get('route', 'N/A')}")
+        print(f"   File: {key_config.get('file', 'N/A')}")
+        print(f"   Username: {key_config.get('username', 'N/A')}")
+        print(f"   Backends: {', '.join(key_config.get('backends', []))}")
+        print(f"   Message: {key_config.get('message', 'N/A')}")
+
+
+def remove_key(config: Dict[str, Any], config_path: str, key_id: str, remove_file: bool = False) -> None:
+    """Remove a key from configuration"""
+    keys = config.get('keys', {})
+
+    if key_id not in keys:
+        print(f"❌ Key '{key_id}' not found")
+        return
+
+    key_config = keys[key_id]
+
+    # Remove file if requested
+    if remove_file and 'file' in key_config:
+        file_path = key_config['file']
+        if os.path.exists(file_path):
+            os.remove(file_path)
+            print(f"🗑️  Removed file: {file_path}")
+
+    # Remove from config
+    del keys[key_id]
+
+    save_config(config, config_path)
+    print(f"✅ Removed key '{key_id}' from configuration")
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Add, list, or remove emergency access keys",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+Examples:
+  # Interactive key addition
+  python add_key.py --interactive
+
+  # Add key programmatically
+  python add_key.py --key-id backup --file /etc/emergency-access/backup.txt --backends matrix_sec,email
+
+  # List existing keys
+  python add_key.py --list
+
+  # Remove a key
+  python add_key.py --remove backup --remove-file
+
+  # Add key with all options
+  python add_key.py --key-id master \\
+    --file /etc/emergency-access/master.txt \\
+    --route /emergency-master \\
+    --username master_user \\
+    --password "secure_password" \\
+    --backends matrix_sec,pushover \\
+    --message "Master key accessed!" \\
+    --create-file \\
+    --key-content "actual key content here"
+        """
+    )
+
+    parser.add_argument(
+        '--config', '-c',
+        default='config.json',
+        help='Configuration file path (default: config.json)'
+    )
+
+    # Add key options
+    parser.add_argument(
+        '--interactive', '-i',
+        action='store_true',
+        help='Interactive key addition'
+    )
+
+    parser.add_argument(
+        '--key-id',
+        help='Key identifier'
+    )
+
+    parser.add_argument(
+        '--file', '-f',
+        help='Path to key file'
+    )
+
+    parser.add_argument(
+        '--route',
+        help='HTTP route for key access (default: /emergency-key-{key_id})'
+    )
+
+    parser.add_argument(
+        '--username', '-u',
+        help='HTTP Basic Auth username (default: emergency_{key_id})'
+    )
+
+    parser.add_argument(
+        '--password', '-p',
+        help='HTTP Basic Auth password (generated if not provided)'
+    )
+
+    parser.add_argument(
+        '--backends', '-b',
+        nargs='+',
+        help='Notification backends (space or comma separated)'
+    )
+
+    parser.add_argument(
+        '--message', '-m',
+        help='Notification message'
+    )
+
+    parser.add_argument(
+        '--create-file',
+        action='store_true',
+        help='Create the key file'
+    )
+
+    parser.add_argument(
+        '--key-content',
+        help='Content for the key file'
+    )
+
+    # List/remove options
+    parser.add_argument(
+        '--list', '-l',
+        action='store_true',
+        help='List existing keys'
+    )
+
+    parser.add_argument(
+        '--remove', '-r',
+        help='Remove key by ID'
+    )
+
+    parser.add_argument(
+        '--remove-file',
+        action='store_true',
+        help='Also remove the key file when removing a key'
+    )
+
+    args = parser.parse_args()
+
+    # Handle backends input (support both space and comma separated)
+    if args.backends:
+        backends = []
+        for backend_group in args.backends:
+            backends.extend([b.strip() for b in backend_group.replace(',', ' ').split() if b.strip()])
+        args.backends = backends
+
+    # Load configuration
+    try:
+        config = load_config(args.config)
+    except Exception as e:
+        print(f"❌ {e}")
+        sys.exit(1)
+
+    try:
+        if args.list:
+            list_keys(config)
+        elif args.remove:
+            remove_key(config, args.config, args.remove, args.remove_file)
+        elif args.interactive:
+            add_key_interactive(config, args.config)
+        elif args.key_id and args.file and args.backends:
+            add_key_programmatic(config, args.config, args)
+        else:
+            parser.print_help()
+            print("\n❌ Missing required arguments for key addition")
+            print("   Use --interactive for guided setup")
+            print("   Or provide --key-id, --file, and --backends")
+
+    except Exception as e:
+        print(f"❌ {e}")
+        sys.exit(1)
+
+
+if __name__ == '__main__':
+    main()

+ 88 - 0
example_key_setup.sh

@@ -0,0 +1,88 @@
+#!/bin/bash
+
+# Example Key Setup Script
+# Demonstrates how to use the key management tools
+
+set -e
+
+# Colors for output
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+NC='\033[0m' # No Color
+
+echo -e "${GREEN}Emergency Access Key Management Example${NC}"
+echo "=========================================="
+
+# Check if we're in the right directory
+if [[ ! -f "add_key.py" || ! -f "manage_keys.py" ]]; then
+    echo "Error: Please run this script from the emergency-access directory"
+    exit 1
+fi
+
+echo
+echo -e "${YELLOW}1. Generating passwords for default keys...${NC}"
+python3 generate_passwords.py --keys backup master recovery --hide-passwords
+
+echo
+echo -e "${YELLOW}2. Adding a backup key interactively...${NC}"
+echo "   (This would normally be interactive - showing programmatic example instead)"
+
+# Example of adding a key programmatically
+python3 add_key.py --key-id example_backup \
+    --file /tmp/emergency-access-example/backup-key.txt \
+    --backends matrix_sec,email_emergency \
+    --message "🚨 EMERGENCY: Backup key accessed from server" \
+    --create-file \
+    --key-content "# Example backup key content\nBACKUP_KEY=example_key_content_here\n" \
+    --config config.json.example || echo "Key already exists or config not found"
+
+echo
+echo -e "${YELLOW}3. Listing all configured keys...${NC}"
+python3 add_key.py --list --config config.json.example || echo "Using example config"
+
+echo
+echo -e "${YELLOW}4. Checking key file status...${NC}"
+python3 manage_keys.py --list-files --config config.json.example || echo "Using example config"
+
+echo
+echo -e "${YELLOW}5. Generating different types of key content...${NC}"
+echo "SSH Key:"
+python3 manage_keys.py --generate-content ssh | head -3
+
+echo
+echo "API Key:"
+python3 manage_keys.py --generate-content api
+
+echo
+echo "Password:"
+python3 manage_keys.py --generate-content password
+
+echo
+echo -e "${YELLOW}6. Example testing (requires running server)...${NC}"
+echo "To test key access when server is running:"
+echo "  python3 manage_keys.py --test-key backup_key --password 'your_password'"
+echo "  python3 manage_keys.py --test-health --password 'health_password'"
+
+echo
+echo -e "${GREEN}Key Management Commands Summary:${NC}"
+echo "================================"
+echo
+echo "Add keys:"
+echo "  python3 add_key.py --interactive"
+echo "  python3 add_key.py --key-id NAME --file /path --backends backend1,backend2"
+echo
+echo "Manage keys:"
+echo "  python3 manage_keys.py --list-files"
+echo "  python3 manage_keys.py --validate"
+echo "  python3 manage_keys.py --rotate-key NAME --key-type ssh"
+echo
+echo "Generate passwords:"
+echo "  python3 generate_passwords.py --keys key1 key2"
+echo "  python3 generate_passwords.py --interactive"
+echo
+echo "Test access:"
+echo "  python3 manage_keys.py --test-key NAME --password PASSWORD"
+echo "  python3 manage_keys.py --test-health --password PASSWORD"
+echo
+echo -e "${GREEN}Example complete!${NC}"
+echo "See README.md for more detailed usage information."

+ 26 - 4
generate_passwords.py

@@ -105,6 +105,25 @@ install_application() {
     cp config.py "$INSTALL_DIR/"
     cp config.py "$INSTALL_DIR/"
     cp requirements.txt "$INSTALL_DIR/"
     cp requirements.txt "$INSTALL_DIR/"
 
 
+    # Copy key management scripts
+    if [[ -f "add_key.py" ]]; then
+        cp add_key.py "$INSTALL_DIR/"
+        chmod 755 "$INSTALL_DIR/add_key.py"
+        print_status "Installed key addition script"
+    fi
+
+    if [[ -f "manage_keys.py" ]]; then
+        cp manage_keys.py "$INSTALL_DIR/"
+        chmod 755 "$INSTALL_DIR/manage_keys.py"
+        print_status "Installed key management script"
+    fi
+
+    if [[ -f "generate_passwords.py" ]]; then
+        cp generate_passwords.py "$INSTALL_DIR/"
+        chmod 755 "$INSTALL_DIR/generate_passwords.py"
+        print_status "Installed password generation script"
+    fi
+
     # Copy example config if config doesn't exist
     # Copy example config if config doesn't exist
     if [[ ! -f "$CONFIG_DIR/config.json" ]]; then
     if [[ ! -f "$CONFIG_DIR/config.json" ]]; then
         cp config.json.example "$CONFIG_DIR/config.json"
         cp config.json.example "$CONFIG_DIR/config.json"
@@ -239,10 +258,13 @@ print_final_instructions() {
     echo "  ntfy config:      $CONFIG_DIR/ntfy.yml"
     echo "  ntfy config:      $CONFIG_DIR/ntfy.yml"
     echo "  Key files:        $CONFIG_DIR/[key-name]-key.txt (create as configured)"
     echo "  Key files:        $CONFIG_DIR/[key-name]-key.txt (create as configured)"
     echo ""
     echo ""
-    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"
+    print_status "Key management tools:"
+    echo "  Generate passwords: cd $INSTALL_DIR && python generate_passwords.py --keys backup master"
+    echo "  Add new key:        cd $INSTALL_DIR && python add_key.py --interactive"
+    echo "  Manage keys:        cd $INSTALL_DIR && python manage_keys.py --list-files"
+    echo "  Test key access:    cd $INSTALL_DIR && python manage_keys.py --test-key KEY_ID"
+    echo "  Dummy file:         $CONFIG_DIR/dummy.txt"
+    echo "  Log file:           $LOG_FILE"
     echo
     echo
     print_warning "Security note: This server provides access to sensitive key material."
     print_warning "Security note: This server provides access to sensitive key material."
     print_warning "Ensure proper network security and monitoring are in place."
     print_warning "Ensure proper network security and monitoring are in place."

+ 544 - 0
manage_keys.py

@@ -0,0 +1,544 @@
+#!/usr/bin/env python3
+
+"""
+Key Management Script for Emergency Access Server
+Manages key files, tests access, and provides key operations
+"""
+
+import os
+import sys
+import json
+import yaml
+import argparse
+import requests
+import base64
+import secrets
+import string
+from pathlib import Path
+from typing import Dict, Any, Optional, List
+import subprocess
+import shutil
+
+
+def load_config(config_path: str) -> Dict[str, Any]:
+    """Load configuration from file"""
+    try:
+        with open(config_path, 'r') as f:
+            if config_path.endswith(('.yaml', '.yml')):
+                return yaml.safe_load(f)
+            else:
+                return json.load(f)
+    except FileNotFoundError:
+        raise Exception(f"Configuration file {config_path} not found")
+    except Exception as e:
+        raise Exception(f"Failed to load configuration: {str(e)}")
+
+
+def get_server_url(config: Dict[str, Any]) -> str:
+    """Get server URL from configuration"""
+    host = config.get('server', {}).get('host', '127.0.0.1')
+    port = config.get('server', {}).get('port', 1127)
+    return f"http://{host}:{port}"
+
+
+def test_key_access(config: Dict[str, Any], key_id: str, password: str) -> Dict[str, Any]:
+    """Test access to a specific key"""
+    keys = config.get('keys', {})
+
+    if key_id not in keys:
+        return {'success': False, 'error': f"Key '{key_id}' not found in configuration"}
+
+    key_config = keys[key_id]
+    server_url = get_server_url(config)
+    url = f"{server_url}{key_config['route']}"
+    username = key_config['username']
+
+    try:
+        # Create basic auth header
+        auth_string = base64.b64encode(f"{username}:{password}".encode()).decode()
+        headers = {
+            'Authorization': f'Basic {auth_string}',
+            'User-Agent': 'emergency-access-test/1.0'
+        }
+
+        # Make request with timeout
+        response = requests.get(url, headers=headers, timeout=10)
+
+        return {
+            'success': response.status_code == 200,
+            'status_code': response.status_code,
+            'response_text': response.text[:500],  # Limit response size
+            'url': url,
+            'username': username
+        }
+
+    except requests.exceptions.RequestException as e:
+        return {
+            'success': False,
+            'error': f"Request failed: {str(e)}",
+            'url': url,
+            'username': username
+        }
+
+
+def test_health_endpoint(config: Dict[str, Any], password: str) -> Dict[str, Any]:
+    """Test health endpoint access"""
+    server_url = get_server_url(config)
+    health_route = config.get('routes', {}).get('health_route', '/health-check')
+    username = config.get('routes', {}).get('health_username', 'health_monitor')
+    url = f"{server_url}{health_route}"
+
+    try:
+        auth_string = base64.b64encode(f"{username}:{password}".encode()).decode()
+        headers = {
+            'Authorization': f'Basic {auth_string}',
+            'User-Agent': 'emergency-access-test/1.0'
+        }
+
+        response = requests.get(url, headers=headers, timeout=10)
+
+        return {
+            'success': response.status_code == 200,
+            'status_code': response.status_code,
+            'response_text': response.text[:500],
+            'url': url,
+            'username': username
+        }
+
+    except requests.exceptions.RequestException as e:
+        return {
+            'success': False,
+            'error': f"Request failed: {str(e)}",
+            'url': url,
+            'username': username
+        }
+
+
+def generate_key_content(key_type: str = 'ssh', bits: int = 4096) -> str:
+    """Generate key content based on type"""
+    if key_type == 'ssh':
+        # Generate SSH key using ssh-keygen
+        temp_key = f"/tmp/emergency_key_{secrets.token_hex(8)}"
+        try:
+            result = subprocess.run([
+                'ssh-keygen', '-t', 'rsa', '-b', str(bits),
+                '-f', temp_key, '-N', '', '-C', 'emergency-access-key'
+            ], capture_output=True, text=True)
+
+            if result.returncode != 0:
+                return f"# SSH Key Generation Failed\n# Error: {result.stderr}\n"
+
+            # Read private key
+            with open(temp_key, 'r') as f:
+                private_key = f.read()
+
+            # Clean up
+            os.unlink(temp_key)
+            if os.path.exists(f"{temp_key}.pub"):
+                os.unlink(f"{temp_key}.pub")
+
+            return private_key
+
+        except FileNotFoundError:
+            return "# SSH key generation requires ssh-keygen to be installed\n# Please install OpenSSH client tools\n"
+        except Exception as e:
+            return f"# SSH key generation failed: {str(e)}\n"
+
+    elif key_type == 'gpg':
+        return """# GPG Private Key Placeholder
+# Replace this with your actual GPG private key
+# Export with: gpg --export-secret-keys --armor KEY_ID
+"""
+
+    elif key_type == 'api':
+        # Generate a secure API key
+        api_key = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
+        return f"API_KEY={api_key}\n"
+
+    elif key_type == 'password':
+        # Generate a secure password
+        password = ''.join(secrets.choice(string.ascii_letters + string.digits + "!@#$%^&*") for _ in range(32))
+        return f"PASSWORD={password}\n"
+
+    else:
+        return f"# {key_type.upper()} Key File\n# Generated on: {os.popen('date').read().strip()}\n# TODO: Replace with actual {key_type} content\n"
+
+
+def create_key_file(file_path: str, content: str, permissions: int = 0o600) -> None:
+    """Create key file with content and secure permissions"""
+    path = Path(file_path)
+
+    # Create parent directory if needed
+    path.parent.mkdir(parents=True, exist_ok=True)
+
+    # Write content
+    with open(path, 'w') as f:
+        f.write(content)
+
+    # Set secure permissions
+    os.chmod(path, permissions)
+    print(f"🔑 Created key file: {file_path} (permissions: {oct(permissions)})")
+
+
+def backup_key_file(file_path: str) -> Optional[str]:
+    """Create backup of key file"""
+    if not os.path.exists(file_path):
+        return None
+
+    backup_path = f"{file_path}.backup.{secrets.token_hex(4)}"
+    shutil.copy2(file_path, backup_path)
+    print(f"📁 Created backup: {backup_path}")
+    return backup_path
+
+
+def list_key_files(config: Dict[str, Any]) -> None:
+    """List all key files and their status"""
+    keys = config.get('keys', {})
+
+    if not keys:
+        print("No keys configured")
+        return
+
+    print("\n" + "=" * 80)
+    print("KEY FILE STATUS")
+    print("=" * 80)
+
+    for key_id, key_config in keys.items():
+        file_path = key_config.get('file', '')
+
+        print(f"\n📋 Key: {key_id}")
+        print(f"   File: {file_path}")
+
+        if not file_path:
+            print("   Status: ❌ No file path configured")
+            continue
+
+        path = Path(file_path)
+
+        if not path.exists():
+            print("   Status: ❌ File does not exist")
+        else:
+            # Get file info
+            stat = path.stat()
+            size = stat.st_size
+            permissions = oct(stat.st_mode)[-3:]
+
+            print(f"   Status: ✅ Exists")
+            print(f"   Size: {size} bytes")
+            print(f"   Permissions: {permissions}")
+
+            # Check if permissions are secure
+            if stat.st_mode & 0o077:  # Check if group/other have any permissions
+                print("   Security: ⚠️  File permissions may be too permissive")
+            else:
+                print("   Security: ✅ Secure permissions")
+
+            # Check if readable
+            try:
+                with open(path, 'r') as f:
+                    first_line = f.readline().strip()
+                    if first_line:
+                        print(f"   Preview: {first_line[:50]}...")
+                    else:
+                        print("   Preview: Empty file")
+            except Exception as e:
+                print(f"   Preview: ❌ Cannot read ({str(e)})")
+
+
+def rotate_key(config: Dict[str, Any], key_id: str, key_type: str = 'ssh', backup: bool = True) -> None:
+    """Rotate a key by generating new content"""
+    keys = config.get('keys', {})
+
+    if key_id not in keys:
+        print(f"❌ Key '{key_id}' not found")
+        return
+
+    key_config = keys[key_id]
+    file_path = key_config.get('file', '')
+
+    if not file_path:
+        print(f"❌ No file path configured for key '{key_id}'")
+        return
+
+    # Create backup if requested and file exists
+    backup_path = None
+    if backup and os.path.exists(file_path):
+        backup_path = backup_key_file(file_path)
+
+    try:
+        # Generate new key content
+        print(f"🔄 Generating new {key_type} key...")
+        new_content = generate_key_content(key_type)
+
+        # Write new content
+        create_key_file(file_path, new_content)
+
+        print(f"✅ Rotated key '{key_id}'")
+        if backup_path:
+            print(f"   Backup saved: {backup_path}")
+
+    except Exception as e:
+        print(f"❌ Failed to rotate key: {str(e)}")
+
+        # Restore backup if available
+        if backup_path and os.path.exists(backup_path):
+            shutil.copy2(backup_path, file_path)
+            print(f"🔄 Restored from backup")
+
+
+def validate_key_files(config: Dict[str, Any]) -> bool:
+    """Validate all key files exist and are accessible"""
+    keys = config.get('keys', {})
+    all_valid = True
+
+    print("\n" + "=" * 60)
+    print("KEY FILE VALIDATION")
+    print("=" * 60)
+
+    for key_id, key_config in keys.items():
+        file_path = key_config.get('file', '')
+
+        print(f"\n🔍 Validating {key_id}...")
+
+        if not file_path:
+            print(f"   ❌ No file path configured")
+            all_valid = False
+            continue
+
+        path = Path(file_path)
+
+        # Check existence
+        if not path.exists():
+            print(f"   ❌ File does not exist: {file_path}")
+            all_valid = False
+            continue
+
+        # Check readability
+        try:
+            with open(path, 'r') as f:
+                content = f.read()
+                if not content.strip():
+                    print(f"   ⚠️  File is empty")
+                else:
+                    print(f"   ✅ File exists and readable ({len(content)} bytes)")
+        except Exception as e:
+            print(f"   ❌ Cannot read file: {str(e)}")
+            all_valid = False
+            continue
+
+        # Check permissions
+        stat = path.stat()
+        if stat.st_mode & 0o077:
+            print(f"   ⚠️  Permissions may be too permissive: {oct(stat.st_mode)[-3:]}")
+        else:
+            print(f"   ✅ Secure permissions: {oct(stat.st_mode)[-3:]}")
+
+    if all_valid:
+        print(f"\n✅ All key files validated successfully")
+    else:
+        print(f"\n❌ Some key files have issues")
+
+    return all_valid
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Manage emergency access key files and test access",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+Examples:
+  # List all key files and their status
+  python manage_keys.py --list-files
+
+  # Test access to a specific key
+  python manage_keys.py --test-key backup --password "your_password"
+
+  # Test health endpoint
+  python manage_keys.py --test-health --password "health_password"
+
+  # Create a new SSH key file
+  python manage_keys.py --create-key-file /path/to/key.txt --key-type ssh
+
+  # Rotate a key (generate new content)
+  python manage_keys.py --rotate-key backup --key-type ssh --backup
+
+  # Validate all key files
+  python manage_keys.py --validate
+
+  # Generate key content only (print to stdout)
+  python manage_keys.py --generate-content ssh
+        """
+    )
+
+    parser.add_argument(
+        '--config', '-c',
+        default='config.json',
+        help='Configuration file path (default: config.json)'
+    )
+
+    # File operations
+    parser.add_argument(
+        '--list-files', '-l',
+        action='store_true',
+        help='List all key files and their status'
+    )
+
+    parser.add_argument(
+        '--validate', '-v',
+        action='store_true',
+        help='Validate all key files'
+    )
+
+    parser.add_argument(
+        '--create-key-file',
+        help='Create a key file at the specified path'
+    )
+
+    parser.add_argument(
+        '--key-type',
+        choices=['ssh', 'gpg', 'api', 'password', 'generic'],
+        default='ssh',
+        help='Type of key to generate (default: ssh)'
+    )
+
+    parser.add_argument(
+        '--rotate-key',
+        help='Rotate (regenerate) a key by key ID'
+    )
+
+    parser.add_argument(
+        '--backup',
+        action='store_true',
+        default=True,
+        help='Create backup before rotation (default: true)'
+    )
+
+    parser.add_argument(
+        '--no-backup',
+        action='store_true',
+        help='Skip backup creation'
+    )
+
+    parser.add_argument(
+        '--generate-content',
+        help='Generate key content and print to stdout'
+    )
+
+    # Testing operations
+    parser.add_argument(
+        '--test-key',
+        help='Test access to a specific key by key ID'
+    )
+
+    parser.add_argument(
+        '--test-health',
+        action='store_true',
+        help='Test health endpoint access'
+    )
+
+    parser.add_argument(
+        '--password', '-p',
+        help='Password for testing (will prompt if not provided)'
+    )
+
+    parser.add_argument(
+        '--test-all',
+        action='store_true',
+        help='Test access to all configured keys'
+    )
+
+    args = parser.parse_args()
+
+    # Handle backup flag
+    if args.no_backup:
+        args.backup = False
+
+    # Handle generate content
+    if args.generate_content:
+        content = generate_key_content(args.generate_content)
+        print(content)
+        return
+
+    # Load configuration for other operations
+    try:
+        config = load_config(args.config)
+    except Exception as e:
+        print(f"❌ {e}")
+        sys.exit(1)
+
+    try:
+        if args.list_files:
+            list_key_files(config)
+
+        elif args.validate:
+            success = validate_key_files(config)
+            sys.exit(0 if success else 1)
+
+        elif args.create_key_file:
+            content = generate_key_content(args.key_type)
+            create_key_file(args.create_key_file, content)
+
+        elif args.rotate_key:
+            rotate_key(config, args.rotate_key, args.key_type, args.backup)
+
+        elif args.test_key:
+            password = args.password
+            if not password:
+                import getpass
+                password = getpass.getpass(f"Enter password for key '{args.test_key}': ")
+
+            result = test_key_access(config, args.test_key, password)
+
+            if result['success']:
+                print(f"✅ Key '{args.test_key}' access successful")
+                print(f"   URL: {result['url']}")
+                print(f"   Username: {result['username']}")
+            else:
+                print(f"❌ Key '{args.test_key}' access failed")
+                print(f"   URL: {result.get('url', 'N/A')}")
+                print(f"   Error: {result.get('error', 'Unknown error')}")
+                if 'status_code' in result:
+                    print(f"   Status: {result['status_code']}")
+
+        elif args.test_health:
+            password = args.password
+            if not password:
+                import getpass
+                password = getpass.getpass("Enter health endpoint password: ")
+
+            result = test_health_endpoint(config, password)
+
+            if result['success']:
+                print(f"✅ Health endpoint access successful")
+            else:
+                print(f"❌ Health endpoint access failed")
+                print(f"   Error: {result.get('error', 'Unknown error')}")
+
+        elif args.test_all:
+            keys = config.get('keys', {})
+            if not keys:
+                print("No keys configured to test")
+                return
+
+            password = args.password
+            if not password:
+                import getpass
+                password = getpass.getpass("Enter password for key testing: ")
+
+            print(f"\n🧪 Testing access to {len(keys)} keys...")
+
+            for key_id in keys:
+                result = test_key_access(config, key_id, password)
+                status = "✅" if result['success'] else "❌"
+                print(f"   {status} {key_id}")
+
+        else:
+            parser.print_help()
+
+    except Exception as e:
+        print(f"❌ {e}")
+        sys.exit(1)
+
+
+if __name__ == '__main__':
+    main()