Skip to content

Security: cachekit-io/cachekit-py

SECURITY.md

Security Policy

Comprehensive security documentation for the cachekit Python SDK.


Table of Contents


Supported Versions

Version Supported
0.4.x βœ…
0.3.x βœ…
< 0.3 ❌

Note

As a young project, we maintain security support for the latest release only. Once we reach 1.0.0, we will establish a longer-term LTS policy.


Reporting a Vulnerability

Important

We take security seriously. If you discover a security vulnerability, please report it responsibly.

Reporting Channels

Channel Use Case
security@cachekit.io Preferred for sensitive issues
GitHub Security Advisory Public vulnerability reports

What to Include

  • Description of the vulnerability
  • Steps to reproduce
  • Affected versions
  • Potential impact
  • Suggested fix (if available)

Response Timeline

Stage Timeline
Initial Response 48 hours
Status Update 7 days
Fix Timeline Varies by severity
πŸ“‹ Disclosure Policy

We follow coordinated disclosure:

  1. Acknowledge receipt within 48 hours
  2. Confirm vulnerability and determine severity
  3. Develop and test fix
  4. Release security patch
  5. Public disclosure after patch availability (coordinated with reporter)

Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     cachekit Python SDK                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   @cache     β”‚  β”‚   @cache     β”‚  β”‚   Redis/CachekitIO    β”‚  β”‚
β”‚  β”‚  Decorator   β”‚  β”‚   .secure    β”‚  β”‚      Backend          β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚         β”‚                 β”‚                      β”‚              β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
β”‚                  β”‚                                              β”‚
β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”                                     β”‚
β”‚         β”‚   PyO3 FFI      β”‚  ◄── This repo                      β”‚
β”‚         β”‚   Wrapper       β”‚                                     β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   β”‚
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚   cachekit-core   β”‚  ◄── Separate crate
         β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
         β”‚  β”‚ AES-256-GCM β”‚  β”‚
         β”‚  β”‚ LZ4 Compressβ”‚  β”‚
         β”‚  β”‚ xxHash3     β”‚  β”‚
         β”‚  β”‚ HKDF        β”‚  β”‚
         β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Component Responsibility
cachekit-core (Rust) Compression, checksums, encryption, formal verification
cachekit SDK (this repo) PyO3 FFI wrapper, decorators, Redis backend, configuration

Tip

For comprehensive security details about core cryptographic operations, see cachekit-core SECURITY.md.

This document focuses on Python SDK-specific security: FFI boundary, configuration, and Python-layer tooling.


Python SDK Security Features

No Untrusted Deserialization

Caution

cachekit NEVER uses Python's pickle module due to arbitrary code execution risks (CWE-502).

We use MessagePack (safe binary serialization) with type preservation via schema metadata.

- import pickle  # NEVER - arbitrary code execution
+ import msgpack  # Safe binary serialization

Zero-Knowledge Encryption

When enabled via @cache.secure, client-side AES-256-GCM encryption ensures the server never sees plaintext:

Property Guarantee
Encryption timing Before data touches Redis
Server visibility Opaque ciphertext only
Key derivation HKDF with per-tenant salts
Authentication GCM tags prevent tampering
Compliance GDPR/HIPAA/PCI-DSS ready
πŸ” Master Key Security
Requirement Implementation
Key size Minimum 32 bytes (256 bits)
Configuration CACHEKIT_MASTER_KEY env var
Logging Never exposed in logs/errors
Derivation HKDF with unique tenant salts
⚑ L1 Cache Behavior
Mode L1 Storage L2 Storage Performance
@cache Plaintext Plaintext ~50ns L1 / ~2-7ms L2
@cache.secure Encrypted Encrypted ~50ns L1 / ~2-7ms L2

Both tiers store encrypted bytes when encryption is enabled (encrypt-at-rest everywhere). Decryption happens at read time only, minimizing plaintext exposure.

Note

All cryptographic operations are implemented in cachekit-core. See cachekit-core SECURITY.md for AES-256-GCM, HKDF, and formal verification details.

Sensitive Configuration Masking

All sensitive values are automatically masked:

Context Masked
Structured logs βœ…
Error messages βœ…
Health endpoints βœ…
Monitoring output βœ…

Implementation: Uses pydantic-settings with SecretStr for automatic redaction.

SSRF Protection

When using @cache.io (CachekitIOBackend), the SDK includes built-in Server-Side Request Forgery (SSRF) protection. Custom API URLs are blocked by default - only api.cachekit.io and its subdomains are permitted.

See SSRF Protection for full details, including custom host configuration for development environments.

Cache Key Redaction in Logs (CWE-532)

