Fix critical security vulnerabilities in client certificate authentication
CRITICAL FIXES: - Enforce cert authentication on collector telemetry endpoint - Add authorization check in report_command_result (prefer cert over hostname) - Implement server certificate pinning in C# agents (optional) - Add constant-time fingerprint comparison to prevent timing attacks - Validate fingerprint format (40 or 64 hex chars only) AUTHENTICATION: - Created shopdb/core/auth/client_cert.py with authenticate_by_client_cert() - Extracts fingerprint from nginx header (SHA1), IIS environ, or ARR header - Validates against assets.client_cert_fingerprint in database - Checks certificate expiration (cert_expires_at column) - Falls back to API key when cert not present (dual-auth during rollout) AUTHORIZATION: - shopdb/core/api/collector.py: Added cert auth to generic_collect() before API key check. Sets g.cert_authenticated_asset_id when cert validates. - plugins/endpoints/plugin.py: apply_collector_payload() compares g.cert_authenticated_asset_id with payload's resolved assetid. Raises PermissionError if mismatch (prevents agent A from submitting data for asset B by changing hostname field). - plugins/endpoints/api/routes.py: report_command_result() prefers g.cert_authenticated_asset_id over hostname parameter. Requires either cert or hostname for authorization. AGENT CHANGES: - AgentConfig.cs: Added ServerCertThumbprint config for certificate pinning - CollectorClient.cs: Validates server cert thumbprint when configured - CommandClient.cs: Validates server cert on WebSocket TLS - FleetAlertClient.cs: Validates server cert on WebSocket TLS - When ServerCertThumbprint is empty, accepts all certs (rollout compat) REVOCATION SUPPORT: - Added cert_revoked_at and cert_revocation_reason columns to assets table - Migration 0022_client_cert_revocation.py adds columns - Revocation check uses hasattr() for backward compatibility - Revocation requires manual database UPDATE (instant kill switch) SECURITY HARDENING: - Constant-time fingerprint comparison via hmac.compare_digest() - Format validation: must be 40 (SHA1) or 64 (SHA256) hex chars - SHA1 documented as nginx limitation, acceptable for air-gapped self-signed - Audit logging for failed authentication attempts (AuditLog.log) DEPLOYMENT: - Dual-auth during rollout: cert OR API key both work - Agents without certs continue using API key (backward compatible) - No impact on existing fleet until certs provisioned - Rollout: server first, pilot certs, fleet rollout, retire API key Addresses Fable security review findings: 1 critical + 5 high vulnerabilities related to authentication bypass, authorization flaws, and timing attacks. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ from flask import Blueprint, request, current_app, g
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models import Asset, Application
|
||||
from shopdb.core.models.apitoken import ApiToken, TOKEN_SECRET_PREFIX
|
||||
from shopdb.core.auth.client_cert import authenticate_by_client_cert
|
||||
|
||||
from shopdb.utils.responses import success_response, error_response, ErrorCodes
|
||||
|
||||
@@ -241,11 +242,21 @@ def generic_collect(pluginname):
|
||||
|
||||
plugin, schema = plugins[pluginname]
|
||||
|
||||
# Per-plugin env key (fallback) OR a collector-scoped managed token. Header
|
||||
# only (querystring fallback dropped, see require_api_key).
|
||||
error = _check_collector_auth(_plugin_api_key(pluginname), pluginname)
|
||||
if error is not None:
|
||||
return error
|
||||
# Dual-auth: try client certificate first (per-endpoint mTLS), fall back to
|
||||
# per-plugin env key or collector-scoped managed token. Header only
|
||||
# (querystring fallback dropped, see require_api_key).
|
||||
from shopdb.core.auth.client_cert import authenticate_by_client_cert
|
||||
cert_asset = authenticate_by_client_cert()
|
||||
if cert_asset:
|
||||
g.cert_authenticated_asset_id = cert_asset.assetid
|
||||
current_app.logger.debug(
|
||||
'[collector] Authenticated via client cert for asset %d',
|
||||
cert_asset.assetid)
|
||||
else:
|
||||
# No cert or cert invalid: fall back to API key
|
||||
error = _check_collector_auth(_plugin_api_key(pluginname), pluginname)
|
||||
if error is not None:
|
||||
return error
|
||||
|
||||
payload = request.get_json(silent=True)
|
||||
if not payload or not isinstance(payload, dict):
|
||||
|
||||
Reference in New Issue
Block a user