If you share files with teammates, clients, or anyone outside your network, you’re almost certainly trusting a third-party server with your data. Even “encrypted” services often mean encryption in transit — the server still stores your plaintext.
This guide shows you how to self-host FileShot.io, a zero-knowledge file sharing server where the decryption key never reaches the server — ever.
What “Zero-Knowledge” Actually Means
Most file sharing services encrypt data in transit (TLS) and at rest (disk encryption). But the service still holds your encryption keys. If their servers are breached, subpoenaed, or compromised internally, your files are exposed.
Zero-knowledge means the server handles only ciphertext. The key lives nowhere on the server — not even transiently. FileShot implements this using the URL fragment (the # part of the URL), which browsers strip from HTTP requests by specification. The server receives the file but genuinely cannot read it.
The Architecture
Browser Server
│ │
├─ Generate AES-256-GCM key │
├─ Encrypt file in memory │
├─ Upload ciphertext ───────────> Store blob (can’t read it)
├─ Construct share URL │
│ https://yourserver.com/d/{id}#{key}
│ │
Recipient opens URL │
├─ Browser strips # fragment │
├─ Fetch ciphertext ────────────> Serve blob (still can’t read it)
├─ Decrypt with key from fragment │
└─ File opens in browser │
The # fragment is a pure client-side transport mechanism. HTTP requests never include it.
Prerequisites
- A Linux server (Ubuntu 22.04+, Debian, or any modern distro)
- Node.js 18+ installed
- A domain or subdomain pointing at your server
- nginx or Caddy for reverse proxy (optional but recommended)
Step 1: Clone and Install
git clone https://github.com/FileShot/FileShotZKE.git
cd FileShotZKE/backend
npm install
Step 2: Configure Environment
cp .env.example .env
nano .env
Key settings:
PORT=3000
UPLOAD_DIR=./uploads
MAX_FILE_SIZE=524288000 # 500MB default
FILE_RETENTION_HOURS=72 # Files auto-delete after 72h
Step 3: Start the Server
node server.js
Or with PM2 for production:
npm install -g pm2
pm2 start server.js --name fileshot
pm2 save
pm2 startup
Step 4: Serve the Frontend
The frontend is a static HTML/JS/CSS site — no build step required. Serve it with nginx:
server {
listen 80;
server_name files.yourdomain.com;
root /path/to/FileShotZKE/public_html/public_html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Then enable HTTPS with Certbot:
sudo certbot --nginx -d files.yourdomain.com
Step 5: Verify Zero-Knowledge Behavior
Open browser DevTools → Network tab. Upload a file. You’ll see:
-
POST /api/upload— request body is binary ciphertext, not your file - The share URL fragment (
#...) is never sent in any network request - Server-side logs show only blob IDs, no key material
You can confirm this by inspecting the payload in Network tab — it’s AES-256-GCM ciphertext, indistinguishable from random bytes.
Production Hardening
Rate limiting:
# nginx rate limiting
limit_req_zone $binary_remote_addr zone=upload:10m rate=10r/m;
location /api/upload {
limit_req zone=upload burst=5;
proxy_pass http://127.0.0.1:3000/upload;
}
Auto-cleanup cron:
# Runs every hour, deletes files older than 72h
0 * * * * find /path/to/uploads -mmin +4320 -delete
Fail2ban to block upload abusers — add a filter for excessive 429 responses.
Why This Is Better Than WeTransfer or Dropbox for Sensitive Files
| Feature | FileShot (self-hosted) | WeTransfer | Dropbox |
|---|---|---|---|
| Server sees plaintext | No | Yes | Yes |
| No account needed | Yes | No | No |
| Self-hostable | Yes | No | No |
| Open source | Yes | No | No |
| Key in URL fragment | Yes | No | N/A |
Live Demo and Source
- Live instance: https://fileshot.io (free, no account)
- Source code: https://github.com/FileShot/FileShotZKE (MIT license)
The live instance is what you’re self-hosting here. If you just need instant sharing without self-hosting, fileshot.io works immediately.
Questions about the encryption implementation? Drop them below — happy to walk through the SubtleCrypto API implementation and how the IV, key derivation, and GCM tag are handled.
Top comments (0)