Cache keys can embed caller-supplied tenant/user identifiers, so the SDK's own loggers (cachekit.*) never emit them verbatim (CWE-532). Every cachekit log path β€” decorator error handling (structured and backwards-compat), cache-operation logs, and SWR/TTL-refresh debug logs β€” replaces the key with a fixed-length blake2b digest (<redacted:…>), keeping log lines correlatable without leaking the key. Error paths are covered centrally at the shared error sink (FeatureOrchestrator.handle_cache_error / log_cache_operation), so new call sites are redacted by construction. BackendError redacts the key in its formatted text (str(e) carries key=<redacted:…>), while the .key attribute keeps the raw caller-supplied key for programmatic use β€” never log e.key. Its free-form message is caller-supplied and third-party exception text (a redis ResponseError naming the key, a pymemcache illegal-input error echoing it) has unknown provenance β€” so no cachekit log line renders str(e). Every logging call that mentions an exception goes through redact_error_for_log, which emits only the exception type plus, for BackendError, its BackendErrorType classification; the full exception stays on the object (original_exception, .message) for programmatic access. Operators lose the provider's message text in the log line and keep it on the exception. An architecture test (tests/unit/test_log_redaction_architecture.py) walks every logging call in the package β€” logger.*(), get_logger().*(), getattr(logger, level)() β€” and fails CI if a key-shaped value reaches one unredacted in the message, %s arguments, or extra=; if an exception β€” any name bound by except ... as, a conventional name (e, exc, err, error, *_err), or an attribute of one β€” reaches one outside redact_error_for_log; or if a call emits a traceback (logger.exception, exc_info=). The guarantee does not depend on the next contributor remembering it. It is flow-insensitive: build log lines inline, not via a pre-formatted variable, and bind exceptions with except ... as or a conventional name (an Exception-typed parameter called failure is invisible to it), or the guard cannot see them.

Scope β€” transport logs are not covered. The CachekitIO backend addresses entries by key in the request path (GET /v1/cache/{key}), and httpx logs every request line β€” method, full URL, status β€” at INFO on its own httpx logger. An application that enables INFO globally (logging.basicConfig(level=logging.INFO)) will therefore see raw keys in httpx's output on every operation, exactly as it would see any REST resource path. cachekit does not mute a third-party logger on your behalf; if your keys carry identifiers, silence or raise the level of that logger in your logging config:

import logging

logging.getLogger("httpx").setLevel(logging.WARNING)

The same applies to any HTTP-layer capture between the SDK and api.cachekit.io β€” see the lock-token paragraph below for why path/query content is treated as logged.

Digest strength. The redaction digest is unkeyed blake2b, so it is exactly as hard to reverse as the key material is to guess β€” and the key material is deterministic from the call: [ns:{ns}:]func:{mod.fn}:args:{blake2b(args)} for generated keys, or whatever you return from @cache(key=...). Namespace and function name are static application config, so a cache on get_user(user_id) is enumerable from its digest by iterating plausible IDs, whether the key was generated (hash the candidate args) or hand-built (default:user:1234). A per-installation secret was considered and rejected for a public library (unset it is theatre; set it breaks cross-process log correlation, the property the digest exists for). Treat the digest as a correlation ID, never as a secret: if a log reader must not be able to confirm which user an entry belongs to, do not grant that reader the logs.

Lock Token Transport (CWE-532)

The distributed-lock capability token (lock_id) is sent in the X-CacheKit-Lock-Id request header when releasing a lock (DELETE /v1/cache/{key}/lock), never in the URL query string. Query strings are routinely captured by access logs, proxy/CDN logs, and OpenTelemetry http.url spans (CWE-532); a leaked token could be replayed to release a lock within its short TTL. The CacheKit SaaS backend dual-reads the header and the legacy ?lock_id= query during migration, preferring the header (removed in protocol 2.0).

Cache-Key Path Encoding (CWE-22)

Custom @cache(key=...) values are percent-encoded before they reach the CachekitIO request path, so a key can only ever address /v1/cache/{key} and never a different api.cachekit.io endpoint. Without encoding, ?/# would be split into a query/fragment and a /-bearing key would introduce extra path segments, both escaping the cache namespace with the application's bearer token; httpx normalizes these client-side before the request leaves the process (CWE-22), so the SaaS-side key validator never sees them. quote(key, safe="") encodes every reserved character (/ β†’ %2F, ? β†’ %3F, # β†’ %23, % β†’ %25), collapsing the whole key into one inert path segment.

RFC-3986 marks . as unreserved, so quote (like cachekit-ts encodeURIComponent and cachekit-rs urlencoding::encode) leaves it raw β€” but a key of exactly . or .. is still a live dot-segment that httpx collapses: .. β†’ GET /v1, and on the sub-resource routes ../ttl β†’ GET /v1/ttl, ../lock β†’ GET /v1/lock, reaching a different route with the bearer token. The encoder special-cases an all-dot segment (.. β†’ %2E%2E) so it can no longer collapse; only a segment that is entirely dots is affected (a:.. is untouched), so canonical keys are unchanged.

