Skip to content

Commit e656566

Browse files
Make Redis optional, improve README documentation
- Add in-memory storage fallback for single-instance deployments - Redis now only required for HA/multi-replica deployments - Rewrite README with clearer Quick Start and How It Works sections - Add comprehensive Kubernetes/Helm production deployment docs - Document credential model and why proxy needs credentials
1 parent 3e4a9f8 commit e656566

4 files changed

Lines changed: 368 additions & 139 deletions

File tree

README.md

Lines changed: 249 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -57,38 +57,78 @@ S3's server-side encryption is great, but your cloud provider still holds the ke
5757

5858
## 🚀 Quick Start
5959

60-
### One-liner with Docker
60+
### 1. Start the proxy
6161

6262
```bash
6363
docker run -p 4433:4433 \
64-
-e S3PROXY_ENCRYPT_KEY="your-super-secret-key" \
64+
-e S3PROXY_ENCRYPT_KEY="your-32-byte-encryption-key-here" \
65+
-e S3PROXY_NO_TLS=true \
6566
-e AWS_ACCESS_KEY_ID="AKIA..." \
66-
-e AWS_SECRET_ACCESS_KEY="..." \
67-
ghcr.io/<owner>/sseproxy-python:latest
67+
-e AWS_SECRET_ACCESS_KEY="wJalr..." \
68+
s3proxy:latest
6869
```
6970

70-
### Or run locally
71+
### 2. Configure your client with the same credentials
7172

72-
```bash
73-
# Install
74-
pip install -e .
75-
76-
# Configure
77-
export S3PROXY_ENCRYPT_KEY="your-super-secret-key"
78-
export AWS_ACCESS_KEY_ID="AKIA..."
79-
export AWS_SECRET_ACCESS_KEY="..."
73+
The client must use the **same credentials** that the proxy is configured with:
8074

81-
# Run
82-
python -m s3proxy.main --no-tls
75+
```bash
76+
export AWS_ACCESS_KEY_ID="AKIA..." # Same as proxy
77+
export AWS_SECRET_ACCESS_KEY="wJalr..." # Same as proxy
8378
```
8479

85-
### Point your app at it
80+
### 3. Point your application at the proxy
8681

8782
```bash
88-
# Instead of s3.amazonaws.com, use localhost:4433
83+
# Upload through S3Proxy - data is encrypted before reaching S3
8984
aws s3 --endpoint-url http://localhost:4433 cp secret.pdf s3://my-bucket/
9085

