Files
shopdb-flask/CERT-AUTH-SECURITY.md
Proudlock 5cd5332397
Some checks failed
CI / backend (push) Has been cancelled
CI / naming (push) Has been cancelled
CI / gates (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / lean-build (push) Has been cancelled
Add agent fingerprinting, client certificate auth, and security audit documentation
This commit adds:
- Database migrations for app_delta fingerprints and client certificates
- Scripts for certificate generation and fingerprint backfilling
- Comprehensive security audit reports and findings tracking
- Implementation documentation for certificate authentication
- Test utilities for certificate renewal and fingerprinting
- GE-ENFORCE collector fix documentation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-09-15 09:18:12 -04:00

12 KiB

Client Certificate Authentication Security Documentation

Overview

ShopDB endpoint agent uses mTLS (mutual TLS) for per-endpoint authentication. This document covers security considerations, deployment requirements, and operational procedures.

Last Updated: 2026-09-14
Status: Production-ready with documented limitations


Security Model

Authentication Flow

  1. Agent Startup: Loads client certificate from LocalMachine\My by thumbprint
  2. HTTPS Connection: Presents certificate to nginx during TLS handshake
  3. Nginx Validation: Accepts certificate (no CA validation - self-signed OK)
  4. Fingerprint Extraction: Nginx computes SHA1, passes to Flask via header
  5. Flask Authentication: Looks up asset by fingerprint, checks revocation/expiration
  6. Authorization: Verifies cert's asset matches payload's hostname asset
  7. Fallback: If cert auth fails, falls back to collector API key

Dual-Auth During Rollout

  • Phase 1-3: Both cert AND API key accepted (backward compatible)
  • Phase 4: API key disabled via plugin.endpoints.envkeyfallback=false
  • Rollback: Re-enable API key fallback instantly if cert auth breaks

Known Limitations

1. SHA1 Fingerprints (nginx limitation)

Issue: Nginx $ssl_client_fingerprint outputs SHA1 (40 hex chars), not SHA256.

Why SHA1?

  • Nginx does not expose SHA256 fingerprint via built-in variables
  • Would require custom nginx module or full certificate parsing in Flask

Is SHA1 Acceptable?

  • ✅ YES for certificate fingerprinting (not signing)
  • ✅ Acceptable in air-gapped environments with:
    • Short-lived certificates (2 years max)
    • Certificate revocation support (cert_revoked_at column)
    • Self-signed certificates only (no external CA trust)

SHA1 Collision Risk:

  • Computationally expensive to generate collision (estimated $100K+ in 2026)
  • Would require attacker to generate cert with same SHA1 as legitimate endpoint
  • Air-gapped environment prevents external certificate injection
  • Revocation support allows instant kill switch if compromised

Migration to SHA256:

Option 1: Custom nginx module to expose SHA256
Option 2: Migrate to IIS (exposes full PEM cert for server-side hashing)
Option 3: Pass full certificate to Flask for SHA256 computation (bandwidth cost)

2. Private Key Permissions (tray access)

Issue: PowerShell script grants BUILTIN\Users read access to private key.

Why? Tray app (running as interactive user) needs cert access for manual agent runs.

Risk: ANY interactive user on the machine can extract the private key.

Mitigations:

  1. Best: Remove tray dependency (tray calls service API, service uses cert)
  2. Acceptable: Grant only to specific user account running tray
  3. Current: Document that tray deployment requires trusted console users only

When Acceptable:

  • Shop floor PCs with single trusted operator per machine
  • Kiosks where all users are authorized operators
  • Environments where console access = trusted

NOT Acceptable:

  • Multi-user workstations with untrusted users
  • Shared administrator workstations
  • Development machines

3. No Certificate Revocation List (CRL)

Issue: No automated CRL distribution. Revocation is manual SQL UPDATE.

Workaround:

-- Revoke certificate (takes effect immediately)
UPDATE assets
SET cert_revoked_at = CURRENT_TIMESTAMP,
    cert_revocation_reason = 'Compromised - laptop stolen - TICKET-456'
WHERE client_cert_fingerprint = 'abc123...';

Future: Web UI for certificate management (provision, revoke, monitor expiration).


Security Features Implemented

✅ Constant-Time Fingerprint Comparison

Prevents timing attacks on fingerprint validation:

if not hmac.compare_digest(asset.client_cert_fingerprint, fingerprint):
    return None  # Constant time, no early exit

✅ Format Validation

Rejects malformed fingerprints before database lookup:

# Must be 40 hex chars (SHA1) or 64 hex chars (SHA256)
if len(normalized) not in (40, 64):
    return None
if not re.match(r'^[0-9a-f]+$', normalized):
    return None

✅ Certificate Revocation Support

Admin can revoke compromised certificates instantly:

-- Revoke
UPDATE assets SET cert_revoked_at = NOW(), cert_revocation_reason = '...' WHERE ...;
-- Un-revoke (if cert was revoked by mistake)
UPDATE assets SET cert_revoked_at = NULL, cert_revocation_reason = NULL WHERE ...;

✅ Expiration Enforcement

Expired certificates rejected automatically:

if asset.cert_expires_at < now:
    return None  # Must regenerate and re-register

✅ Authorization Check

Certificate authenticates asset X, but payload reports for asset Y → DENIED:

if g.cert_authenticated_asset_id != assetid:
    raise PermissionError(...)  # Prevents cross-asset impersonation

✅ Audit Logging

All authentication attempts logged to AuditLog:

  • auth.cert.success - Successful authentication
  • auth.cert.unknown - Unknown fingerprint
  • auth.cert.revoked - Attempt to use revoked cert
  • auth.cert.expired - Attempt to use expired cert

Deployment Requirements

Phase 1: Server Deployment

  1. Deploy nginx with mTLS support:

    # docker-compose.yml
    services:
      nginx:
        image: nginx:alpine
        volumes:
          - ./nginx.conf:/etc/nginx/nginx.conf
          - ./certs:/etc/nginx/certs
    
  2. Run database migrations:

    # Adds client_cert_fingerprint, cert_issued_at, cert_expires_at, 
    # cert_revoked_at, cert_revocation_reason columns
    flask db upgrade
    
  3. Verify fallback still works:

    # Existing agents continue using API key
    curl -H "X-API-Key: $COLLECTOR_API_KEY" https://shopdb/api/collector/endpoints
    

Phase 2: Certificate Generation (per endpoint)

  1. Run PowerShell script on endpoint:

    .\generate-agent-cert.ps1 -ValidityYears 2
    
  2. Script outputs SQL to register certificate:

    UPDATE assets
    SET client_cert_fingerprint = 'abc123...',
        cert_issued_at = CURRENT_TIMESTAMP,
        cert_expires_at = '2028-09-14 12:00:00',
        cert_revoked_at = NULL,
        cert_revocation_reason = NULL
    WHERE assetnumber = 'G5QX1GT3ESF';
    
  3. Verify agent config updated:

    {
      "baseurl": "https://shopdb.company.com",
      "apikey": "collector_key_here",
      "clientCertThumbprint": "ABC123...",
      "cadenceseconds": 900
    }
    
  4. Restart agent service:

    Restart-Service ShopDbEndpointAgent
    
  5. Verify cert auth in logs:

    [client_cert] Authenticated asset 1234 (number=G5QX1GT3ESF) via certificate abc123...
    

Phase 3: Fleet Rollout

Automation Required:

  • PowerShell script to generate certs across fleet
  • CSV export of asset_number,fingerprint,expires_at
  • SQL bulk INSERT script
  • Group Policy or deployment tool to push agent config updates

Validation:

  • Monitor auth.cert.success audit log entries
  • Dashboard showing cert expiration dates
  • Alert on endpoints still using API key after N days

Phase 4: Retire API Key Fallback

Only after 100% cert coverage verified:

UPDATE settings
SET value = 'false'
WHERE key = 'plugin.endpoints.envkeyfallback';

Revoke old collector API key tokens:

# In Flask admin UI or API
revoke_service_tokens('collector.ingest.endpoints')

CRITICAL: Test rollback plan before this step!


Operational Procedures

Certificate Rotation

When to rotate:

  • 30 days before expiration (automated alert recommended)
  • After suspected compromise
  • After employee termination (if they had console access)

How to rotate:

  1. Generate new certificate (keep old one installed temporarily)
  2. Update database with new fingerprint
  3. Update agent config with new thumbprint
  4. Restart agent
  5. Verify new cert working
  6. Remove old cert from certificate store

Certificate Revocation

Immediate revocation (compromised/lost endpoint):

UPDATE assets
SET cert_revoked_at = CURRENT_TIMESTAMP,
    cert_revocation_reason = 'Laptop stolen - INCIDENT-789'
WHERE assetnumber = 'G5QX1GT3ESF';

Effect: Agent's next request denied, falls back to API key if enabled.

Monitoring

Key metrics to monitor:

  1. Cert Expiration:

    SELECT assetnumber, cert_expires_at,
           DATEDIFF(cert_expires_at, NOW()) AS days_remaining
    FROM assets
    WHERE cert_expires_at IS NOT NULL
      AND cert_expires_at < DATE_ADD(NOW(), INTERVAL 30 DAY)
    ORDER BY cert_expires_at;
    
  2. Auth Failure Rate:

    SELECT action, COUNT(*) AS count
    FROM auditlog
    WHERE action LIKE 'auth.cert.%'
      AND createdat > NOW() - INTERVAL 24 HOUR
    GROUP BY action;
    
  3. Endpoints Still Using API Key:

    SELECT e.hostname, e.lastreportedat
    FROM endpointstates e
    JOIN assets a ON a.assetid = e.assetid
    WHERE a.client_cert_fingerprint IS NULL
      AND e.lastreportedat > NOW() - INTERVAL 1 HOUR;
    

Troubleshooting

Agent reports but logs show API key auth (not cert)

Cause: Client certificate not configured or not found.

Fix:

# Verify cert exists
Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Subject -like "*$env:COMPUTERNAME*" }

