Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ Used for security properties in `tests/unit/test_security_properties.py`:
|:---------|:----------|
| Encryption roundtrip | `decrypt(encrypt(data)) == data` |
| Compression integrity | `decompress(compress(data)) == data` |
| Tenant isolation | Different keys for different tenants |
| Per-tenant key derivation | Different derived keys for different tenants |

```python
from hypothesis import given, strategies as st
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ When enabled via `@cache.secure`, client-side AES-256-GCM encryption ensures the
| Server visibility | Opaque ciphertext only |
| Key derivation | HKDF with per-tenant salts |
| Authentication | GCM tags prevent tampering |
| Compliance | GDPR/HIPAA/PCI-DSS ready |
| Compliance | May *reduce* GDPR/HIPAA/PCI DSS scope, subject to assessment — not a compliance guarantee ([details](docs/features/zero-knowledge-encryption.md#compliance-implications)) |

<details>
<summary><strong>🔐 Master Key Security</strong></summary>
Expand Down
3 changes: 3 additions & 0 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ These functions are provided for documentation examples and return mock data:
`CACHEKIT_MASTER_KEY` in the environment because an ambient key can still auto-activate
encryption (deprecated) and breaks plain `@cache` fences that use `serializer="auto"` or custom
serializers.
- `master_key_env` - opt-in pytest fixture for a fence that binds its own key the way an
application does (`secret_key = os.environ["CACHEKIT_MASTER_KEY"]`). Request it in the info
string, `` ```python fixture:master_key_env ``; it sets `CACHEKIT_MASTER_KEY` for that fence only.

## Skipping Examples with `notest`

Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,7 @@ When `@cache` is used without an explicit `backend` parameter, resolution follow

1. **Explicit backend** — `@cache(backend=...)`, then a backend inside `config=`
2. **Module-level default** — `set_default_backend(...)`
3. **Environment auto-detection** — `CACHEKIT_API_KEY`, `CACHEKIT_REDIS_URL`, `CACHEKIT_MEMCACHED_SERVERS` or `CACHEKIT_FILE_CACHE_DIR`, with `REDIS_URL` as a fallback
3. **Environment auto-detection** — exactly one of `CACHEKIT_API_KEY`, `CACHEKIT_REDIS_URL`, `CACHEKIT_MEMCACHED_SERVERS` or `CACHEKIT_FILE_CACHE_DIR` (they are mutually exclusive, with no precedence), with `REDIS_URL` as a fallback