91-
# That's it. Your file is now encrypted in S3.
86+
# Download through S3Proxy - data is decrypted automatically
87+
aws s3 --endpoint-url http://localhost:4433 cp s3://my-bucket/secret.pdf ./
88+
89+
# Works with any S3 client/SDK - just change the endpoint URL
90+
```
91+
92+
Your file is now encrypted at rest with AES-256-GCM. The encryption is transparent—your application code doesn't change, only the endpoint URL.
93+
94+
> **Note:** The proxy supports any bucket accessible with the configured credentials. You don't configure a specific bucket—just point any S3 request at the proxy and it forwards to the appropriate bucket.
95+
96+
---
97+
98+
## 🔍 How It Works
99+
100+
S3Proxy sits between your application and S3, transparently encrypting all data before it reaches storage.
101+
102+
### Request Flow
103+
104+
```
105+
1. Client signs request with credentials (same credentials configured on proxy)
106+
2. Proxy receives request and verifies SigV4 signature
107+
3. Proxy encrypts the payload with AES-256-GCM
108+
4. Proxy re-signs the request (encryption changes the body, invalidating original signature)
109+
5. Proxy forwards to S3
110+
6. S3 stores the encrypted data
111+
```
112+
113+
### Why Does the Proxy Need My Credentials?
114+
115+
**Short answer:** Because encryption changes the request body, which invalidates the client's signature. The proxy must re-sign requests, and re-signing requires the secret key.
116+
117+
With S3's SigV4 authentication, clients sign requests using their secret key but only send the signature—never the key itself. When S3Proxy encrypts your data, it modifies:
118+
- The request body (now ciphertext instead of plaintext)
119+
- The `Content-Length` header
120+
- The `Content-MD5` / `x-amz-content-sha256` headers
121+
122+
This breaks the original signature. To forward the request to S3, the proxy must create a new valid signature, which requires having the secret key.
123+
124+
**The proxy acts as a trusted intermediary**, not a transparent passthrough. You configure credentials once on the proxy, and all clients use those same credentials to authenticate.
125+
126+
```
127+
┌──────────────┐ SigV4 signed ┌──────────────┐ Re-signed ┌──────────────┐
128+
│ │ (credentials │ │ (same │ │
129+
│ Client │ ─────────────▶ │ S3Proxy │ ─────────────▶ │ AWS S3 │
130+
│ │ from proxy) │ │ credentials) │ │
131+
└──────────────┘ └──────────────┘ └──────────────┘
92132
```
93133

94134
---
@@ -105,97 +145,237 @@ S3Proxy uses a **layered key architecture** for maximum security:
105145

106146
Your master key never touches S3. DEKs are wrapped and stored as object metadata. Even if someone accesses your bucket, they get nothing but ciphertext.
107147

148+
### Multipart Uploads
149+
150+
Large files are automatically handled via S3 multipart upload:
151+
152+
1. Each part is encrypted independently with its own nonce
153+
2. Part metadata is tracked in Redis for distributed consistency
154+
3. On completion, parts are assembled server-side by S3
155+
4. The final object's metadata contains all part encryption info
156+
157+
This enables streaming uploads of arbitrary size without buffering entire files in memory.
158+
108159
---
109160

110161
## ⚙️ Configuration
111162

112-
All settings via environment variables (prefix: `S3PROXY_`):
163+
All settings are configured via environment variables with the `S3PROXY_` prefix.
164+
165+
### Required
166+
167+
| Variable | Description |
168+
|----------|-------------|
169+
| `S3PROXY_ENCRYPT_KEY` | Master encryption key (32 bytes recommended) |
170+
| `AWS_ACCESS_KEY_ID` | AWS credentials—used to verify client requests AND sign upstream requests |
171+
| `AWS_SECRET_ACCESS_KEY` | AWS credentials—clients must use these same credentials |
172+
173+
> **Important:** Clients must authenticate using the same `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` configured on the proxy. The proxy verifies incoming signatures and re-signs requests before forwarding to S3. See [How It Works](#-how-it-works) for details.
174+
175+
### S3 Connection
176+
177+
| Variable | Default | Description |
178+
|----------|---------|-------------|
179+
| `S3PROXY_HOST` | `s3.amazonaws.com` | Upstream S3 endpoint |
180+
| `S3PROXY_REGION` | `us-east-1` | AWS region for signing |
181+
182+
> Works with any S3-compatible storage: AWS S3, MinIO, Cloudflare R2, DigitalOcean Spaces, etc.
183+
184+
### Server
185+
186+
| Variable | Default | Description |
187+
|----------|---------|-------------|
188+
| `S3PROXY_PORT` | `4433` | Listen port |
189+
| `S3PROXY_NO_TLS` | `false` | Disable TLS (for local development) |
190+
| `S3PROXY_LOG_LEVEL` | `INFO` | Logging verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
191+
192+
### State & Performance
113193

114194
| Variable | Default | Description |
115195
|----------|---------|-------------|
116-
| `ENCRYPT_KEY` | *required* | Your master encryption key |
117-
| `HOST` | `s3.amazonaws.com` | S3 endpoint |
118-
| `REGION` | `us-east-1` | AWS region |
119-
| `PORT` | `4433` | Listen port |
120-
| `NO_TLS` | `false` | Disable TLS |
121-
| `REDIS_URL` | `redis://localhost:6379/0` | Redis for multipart state |
122-
| `MAX_CONCURRENT_UPLOADS` | `10` | Parallel upload limit |
123-
| `MAX_CONCURRENT_DOWNLOADS` | `10` | Parallel download limit |
124-
| `LOG_LEVEL` | `INFO` | Logging verbosity |
196+
| `S3PROXY_REDIS_URL` | *(empty)* | Redis URL for HA mode (omit for single-instance in-memory storage) |
197+
| `S3PROXY_THROTTLING_REQUESTS_MAX` | `10` | Max concurrent requests (0 = unlimited) |
198+
| `S3PROXY_MAX_UPLOAD_SIZE_MB` | `45` | Max single-request upload size in MB |
199+
200+
> **Note:** Redis is only required for multi-instance (HA) deployments where multipart upload state needs to be shared across replicas. For single-instance deployments, the proxy uses in-memory storage.
125201
126202
---
127203

128-
## 🐳 Deploy to Production
204+
## ☸️ Production Deployment
129205

130-
### Docker Compose (with Redis)
206+
### Kubernetes with Helm
207+
208+
The Helm chart is in `manifests/` and includes Redis HA with Sentinel for distributed state.
209+
210+
#### Quick Start
131211