# Verify config
Get-Content C:\ProgramData\ShopDB\config.json | ConvertFrom-Json

# Verify cert has private key
$cert = Get-Item "Cert:\LocalMachine\My\$thumbprint"
$cert.HasPrivateKey  # Should be True

"Revoked certificate" in logs but cert not revoked

Cause: Database cert_revoked_at column set incorrectly.

Fix:

UPDATE assets
SET cert_revoked_at = NULL,
    cert_revocation_reason = NULL
WHERE assetnumber = 'G5QX1GT3ESF';

"Unknown fingerprint" in logs

Cause: Certificate fingerprint not registered in database.

Fix:

# Get fingerprint from cert
$cert = Get-Item "Cert:\LocalMachine\My\$thumbprint"
$certBytes = $cert.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
$sha1 = [System.Security.Cryptography.SHA1]::Create()
$fp = ($sha1.ComputeHash($certBytes) | ForEach-Object { $_.ToString("x2") }) -join ""

# Register in database
UPDATE assets SET client_cert_fingerprint = '$fp' WHERE assetnumber = 'G5QX1GT3ESF';

Agent fails after cert rotation

Cause: Old cert removed before new cert registered in database.

Fix:

  1. Reinstall old cert temporarily
  2. Register new cert fingerprint in database
  3. Update agent config to new thumbprint
  4. Restart agent
  5. Remove old cert

Security Best Practices

✅ DO

  • Generate certificates with 2-year maximum validity
  • Rotate certificates 30 days before expiration
  • Revoke certificates immediately when endpoint compromised
  • Monitor audit logs for revoked/expired cert attempts
  • Document certificate issuance and revocation in tickets
  • Test rollback to API key before disabling fallback
  • Use short-lived certificates in untrusted environments

❌ DON'T

  • Grant BUILTIN\Users access to private keys on multi-user machines
  • Skip hostname validation in cert generation script
  • Deploy to production without testing cert revocation
  • Disable API key fallback without verified 100% cert coverage
  • Use expired certificates (regenerate immediately)
  • Share certificates between endpoints
  • Export private keys to network shares

Future Enhancements

  1. Web UI for Certificate Management

    • Upload CSRs or paste fingerprints
    • View expiration dashboard
    • One-click revocation
    • Automated renewal reminders
  2. SHA256 Migration

    • Custom nginx module for SHA256
    • Or migrate to IIS deployment
    • Dual-column support (SHA1 + SHA256) during migration
  3. Automated Certificate Rotation

    • Agent auto-generates new cert 30 days before expiration
    • Server auto-accepts new fingerprint after validating old cert
    • Zero-touch renewal
  4. Per-User Certificate ACLs

    • Grant tray access to specific user account only
    • Service creates runas token for tray HTTP requests

References

  • ADR-006: Collector Contract (dual-auth model)
  • ADR-016: Credential Delivery (provisioning workflow)
  • AGENT-CONTRACT.md: Wire protocol and authentication
  • nginx.conf: mTLS configuration and SHA1 limitation
  • generate-agent-cert.ps1: Certificate generation script

Contact

For security questions or to report vulnerabilities:

  • Internal: File ticket in JIRA under "Security" component
  • Emergency: Page on-call security engineer via PagerDuty