Comprehensive security documentation for the cachekit Python SDK.
- Supported Versions
- Reporting a Vulnerability
- Architecture Overview
- Python SDK Security Features
- FFI Boundary Security
- Dependency Security
- CI/CD Security
- Known Limitations
- Security Roadmap
| 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.
Important
We take security seriously. If you discover a security vulnerability, please report it responsibly.
| Channel | Use Case |
|---|---|
| security@cachekit.io | Preferred for sensitive issues |
| GitHub Security Advisory | Public vulnerability reports |
- Description of the vulnerability
- Steps to reproduce
- Affected versions
- Potential impact
- Suggested fix (if available)
| Stage | Timeline |
|---|---|
| Initial Response | 48 hours |
| Status Update | 7 days |
| Fix Timeline | Varies by severity |
π Disclosure Policy
We follow coordinated disclosure:
- Acknowledge receipt within 48 hours
- Confirm vulnerability and determine severity
- Develop and test fix
- Release security patch
- Public disclosure after patch availability (coordinated with reporter)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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.
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 serializationWhen 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.
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.
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 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.
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).
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.
Important
The PyO3 FFI boundary between Python and Rust is security-critical.
| 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 |
| 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.
| 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.
| Tool | Purpose | Command |
|---|---|---|
| pip-audit | CVE scanning | make security-audit |
| 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.
# 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-reportReports are archived in reports/security/ for compliance and audit trails.
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:
β οΈ 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
| 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.
| 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 |
| Purpose | Channel |
|---|---|
| Security issues | security@cachekit.io |
| General issues | GitHub Issues |
| Maintainers | GitHub Repository |
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