132212
```bash
133-
docker-compose -f e2e/docker-compose.e2e.yml up
213+
# Install Helm dependencies (redis-ha)
214+
cd manifests && helm dependency update && cd ..
215+
216+
# Install with inline secrets (dev/test only)
217+
helm install s3proxy ./manifests \
218+
--set secrets.encryptKey="your-32-byte-encryption-key" \
219+
--set secrets.awsAccessKeyId="AKIA..." \
220+
--set secrets.awsSecretAccessKey="wJalr..."
134221
```
135222

136-
### Kubernetes with Helm
223+
#### Production Setup
224+
225+
For production, use Kubernetes secrets instead of inline values:
137226

138227
```bash
139-
# Pull from GitHub Container Registry (OCI)
140-
helm install s3proxy oci://ghcr.io/<owner>/charts/s3proxy-python \
141-
--set config.encryptKey="your-key" \
142-
--set redis.enabled=true
228+
# Create secret manually
229+
kubectl create secret generic s3proxy-secrets \
230+
--from-literal=S3PROXY_ENCRYPT_KEY="your-32-byte-encryption-key" \
231+
--from-literal=AWS_ACCESS_KEY_ID="AKIA..." \
232+
--from-literal=AWS_SECRET_ACCESS_KEY="wJalr..."
233+
234+
# Install referencing the existing secret
235+
helm install s3proxy ./manifests \
236+
--set secrets.existingSecrets.enabled=true \
237+
--set secrets.existingSecrets.name=s3proxy-secrets
143238
```
144239

145-
The Helm chart includes:
146-
- 3 replicas by default
147-
- Redis HA with Sentinel
148-
- Health checks & readiness probes
149-
- Configurable resource limits
240+
#### Configuration Reference
241+
242+
**Core Settings:**
243+
244+
| Option | Default | Description |
245+
|--------|---------|-------------|
246+
| `replicaCount` | `3` | Number of proxy replicas |
247+
| `s3.host` | `s3.amazonaws.com` | S3 endpoint (or S3-compatible) |
248+
| `s3.region` | `us-east-1` | AWS region for signing |
249+
| `server.port` | `4433` | Proxy listen port |
250+
| `server.noTls` | `true` | Disable TLS (terminate at ingress) |
251+
252+
**Redis (choose one):**
253+
254+
| Option | Default | Description |
255+
|--------|---------|-------------|
256+
| `redis-ha.enabled` | `true` | Deploy embedded Redis HA with Sentinel |
257+
| `externalRedis.url` | `""` | Use external Redis (e.g., ElastiCache) |
258+
259+
**Performance:**
260+
261+
| Option | Default | Description |
262+
|--------|---------|-------------|
263+
| `performance.throttlingRequestsMax` | `10` | Max concurrent requests per pod |
264+
| `performance.maxUploadSizeMb` | `45` | Max single-request upload size |
265+
| `resources.requests.memory` | `512Mi` | Memory request per pod |
266+
| `resources.limits.memory` | `512Mi` | Memory limit per pod |
267+
268+
**Ingress:**
269+
270+
| Option | Default | Description |
271+
|--------|---------|-------------|
272+
| `ingress.enabled` | `false` | Enable ingress |
273+
| `ingress.className` | `nginx` | Ingress class |
274+
| `ingress.hosts` | `[]` | Hostnames for external access |
275+
| `gateway.enabled` | `false` | Enable internal gateway service |
276+
277+
#### Example: External Access with Ingress
278+
279+
```yaml
280+
# values-prod.yaml
281+
ingress:
282+
enabled: true
283+
className: nginx
284+
hosts:
285+
- s3proxy.example.com
286+
tls:
287+
- secretName: s3proxy-tls
288+
hosts:
289+
- s3proxy.example.com
290+
```
150291
151-
---
292+
```bash
293+
helm install s3proxy ./manifests -f values-prod.yaml \
294+
--set secrets.existingSecrets.enabled=true \
295+
--set secrets.existingSecrets.name=s3proxy-secrets
296+
```
152297

153-
## 🧪 Testing
298+
#### Example: Using External Redis (ElastiCache, etc.)
154299

155300
```bash
156-
# Unit tests
157-
make test
301+
helm install s3proxy ./manifests \
302+
--set redis-ha.enabled=false \
303+
--set externalRedis.url="redis://my-elasticache.xxx.cache.amazonaws.com:6379/0" \
304+
--set secrets.existingSecrets.enabled=true \
305+
--set secrets.existingSecrets.name=s3proxy-secrets
306+
```
158307

159-
# E2E tests (Docker Compose)
160-
make e2e
308+
### Health Checks
161309

