Security¶
Passwords¶
- Hashed with Argon2 (the
argon2crate's default parameters, which are Argon2id) with a random salt, and stored in PHC format. - Must be at least 8 characters.
- Changing your password needs the current one.
- Changing or resetting a password deletes all of that user's sessions.
Sessions¶
| Property | Value |
|---|---|
| Cookie | vistilla_session |
| Value | 40 random characters |
| Stored as | SHA-256 of the value (a database leak does not leak live sessions) |
| Attributes | Path=/; HttpOnly; SameSite=Lax, plus Secure when VISTILLA_SECURE_COOKIES=true |
| Lifetime | VISTILLA_SESSION_TTL_DAYS, default 30 days |
Sign-in failures always answer "incorrect email or password", whether the email exists or not.
Sign-up verification¶
New accounts are only created after the emailed 6-digit code is typed:
- the code is stored hashed;
- it expires after 15 minutes;
- it allows 5 attempts;
- a new code can be requested every 30 seconds.
Invited people skip the code, because the invitation link was already sent to their address and only that address can accept it.
VISTILLA_SIGNUP_ENABLED=false closes open sign-up: then only invited people
can create accounts.
Tokens¶
All random values come from the operating system's secure random generator.
| Token | Length | Stored as | Lifetime |
|---|---|---|---|
| Session | 40 | SHA-256 | 30 days |
| Password reset | 40 | SHA-256 | 1 hour, one use |
| Sign-up code | 6 digits | SHA-256 | 15 minutes, 5 tries |
| Team invitation | 28 | as is | 14 days |
| Share link | 22 | as is | until revoked or expired |
Guest cookie (vistilla_guest) |
32 | SHA-256 | 365 days |
Roles and access¶
- Roles are ordered viewer < member < admin < owner:
- members and above can upload;
- admins and above manage people, moderate comments and change AI settings.
- Workspace owners and admins hold their role on every team.
- Video visibility:
team: team members;private: the uploader and team admins;org: every workspace member.
- Anyone without access gets 404, so the video's existence is not revealed.
- Editing a video (rename, delete, share, trim, chapters, Insights) is for the uploader and admins.
- Share links give access to one video without an account. A revoked or expired link answers 403.
- Password-protected links (an API option):
- unlocking sets a 12-hour cookie tied to the current password, so changing the password locks everyone out again;
- signed-in people who can already see the video skip the password.
Encrypted AI keys¶
Workspace AI keys are sealed with XChaCha20-Poly1305:
- a fresh 24-byte nonce for each key;
- stored as
base64(nonce ‖ ciphertext).
The master key comes from VISTILLA_SECRET_KEY (base64, 32 bytes). Otherwise it
comes from a secret.key file in VISTILLA_SECRET_DIR (by default the
database's folder), which is created with mode 0600 on first start.
Keep the secret key
If the master key changes, stored AI keys can no longer be decrypted,
and admins must paste them again. The hosted instance sets
VISTILLA_SECRET_KEY in its environment so it survives restarts.
HTTP headers¶
Every response carries:
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'self'; img-src 'self' data: blob:;
media-src 'self' blob: [bucket origin]; script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline'; connect-src 'self';
frame-ancestors 'self'; base-uri 'none'
- The bucket's origin (
VISTILLA_S3_PUBLIC_ENDPOINT) is added tomedia-srconly when streams are redirected to presigned URLs. A download proxy (VISTILLA_S3_DOWNLOAD_PROXY) is not added: on the hosted instance it is the Worker's/_b2/, on the same origin as the app, so'self'covers it. A proxy on another origin would need adding there. - Inline scripts and styles are allowed because the UI has no build step.
- Nothing else from another origin can load.
Profile photos¶
GET /api/users/{id}/avatar needs no sign-in, so a photo can show beside a
comment on a public share page. User ids are random and not listed
anywhere public, and a photo is only as exposed as the name next to it. The
answer is cached for a year (public, max-age=31536000, immutable); a new
photo gets a new address (?v=), and a removed one stops being linked,
though a browser that already has it may keep it.
Other safeguards¶
- Uploads:
- content types are checked;
- sizes are checked against the limit before and during upload;
- the filesystem store rejects
..and absolute keys.
- Internal errors are logged in full, but the client only sees "internal error".
See Known limitations for what is not covered yet, such as rate limiting.