Browse Source

reasonable key import

zehe 3 months ago
parent
commit
d54fcf4b8e
4 changed files with 87 additions and 1069 deletions
  1. 85 427
      add_key.py
  2. 0 88
      example_key_setup.sh
  3. 2 10
      install.sh
  4. 0 544
      manage_keys.py

+ 85 - 427
add_key.py

@@ -1,484 +1,142 @@
 #!/usr/bin/env python3
 
 """
-Add Key Script for Emergency Access Server
-Adds new emergency access keys to the configuration with proper validation
+Simple script to add a key to the emergency access server
+Takes an existing key file, copies it to the configured directory, and adds it to config
 """
 
 import json
-import yaml
 import os
 import sys
-import argparse
+import shutil
 import secrets
-import string
 import hashlib
-import shutil
+import argparse
 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"""
+    """Generate 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}")
+def generate_secure_password(length: int = 32) -> str:
+    """Generate a secure password"""
+    import string
+    alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
+    return ''.join(secrets.choice(alphabet) for _ in range(length))
 
-    # 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
+def load_config(config_path: str):
+    """Load configuration file"""
+    with open(config_path, 'r') as f:
+        return json.load(f)
 
-        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
+def save_config(config, config_path: str):
+    """Save configuration file"""
+    with open(config_path, 'w') as f:
+        json.dump(config, f, indent=2)
 
-    # 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)
+def copy_key_file(source_path: str, dest_path: str):
+    """Copy key file to destination with proper permissions"""
+    # Create destination directory if needed
+    dest_dir = os.path.dirname(dest_path)
+    os.makedirs(dest_dir, exist_ok=True)
 
-    # Get notification backends
-    if available_backends:
-        print(f"\nAvailable backends: {', '.join(available_backends)}")
+    # Copy file
+    shutil.copy2(source_path, dest_path)
 
-    while True:
-        backends_input = input("Enter notification backends (comma-separated): ").strip()
-        backends = [b.strip() for b in backends_input.split(',') if b.strip()]
+    # Set secure permissions
+    os.chmod(dest_path, 0o600)
 
+    # Try to set ownership to emergency-access user if running as root
+    if os.geteuid() == 0:  # Running as root
         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)}")
+            import pwd, grp
+            user = pwd.getpwnam('emergency-access')
+            group = grp.getgrnam('emergency-access')
+            os.chown(dest_path, user.pw_uid, group.gr_gid)
+            print(f"✅ Copied key file with emergency-access ownership: {dest_path}")
+        except (KeyError, OSError):
+            print(f"✅ Copied key file: {dest_path}")
+            print(f"⚠️  Run: sudo chown emergency-access:emergency-access {dest_path}")
+    else:
+        print(f"✅ Copied key file: {dest_path}")
+        print(f"⚠️  Run: sudo chown emergency-access:emergency-access {dest_path}")
 
 
-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'] = {}
+def main():
+    parser = argparse.ArgumentParser(description="Add a key to emergency access server")
+    parser.add_argument('key_id', help='Key identifier (e.g., backup, master)')
+    parser.add_argument('key_file', help='Path to existing key file')
+    parser.add_argument('backends', help='Comma-separated notification backends')
+    parser.add_argument('--config', default='config.json', help='Config file path')
+    parser.add_argument('--dest-dir', default='/etc/emergency-access', help='Destination directory')
+    parser.add_argument('--password', help='Password (generated if not provided)')
+    parser.add_argument('--message', help='Custom notification message')
 
-    config['keys'][key_id] = key_config
+    args = parser.parse_args()
 
+    # Check if source key file exists
+    if not os.path.exists(args.key_file):
+        print(f"❌ Key file not found: {args.key_file}")
+        sys.exit(1)
 
-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)
+    # Load config
+    try:
+        config = load_config(args.config)
+    except FileNotFoundError:
+        print(f"❌ Config file not found: {args.config}")
+        sys.exit(1)
 
-    # 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)
+    # Check if key already exists
+    if args.key_id in config.get('keys', {}):
+        print(f"❌ Key '{args.key_id}' already exists")
+        sys.exit(1)
 
     # Generate password if not provided
     if args.password:
         password = args.password
     else:
-        password = generate_secure_password(64)
-        print(f"🔐 Generated password: {password}")
+        password = generate_secure_password()
+        print(f"🔑 Generated password: {password}")
 
+    # Set up paths and config
+    dest_file = os.path.join(args.dest_dir, f"{args.key_id}-key.txt")
+    route = f"/emergency-key-{args.key_id}"
+    username = f"emergency_{args.key_id}"
     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}"
+    backends = [b.strip() for b in args.backends.split(',')]
     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)
+    # Copy key file
+    copy_key_file(args.key_file, dest_file)
+
+    # Add to config
+    if 'keys' not in config:
+        config['keys'] = {}
 
-    # Add key to config
-    add_key_to_config(config, args.key_id, {
+    config['keys'][args.key_id] = {
         'route': route,
-        'file': args.file,
+        'file': dest_file,
         'username': username,
         'password_hash': password_hash,
-        'backends': args.backends,
+        'backends': backends,
         'message': message
-    })
+    }
 
-    save_config(config, config_path)
+    # Save config
+    save_config(config, args.config)
 
-    print(f"\n✅ Successfully added key '{args.key_id}'")
+    print(f"✅ Added key '{args.key_id}' to configuration")
     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)
+    print(f"   Backends: {', '.join(backends)}")
 
 
 if __name__ == '__main__':

+ 0 - 88
example_key_setup.sh

@@ -1,88 +0,0 @@
-#!/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."

+ 2 - 10
install.sh

@@ -105,19 +105,13 @@ install_application() {
     cp config.py "$INSTALL_DIR/"
     cp requirements.txt "$INSTALL_DIR/"
 
-    # Copy key management scripts
+    # Copy key management script
     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"
@@ -260,9 +254,7 @@ print_final_instructions() {
     echo ""
     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 "  Add new key:        cd $INSTALL_DIR && python add_key.py KEY_ID /path/to/existing/key.txt backends"
     echo "  Dummy file:         $CONFIG_DIR/dummy.txt"
     echo "  Log file:           $LOG_FILE"
     echo

+ 0 - 544
manage_keys.py

@@ -1,544 +0,0 @@
-#!/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()