Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions fvserver/system_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
115 changes: 115 additions & 0 deletions server/middleware.py
Original file line number Diff line number Diff line change
@@ -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/<serial>/<secret_type>/ (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."
)
Comment on lines +51 to +58
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This middleware is always installed via MIDDLEWARE, and when CRYPT_API_KEY is not set it logs a WARNING that endpoints are "UNPROTECTED". In environments that intentionally run without an API key (backward compatibility), this can create noisy/alert-worthy logs on every startup. Consider lowering this to INFO/DEBUG or only warning when an explicit setting indicates API key auth is required.

Suggested change
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."
)
# Optional setting to indicate that API key auth is required and absence is a misconfiguration.
self.require_api_key = getattr(settings, 'REQUIRE_CRYPT_API_KEY_AUTH', False)
if self.api_key:
logger.info("API key authentication enabled for /checkin/ and /verify/ endpoints")
else:
if self.require_api_key:
logger.warning(
"No CRYPT_API_KEY configured while REQUIRE_CRYPT_API_KEY_AUTH is True. "
"API endpoints are UNPROTECTED and this is likely a misconfiguration. "
"Set CRYPT_API_KEY environment variable to enable authentication."
)
else:
logger.info(
"No CRYPT_API_KEY configured. API endpoints are UNPROTECTED "
"(backward-compatible mode). Set CRYPT_API_KEY environment "
"variable to enable authentication."
)

Copilot uses AI. Check for mistakes.

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()

Comment on lines +66 to +70
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_api_key() assumes settings.CRYPT_API_KEY is a string and calls .strip() unconditionally when the attribute exists. If a deployment sets CRYPT_API_KEY = None (or any non-str), this will raise at startup and break all requests. Consider handling None/non-string values safely (e.g., coerce to ''/str before strip, or return None when falsy).

Suggested change
# Fall back to settings
if hasattr(settings, 'CRYPT_API_KEY'):
return getattr(settings, 'CRYPT_API_KEY', '').strip()
# Fall back to settings
if hasattr(settings, 'CRYPT_API_KEY'):
raw_key = getattr(settings, 'CRYPT_API_KEY', None)
if not raw_key:
return None
if isinstance(raw_key, str):
api_key = raw_key.strip()
else:
# Coerce non-string values to string safely
api_key = str(raw_key).strip()
return api_key or None

Copilot uses AI. Check for mistakes.
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(
Comment on lines +87 to +103
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The incoming header value isn’t normalized before comparison. A client/proxy can include leading/trailing whitespace in X-API-Key, which will cause a mismatch even when the key is otherwise correct. Consider stripping the request-provided key (and possibly rejecting empty-after-strip) before calling compare_digest().

Copilot uses AI. Check for mistakes.
"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)
Comment on lines +80 to +115
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new auth behavior is security-sensitive and currently has no automated tests. Since the repo already has Django TestCase coverage in server/tests.py, it would be good to add tests for: (1) when CRYPT_API_KEY is unset, requests to /checkin/ and /verify/ succeed as before; (2) when set, missing key returns 401; (3) wrong key returns 403; (4) correct key passes through.

Copilot uses AI. Check for mistakes.
Loading