Security notes
what the product protects, and what is yours to protect.
How Vodmill handles secrets, sign-in and outside requests, and the short list of things only you, the operator, can do: https, the secret key, the server itself.
01Go-live checklist
PUBLIC_URLis your https address and the console check "Public URL is https" is green.ADMIN_SECRET_KEYis set in.envand a copy is in your password manager.- The port
8787is not reachable from outside (firewall, orHOST=127.0.0.1); visitors come through the https proxy. TRUST_PROXY_HOPS=1behind one proxy, so rate limits see real visitor addresses.- The console owner uses two-factor sign-in.
.envis readable only by the service user:sudo chmod 600 /opt/vodmill/.env.- No
DEMO_MODE,DEMO_PASSWORD,ADMIN_DEMO_PASSWORDorFRAME_ANCESTORSin.env. - The nightly backup runs and its files leave the server (Backups and restore).
- SSH uses keys, and system updates are installed.
02Secrets are sealed at rest
Every API key you save in the console, a workspace's own Anthropic key, the refresh tokens of connected YouTube channels, Postiz keys, webhook addresses and signing secrets are stored encrypted with AES-256-GCM, using a key derived from ADMIN_SECRET_KEY (src/server/sealed.ts). They are never shown again after saving, and no API route returns them.
| Stored as | What |
|---|---|
| Sealed (encrypted) | Provider keys, OAuth client secret, YouTube refresh tokens, Postiz keys, webhook addresses and secrets, the import proxy |
| Hash only | Passwords (scrypt), session tokens, API keys, invite and reset links, console setup codes |
03ADMIN_SECRET_KEY
- It is 32 random bytes as 64 hex characters:
openssl rand -hex 32. - If it is missing, the server generates one into
DATA_DIR/.admin-secret-keyand warns in the log. Move it into.env(First steps). - Losing it means every sealed value is unreadable: you enter all keys again, customers reconnect YouTube channels, Postiz and webhooks.
- Changing it has the same effect. Do not rotate it casually.
- When it is in
.env, backups do not contain it. Keep a copy with your backups, but not in the same place. - Signed file links (API, webhooks, publishing) are derived from it too.
04The public URL is required
Several features build links from the public https address and do not work, or are unsafe, without it:
- Password reset and invite e-mails: the links in them start with the public URL. Without it the server logs "reset e-mail not sent: set the public address (PUBLIC_URL) first" and sends nothing.
- The YouTube connection: Google sends people back to
<public URL>/api/publish/youtube/callback, and that exact address must be registered in your OAuth client. - Stripe: its return addresses after checkout.
- Signed file links handed to Postiz, webhooks and API users.
- Cookies get the Secure flag when the public URL starts with
https://.
05Sign-in and accounts
- The console and the app have separate accounts. No
/apiroute and no console API route answers without a session, and the public API wants a key. - App passwords need at least 10 characters and are stored as scrypt hashes. Sessions last 30 days.
- Rate limits: 10 sign-in attempts per 15 minutes per address and per e-mail; 5 sign-ups and 5 reset requests per hour per address.
- Every request from the app that changes something must carry a custom header, which a foreign web page cannot send (a guard against cross-site request forgery).
- Pages are served with
X-Frame-Options: SAMEORIGIN, a content security policy,nosniffand a same-origin referrer policy. - A workspace's data is only reachable by its members. Asking for another workspace's object answers "not found".
06Requests the server makes for users
Users can make the server call addresses they type: webhooks, a Postiz server, a direct import link. Each of these goes through a guard: https (webhooks and Postiz), public addresses only, no localhost or private networks, no user name or password in the address, the connection pinned to the checked address, redirects not followed (src/server/platform.ts, src/server/publish/net.ts, packages/ingest/src/url.ts).
07Programs the server starts
Vodmill starts ffmpeg and ffprobe, and, when you configure them, whisper.cpp and yt-dlp, as separate programs with fixed argument lists. Links from users are passed after --, and yt-dlp is started with --ignore-config. Run the service as its own unprivileged user (the vodmill user in this guide), keep those programs updated, and enter their paths only from trusted locations.
09Request size limits
The server stops reading a request body at a fixed size and answers 413, whether or not the request states its length (src/server/app.ts):
| Requests | Limit |
|---|---|
| JSON requests to the app, the console, the Stripe webhook and share pages | 2.5 MB |
JSON bodies of the public API (/api/v1) | 1.5 MB |
| Files sent in one request: workspace assets, campaign proof pictures, music tracks | 210 MB at most; each route has its own smaller limit and checks the file's first bytes |
| Source video uploads | Sent in pieces and written to disk as they arrive; the limit is "Largest upload (GB)" in the console |
10Blocked countries
Console, Product, Access has a list of Blocked countries. Vodmill does not look up addresses itself. The block works only when both are true (src/server/app.ts):
- A proxy in front of the server adds a header with the visitor's two-letter country code, for example
CF-IPCountry(Cloudflare) orCloudFront-Viewer-Country. - You entered that header's name in Country header set by your proxy. Empty: nobody is blocked.
A blocked visitor's requests to the app's API are answered with 451 and "This service is not available in your country." The sign-in page still loads so that it can say why. The operator console is never blocked, so you cannot lock yourself out. The public API with keys (/api/v1) is not covered by this block.
Use this only when every request reaches the server through the proxy that sets the header. A visitor who can reach the server directly can send any header. A country block is a policy tool, not a security boundary.
11Data you hold
Your install stores account e-mails, uploaded videos, transcripts, clipper handles, payout details that clippers type, and screenshots they upload. Audio is sent to the transcription provider you choose, and transcripts to Anthropic when the AI key is set; local whisper.cpp keeps audio on your server. You are the one responsible for telling users this in your privacy policy and for the data protection rules of your country.