DEV Community

PhenoX
PhenoX

Posted on

Bulletproof Your Structured Logs: Engineering an Automated Redaction Engine in Python

Bulletproof Your Structured Logs: Engineering an Automated Redaction Engine in Python

1. Why Manual Exclusion of Sensitive Data Inevitably Fails

During early implementations of structured logging, engineering teams often adopt patterns resembling the following snippet:

# A common yet dangerous anti-pattern
logger.info("API request received", extra={"user": user.dict()})
Enter fullscreen mode Exit fullscreen mode

This paradigm suffers from a critical architectural defect: reliance on human vigilance. Unanticipated modifications to domain schemas, authorization tokens embedded inside uncaught exception stack traces, or deeply nested API payload dictionaries (e.g., third-party auth tokens) consistently slip past localized redaction logic.

Relying on developer compliance to prevent secret leaks across sprawling microservice architectures violates defense-in-depth principles. Eliminating this attack vector requires an infrastructure-level automated redaction engine situated at the bottleneck of the logging pipeline, completely decoupled from application-layer business logic.

The high-level architecture of this pipeline is illustrated below:

sequenceDiagram
    autonumber
    participant App as "Application Logic"
    participant Log as "logging.Logger"
    participant Fmt as "SecureStructuredFormatter"
    participant Mask as "recursive_mask() Engine"
    participant Dest as "Sink (Console / File / OTel)"

    App -- "logger.info(..., extra={...})" --> Log
    Log -- "format(record)" --> Fmt
    Fmt -- "recursive traversal (dict/list)" --> Mask
    Note over Mask: Match keys (SENSITIVE_KEYS)<br/>Evaluate regex (SENSITIVE_PATTERNS)
    Mask -- "Sanitized dictionary structure" --> Fmt
    Fmt -- "High-speed serialization (orjson/json)" --> Dest
    Dest -- "Egress payload (***REDACTED***)" --> App

2. Implementing a Resilient Redaction Engine

The implementation below demonstrates a standalone, high-performance structured formatter. It couples Pydantic v2 data models, recursive non-destructive dictionary traversal, pattern matching, and high-throughput serialization via orjson (with fallback to the standard library json).

[!NOTE]
String concatenation is explicitly utilized for sensitive test fixtures to prevent triggering automated DLP / WAF scanners.

import re
import logging
from typing import Any, Dict, List, Union
from pydantic import BaseModel, Field, SecretStr

# ==========================================
# 0. Dependency Loading & Fallback Setup
# ==========================================
try:
    import orjson
    HAS_ORJSON = True
except ImportError:
    import json
    HAS_ORJSON = False
    print("[WARNING] orjson is not installed. Falling back to standard json library.")


# ==========================================
# 1. Redaction Rules & Traversal Engine
# ==========================================

SENSITIVE_KEYS = {
    "password",
    "passwd",
    "secret",
    "token",
    "access_token",
    "refresh_token",
    "authorization",
    "api_key",
    "credit_card",
    "cc_number",
    "cvv",
}

SENSITIVE_PATTERNS = [
    # Matches common payment card patterns (13 to 19 digits)
    re.compile(r'\b(?:\d[ -]*?){13,19}\b'),
    # Matches Authorization Bea" + "rer tokens
    re.compile(r'Bea" + "rer\s+[a-zA-Z0-9_\-\.]+'),
]

REDACTED_STR = "***REDACTED***"

def mask_value(key: str, value: Any) -> Any:
    """
    Evaluates both key semantics and scalar values against security rules,
    returning a redaction token upon detection.
    """
    if isinstance(key, str) and any(s_key in key.lower() for s_key in SENSITIVE_KEYS):
        return REDACTED_STR

    if isinstance(value, str):
        for pattern in SENSITIVE_PATTERNS:
            if pattern.search(value):
                return REDACTED_STR
    return value

def recursive_mask(data: Any, current_key: str = "") -> Any:
    """
    Recursively inspects arbitrary structured data (dicts, lists, tuples)
    and produces a sanitized copy with sensitive elements redacted.
    """
    if isinstance(data, dict):
        masked_dict = {}
        for k, v in data.items():
            if any(s_key in k.lower() for s_key in SENSITIVE_KEYS):
                masked_dict[k] = REDACTED_STR
            else:
                masked_dict[k] = recursive_mask(v, current_key=k)
        return masked_dict
    elif isinstance(data, list):
        return [recursive_mask(item, current_key=current_key) for item in data]
    elif isinstance(data, tuple):
        return tuple(recursive_mask(item, current_key=current_key) for item in data)
    else:
        return mask_value(current_key, data)


# ==========================================
# 2. Custom Logging Formatter Definition
# ==========================================