Examples and the auto-detection table: **[Backend Resolution Priority](backends/README.md#backend-resolution-priority)**.

Expand Down
34 changes: 21 additions & 13 deletions docs/backends/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,26 +211,34 @@ without `backend=` pins the default when it is first seen — at decoration if
already set, otherwise at first call — so the usual layout (business modules
imported at the top of the file, `set_default_backend()` in `main()`) works.
Later `set_default_backend()` calls do not re-point already-pinned functions.
Exception: `stale_ttl` and `@cache.io`'s default stale window validate SWR
capability at decoration, so set a CachekitIO default *before* importing modules
that use them.
Exception: an explicit `stale_ttl` validates SWR capability at decoration, so set a
CachekitIO default *before* importing modules that use it. `@cache.io` is not affected:
it builds its own `CachekitIOBackend` and never consults `set_default_backend()`.

### 3. Environment Variable Auto-Detection (Lowest Priority)

If no explicit backend and no module-level default, `DefaultBackendProvider`
picks a backend from exactly one environment selector, in this order:

| Priority | Environment variable | Backend |
|----------|-----------------------------|--------------------|
| 1 | `CACHEKIT_API_KEY` | `CachekitIOBackend` (SaaS) |
| 2 | `CACHEKIT_REDIS_URL` | Redis (tenant-scoped, keys prefixed `t:{tenant}:`) |
| 3 | `CACHEKIT_MEMCACHED_SERVERS`| `MemcachedBackend` |
| 4 | `CACHEKIT_FILE_CACHE_DIR` | `FileBackend` |
| 5 | `REDIS_URL`, or nothing set | Redis, as 2 (localhost fallback) |
picks a backend at the function's first call from the one environment selector that is
set. The four prefixed selectors are mutually exclusive, with no precedence between them:
set exactly one.

| Environment variable | Backend |
|-----------------------------|--------------------|
| `CACHEKIT_API_KEY` | `CachekitIOBackend` (SaaS) |
| `CACHEKIT_REDIS_URL` | Redis (tenant-scoped, keys prefixed `t:{tenant}:`) |
| `CACHEKIT_MEMCACHED_SERVERS`| `MemcachedBackend` |
| `CACHEKIT_FILE_CACHE_DIR` | `FileBackend` |
| none of the above: `REDIS_URL`, or nothing set | Redis, as above (localhost fallback) |

Setting more than one of the four `CACHEKIT_*` selectors is ambiguous and raises
`ConfigurationError` at first call. The decorator catches it, logs a WARNING on
the `cachekit.decorators.orchestrator` logger, and runs the function uncached.
the `cachekit.decorators.orchestrator` logger, and runs the function uncached. The
error recurs, so this happens on every call, and the log line is the only signal:

```text
Cache operation 'client_creation' failed for key '<redacted:...>': ConfigurationError
```

`REDIS_URL` is a 12-factor fallback and never counts as a conflict.

The Redis prefix scopes L2 only. L1 is shared by every tenant in the process; see
Expand Down
3 changes: 2 additions & 1 deletion docs/backends/cachekitio.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,9 @@ def get_user_profile(user_id: str) -> dict:
- `@cache.secure` applies AES-256-GCM client-side encryption before any data leaves the process
- Per-tenant key derivation via HKDF — cryptographic isolation between namespaces
- The SaaS backend is a zero-knowledge conduit: it stores whatever bytes arrive
- With `@cache.secure`: SaaS is out of scope for HIPAA/PCI (stores only ciphertext)
- With `@cache.secure`: the SaaS holds only ciphertext values, which supports a HIPAA/PCI DSS scope-*reduction* argument — not a guarantee; see [Compliance Implications](../features/zero-knowledge-encryption.md#compliance-implications)
- Without encryption: SaaS stores plaintext, may be in compliance scope
- Pass `backend=` explicitly, as above: `@cache.secure` does not pin the SaaS on its own ([why](../features/zero-knowledge-encryption.md#cachesecure-does-not-pin-a-backend))

**Requirements**:

Expand Down
12 changes: 7 additions & 5 deletions docs/backends/none.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ No network calls. No serialization to bytes. No backend initialization.

## With Intent Presets

Every preset except `secure` works with `backend=None`:
`@cache.minimal`, `@cache.production`, `@cache.dev` and `@cache.test` accept `backend=None`:

```python notest
from cachekit import cache
Expand All @@ -61,9 +61,11 @@ def fast_lookup(key: str) -> dict:
return fetch_data(key)
```

`@cache.secure(backend=None)` is refused at decoration time with `ConfigurationError`:
L1-only stores raw Python objects, which cannot be ciphertext. The same applies to
`encryption=True` and to an `EncryptionWrapper` serializer.
Three presets do not. `@cache.secure(master_key=…, backend=None)` is refused at decoration
time with `ConfigurationError`: L1-only stores raw Python objects, which cannot be ciphertext.
The same applies to `encryption=True` and to an `EncryptionWrapper` serializer. `@cache.io`
builds its own backend and raises `ConfigurationError` on any `backend=`, `None` included.
`@cache.local` is always in-process and raises `TypeError` on a `backend=` argument.

## Upgrade Path

Expand All @@ -90,7 +92,7 @@ No API changes. No code rewrite. Same decorator, same function signature.
- Shared across processes: No (per-process only)
- Persistence: No (lost on restart)
- TTL support: Yes
- Encryption: No — `@cache.secure` / `encryption=True` / `EncryptionWrapper` with `backend=None` raise `ConfigurationError` (raw objects cannot be ciphertext). A fleet-wide `CACHEKIT_MASTER_KEY` does not encrypt L1-only caches either.
- Encryption: No — `@cache.secure(master_key=…)` / `encryption=True` / `EncryptionWrapper` with `backend=None` raise `ConfigurationError` (raw objects cannot be ciphertext). A fleet-wide `CACHEKIT_MASTER_KEY` does not encrypt L1-only caches either.
- Metrics: Yes (if monitoring configured)

---
Expand Down
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ def secure_function():

> [!IMPORTANT]
> When multiple environment variables could apply, cachekit follows this priority order.
> The four backend selectors are the exception: they have no order, so set exactly one
> ([details](backends/README.md#3-environment-variable-auto-detection-lowest-priority)).

### Redis URL Priority

Expand Down
17 changes: 17 additions & 0 deletions docs/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,25 @@

import numpy as np
import pandas as pd
import pytest

from cachekit import cache
from cachekit.config.singleton import reset_settings


@pytest.fixture
def master_key_env(monkeypatch):
"""Set CACHEKIT_MASTER_KEY for one fence that reads its key from the environment.

Opt-in per fence (```python fixture:master_key_env) so the fence can bind the key the
way an application does instead of relying on the injected secret_key. Not global:
an ambient key still auto-activates encryption (deprecated) — see secret_key below.
"""
monkeypatch.setenv("CACHEKIT_MASTER_KEY", "a" * 64)
reset_settings()
yield
monkeypatch.undo()
reset_settings()


def pytest_markdown_docs_globals():
Expand Down
Loading
Loading