Encode-once matches the SaaS validator's single decode, so a canonical key round-trips byte-for-byte. Python's quote(key, safe="") is byte-identical to cachekit-rs urlencoding::encode, and resolves to the same server-side key as cachekit-ts encodeURIComponent after that single decode, so cross-SDK cache lookups still coincide.


FFI Boundary Security

Important

The PyO3 FFI boundary between Python and Rust is security-critical.

Memory Safety

Guarantee Mechanism
Type safety PyO3's compile-time type system
No unsafe serialization MessagePack only (no pickle)
Buffer validation Inputs validated before Rust calls
Panic handling Rust panics β†’ Python exceptions

Thread Safety

Guarantee Mechanism
GIL protection All FFI calls acquire GIL
Rust synchronization Send/Sync guarantees in cachekit-core
TSan validation PyO3 false positives documented

Warning

TSan suppressions in rust/tsan_suppressions.txt only cover PyO3/Python runtime false positives. Any data races in cachekit code are real bugs and must be fixed.


Dependency Security

Rust Dependencies

Tool Purpose Config
cargo-deny License + vulnerability scanning deny.toml
cargo-audit CVE scanning against RustSec Advisory Database .github/workflows/security-fast.yml (inline ignore list)
πŸ“‹ Policy Details

Allowed licenses: MIT, Apache-2.0, BSD-3-Clause

Denied licenses: GPL (all variants)

Vulnerability scanning: RustSec Advisory Database

Note

Core dependencies (ring / aes-gcm for AES-256-GCM, lz4_flex, xxhash-rust, rmp-serde, hkdf, sha2) are audited in cachekit-core. See cachekit-core dependency docs. blake3 is not a cachekit-core dependency: it is a cachekit-py (Python) dependency used for cache-key hashing in src/cachekit/hash_utils.py, audited in this repo's own Python dependencies below.

Python Dependencies

Tool Purpose Command
pip-audit CVE scanning make security-audit

CI/CD Security

Tiered Security Checks

Tier Timing Trigger Checks
Fast < 3 min Every PR cargo-audit, cargo-deny, clippy, machete, pip-audit
Medium < 15 min Post-merge cargo-geiger (<5% unsafe), semver-checks
Deep < 2 hr Nightly Sanitizers (ASan, TSan, MSan), security report
πŸ“ Workflow Files
Tier Workflow
Fast .github/workflows/security-fast.yml
Medium .github/workflows/security-medium.yml
Deep .github/workflows/security-deep.yml

Tip

Kani formal verification and cargo-fuzz run in cachekit-core CI. This SDK relies on cachekit-core's verification results.

Local Development

# One-time setup
make security-install

# Quick checks (< 3 min)
make security-fast

# Comprehensive (< 15 min)
make security-medium

# Python dependencies
make security-audit

# Generate report
make security-report

Reports are archived in reports/security/ for compliance and audit trails.


Known Limitations

Cryptographic Security

Note

This SDK does not implement cryptography directly. All cryptographic operations are in cachekit-core.

SDK Responsibilities:

  • Safely calling cachekit-core via FFI
  • Protecting master keys in memory (SecretStr)
  • Preventing key leakage in logs/errors
  • Validating inputs before FFI calls

For cryptographic guarantees, see:

CI Workflow Validation

⚠️ Validation Status

Validated:

  • Workflow syntax
  • Job structure and dependencies
  • Tool installation procedures
  • Trigger configuration

Requires validation on first PR:

  • Actual timing (fast < 3min, medium < 15min, deep < 2h)
  • Sanitizer execution on Linux runners
  • Caching effectiveness
  • Resource limits and timeouts

Version Policy

Release Type Scope Breaking Changes
Patch (0.1.x) Security fixes ❌
Minor (0.x.0) New features ❌
Major (x.0.0) Breaking changes βœ…

Note

Pre-1.0: Minor versions may include breaking changes.

Security patches are backported to the latest supported version.


Security Roadmap

Quarter Milestone
Q2 2026 Add Hypothesis fuzzing for Python layer
Q3 2026 Third-party security audit (SDK + FFI boundary)
Q4 2026 SLSA Level 3 compliance

Contact

Purpose Channel
Security issues security@cachekit.io
General issues GitHub Issues
Maintainers GitHub Repository

Acknowledgments

We appreciate responsible disclosure from the security community. Security researchers who report valid vulnerabilities will be acknowledged in release notes (with permission).


Report Vulnerability Β· cachekit-core Security Β· GitHub

Last Updated: 2025-12-09

There aren't any published security advisories