162-
# Full cluster test (Kind + Helm + load test)
163-
make cluster-test
164-
```
310+
The proxy exposes health endpoints for Kubernetes probes:
311+
- `GET /healthz` — Liveness probe
312+
- `GET /readyz` — Readiness probe
313+
314+
### Security Considerations
315+
316+
- **TLS Termination**: The chart defaults to `noTls=true`, expecting TLS termination at the ingress/load balancer
317+
- **Secrets**: Always use `secrets.existingSecrets` in production—never commit secrets to values files
318+
- **Network Policy**: Consider restricting pod-to-pod traffic to only allow proxy → Redis
319+
- **Encryption Key**: Back up your encryption key securely. Losing it means losing access to all encrypted data
320+
321+
### Resource Recommendations
322+
323+
| Workload | Memory | CPU | Concurrency | Notes |
324+
|----------|--------|-----|-------------|-------|
325+
| Standard | 512Mi | 100m | 10 | Default settings |
326+
| Heavy | 1Gi+ | 500m | 20+ | Large files, high concurrency |
327+
328+
Memory scales with concurrent uploads. Use `performance.throttlingRequestsMax` to bound memory usage
165329

166330
---
167331

168-
## 🛡️ Security Model
332+
## 🧪 Testing
169333

170-
| Threat | Mitigation |
171-
|--------|------------|
172-
| S3 bucket breach | All data encrypted with AES-256-GCM |
173-
| Key extraction from S3 | DEKs wrapped with KEK, KEK never stored |
174-
| Request tampering | Full AWS SigV4 signature verification |
175-
| Replay attacks | Nonce uniqueness per object |
334+
```bash
335+
make test # Unit tests
336+
make cluster-test # Full Kubernetes cluster test
337+
```
176338

177339
---
178340

179-
## 🤝 Contributing
341+
## ❓ FAQ
180342

181-
PRs welcome! Please include tests for new functionality.
343+
**Why can't I use my own AWS credentials with the proxy?**
182344

183-
```bash
184-
# Setup dev environment
185-
uv sync
345+
The proxy must re-sign requests after encryption (see [How It Works](#-how-it-works)). Re-signing requires the secret key, but S3's SigV4 protocol only sends signatures—never the secret key itself. So the proxy must already have the credentials configured. All clients share the same credentials configured on the proxy.
346+
347+
**Can I use different credentials for different clients?**
348+
349+
Not currently. The proxy supports one credential pair. If you need per-client credentials, you would deploy multiple proxy instances or implement a credential lookup mechanism.
350+
351+
**Can I use this with existing unencrypted data?**
352+
353+
Yes. S3Proxy only encrypts data written through it. Existing objects remain readable—S3Proxy detects unencrypted objects and returns them as-is. To migrate, simply copy objects through S3Proxy:
186354

187-
# Run tests before submitting
188-
make test
355+
```bash
356+
aws s3 cp --endpoint-url http://localhost:4433 s3://bucket/file.txt s3://bucket/file.txt
189357
```
190358

359+
**What happens if I lose my encryption key?**
360+
361+
Your data is unrecoverable. The KEK is never stored—it exists only in your environment variables. Back up your key securely.
362+
363+
**Can I rotate encryption keys?**
364+
365+
Not currently. Key rotation would require re-encrypting all objects. This is on the roadmap.
366+
367+
**Does S3Proxy support SSE-C or SSE-KMS?**
368+
369+
No. S3Proxy implements its own client-side encryption. Server-side encryption options are orthogonal—you can enable both if desired.
370+
191371
---
192372

193-
## 📄 License
373+
## 🤝 Contributing
194374

195-
MIT
375+
Contributions are welcome.
196376

197377
---
198378

199-
<p align="center">
200-
<sub>Built with 🔐 by engineers who believe encryption should be easy.</sub>
201-
</p>
379+
## 📄 License
380+
381+
MIT

s3proxy/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ class Settings(BaseSettings):
3333
throttling_requests_max: int = Field(default=10, description="Max concurrent requests (0=unlimited)")
3434
max_upload_size_mb: int = Field(default=45, description="Max single-request upload size (MB)")
3535

36-
# Redis settings (for distributed state)
37-
redis_url: str = Field(default="redis://localhost:6379/0", description="Redis connection URL")
36+
# Redis settings (for distributed state in HA deployments)
37+
redis_url: str = Field(default="", description="Redis URL for HA mode (empty = in-memory single-instance)")
3838
redis_upload_ttl_hours: int = Field(default=24, description="TTL for upload state in Redis (hours)")
3939

4040
# Logging

0 commit comments

Comments
 (0)