Vodmill docs
v1.0.0
Get help
● Ship · Before you go live

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.

Secrets sealed at restADMIN_SECRET_KEYhttps required

01Go-live checklist

  • PUBLIC_URL is your https address and the console check "Public URL is https" is green.
  • ADMIN_SECRET_KEY is set in .env and a copy is in your password manager.
  • The port 8787 is not reachable from outside (firewall, or HOST=127.0.0.1); visitors come through the https proxy.
  • TRUST_PROXY_HOPS=1 behind one proxy, so rate limits see real visitor addresses.
  • The console owner uses two-factor sign-in.
  • .env is readable only by the service user: sudo chmod 600 /opt/vodmill/.env.
  • No DEMO_MODE, DEMO_PASSWORD, ADMIN_DEMO_PASSWORD or FRAME_ANCESTORS in .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 asWhat
Sealed (encrypted)Provider keys, OAuth client secret, YouTube refresh tokens, Postiz keys, webhook addresses and secrets, the import proxy
Hash onlyPasswords (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-key and 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 /api route 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, nosniff and 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.

08Share links and public pages

A share link shows one finished render to anyone who has the link. The token is random; the page asks search engines not to index it; "Take the link back" ends it. Tell customers a share link is public to whoever holds it.

Guessing links is slowed down per address: after 30 links that do not exist within ten minutes, that address gets 429 for a few minutes. Opening links that exist is never counted (src/server/routes/share.ts).

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):

RequestsLimit
JSON requests to the app, the console, the Stripe webhook and share pages2.5 MB
JSON bodies of the public API (/api/v1)1.5 MB
Files sent in one request: workspace assets, campaign proof pictures, music tracks210 MB at most; each route has its own smaller limit and checks the file's first bytes
Source video uploadsSent 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) or CloudFront-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.

Only behind that proxy

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.