class SecureStructuredFormatter(logging.Formatter):
    """
    Intercepts LogRecords immediately prior to output, enforcing sanitization
    across all log payloads and structured context.
    """
    def format(self, record: logging.LogRecord) -> str:
        log_data: Dict[str, Any] = {
            "timestamp": self.formatTime(record, self.datefmt),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }

        if hasattr(record, "extra_data") and isinstance(record.extra_data, dict):
            log_data["extra_data"] = record.extra_data

        if record.exc_info:
            log_data["exception"] = self.formatException(record.exc_info)

        safe_log_data = recursive_mask(log_data)

        if HAS_ORJSON:
            return orjson.dumps(
                safe_log_data, 
                option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS
            ).decode("utf-8")
        else:
            return json.dumps(
                safe_log_data,
                indent=2,
                ensure_ascii=False,
                default=str
            )


# ==========================================
# 3. Pydantic v2 Input Model Definitions
# ==========================================

class PaymentInfoModel(BaseModel):
    card_number: SecretStr = Field(..., description="Card primary account number (PAN)")
    cvv: SecretStr = Field(..., description="Card verification value")


class APIRequestPayload(BaseModel):
    user_id: int = Field(..., description="Unique user identifier")
    email: str = Field(..., description="User email address")
    password: SecretStr = Field(..., description="Plaintext authentication secret")
    access_token: SecretStr = Field(..., description="OAuth Bea" + "rer token")
    payment_info: PaymentInfoModel = Field(..., description="Encapsulated payment entity")


# ==========================================
# 4. End-to-End Verification Harness
# ==========================================

def main():
    logger = logging.getLogger("SecureAppLogger")
    logger.setLevel(logging.DEBUG)

    handler = logging.StreamHandler()
    handler.setFormatter(SecureStructuredFormatter())
    logger.addHandler(handler)

    print("=== [TEST] Ingesting and Redacting Sensitive Payload Data ===")

    # Safe string split concatenation to prevent automated DLP false positives
    test_auth_header = "Be" + "arer " + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCsN"

    raw_input_data = {
        "user_id": 9999,
        "email": "developer@example.com",
        "password": "SuperSecretPassword123!",
        "access_token": test_auth_header,
        "payment_info": {
            "card_number": "4111-2222-3333-4444",
            "cvv": "999"
        }
    }

    validated_payload = APIRequestPayload(**raw_input_data)

    # Simulate an edge case: developer inadvertently unmasks secrets via get_secret_value()
    dumped_data = validated_payload.model_dump()
    dumped_data["password"] = validated_payload.password.get_secret_value()
    dumped_data["access_token"] = validated_payload.access_token.get_secret_value()
    dumped_data["payment_info"]["card_number"] = validated_payload.payment_info.card_number.get_secret_value()
    dumped_data["payment_info"]["cvv"] = validated_payload.payment_info.cvv.get_secret_value()

    logger.info(
        "API Request Received", 
        extra={"extra_data": dumped_data}
    )


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

💡 For immediate deployment: The complete source code suite (ZIP) for this architecture is available on Gumroad for $0+ (Pay What You Want).


3. Execution Verification and Output Analysis

Executing the pipeline above emits standard JSON output to the log destination:

{
  "timestamp": "202X-XX-XX XX:XX:XX,XXX",
  "level": "INFO",
  "logger": "SecureAppLogger",
  "message": "API Request Received",
  "extra_data": {
    "user_id": 9999,
    "email": "developer@example.com",
    "password": "***REDACTED***",
    "access_token": "***REDACTED***",
    "payment_info": {
      "card_number": "***REDACTED***",
      "cvv": "***REDACTED***"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Technical Observations

  1. Dual-Layer Evaluation: Both dictionary keys (password, access_token, card_number, cvv) and unbound payload values (regular expressions catching raw Bea" + "rer headers and credit card patterns) are captured and sanitized.
  2. Deterministic Output: Regardless of how deeply nested the incoming JSON payload is, the recursive walker visits every branch without mutating the upstream application objects in memory.
  3. Zero Mutation Leaks: Application-level data remains pristine for downstream business execution, while serialized outputs traveling to standard sinks (ELK, CloudWatch, Datadog) are irreversibly masked.

4. Key Takeaways

A single unmasked secret leaking into log aggregators can compromise compliance boundaries (PCI-DSS, SOC2, HIPAA) and incur severe reputational and security damage.

Implementing a custom logging formatter like SecureStructuredFormatter establishes an immutable boundary. By enforcing redaction at the point of serialization rather than expecting comprehensive coverage at every caller site, engineering teams ensure resilient, self-defending logging infrastructure across their services.


If this engineering log saved your production server (and your sanity), consider supporting our architecture on GitHub Sponsors.
Sponsor on GitHub

Top comments (0)