S3-backed caching proxy for mitmproxy (Pas³age = Passage + S3)
Project description
Passsage proxy
Passsage (or Pas³age to emphasize the backend choice) is named for the three S's in S3: it uses Amazon S3 or an S3-compatible object store to persist its cached objects.
A caching HTTP(S) proxy built on mitmproxy that stores responses in S3 storage. Useful for caching package repositories, API responses, and other HTTP traffic for improved performance and offline access.
Mitmproxy is used so Passsage can terminate TLS, inspect HTTP responses, and cache HTTPS
content instead of blindly tunneling encrypted bytes. In explicit proxy mode, clients
connect to the proxy and issue CONNECT requests for HTTPS; mitmproxy completes the
upstream TLS handshake, generates a matching interception certificate signed by its
local CA, and then speaks TLS with the client so it can read and cache the HTTP payloads.
See https://docs.mitmproxy.org/stable/concepts/how-mitmproxy-works/ for the details.
Features
- S3-backed caching: Store cached responses in AWS S3 or S3-compatible storage (MinIO, LocalStack, etc.)
- Flexible caching policies: Configure how different URLs are cached
NoRefresh: Serve from cache without revalidation; fetch on missStandard: RFC 9111 compliant caching and revalidationStaleIfError: Serve stale cache on upstream failure (4xx/5xx/timeout)AlwaysUpstream: Always fetch from upstream, cache as fallbackNoCache: Pass through without caching- Default policy when no rule matches:
Standard(override with--default-policy)
- Automatic failover: Serve cached content when upstream is unavailable
- Ban management: Temporarily ban unresponsive upstreams to avoid timeouts
Cache hits and misses
Passsage resolves each request to a cache policy, then uses S3 object metadata to decide whether to serve from cache or go upstream. In brief:
- Cache hits are served by rewriting the request to the S3 object (Cache-Status is set to
hitandAgeis derived from the stored timestamp). NoRefreshserves from cache immediately on a hit (no revalidation).Standardrevalidates with an upstreamHEADwhen stale; if the cachedETag/Last-Modifiedmatches, the cached object is served.StaleIfErrorserves stale cache on upstream failure, andStandardhonorsstale-if-error/stale-while-revalidatedirectives when present.AlwaysUpstreamalways fetches from upstream, even if cached.NoCachebypasses cache lookup for non-GET/HEAD or policy override.
Standard follows RFC 9111 HTTP caching semantics; stale-if-error and
stale-while-revalidate are supported when present.
On cache misses, Passsage fetches from upstream and streams the response to the client.
Responses are saved to S3 unless caching is disabled by policy or response headers
(Cache-Control: no-store or private, or Vary: *). When Vary is present, cache
keys include the Vary request headers so separate variants are stored and served.
Installation
pip install passsage
For development:
pip install -e ".[dev]"
Usage
Basic Usage
# Run with default settings (uses AWS S3)
passsage
# Run on a specific port (CLI or env var)
PASSSAGE_PORT=9090 passsage
passsage -p 9090
# Bind to a specific interface (CLI or env var)
PASSSAGE_HOST=127.0.0.1 passsage
passsage --bind 127.0.0.1
# Run with web interface
passsage --web
Client Setup (Certificate + Proxy Env Vars)
Passsage runs on mitmproxy, so clients must trust the mitmproxy CA certificate to avoid
TLS errors. Mitmproxy exposes a magic domain, mitm.it, which serves the local
certificate authority for download; see https://docs.mitmproxy.org/stable/concepts/certificates/.
The examples below assume a localhost proxy; if you deploy Passsage on an intranet host,
replace localhost:8080 with the proxy hostname/IP.
- Start Passsage (example on localhost):
PASSSAGE_PORT=8080 passsage
- Fetch the mitmproxy CA certificate from a client (as root, or with sudo):
curl -x http://localhost:${PASSSAGE_PORT} http://mitm.it/cert/pem -o /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt
update-ca-certificates
Open the page and download the certificate for your OS or browser, or use the direct download above.
- Install the certificate on the client:
- Linux (system trust store):
sudo cp ~/Downloads/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt
sudo update-ca-certificates
- macOS:
- Open the downloaded
.pemin Keychain Access, add to "System", and set to "Always Trust".
- Open the downloaded
- Windows:
- Run
mmc, add Certificates snap-in for "Computer account", then import the.peminto "Trusted Root Certification Authorities".
- Run
- Set proxy environment variables on the client (some tools only honor lowercase):
export HTTP_PROXY="http://localhost:${PASSSAGE_PORT}"
export HTTPS_PROXY="http://localhost:${PASSSAGE_PORT}"
export NO_PROXY="localhost,127.0.0.1,::1"
export http_proxy="http://localhost:${PASSSAGE_PORT}"
export https_proxy="http://localhost:${PASSSAGE_PORT}"
export no_proxy="localhost,127.0.0.1,::1"
# Python uv requires this
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
You can also use per-command proxies:
curl -x http://localhost:${PASSSAGE_PORT} https://example.com/
With LocalStack (Local Development)
Start LocalStack:
docker run --rm -p 4566:4566 localstack/localstack
Create and configure the bucket:
# Using awslocal (pip install awscli-local)
awslocal s3 mb s3://proxy-cache
awslocal s3api put-bucket-policy --bucket proxy-cache --policy '{
"Version": "2012-10-17",
"Statement": [{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject", "s3:HeadObject"],
"Resource": "arn:aws:s3:::proxy-cache/*"
}]
}'
Run Passsage:
passsage --s3-endpoint http://localhost:4566 --s3-bucket proxy-cache
Environment Variables
| Variable | Description | Default |
|---|---|---|
PASSSAGE_PORT |
Proxy listen port | 8080 |
PASSSAGE_HOST |
Proxy bind host | 0.0.0.0 |
S3_BUCKET |
S3 bucket name for cache storage | 364189071156-ds-proxy-us-west-2 (AWS) or proxy-cache (custom endpoint) |
S3_ENDPOINT_URL |
Custom S3 endpoint URL | None (uses AWS) |
CLI Options
Usage: passsage [OPTIONS]
Options:
-p, --port INTEGER Port to listen on (default: 8080)
-b, --bind TEXT Address to bind to (default: 0.0.0.0)
--s3-bucket TEXT S3 bucket for cache storage
--s3-endpoint TEXT S3 endpoint URL for S3-compatible services
--test Run in test mode
-m, --mode [regular|transparent|wireguard|upstream]
Proxy mode (default: regular)
-v, --verbose Enable verbose logging
--health-port INTEGER Health endpoint port (env: PASSAGE_HEALTH_PORT, 0 disables)
--health-host TEXT Health endpoint bind host (env: PASSAGE_HEALTH_HOST)
--web Enable mitmproxy web interface
--version Show the version and exit.
--help Show this message and exit.
As a mitmproxy Script
You can also use Passsage directly as a mitmproxy script:
mitmproxy -s $(python -c "import passsage; print(passsage.get_proxy_path())")
Docker Development
The easiest way to develop and test Passsage is with Docker Compose, which sets up LocalStack S3 automatically.
Quick Start
# Production-like setup
docker compose up --build
# Development setup (with live code editing; mitmproxy reloads proxy on change)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
This starts:
- LocalStack S3 on port 4566 with pre-configured
proxy-cachebucket - Passsage proxy on port 8080
- Health endpoint on port 8082 (
/health)
Development Workflow
- Start dev environment:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build - Edit code in
src/passsage/; mitmproxy reloads the proxy on change
See DEVELOPMENT.md for the full guide.
Building and Publishing
Build the Package
pip install build
python -m build
This creates dist/passsage-*.whl and dist/passsage-*.tar.gz.
Publish to PyPI
pip install twine
# Upload to Test PyPI first
twine upload --repository testpypi dist/*
# Upload to PyPI
twine upload dist/*
Publish to Private PyPI (CodeArtifact, etc.)
# Configure your repository in ~/.pypirc or use environment variables
twine upload --repository your-repo dist/*
Development
# Install with dev dependencies
pip install -e ".[dev]"
# Run tests
pytest
# Run linter
ruff check src/
# Format code
ruff format src/
Integration tests with Docker Compose
The integration suite expects a running proxy that allows X-Passsage-Policy overrides.
Start Passsage with --allow-policy-header or set PASSAGE_ALLOW_POLICY_HEADER=1.
# Default port 8080 and health port 8082
PASSAGE_ALLOW_POLICY_HEADER=1 docker compose up --build
# Override the host port mappings
PROXY_PORT=9090 HEALTH_PORT=9092 PASSAGE_ALLOW_POLICY_HEADER=1 docker compose up --build
When the proxy runs in a container, the test server must be reachable by Passsage. On Linux, set the bind host and public host so the proxy can reach the test server:
export PASSAGE_TEST_SERVER_BIND_HOST=0.0.0.0
export PASSAGE_TEST_SERVER_HOST=host.docker.internal
export PROXY_URL=http://localhost:9090
pytest -m "not slow"
Health endpoint
Passsage starts a lightweight HTTP server for health checks on a separate port.
curl -f http://localhost:8082/health
Configure it via environment variables:
export PASSAGE_HEALTH_PORT=8082
export PASSAGE_HEALTH_HOST=0.0.0.0
Policy Overrides
You can override caching policies by pointing Passsage at a Python file. The file
can define a RULES list, a get_rules() function, or a get_resolver() function.
These are evaluated in this order:
get_resolver()-> return aPolicyResolverget_rules()-> return a list of rulesRULES-> a list of rules
The file is loaded at runtime and does not need to be installed as a package.
Header-based policy override (client-side)
You can force a policy per request by sending the X-Passsage-Policy header.
This override is disabled by default and must be enabled on the proxy.
passsage --allow-policy-header
curl -x http://localhost:8080 \
-H "X-Passsage-Policy: NoRefresh" \
http://example.com/data.csv
Supported values: NoRefresh, Standard, StaleIfError, AlwaysUpstream, NoCache.
Security note: this allows clients to bypass normal policy rules. A malicious or misconfigured client could force caching of sensitive responses or disable caching to increase upstream load. Only expose the proxy to trusted clients if you rely on header overrides.
Use a custom policy file
passsage --policy-file /path/to/policies.py
You can also set the environment variable:
export PASSAGE_POLICY_FILE=/path/to/policies.py
passsage
Example: simple rules list
# /path/to/policies.py
from passsage.policy import NoCache, NoRefresh, PathContainsRule, RegexRule
RULES = [
PathContainsRule("/assets/", NoRefresh),
RegexRule(r".*\\.csv$", NoRefresh),
PathContainsRule("/api/private", NoCache),
]
Example: programmatic rules with defaults
# /path/to/policies.py
from passsage.default_policies import default_rules
from passsage.policy import AlwaysUpstream, PathContainsRule
def get_rules():
rules = default_rules()
rules.insert(0, PathContainsRule("/debug/", AlwaysUpstream))
return rules
Example: full resolver with custom default policy
# /path/to/policies.py
from passsage.default_policies import default_rules
from passsage.policy import PolicyResolver, Standard
def get_resolver():
return PolicyResolver(rules=default_rules(), default_policy=Standard)
Example: dynamic rule based on headers
# /path/to/policies.py
from passsage.policy import CallableRule, Context, NoCache, NoRefresh
def choose_policy(ctx: Context):
for key, value in (ctx.headers or []):
if key.lower() == "x-no-cache" and value == "1":
return NoCache
return NoRefresh
RULES = [
CallableRule(choose_policy),
]
License
See LICENSE and NOTICE for copyright and attribution details.
MIT License - see LICENSE for details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file passsage-0.1.0.tar.gz.
File metadata
- Download URL: passsage-0.1.0.tar.gz
- Upload date:
- Size: 26.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
99f77a8f6484971a03ab0b20b70f60ac132ffb90f2fc6a19280676caf09ba56e
|
|
| MD5 |
012d5611d2621f04be188d291121dc5e
|
|
| BLAKE2b-256 |
0508974f81786f2a8edb4100b5e858e4eb1aeb4b000c33f790e107d6822b23b8
|
Provenance
The following attestation bundles were made for passsage-0.1.0.tar.gz:
Publisher:
pypi.yml on bra-fsn/passsage
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
passsage-0.1.0.tar.gz -
Subject digest:
99f77a8f6484971a03ab0b20b70f60ac132ffb90f2fc6a19280676caf09ba56e - Sigstore transparency entry: 908074934
- Sigstore integration time:
-
Permalink:
bra-fsn/passsage@9bf570809e284adb4f9a3cad7994516197ebe816 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/bra-fsn
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@9bf570809e284adb4f9a3cad7994516197ebe816 -
Trigger Event:
push
-
Statement type:
File details
Details for the file passsage-0.1.0-py3-none-any.whl.
File metadata
- Download URL: passsage-0.1.0-py3-none-any.whl
- Upload date:
- Size: 27.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6e8e6c67fc6112e1f41252046b13ac6f8fada0be6e0b6fd07c61329119ca9a0f
|
|
| MD5 |
393939b1fa8893f5e428b06301dbf226
|
|
| BLAKE2b-256 |
3fa6235c947277b3a58f82e38bf4b8ea21fed6d85076132664e7124fa9e2a7b4
|
Provenance
The following attestation bundles were made for passsage-0.1.0-py3-none-any.whl:
Publisher:
pypi.yml on bra-fsn/passsage
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
passsage-0.1.0-py3-none-any.whl -
Subject digest:
6e8e6c67fc6112e1f41252046b13ac6f8fada0be6e0b6fd07c61329119ca9a0f - Sigstore transparency entry: 908074935
- Sigstore integration time:
-
Permalink:
bra-fsn/passsage@9bf570809e284adb4f9a3cad7994516197ebe816 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/bra-fsn
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@9bf570809e284adb4f9a3cad7994516197ebe816 -
Trigger Event:
push
-
Statement type: