From 6c7050c10272e23550cdd1c165ac02f59f65c48d Mon Sep 17 00:00:00 2001 From: Rod Christiansen Date: Wed, 21 Jan 2026 19:02:17 -0800 Subject: [PATCH] Add API key authentication for /checkin/ and /verify/ endpoints - Add APIKeyAuthMiddleware in server/middleware.py - Middleware checks X-API-Key header against CRYPT_API_KEY env var - Returns 401 Unauthorized if key is missing/invalid - Backward compatible: if CRYPT_API_KEY not set, endpoints remain open - Uses timing-safe comparison to prevent timing attacks - Update system_settings.py to include middleware - Add documentation to README --- README.md | 39 ++++++++++++ fvserver/system_settings.py | 1 + server/middleware.py | 115 ++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 server/middleware.py diff --git a/README.md b/README.md index eaae4c1..2cc6f96 100644 --- a/README.md +++ b/README.md @@ -60,3 +60,42 @@ Approve Request: Key Retrieval: ![Key Retrieval](https://raw.github.com/grahamgilbert/Crypt-Server/master/docs/images/key_retrieval.png) + +## API Key Authentication + +Crypt Server supports API key authentication for the `/checkin/` and `/verify/` endpoints. This adds a layer of security to prevent unauthorized key escrow requests. + +### Configuration + +Set the `CRYPT_API_KEY` environment variable to enable API key authentication: + +```bash +# Docker Compose +environment: + - CRYPT_API_KEY=your-secret-api-key-here + +# Or export directly +export CRYPT_API_KEY=your-secret-api-key-here +``` + +### Client Configuration + +Clients must include the API key in the `X-API-Key` header: + +```bash +curl -X POST https://crypt.example.com/checkin/ \ + -H "X-API-Key: your-secret-api-key-here" \ + -d "serial=ABC123&recovery_password=..." +``` + +### Generating a Secure API Key + +Generate a strong API key using Python: + +```bash +python3 -c "import secrets; print(secrets.token_urlsafe(32))" +``` + +### Backward Compatibility + +If `CRYPT_API_KEY` is not set, the endpoints remain open for backward compatibility with existing clients that don't support API key authentication. diff --git a/fvserver/system_settings.py b/fvserver/system_settings.py index 0acc033..09fc646 100644 --- a/fvserver/system_settings.py +++ b/fvserver/system_settings.py @@ -125,6 +125,7 @@ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", + "server.middleware.APIKeyAuthMiddleware", # API key auth for /checkin/ and /verify/ "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", diff --git a/server/middleware.py b/server/middleware.py new file mode 100644 index 0000000..05196ca --- /dev/null +++ b/server/middleware.py @@ -0,0 +1,115 @@ +""" +API Key Authentication Middleware for Crypt Server. + +This middleware protects the /checkin/ and /verify/ API endpoints +with API key authentication while leaving the web UI accessible +for SSO/session-based authentication. + +Configuration: + Set the CRYPT_API_KEY environment variable to enable API key authentication. + If not set, the endpoints remain open (backward compatible). + +Usage: + Clients must include the API key in the X-API-Key header: + curl -X POST https://crypt.example.com/checkin/ \ + -H "X-API-Key: your-secret-key" \ + -d "serial=ABC123&recovery_password=..." +""" +import os +import hmac +import logging +from django.http import JsonResponse +from django.conf import settings + +logger = logging.getLogger(__name__) + + +class APIKeyAuthMiddleware: + """ + Middleware to authenticate API requests using an API key. + + Only applies to: + - /checkin/ (POST) + - /verify/// (GET) + + The API key is read from: + 1. CRYPT_API_KEY environment variable + 2. settings.CRYPT_API_KEY (if defined in settings.py) + + If no API key is configured, requests are allowed through (backward compatible). + """ + + # Paths that require API key authentication + PROTECTED_PATHS = ['/checkin/', '/verify/'] + + # Header name for API key + API_KEY_HEADER = 'HTTP_X_API_KEY' # Django converts X-API-Key to HTTP_X_API_KEY + + def __init__(self, get_response): + self.get_response = get_response + self.api_key = self._get_api_key() + + if self.api_key: + logger.info("API key authentication enabled for /checkin/ and /verify/ endpoints") + else: + logger.warning( + "No CRYPT_API_KEY configured. API endpoints are UNPROTECTED. " + "Set CRYPT_API_KEY environment variable to enable authentication." + ) + + def _get_api_key(self): + """Get API key from environment or settings.""" + # Try environment variable first + api_key = os.environ.get('CRYPT_API_KEY') + if api_key: + return api_key.strip() + + # Fall back to settings + if hasattr(settings, 'CRYPT_API_KEY'): + return getattr(settings, 'CRYPT_API_KEY', '').strip() + + return None + + def _is_protected_path(self, path): + """Check if the request path requires API key authentication.""" + for protected in self.PROTECTED_PATHS: + if path.startswith(protected): + return True + return False + + def __call__(self, request): + # Only check protected API endpoints + if self._is_protected_path(request.path): + # If no API key is configured, allow request (backward compatible) + if not self.api_key: + return self.get_response(request) + + # Get API key from request header + request_api_key = request.META.get(self.API_KEY_HEADER) + + if not request_api_key: + logger.warning( + "API request to %s without API key from %s", + request.path, + request.META.get('REMOTE_ADDR', 'unknown') + ) + return JsonResponse( + {'error': 'API key required. Include X-API-Key header.'}, + status=401 + ) + + # Validate API key (constant-time comparison to prevent timing attacks) + if not hmac.compare_digest(request_api_key, self.api_key): + logger.warning( + "Invalid API key for %s from %s", + request.path, + request.META.get('REMOTE_ADDR', 'unknown') + ) + return JsonResponse( + {'error': 'Invalid API key.'}, + status=403 + ) + + logger.debug("API key validated for %s", request.path) + + return self.get_response(request)